diff --git a/src/MidiSplit/FileReaderFactory.cs b/src/MidiSplit/FileReaderFactory.cs new file mode 100644 index 0000000..f15099b --- /dev/null +++ b/src/MidiSplit/FileReaderFactory.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.Composition.Hosting; +using CannedBytes.ComponentModel.Composition; +using CannedBytes.Media.IO; +using CannedBytes.Media.IO.Services; +using CannedBytes.Midi.IO; + +namespace MidiSplit +{ + internal class FileReaderFactory + { + public static FileChunkReader CreateReader(string filePath) + { + var context = CreateFileContextForReading(filePath); + + var reader = context.CompositionContainer.CreateFileChunkReader(); + + return reader; + } + + public static ChunkFileContext CreateFileContextForReading(string filePath) + { + var context = new ChunkFileContext(); + context.ChunkFile = ChunkFileInfo.OpenRead(filePath); + + context.CompositionContainer = CreateCompositionContextForReading(); + + return context; + } + + public static CompositionContainer CreateCompositionContextForReading() + { + var factory = new CompositionContainerFactory(); + + factory.AddMarkedTypesInAssembly(null, typeof(IFileChunkHandler)); + // add midi exports + factory.AddMarkedTypesInAssembly(typeof(MTrkChunkHandler).Assembly, typeof(IFileChunkHandler)); + + // note that Midi files use big endian. + // and the chunks are not aligned. + factory.AddTypes( + typeof(BigEndianNumberReader), + typeof(SizePrefixedStringReader), + typeof(ChunkTypeFactory), + typeof(FileChunkHandlerManager)); + + var container = factory.CreateNew(); + + var chunkFactory = container.GetService(); + // add midi chunks + chunkFactory.AddChunksFrom(typeof(MTrkChunkHandler).Assembly, true); + + return container; + } + } +} \ No newline at end of file diff --git a/src/MidiSplit/MidiFileData.cs b/src/MidiSplit/MidiFileData.cs new file mode 100644 index 0000000..60c94da --- /dev/null +++ b/src/MidiSplit/MidiFileData.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using CannedBytes.Midi.IO; + +namespace MidiSplit +{ + class MidiFileData + { + public MThdChunk Header; + public IEnumerable Tracks; + } +} \ No newline at end of file diff --git a/src/MidiSplit/MidiFileSerializer.cs b/src/MidiSplit/MidiFileSerializer.cs new file mode 100644 index 0000000..58379c7 --- /dev/null +++ b/src/MidiSplit/MidiFileSerializer.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition.Hosting; +using System.Linq; +using CannedBytes; +using CannedBytes.ComponentModel.Composition; +using CannedBytes.Media.IO; +using CannedBytes.Media.IO.Services; +using CannedBytes.Midi.IO; + +namespace MidiSplit +{ + class MidiFileSerializer : DisposableBase + { + private ChunkFileContext context = new ChunkFileContext(); + + public MidiFileSerializer(string filePath) + { + this.context.ChunkFile = ChunkFileInfo.OpenWrite(filePath); + this.context.CompositionContainer = CreateCompositionContainer(); + } + + private CompositionContainer CreateCompositionContainer() + { + var factory = new CompositionContainerFactory(); + var midiIOAssembly = typeof(MTrkChunkHandler).Assembly; + + // add basic file handlers + factory.AddMarkedTypesInAssembly(null, typeof(IFileChunkHandler)); + + // add midi file handlers + factory.AddMarkedTypesInAssembly(midiIOAssembly, typeof(IFileChunkHandler)); + + // note that Midi files use big endian. + // and the chunks are not aligned. + factory.AddTypes( + typeof(BigEndianNumberWriter), + typeof(SizePrefixedStringWriter), + typeof(ChunkTypeFactory), + typeof(FileChunkHandlerManager)); + + var container = factory.CreateNew(); + + var chunkFactory = container.GetService(); + + // add midi chunks + chunkFactory.AddChunksFrom(midiIOAssembly, true); + + return container; + } + + public void Serialize(MidiFileData midiFileData) + { + FileChunkWriter writer = new FileChunkWriter(this.context); + + writer.WriteNextChunk(midiFileData.Header); + + foreach (MTrkChunk trackChunk in midiFileData.Tracks) + { + writer.WriteNextChunk(trackChunk); + } + } + + protected override void Dispose(DisposeObjectKind disposeKind) + { + this.context.Dispose(); + } + } +} diff --git a/src/MidiSplit/MidiSplit.cs b/src/MidiSplit/MidiSplit.cs new file mode 100644 index 0000000..14a31ae --- /dev/null +++ b/src/MidiSplit/MidiSplit.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Text; +using System.IO; +using CannedBytes.Media.IO; +using CannedBytes.Midi; +using CannedBytes.Midi.IO; +using CannedBytes.Midi.Message; + +namespace MidiSplit +{ + public class MidiSplit + { + public static int Main(string[] args) + { + try + { + string midiInFilePath = null; + string midiOutFilePath = null; + + // parse argument + if (args.Length == 0) + { + Console.WriteLine("Usage: MidiSplit input.mid output.mid"); + return 1; + } + else if (args.Length == 1) + { + midiInFilePath = args[0]; + midiOutFilePath = Path.Combine( + Path.GetDirectoryName(midiInFilePath), + Path.GetFileNameWithoutExtension(midiInFilePath) + "-split.mid" + ); + } + else if (args.Length == 2) + { + midiInFilePath = args[0]; + midiOutFilePath = args[1]; + } + else + { + Console.WriteLine("too many arguments"); + return 1; + } + + // for debug... + Console.WriteLine("Reading midi file: " + midiInFilePath); + Console.WriteLine("Output midi file: " + midiOutFilePath); + + MidiFileData midiInData = MidiSplit.ReadMidiFile(midiInFilePath); + MidiFileData midiOutData = MidiSplit.SplitMidiFile(midiInData); + using (MidiFileSerializer midiFileSerializer = new MidiFileSerializer(midiOutFilePath)) + { + midiFileSerializer.Serialize(midiOutData); + } + return 0; + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return 1; + } + } + + static MidiFileData SplitMidiFile(MidiFileData midiInData) + { + MidiFileData midiOutData = new MidiFileData(); + midiOutData.Header = new MThdChunk(); + midiOutData.Header.Format = (ushort)MidiFileFormat.MultipleTracks; + midiOutData.Header.TimeDivision = midiInData.Header.TimeDivision; + + IList tracks = new List(); + foreach (MTrkChunk midiTrackIn in midiInData.Tracks) + { + foreach (MTrkChunk midiTrackOut in MidiSplit.SplitMidiTrack(midiTrackIn)) + { + tracks.Add(midiTrackOut); + } + } + midiOutData.Tracks = tracks; + midiOutData.Header.NumberOfTracks = (ushort)tracks.Count; + return midiOutData; + } + + protected class MTrkChunkWithInstrInfo + { + public int? MidiChannel; + public int? ProgramNumber; + public MTrkChunk Track; + } + + static IEnumerable SplitMidiTrack(MTrkChunk midiTrackIn) + { + const int MaxChannels = 16; + const int MaxNotes = 128; + int?[] currentProgramNumber = new int?[MaxChannels]; + IList trackInfos = new List(); + + // create default output track + trackInfos.Add(new MTrkChunkWithInstrInfo()); + trackInfos[0].Track = new MTrkChunk(); + trackInfos[0].Track.Events = new List(); + + // dispatch events from beginning + // (events must be sorted by absolute time) + MidiFileEvent midiLastEvent = null; + MTrkChunk[,] tracksWithMissingNoteOff = new MTrkChunk[MaxChannels, MaxNotes]; + foreach (MidiFileEvent midiEvent in midiTrackIn.Events) + { + MTrkChunk targetTrack = null; + + // save the last event to verify end of track + midiLastEvent = midiEvent; + + if (midiEvent.Message is MidiChannelMessage) + { + MidiChannelMessage channelMessage = midiEvent.Message as MidiChannelMessage; + + // if this is the first channel message + if (trackInfos[0].MidiChannel == null) + { + // set the channel number to the first track + trackInfos[0].MidiChannel = channelMessage.MidiChannel; + } + + // update current patch # + if (channelMessage.Command == MidiChannelCommand.ProgramChange) + { + currentProgramNumber[channelMessage.MidiChannel] = channelMessage.Parameter1; + } + + // the location to put expected note-off event needs to be determined by note-on location + bool midiEventIsNoteOff = (channelMessage.Command == MidiChannelCommand.NoteOff || + (channelMessage.Command == MidiChannelCommand.NoteOn && channelMessage.Parameter2 == 0)); + if (midiEventIsNoteOff && tracksWithMissingNoteOff[channelMessage.MidiChannel, channelMessage.Parameter1] != null) + { + targetTrack = tracksWithMissingNoteOff[channelMessage.MidiChannel, channelMessage.Parameter1]; + tracksWithMissingNoteOff[channelMessage.MidiChannel, channelMessage.Parameter1] = null; + } + else + { + // search target track + int trackIndex; + for (trackIndex = 0; trackIndex < trackInfos.Count; trackIndex++) + { + MTrkChunkWithInstrInfo trackInfo = trackInfos[trackIndex]; + + if (trackInfo.MidiChannel == channelMessage.MidiChannel && + (trackInfo.ProgramNumber == null || + trackInfo.ProgramNumber == currentProgramNumber[channelMessage.MidiChannel])) + { + // set program number (we need to set it for the first time) + trackInfo.ProgramNumber = currentProgramNumber[channelMessage.MidiChannel]; + + // target track is determined, exit the loop + targetTrack = trackInfo.Track; + break; + } + else if (trackInfo.MidiChannel > channelMessage.MidiChannel) + { + // track list is sorted by channel number + // therefore, the rest isn't what we are searching for + // a new track needs to be assigned to the current index + break; + } + } + + // add a new track if necessary + if (targetTrack == null) + { + MTrkChunkWithInstrInfo newTrackInfo = new MTrkChunkWithInstrInfo(); + newTrackInfo.Track = new MTrkChunk(); + newTrackInfo.Track.Events = new List(); + newTrackInfo.MidiChannel = channelMessage.MidiChannel; + newTrackInfo.ProgramNumber = currentProgramNumber[channelMessage.MidiChannel]; + trackInfos.Insert(trackIndex, newTrackInfo); + targetTrack = newTrackInfo.Track; + } + + // remember new note, to know appropriate note-off location + if (channelMessage.Command == MidiChannelCommand.NoteOn && channelMessage.Parameter2 != 0) + { + tracksWithMissingNoteOff[channelMessage.MidiChannel, channelMessage.Parameter1] = targetTrack; + } + } + } + else + { + targetTrack = trackInfos[0].Track; + } + + // add event to the list, if it's not end of track + if (!(midiEvent.Message is MidiMetaMessage) || + (midiEvent.Message as MidiMetaMessage).MetaType != MidiMetaType.EndOfTrack) + { + IList targetEventList = targetTrack.Events as IList; + targetEventList.Add(midiEvent); + } + } + + // determine the location of end of track + long absoluteTimeOfEndOfTrack = 0; + if (midiLastEvent != null) + { + absoluteTimeOfEndOfTrack = midiLastEvent.AbsoluteTime; + } + + // construct the track list without extra info + IList tracks = new List(); + foreach (MTrkChunkWithInstrInfo trackInfo in trackInfos) + { + tracks.Add(trackInfo.Track); + } + + // fix some conversion problems + foreach (MTrkChunk track in tracks) + { + // fixup delta time artifically... + midiLastEvent = null; + foreach (MidiFileEvent midiEvent in track.Events) + { + midiEvent.DeltaTime = midiEvent.AbsoluteTime - (midiLastEvent != null ? midiLastEvent.AbsoluteTime : 0); + midiLastEvent = midiEvent; + } + + // add end of track manually + MidiFileEvent endOfTrack = new MidiFileEvent(); + endOfTrack.AbsoluteTime = absoluteTimeOfEndOfTrack; + endOfTrack.DeltaTime = absoluteTimeOfEndOfTrack - midiLastEvent.AbsoluteTime; + endOfTrack.Message = new MidiMetaMessage(MidiMetaType.EndOfTrack, new byte[] { }); + (track.Events as IList).Add(endOfTrack); + } + + return tracks; + } + + static MidiFileData ReadMidiFile(string filePath) + { + MidiFileData data = new MidiFileData(); + FileChunkReader reader = FileReaderFactory.CreateReader(filePath); + + data.Header = reader.ReadNextChunk() as MThdChunk; + + List tracks = new List(); + + for (int i = 0; i < data.Header.NumberOfTracks; i++) + { + try + { + var track = reader.ReadNextChunk() as MTrkChunk; + + if (track != null) + { + tracks.Add(track); + } + else + { + Console.WriteLine(String.Format("Track '{0}' was not read successfully.", i + 1)); + } + } + catch (Exception e) + { + reader.SkipCurrentChunk(); + + ConsoleColor prevConsoleColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Failed to read track: " + (i + 1)); + Console.WriteLine(e); + Console.ForegroundColor = prevConsoleColor; + } + } + + data.Tracks = tracks; + return data; + } + } +} \ No newline at end of file diff --git a/src/MidiSplit/MidiSplit.csproj b/src/MidiSplit/MidiSplit.csproj new file mode 100644 index 0000000..b8ac1de --- /dev/null +++ b/src/MidiSplit/MidiSplit.csproj @@ -0,0 +1,82 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2} + Exe + Properties + MidiSplit + MidiSplit + v4.0 + Client + 512 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\_SharedAssemblies\CannedBytes\CannedBytes.dll + + + ..\_SharedAssemblies\CannedBytes\CannedBytes.IO.dll + + + ..\_SharedAssemblies\CannedBytes\CannedBytes.Media.IO.dll + + + ..\_SharedAssemblies\CannedBytes.Midi.dll + + + ..\_SharedAssemblies\CannedBytes.Midi.Components.dll + + + ..\_SharedAssemblies\CannedBytes.Midi.IO.dll + + + ..\_SharedAssemblies\CannedBytes.Midi.Message.dll + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MidiSplit/MidiSplit.sln b/src/MidiSplit/MidiSplit.sln new file mode 100644 index 0000000..30fcabb --- /dev/null +++ b/src/MidiSplit/MidiSplit.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MidiSplit", "MidiSplit.csproj", "{B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Debug|x86.ActiveCfg = Debug|x86 + {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Debug|x86.Build.0 = Debug|x86 + {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Release|x86.ActiveCfg = Release|x86 + {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MidiSplit/Properties/AssemblyInfo.cs b/src/MidiSplit/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8eb2177 --- /dev/null +++ b/src/MidiSplit/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CannedBytes.Midi.MidiFilePlayer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CannedBytes.Midi.MidiFilePlayer")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c899da5b-38e8-4c4a-8c66-6165880d2913")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/_SharedAssemblies/CannedBytes.Midi.Components.dll b/src/_SharedAssemblies/CannedBytes.Midi.Components.dll new file mode 100644 index 0000000..a343890 Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes.Midi.Components.dll differ diff --git a/src/_SharedAssemblies/CannedBytes.Midi.IO.dll b/src/_SharedAssemblies/CannedBytes.Midi.IO.dll new file mode 100644 index 0000000..706bcc1 Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes.Midi.IO.dll differ diff --git a/src/_SharedAssemblies/CannedBytes.Midi.Message.dll b/src/_SharedAssemblies/CannedBytes.Midi.Message.dll new file mode 100644 index 0000000..2a0654a Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes.Midi.Message.dll differ diff --git a/src/_SharedAssemblies/CannedBytes.Midi.Xml.dll b/src/_SharedAssemblies/CannedBytes.Midi.Xml.dll new file mode 100644 index 0000000..6d21a38 Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes.Midi.Xml.dll differ diff --git a/src/_SharedAssemblies/CannedBytes.Midi.dll b/src/_SharedAssemblies/CannedBytes.Midi.dll new file mode 100644 index 0000000..9cb1592 Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes.Midi.dll differ diff --git a/src/_SharedAssemblies/CannedBytes/CannedBytes.IO.dll b/src/_SharedAssemblies/CannedBytes/CannedBytes.IO.dll new file mode 100644 index 0000000..70cbc9c Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes/CannedBytes.IO.dll differ diff --git a/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.IO.dll b/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.IO.dll new file mode 100644 index 0000000..6df4842 Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.IO.dll differ diff --git a/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.dll b/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.dll new file mode 100644 index 0000000..2120c5e Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.dll differ diff --git a/src/_SharedAssemblies/CannedBytes/CannedBytes.dll b/src/_SharedAssemblies/CannedBytes/CannedBytes.dll new file mode 100644 index 0000000..73ba280 Binary files /dev/null and b/src/_SharedAssemblies/CannedBytes/CannedBytes.dll differ diff --git a/src/_SharedAssemblies/CompilationNote.txt b/src/_SharedAssemblies/CompilationNote.txt new file mode 100644 index 0000000..99368db --- /dev/null +++ b/src/_SharedAssemblies/CompilationNote.txt @@ -0,0 +1,12 @@ + +MidiSplit uses MIDI.NET C# library +http://midinet.codeplex.com/ + +I encountered a few problems of MIDI.NET during the development of MidiSplit. +Therefore, I modified the library source code, and recompiled them. +Changes are written in *.patch file. They are small, but important. + +How to build MIDI.NET is explained in VST.NET wiki page. +http://vstnet.codeplex.com/wikipage?title=Building%20the%20Source%20Code +VST.NET is another project by the author of MIDI.NET. +Both they have similar structure, and the explanation is adaptable to MIDI.NET. diff --git a/src/_SharedAssemblies/midinet-AcceptAnyControlChange.patch b/src/_SharedAssemblies/midinet-AcceptAnyControlChange.patch new file mode 100644 index 0000000..0d001db --- /dev/null +++ b/src/_SharedAssemblies/midinet-AcceptAnyControlChange.patch @@ -0,0 +1,22 @@ +--- Code/CannedBytes.Midi.Message/MidiControllerMessage.cs.bak 2013-01-05 05:47:04.359060700 +0900 ++++ Code/CannedBytes.Midi.Message/MidiControllerMessage.cs 2013-01-26 20:06:30.659296900 +0900 +@@ -24,11 +24,14 @@ + "Cannot construct a MidiControllerMessage instance other than MidiChannelCommand.Controller.", "data"); + } + +- if (!Enum.IsDefined(typeof(MidiControllerType), (int)Parameter1)) +- { +- throw new ArgumentException( +- "Invalid type of controller specified in data.", "data"); +- } ++ // gocha: ++ // Even if it's not a well-known controller type, it's still a valid midi message. ++ // I refuse this error check, to accept more MIDI file. ++ //if (!Enum.IsDefined(typeof(MidiControllerType), (int)Parameter1)) ++ //{ ++ // throw new ArgumentException( ++ // "Invalid type of controller specified in data.", "data"); ++ //} + } + + /// diff --git a/src/_SharedAssemblies/midinet-MidiFileSysExWriterFix.patch b/src/_SharedAssemblies/midinet-MidiFileSysExWriterFix.patch new file mode 100644 index 0000000..52123ca --- /dev/null +++ b/src/_SharedAssemblies/midinet-MidiFileSysExWriterFix.patch @@ -0,0 +1,37 @@ +--- Code/CannedBytes.Midi.IO/MidiFileStreamWriter.cs 2013-01-26 23:32:57.612790100 +0900 ++++ Code/CannedBytes.Midi.IO/MidiFileStreamWriter.cs 2013-01-27 08:22:14.736562200 +0900 +@@ -116,11 +116,30 @@ + + this.WriteVariableLength((uint)deltaTime); + +- // length of data +- this.WriteVariableLength((uint)data.Length); ++ if (data.Length > 0 && data[0] == 0xF0) ++ { ++ // sysex marker ++ this.writer.Write((byte)0xF0); + +- // meta data +- this.writer.Write(data); ++ // length of data ++ this.WriteVariableLength((uint)(data.Length - 1)); ++ ++ // meta data ++ byte[] dataTrimmed = new byte[data.Length - 1]; ++ Array.Copy(data, 1, dataTrimmed, 0, data.Length - 1); ++ this.writer.Write(dataTrimmed); ++ } ++ else ++ { ++ // sysex continuation marker ++ this.writer.Write((byte)0xF7); ++ ++ // length of data ++ this.WriteVariableLength((uint)data.Length); ++ ++ // meta data ++ this.writer.Write(data); ++ } + } + + ///