├── doc └── assets │ └── images │ └── midisplit-concept.png ├── src ├── _SharedAssemblies │ ├── CannedBytes.Midi.dll │ ├── CannedBytes.Midi.IO.dll │ ├── CannedBytes.Midi.Xml.dll │ ├── CannedBytes.Midi.Message.dll │ ├── CannedBytes │ │ ├── CannedBytes.dll │ │ ├── CannedBytes.IO.dll │ │ ├── CannedBytes.Media.dll │ │ └── CannedBytes.Media.IO.dll │ ├── CannedBytes.Midi.Components.dll │ ├── CompilationNote.txt │ ├── midinet-AcceptAnyControlChange.patch │ └── midinet-MidiFileSysExWriterFix.patch └── MidiSplit │ ├── MidiFileData.cs │ ├── MidiSplit.sln │ ├── Properties │ └── AssemblyInfo.cs │ ├── FileReaderFactory.cs │ ├── MidiFileSerializer.cs │ ├── MidiSplit.csproj │ ├── MidiChannelStatus.cs │ └── MidiSplit.cs ├── bin └── MidiSplitAll.bat ├── appveyor.yml ├── README_ja.md ├── LICENSE └── README.md /doc/assets/images/midisplit-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/doc/assets/images/midisplit-concept.png -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes.Midi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes.Midi.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes.Midi.IO.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes.Midi.IO.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes.Midi.Xml.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes.Midi.Xml.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes.Midi.Message.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes.Midi.Message.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes/CannedBytes.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes/CannedBytes.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes.Midi.Components.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes.Midi.Components.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes/CannedBytes.IO.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes/CannedBytes.IO.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes/CannedBytes.Media.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.dll -------------------------------------------------------------------------------- /src/_SharedAssemblies/CannedBytes/CannedBytes.Media.IO.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocha/midisplit/HEAD/src/_SharedAssemblies/CannedBytes/CannedBytes.Media.IO.dll -------------------------------------------------------------------------------- /src/MidiSplit/MidiFileData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CannedBytes.Midi.IO; 3 | 4 | namespace MidiSplit 5 | { 6 | class MidiFileData 7 | { 8 | public MThdChunk Header; 9 | public IEnumerable Tracks; 10 | } 11 | } -------------------------------------------------------------------------------- /bin/MidiSplitAll.bat: -------------------------------------------------------------------------------- 1 | @for %%a in (*.mid) do @midisplit "%%a" "%%~na-split.mid" 2 | 3 | @rem @set midisplit_dir=%~dp0 4 | @rem 5 | @rem :dispatch_loop 6 | @rem @if "%1"=="" @goto dispatch_done 7 | @rem @echo %midisplit_dir%midisplit "%1" "%~dpn1.mid" 8 | @rem @shift 9 | @rem @goto dispatch_loop 10 | @rem :dispatch_done 11 | -------------------------------------------------------------------------------- /src/_SharedAssemblies/CompilationNote.txt: -------------------------------------------------------------------------------- 1 | 2 | MidiSplit uses MIDI.NET C# library 3 | http://midinet.codeplex.com/ 4 | 5 | I encountered a few problems of MIDI.NET during the development of MidiSplit. 6 | Therefore, I modified the library source code, and recompiled them. 7 | Changes are written in *.patch file. They are small, but important. 8 | 9 | How to build MIDI.NET is explained in VST.NET wiki page. 10 | http://vstnet.codeplex.com/wikipage?title=Building%20the%20Source%20Code 11 | VST.NET is another project by the author of MIDI.NET. 12 | Both they have similar structure, and the explanation is adaptable to MIDI.NET. 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.2.{build} 2 | 3 | image: Visual Studio 2017 4 | 5 | environment: 6 | matrix: 7 | - config: Release 8 | APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 9 | 10 | init: 11 | - git config --global core.autocrlf input 12 | 13 | build_script: 14 | - msbuild src\MidiSplit\MidiSplit.sln /t:build /p:Configuration=%config% /m /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" 15 | 16 | after_build: 17 | - ps: $env:gitrev = git describe --tags 18 | - ps: $env:my_version = "$env:gitrev" 19 | - set package_name=midisplit-%my_version% 20 | - if not exist bin mkdir bin 21 | - move src\MidiSplit\bin\%config%\*.* bin 22 | - del bin\*.pdb 23 | - 7z a %package_name%.zip bin doc README.md README_ja.md LICENSE 24 | 25 | artifacts: 26 | - path: $(package_name).zip 27 | name: $(my_version) 28 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | MidiSplit 2 | ========= 3 | [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/fbysw0bkfuy18ubq/branch/master?svg=true)](https://ci.appveyor.com/project/gocha/midisplit/branch/master) 4 | 5 | MIDI トラックをプログラムナンバー(楽器)ごとに分割します。 6 | 7 | ![MidiSplit のコンセプト](doc/assets/images/midisplit-concept.png) 8 | 9 | MidiSplit はチャンネル数が制限されたシーケンス(例:レトロゲームの BGM)のトラックを再配置するのに効果的です。そのようなシーケンスはプログラムチェンジによって、1チャンネル(1トラック)で楽器を複数回変更します。MidiSplit は、あなたが使用されている楽器数を調べたり、各楽器の音量バランスを調整したりするのを支援します。 10 | 11 | 注意 12 | ------------------------ 13 | 14 | - MidiSplit はプログラムチェンジを発見した際にトラックを分割します。プログラムチェンジの前に配置されたノート以外のチャンネルメッセージ(コントロールチェンジなど)も新しいトラックに移されます。 15 | - `-cs` オプションによって、MidiSplit は切り替えポイントに他トラックで配置されたコントロールチェンジを複製することができます。 16 | - 非チャンネルメッセージ(Sysex など)は入力トラックに残されます。 17 | - リズムチャンネルは、メロディーチャンネルと同様に処理されます。 18 | - `-sp` オプションによって、MidiSplit はメロディーをノート単位でトラックに分割することができます。 (例: `-sp "ch10, prg127:0:1"`) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 gocha / ごちゃ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/MidiSplit/MidiSplit.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.779 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MidiSplit", "MidiSplit.csproj", "{B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Debug|x86.ActiveCfg = Debug|x86 15 | {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Debug|x86.Build.0 = Debug|x86 16 | {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Release|x86.ActiveCfg = Release|x86 17 | {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2}.Release|x86.Build.0 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2972D3B5-C4C4-4E8C-83E9-CAA8C733A693} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/_SharedAssemblies/midinet-AcceptAnyControlChange.patch: -------------------------------------------------------------------------------- 1 | --- Code/CannedBytes.Midi.Message/MidiControllerMessage.cs.bak 2013-01-05 05:47:04.359060700 +0900 2 | +++ Code/CannedBytes.Midi.Message/MidiControllerMessage.cs 2013-01-26 20:06:30.659296900 +0900 3 | @@ -24,11 +24,14 @@ 4 | "Cannot construct a MidiControllerMessage instance other than MidiChannelCommand.Controller.", "data"); 5 | } 6 | 7 | - if (!Enum.IsDefined(typeof(MidiControllerType), (int)Parameter1)) 8 | - { 9 | - throw new ArgumentException( 10 | - "Invalid type of controller specified in data.", "data"); 11 | - } 12 | + // gocha: 13 | + // Even if it's not a well-known controller type, it's still a valid midi message. 14 | + // I refuse this error check, to accept more MIDI file. 15 | + //if (!Enum.IsDefined(typeof(MidiControllerType), (int)Parameter1)) 16 | + //{ 17 | + // throw new ArgumentException( 18 | + // "Invalid type of controller specified in data.", "data"); 19 | + //} 20 | } 21 | 22 | /// 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MidiSplit 2 | ========= 3 | [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/fbysw0bkfuy18ubq/branch/master?svg=true)](https://ci.appveyor.com/project/gocha/midisplit/branch/master) 4 | 5 | Split MIDI tracks for each program number (instruments). 6 | 7 | ![Concept of MidiSplit](doc/assets/images/midisplit-concept.png) 8 | 9 | MidiSplit is effective for relocating tracks of a sequence that has only a limited number of channels (e.g. retrogame BGM). Such a sequence often changes instrument by a program change several times in a track. MidiSplit helps you to know how many instruments are used, and adjust volume balance for each instruments. 10 | 11 | Note 12 | ------------------------ 13 | 14 | - MidiSplit splits track when it finds a program change. Channel messages excluding notes (e.g. control changes) located before the program change, will also be moved to the new track. 15 | - By `-cs` option, MidiSplit can copy control changes, that are located in other tracks, to switching point. 16 | - Non-channel messages (e.g. sysex) will be kept in the input track. 17 | - Rhythm channel is processed as same as a melody channel. 18 | - By `-sp` option, MidiSplit can divide a melody into tracks by note numbers. (example: `-sp "ch10, prg127:0:1"`) 19 | -------------------------------------------------------------------------------- /src/_SharedAssemblies/midinet-MidiFileSysExWriterFix.patch: -------------------------------------------------------------------------------- 1 | --- Code/CannedBytes.Midi.IO/MidiFileStreamWriter.cs 2013-01-26 23:32:57.612790100 +0900 2 | +++ Code/CannedBytes.Midi.IO/MidiFileStreamWriter.cs 2013-01-27 08:22:14.736562200 +0900 3 | @@ -116,11 +116,30 @@ 4 | 5 | this.WriteVariableLength((uint)deltaTime); 6 | 7 | - // length of data 8 | - this.WriteVariableLength((uint)data.Length); 9 | + if (data.Length > 0 && data[0] == 0xF0) 10 | + { 11 | + // sysex marker 12 | + this.writer.Write((byte)0xF0); 13 | 14 | - // meta data 15 | - this.writer.Write(data); 16 | + // length of data 17 | + this.WriteVariableLength((uint)(data.Length - 1)); 18 | + 19 | + // meta data 20 | + byte[] dataTrimmed = new byte[data.Length - 1]; 21 | + Array.Copy(data, 1, dataTrimmed, 0, data.Length - 1); 22 | + this.writer.Write(dataTrimmed); 23 | + } 24 | + else 25 | + { 26 | + // sysex continuation marker 27 | + this.writer.Write((byte)0xF7); 28 | + 29 | + // length of data 30 | + this.WriteVariableLength((uint)data.Length); 31 | + 32 | + // meta data 33 | + this.writer.Write(data); 34 | + } 35 | } 36 | 37 | /// 38 | -------------------------------------------------------------------------------- /src/MidiSplit/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CannedBytes.Midi.MidiFilePlayer")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CannedBytes.Midi.MidiFilePlayer")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c899da5b-38e8-4c4a-8c66-6165880d2913")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/MidiSplit/FileReaderFactory.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition.Hosting; 2 | using CannedBytes.ComponentModel.Composition; 3 | using CannedBytes.Media.IO; 4 | using CannedBytes.Media.IO.Services; 5 | using CannedBytes.Midi.IO; 6 | 7 | namespace MidiSplit 8 | { 9 | internal class FileReaderFactory 10 | { 11 | public static FileChunkReader CreateReader(string filePath) 12 | { 13 | var context = CreateFileContextForReading(filePath); 14 | 15 | var reader = context.CompositionContainer.CreateFileChunkReader(); 16 | 17 | return reader; 18 | } 19 | 20 | public static ChunkFileContext CreateFileContextForReading(string filePath) 21 | { 22 | var context = new ChunkFileContext(); 23 | context.ChunkFile = ChunkFileInfo.OpenRead(filePath); 24 | 25 | context.CompositionContainer = CreateCompositionContextForReading(); 26 | 27 | return context; 28 | } 29 | 30 | public static CompositionContainer CreateCompositionContextForReading() 31 | { 32 | var factory = new CompositionContainerFactory(); 33 | 34 | factory.AddMarkedTypesInAssembly(null, typeof(IFileChunkHandler)); 35 | // add midi exports 36 | factory.AddMarkedTypesInAssembly(typeof(MTrkChunkHandler).Assembly, typeof(IFileChunkHandler)); 37 | 38 | // note that Midi files use big endian. 39 | // and the chunks are not aligned. 40 | factory.AddTypes( 41 | typeof(BigEndianNumberReader), 42 | typeof(SizePrefixedStringReader), 43 | typeof(ChunkTypeFactory), 44 | typeof(FileChunkHandlerManager)); 45 | 46 | var container = factory.CreateNew(); 47 | 48 | var chunkFactory = container.GetService(); 49 | // add midi chunks 50 | chunkFactory.AddChunksFrom(typeof(MTrkChunkHandler).Assembly, true); 51 | 52 | return container; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/MidiSplit/MidiFileSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition.Hosting; 3 | using System.Linq; 4 | using CannedBytes; 5 | using CannedBytes.ComponentModel.Composition; 6 | using CannedBytes.Media.IO; 7 | using CannedBytes.Media.IO.Services; 8 | using CannedBytes.Midi.IO; 9 | 10 | namespace MidiSplit 11 | { 12 | class MidiFileSerializer : DisposableBase 13 | { 14 | private ChunkFileContext context = new ChunkFileContext(); 15 | 16 | public MidiFileSerializer(string filePath) 17 | { 18 | this.context.ChunkFile = ChunkFileInfo.OpenWrite(filePath); 19 | this.context.CompositionContainer = CreateCompositionContainer(); 20 | } 21 | 22 | private CompositionContainer CreateCompositionContainer() 23 | { 24 | var factory = new CompositionContainerFactory(); 25 | var midiIOAssembly = typeof(MTrkChunkHandler).Assembly; 26 | 27 | // add basic file handlers 28 | factory.AddMarkedTypesInAssembly(null, typeof(IFileChunkHandler)); 29 | 30 | // add midi file handlers 31 | factory.AddMarkedTypesInAssembly(midiIOAssembly, typeof(IFileChunkHandler)); 32 | 33 | // note that Midi files use big endian. 34 | // and the chunks are not aligned. 35 | factory.AddTypes( 36 | typeof(BigEndianNumberWriter), 37 | typeof(SizePrefixedStringWriter), 38 | typeof(ChunkTypeFactory), 39 | typeof(FileChunkHandlerManager)); 40 | 41 | var container = factory.CreateNew(); 42 | 43 | var chunkFactory = container.GetService(); 44 | 45 | // add midi chunks 46 | chunkFactory.AddChunksFrom(midiIOAssembly, true); 47 | 48 | return container; 49 | } 50 | 51 | public void Serialize(MidiFileData midiFileData) 52 | { 53 | FileChunkWriter writer = new FileChunkWriter(this.context); 54 | 55 | writer.WriteNextChunk(midiFileData.Header); 56 | 57 | foreach (MTrkChunk trackChunk in midiFileData.Tracks) 58 | { 59 | writer.WriteNextChunk(trackChunk); 60 | } 61 | } 62 | 63 | protected override void Dispose(DisposeObjectKind disposeKind) 64 | { 65 | this.context.Dispose(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/MidiSplit/MidiSplit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {B20B545B-6BB1-4C0C-BBDD-06E3E61CECA2} 9 | Exe 10 | Properties 11 | MidiSplit 12 | MidiSplit 13 | v4.0 14 | Client 15 | 512 16 | 17 | 18 | x86 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | x86 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | ..\_SharedAssemblies\CannedBytes\CannedBytes.dll 39 | 40 | 41 | ..\_SharedAssemblies\CannedBytes\CannedBytes.IO.dll 42 | 43 | 44 | ..\_SharedAssemblies\CannedBytes\CannedBytes.Media.IO.dll 45 | 46 | 47 | ..\_SharedAssemblies\CannedBytes.Midi.dll 48 | 49 | 50 | ..\_SharedAssemblies\CannedBytes.Midi.Components.dll 51 | 52 | 53 | ..\_SharedAssemblies\CannedBytes.Midi.IO.dll 54 | 55 | 56 | ..\_SharedAssemblies\CannedBytes.Midi.Message.dll 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /src/MidiSplit/MidiChannelStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using CannedBytes.Midi.IO; 6 | using CannedBytes.Midi.Message; 7 | 8 | namespace MidiSplit 9 | { 10 | public class MidiChannelStatus 11 | { 12 | public IDictionary ControlValue; 13 | public IDictionary RPNValue; 14 | public IDictionary NRPNValue; 15 | public int? PitchBendValue; 16 | public int? CurrentRPN; 17 | public int? CurrentNRPN; 18 | public bool DataEntryForRPN; 19 | 20 | public MidiChannelStatus() 21 | { 22 | ControlValue = new Dictionary(); 23 | RPNValue = new Dictionary(); 24 | NRPNValue = new Dictionary(); 25 | CurrentRPN = null; 26 | CurrentNRPN = null; 27 | PitchBendValue = null; 28 | DataEntryForRPN = true; 29 | } 30 | 31 | public MidiChannelStatus(MidiChannelStatus previousStatus) 32 | { 33 | ControlValue = new Dictionary(previousStatus.ControlValue); 34 | RPNValue = new Dictionary(previousStatus.RPNValue); 35 | NRPNValue = new Dictionary(previousStatus.NRPNValue); 36 | CurrentRPN = previousStatus.CurrentRPN; 37 | CurrentNRPN = previousStatus.CurrentNRPN; 38 | PitchBendValue = previousStatus.PitchBendValue; 39 | DataEntryForRPN = previousStatus.DataEntryForRPN; 40 | } 41 | 42 | public void ParseMidiEvents(IEnumerable midiEventList, byte midiChannel) 43 | { 44 | foreach (var midiEvent in midiEventList) 45 | { 46 | if (midiEvent.Message is MidiChannelMessage) 47 | { 48 | MidiChannelMessage channelMessage = midiEvent.Message as MidiChannelMessage; 49 | 50 | if (channelMessage.MidiChannel == midiChannel) 51 | { 52 | if (channelMessage.Command == MidiChannelCommand.ControlChange) 53 | { 54 | MidiControllerMessage controllerMessage = midiEvent.Message as MidiControllerMessage; 55 | 56 | if (controllerMessage.ControllerType == MidiControllerType.RegisteredParameterCoarse) 57 | { 58 | if (!CurrentRPN.HasValue) 59 | { 60 | CurrentRPN = 0; 61 | } 62 | 63 | CurrentRPN &= ~(127 << 7); 64 | CurrentRPN |= controllerMessage.Value << 7; 65 | DataEntryForRPN = true; 66 | } 67 | else if (controllerMessage.ControllerType == MidiControllerType.RegisteredParameterFine) 68 | { 69 | if (!CurrentRPN.HasValue) 70 | { 71 | CurrentRPN = 0; 72 | } 73 | 74 | CurrentRPN &= ~127; 75 | CurrentRPN |= controllerMessage.Value; 76 | DataEntryForRPN = true; 77 | } 78 | else if (controllerMessage.ControllerType == MidiControllerType.NonregisteredParameterCoarse) 79 | { 80 | if (!CurrentNRPN.HasValue) 81 | { 82 | CurrentNRPN = 0; 83 | } 84 | 85 | CurrentNRPN &= ~(127 << 7); 86 | CurrentNRPN |= controllerMessage.Value << 7; 87 | DataEntryForRPN = false; 88 | } 89 | else if (controllerMessage.ControllerType == MidiControllerType.NonregisteredParameterFine) 90 | { 91 | if (!CurrentNRPN.HasValue) 92 | { 93 | CurrentNRPN = 0; 94 | } 95 | 96 | CurrentNRPN &= ~127; 97 | CurrentNRPN |= controllerMessage.Value; 98 | DataEntryForRPN = false; 99 | } 100 | // Data Entry 101 | else if (controllerMessage.ControllerType == MidiControllerType.DataEntrySlider || 102 | controllerMessage.ControllerType == MidiControllerType.DataEntrySliderFine) 103 | { 104 | // RPN or NRPN? 105 | IDictionary currentValue; 106 | int? targetSlot; 107 | if (DataEntryForRPN) 108 | { 109 | currentValue = RPNValue; 110 | targetSlot = CurrentRPN; 111 | } 112 | else 113 | { 114 | currentValue = NRPNValue; 115 | targetSlot = CurrentNRPN; 116 | } 117 | 118 | // MSB or LSB? 119 | int bitShift = 0; 120 | if (controllerMessage.ControllerType == MidiControllerType.DataEntrySlider) 121 | { 122 | bitShift = 7; 123 | } 124 | 125 | // save the data value 126 | if (targetSlot.HasValue) 127 | { 128 | if (currentValue.ContainsKey(targetSlot.Value)) 129 | { 130 | currentValue[targetSlot.Value] &= ~(127 << bitShift); 131 | currentValue[targetSlot.Value] |= controllerMessage.Value << bitShift; 132 | } 133 | else 134 | { 135 | currentValue[targetSlot.Value] = controllerMessage.Value << bitShift; 136 | } 137 | } 138 | } 139 | // Reset All Controllers 140 | else if (controllerMessage.ControllerType == MidiControllerType.AllControllersOff) 141 | { 142 | // See: General MIDI Level 2 Recommended Practice (RP024) 143 | ControlValue[(int)MidiControllerType.ModulationWheel] = 0; 144 | ControlValue[(int)MidiControllerType.Expression] = 127; 145 | ControlValue[(int)MidiControllerType.HoldPedal] = 0; 146 | ControlValue[(int)MidiControllerType.Portamento] = 0; 147 | ControlValue[(int)MidiControllerType.SustenutoPedal] = 0; 148 | ControlValue[(int)MidiControllerType.SoftPedal] = 0; 149 | CurrentRPN = 0x3fff; 150 | PitchBendValue = 0; 151 | // Channel pressure 0 (off) 152 | } 153 | // anything else 154 | else if (controllerMessage.ControllerType != MidiControllerType.BankSelect && 155 | controllerMessage.ControllerType != MidiControllerType.BankSelectFine && 156 | controllerMessage.ControllerType != MidiControllerType.AllSoundOff && 157 | controllerMessage.ControllerType != MidiControllerType.LocalKeyboard && 158 | controllerMessage.ControllerType != MidiControllerType.AllNotesOff && 159 | controllerMessage.ControllerType != MidiControllerType.OmniModeOff && 160 | controllerMessage.ControllerType != MidiControllerType.OmniModeOn && 161 | controllerMessage.ControllerType != MidiControllerType.MonoOperation && 162 | controllerMessage.ControllerType != MidiControllerType.PolyOperation) 163 | { 164 | IDictionary currentValue = ControlValue; 165 | currentValue[(int)controllerMessage.ControllerType] = controllerMessage.Value; 166 | } 167 | } 168 | else if (channelMessage.Command == MidiChannelCommand.PitchWheel) 169 | { 170 | int pitchBendValue = (channelMessage.Parameter1 | (channelMessage.Parameter2 << 7)) - 8192; 171 | PitchBendValue = pitchBendValue; 172 | } 173 | else if (channelMessage.Command == MidiChannelCommand.ChannelPressure) 174 | { 175 | // not supported 176 | } 177 | else if (channelMessage.Command == MidiChannelCommand.PolyPressure) 178 | { 179 | // not supported 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | public void AddControllerMessage(MTrkChunk midiTrack, long absoluteTime, int channel, MidiControllerType controller, int value) 187 | { 188 | IList midiEvents = midiTrack.Events as IList; 189 | MidiMessageFactory midiMessageFactory = new MidiMessageFactory(); 190 | MidiControllerMessage message = midiMessageFactory.CreateControllerMessage((byte)channel, controller, (byte)value); 191 | MidiFileEvent midiEvent = new MidiFileEvent(); 192 | midiEvent.AbsoluteTime = absoluteTime; 193 | midiEvent.Message = message; 194 | midiEvents.Add(midiEvent); 195 | } 196 | 197 | public void AddPitchWheelMessage(MTrkChunk midiTrack, long absoluteTime, int channel, int value) 198 | { 199 | value = Math.Max(0, Math.Min(0x3fff, value + 8192)); 200 | 201 | IList midiEvents = midiTrack.Events as IList; 202 | MidiMessageFactory midiMessageFactory = new MidiMessageFactory(); 203 | MidiChannelMessage message = midiMessageFactory.CreateChannelMessage(MidiChannelCommand.PitchWheel, (byte)channel, (byte)(value & 0x7f), (byte)((value >> 7) & 0x7f)); 204 | MidiFileEvent midiEvent = new MidiFileEvent(); 205 | midiEvent.AbsoluteTime = absoluteTime; 206 | midiEvent.Message = message; 207 | midiEvents.Add(midiEvent); 208 | } 209 | 210 | public void AddRPNMessage(MTrkChunk midiTrack, long absoluteTime, int channel, int controller, int? value) 211 | { 212 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.RegisteredParameterCoarse, (controller >> 7) & 0x7f); 213 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.RegisteredParameterFine, controller & 0x7f); 214 | if (value != null && controller != 0x3fff) // not RPN NULL 215 | { 216 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.DataEntrySlider, (value.Value >> 7) & 0x7f); 217 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.DataEntrySliderFine, value.Value & 0x7f); 218 | } 219 | } 220 | 221 | public void AddNRPNMessage(MTrkChunk midiTrack, long absoluteTime, int channel, int controller, int? value) 222 | { 223 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.NonregisteredParameterCoarse, (controller >> 7) & 0x7f); 224 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.NonregisteredParameterFine, controller & 0x7f); 225 | if (value != null && controller != 0x3fff) // not NRPN NULL 226 | { 227 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.DataEntrySlider, (value.Value >> 7) & 0x7f); 228 | AddControllerMessage(midiTrack, absoluteTime, channel, MidiControllerType.DataEntrySliderFine, value.Value & 0x7f); 229 | } 230 | } 231 | 232 | public void AddUpdatedMidiEvents(MTrkChunk midiTrack, MidiChannelStatus previousStatus, long absoluteTime, int channel, MidiChannelStatus excludeStatus) 233 | { 234 | IDictionary updatedControlValue = new Dictionary(); 235 | foreach (var control in ControlValue) 236 | { 237 | IDictionary previousValue = (previousStatus != null) ? previousStatus.ControlValue : null; 238 | bool hasPreviousValue = previousValue != null && previousValue.ContainsKey(control.Key); 239 | if (hasPreviousValue && previousValue[control.Key] == control.Value) 240 | { 241 | continue; 242 | } 243 | if (excludeStatus != null && excludeStatus.ControlValue.ContainsKey(control.Key)) 244 | { 245 | continue; 246 | } 247 | updatedControlValue.Add(control); 248 | } 249 | 250 | IDictionary updatedRPNValue = new Dictionary(); 251 | foreach (var control in RPNValue) 252 | { 253 | IDictionary previousValue = (previousStatus != null) ? previousStatus.RPNValue : null; 254 | bool hasPreviousValue = previousValue != null && previousValue.ContainsKey(control.Key); 255 | if (hasPreviousValue && previousValue[control.Key] == control.Value) 256 | { 257 | continue; 258 | } 259 | if (excludeStatus != null && excludeStatus.RPNValue.ContainsKey(control.Key)) 260 | { 261 | continue; 262 | } 263 | updatedRPNValue.Add(control); 264 | } 265 | 266 | IDictionary updatedNRPNValue = new Dictionary(); 267 | foreach (var control in NRPNValue) 268 | { 269 | IDictionary previousValue = (previousStatus != null) ? previousStatus.NRPNValue : null; 270 | bool hasPreviousValue = previousValue != null && previousValue.ContainsKey(control.Key); 271 | if (hasPreviousValue && previousValue[control.Key] == control.Value) 272 | { 273 | continue; 274 | } 275 | if (excludeStatus != null && excludeStatus.NRPNValue.ContainsKey(control.Key)) 276 | { 277 | continue; 278 | } 279 | updatedNRPNValue.Add(control); 280 | } 281 | 282 | // control change 283 | foreach (var control in updatedControlValue) 284 | { 285 | AddControllerMessage(midiTrack, absoluteTime, channel, (MidiControllerType)control.Key, (byte)control.Value); 286 | } 287 | 288 | // pitch bend 289 | if (PitchBendValue.HasValue && (previousStatus == null || previousStatus.PitchBendValue != PitchBendValue)) 290 | { 291 | if (excludeStatus == null || !excludeStatus.PitchBendValue.HasValue) 292 | { 293 | AddPitchWheelMessage(midiTrack, absoluteTime, channel, PitchBendValue.Value); 294 | } 295 | } 296 | 297 | // Swap RPN/NRPN order by current selection 298 | bool writeRPN = !DataEntryForRPN; 299 | for (int selectRPNorNRPN = 0; selectRPNorNRPN < 2; selectRPNorNRPN++) 300 | { 301 | if (writeRPN) 302 | { 303 | // RPN 304 | foreach (var control in updatedRPNValue) 305 | { 306 | // process write for current RPN at last, not now 307 | if (DataEntryForRPN && control.Key == CurrentRPN) 308 | { 309 | continue; 310 | } 311 | 312 | AddRPNMessage(midiTrack, absoluteTime, channel, control.Key, control.Value); 313 | } 314 | // restore current RPN 315 | if (DataEntryForRPN && CurrentRPN.HasValue) 316 | { 317 | if (updatedRPNValue.ContainsKey(CurrentRPN.Value)) 318 | { 319 | AddRPNMessage(midiTrack, absoluteTime, channel, CurrentRPN.Value, RPNValue[CurrentRPN.Value]); 320 | } 321 | else if ((excludeStatus == null || !excludeStatus.RPNValue.ContainsKey(CurrentRPN.Value)) && 322 | (previousStatus == null || CurrentRPN != previousStatus.CurrentRPN)) 323 | { 324 | AddRPNMessage(midiTrack, absoluteTime, channel, CurrentRPN.Value, null); 325 | } 326 | } 327 | } 328 | else 329 | { 330 | // NRPN 331 | foreach (var control in updatedNRPNValue) 332 | { 333 | IDictionary previousValue = (previousStatus != null) ? previousStatus.NRPNValue : null; 334 | bool hasPreviousValue = previousValue != null && previousValue.ContainsKey(control.Key); 335 | 336 | // process write for current NRPN at last, not now 337 | if (!DataEntryForRPN && control.Key == CurrentNRPN) 338 | { 339 | continue; 340 | } 341 | 342 | AddNRPNMessage(midiTrack, absoluteTime, channel, control.Key, control.Value); 343 | } 344 | // restore current NRPN 345 | if (!DataEntryForRPN && CurrentNRPN.HasValue) 346 | { 347 | if (updatedNRPNValue.ContainsKey(CurrentNRPN.Value)) 348 | { 349 | AddNRPNMessage(midiTrack, absoluteTime, channel, CurrentNRPN.Value, NRPNValue[CurrentNRPN.Value]); 350 | } 351 | else if ((excludeStatus == null || !excludeStatus.NRPNValue.ContainsKey(CurrentNRPN.Value)) && 352 | (previousStatus == null || CurrentNRPN != previousStatus.CurrentNRPN)) 353 | { 354 | AddNRPNMessage(midiTrack, absoluteTime, channel, CurrentNRPN.Value, null); 355 | } 356 | } 357 | } 358 | writeRPN = !writeRPN; 359 | } 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/MidiSplit/MidiSplit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Text; 6 | using System.IO; 7 | using CannedBytes.Media.IO; 8 | using CannedBytes.Midi; 9 | using CannedBytes.Midi.IO; 10 | using CannedBytes.Midi.Message; 11 | using System.Text.RegularExpressions; 12 | 13 | namespace MidiSplit 14 | { 15 | public class MidiSplit 16 | { 17 | public const string NAME = "MidiSplit"; 18 | public const string VERSION = "1.2.5"; 19 | public const string AUTHOR = "gocha"; 20 | public const string URL = "http://github.com/gocha/midisplit"; 21 | 22 | public static int Main(string[] args) 23 | { 24 | try 25 | { 26 | string midiInFilePath = null; 27 | string midiOutFilePath = null; 28 | bool copySeparatedControllers = false; 29 | IList percMidiChannels = new List(); 30 | IList percProgChanges = new List(); 31 | 32 | // parse options 33 | int argIndex = 0; 34 | while (argIndex < args.Length && args[argIndex].StartsWith("-")) 35 | { 36 | string option = args[argIndex++]; 37 | if (option == "--help") 38 | { 39 | ShowUsage(); 40 | return 1; 41 | } 42 | else if (option == "-cs" || option == "--copy-separated") 43 | { 44 | copySeparatedControllers = true; 45 | } 46 | else if (option == "-sp" || option == "--split-note") 47 | { 48 | if (argIndex + 1 >= args.Length) 49 | { 50 | Console.WriteLine("Too few arguments for " + option); 51 | return 1; 52 | } 53 | 54 | string[] targets = args[argIndex].Split(','); 55 | foreach (var target_ in targets) 56 | { 57 | string target = target_.Trim(); 58 | if (target.StartsWith("ch")) 59 | { 60 | int midiChannel; 61 | if (!int.TryParse(target.Substring(2), out midiChannel)) 62 | { 63 | Console.WriteLine("Invalid channel number format for " + option); 64 | return 1; 65 | } 66 | 67 | if (midiChannel < 1 || midiChannel > 16) 68 | { 69 | Console.WriteLine("Invalid channel number for " + option + " (valid range is 1..16)"); 70 | return 1; 71 | } 72 | midiChannel--; 73 | 74 | if (!percMidiChannels.Contains(midiChannel)) 75 | { 76 | percMidiChannels.Add(midiChannel); 77 | } 78 | } 79 | else if (target.StartsWith("prg")) 80 | { 81 | string param = target.Trim().Substring(3); 82 | int progChange; 83 | int bankMSB = 0; 84 | int bankLSB = 0; 85 | 86 | try 87 | { 88 | if (Regex.IsMatch(param, "\\d+:\\d+:\\d+", RegexOptions.ECMAScript)) 89 | { 90 | string[] tokens = param.Split(':'); 91 | bankMSB = int.Parse(tokens[0]); 92 | bankLSB = int.Parse(tokens[1]); 93 | progChange = int.Parse(tokens[2]); 94 | } 95 | else 96 | { 97 | progChange = int.Parse(param); 98 | } 99 | } 100 | catch (FormatException) 101 | { 102 | Console.WriteLine("Invalid program number format for " + option); 103 | return 1; 104 | } 105 | 106 | if (progChange < 1 || progChange > 128) 107 | { 108 | Console.WriteLine("Invalid program number for " + option + " (valid range is 1..128)"); 109 | return 1; 110 | } 111 | if (bankMSB < 0 || bankMSB > 127) 112 | { 113 | Console.WriteLine("Invalid bank MSB for " + option + " (valid range is 0..127)"); 114 | return 1; 115 | } 116 | if (bankLSB < 0 || bankLSB > 127) 117 | { 118 | Console.WriteLine("Invalid bank LSB for " + option + " (valid range is 0..127)"); 119 | return 1; 120 | } 121 | progChange--; 122 | 123 | int progChangeWithBank = progChange + (bankLSB << 7) + (bankMSB << 14); 124 | if (!percProgChanges.Contains(progChangeWithBank)) 125 | { 126 | percProgChanges.Add(progChangeWithBank); 127 | } 128 | } 129 | else 130 | { 131 | Console.WriteLine("Invalid parameter \"" + target + "\" for option " + option); 132 | return 1; 133 | } 134 | } 135 | argIndex++; 136 | } 137 | else 138 | { 139 | Console.WriteLine("Unknown option " + option); 140 | return 1; 141 | } 142 | } 143 | 144 | // parse argument 145 | int mainArgCount = args.Length - argIndex; 146 | if (mainArgCount == 0) 147 | { 148 | ShowUsage(); 149 | return 1; 150 | } 151 | else if (mainArgCount == 1) 152 | { 153 | midiInFilePath = args[argIndex]; 154 | midiOutFilePath = Path.Combine( 155 | Path.GetDirectoryName(midiInFilePath), 156 | Path.GetFileNameWithoutExtension(midiInFilePath) + "-split.mid" 157 | ); 158 | } 159 | else if (mainArgCount == 2) 160 | { 161 | midiInFilePath = args[argIndex]; 162 | midiOutFilePath = args[argIndex + 1]; 163 | } 164 | else 165 | { 166 | Console.WriteLine("too many arguments"); 167 | return 1; 168 | } 169 | 170 | // for debug... 171 | Console.WriteLine("Reading midi file: " + midiInFilePath); 172 | Console.WriteLine("Output midi file: " + midiOutFilePath); 173 | 174 | MidiFileData midiInData = MidiSplit.ReadMidiFile(midiInFilePath); 175 | MidiFileData midiOutData = MidiSplit.SplitMidiFile(midiInData, copySeparatedControllers, percMidiChannels, percProgChanges); 176 | using (MidiFileSerializer midiFileSerializer = new MidiFileSerializer(midiOutFilePath)) 177 | { 178 | midiFileSerializer.Serialize(midiOutData); 179 | } 180 | return 0; 181 | } 182 | catch (Exception e) 183 | { 184 | Console.WriteLine(e.ToString()); 185 | return 1; 186 | } 187 | } 188 | 189 | public static void ShowUsage() 190 | { 191 | Console.WriteLine("# " + NAME); 192 | Console.WriteLine(); 193 | Console.WriteLine(NAME + " version " + VERSION + " by " + AUTHOR + " <" + URL + ">"); 194 | Console.WriteLine(); 195 | Console.WriteLine("Usage: MidiSplit (options) input.mid output.mid"); 196 | Console.WriteLine(); 197 | Console.WriteLine("### Options"); 198 | Console.WriteLine(); 199 | Console.WriteLine("- `-cs` `--copy-separated`: Copy controller events that are updated by other tracks."); 200 | Console.WriteLine("- `-sp targets` `--split-note targets`: Split given channel/instrument into each note numbers. Example: `-sp \"ch10, prg127, prg127:0:1\"`"); 201 | } 202 | 203 | static MidiFileData SplitMidiFile(MidiFileData midiInData, bool copySeparatedControllers, IList percMidiChannels, IList percProgChanges) 204 | { 205 | if (percMidiChannels == null) 206 | { 207 | percMidiChannels = new List(); 208 | } 209 | 210 | if (percProgChanges == null) 211 | { 212 | percProgChanges = new List(); 213 | } 214 | 215 | MidiFileData midiOutData = new MidiFileData(); 216 | midiOutData.Header = new MThdChunk(); 217 | midiOutData.Header.Format = (ushort)MidiFileFormat.MultipleTracks; 218 | midiOutData.Header.TimeDivision = midiInData.Header.TimeDivision; 219 | 220 | IList tracks = new List(); 221 | foreach (MTrkChunk midiTrackIn in midiInData.Tracks) 222 | { 223 | foreach (MTrkChunk midiTrackOut in MidiSplit.SplitMidiTrack(midiTrackIn, copySeparatedControllers, percMidiChannels, percProgChanges)) 224 | { 225 | tracks.Add(midiTrackOut); 226 | } 227 | } 228 | midiOutData.Tracks = tracks; 229 | midiOutData.Header.NumberOfTracks = (ushort)tracks.Count; 230 | return midiOutData; 231 | } 232 | 233 | // keys for switcing tracks 234 | protected struct MTrkChannelParam 235 | { 236 | public int MidiChannel; 237 | public int ProgramNumber; 238 | public int BankNumber; 239 | public int NoteNumber; 240 | 241 | public override string ToString() 242 | { 243 | return "(" + this.GetType().Name + ")[MidiChannel: " + MidiChannel + ", ProgramNumber: " + ProgramNumber + ", BankNumber: " + BankNumber + ", NoteNumber: " + NoteNumber + "]"; 244 | } 245 | } 246 | 247 | protected class MTrkChunkWithInfo 248 | { 249 | public MTrkChannelParam Channel; 250 | public MTrkChunk Track; 251 | public int SortIndex; // a number used for stable sort 252 | public MidiChannelStatus Status; 253 | } 254 | 255 | static IEnumerable SplitMidiTrack(MTrkChunk midiTrackIn, bool copySeparatedControllers, IList percMidiChannels, IList percProgChanges) 256 | { 257 | bool verbose = false; 258 | 259 | if (percMidiChannels == null) 260 | { 261 | percMidiChannels = new List(); 262 | } 263 | 264 | if (percProgChanges == null) 265 | { 266 | percProgChanges = new List(); 267 | } 268 | 269 | const int MaxChannels = 16; 270 | const int MaxNotes = 128; 271 | 272 | //int midiEventIndex; 273 | long absoluteEndTime = 0; 274 | IDictionary midiEventMapTo = new Dictionary(); 275 | 276 | // initialize channel variables 277 | int[] newBankNumber = new int[MaxChannels]; 278 | int[] currentBankNumber = new int[MaxChannels]; 279 | int[] currentProgramNumber = new int[MaxChannels]; 280 | int[] lastNoteNumber = new int[MaxChannels]; 281 | int[] firstChannelEventIndex = new int[MaxChannels]; 282 | int[] boundaryEventIndex = new int[MaxChannels]; 283 | int[] lastTrackMapIndex = new int[MaxChannels]; 284 | int[,] midiNoteOn = new int[MaxChannels, MaxNotes]; 285 | for (int channel = 0; channel < MaxChannels; channel++) 286 | { 287 | newBankNumber[channel] = 0; 288 | currentBankNumber[channel] = 0; 289 | currentProgramNumber[channel] = -1; 290 | firstChannelEventIndex[channel] = -1; 291 | lastNoteNumber[channel] = -1; 292 | boundaryEventIndex[channel] = 0; 293 | lastTrackMapIndex[channel] = -1; 294 | for (int note = 0; note < MaxNotes; note++) 295 | { 296 | midiNoteOn[channel, note] = 0; 297 | } 298 | } 299 | 300 | // copy midi events to a list 301 | IList midiEventListIn = new List(midiTrackIn.Events); 302 | 303 | // pre-scan input events 304 | for (int midiEventIndex = 0; midiEventIndex < midiEventListIn.Count; midiEventIndex++) 305 | { 306 | MidiFileEvent midiEvent = midiEventListIn[midiEventIndex]; 307 | 308 | // update the end time of the track 309 | absoluteEndTime = midiEvent.AbsoluteTime; 310 | 311 | // dispatch message 312 | const int DEFAULT_PROGNUMBER = 0; 313 | if (midiEvent.Message is MidiChannelMessage) 314 | { 315 | MidiChannelMessage channelMessage = midiEvent.Message as MidiChannelMessage; 316 | byte midiChannel = channelMessage.MidiChannel; 317 | int currentProgNumber = (currentProgramNumber[midiChannel] != -1) ? currentProgramNumber[midiChannel] : DEFAULT_PROGNUMBER; 318 | bool percussion = percMidiChannels.Contains(midiChannel) || 319 | percProgChanges.Contains(currentProgNumber | (currentBankNumber[midiChannel] << 7)); 320 | 321 | // remember the first channel messeage index 322 | if (firstChannelEventIndex[midiChannel] == -1) 323 | { 324 | firstChannelEventIndex[midiChannel] = midiEventIndex; 325 | 326 | // determine the output track temporalily, 327 | // for tracks that do not have any program changes 328 | MTrkChannelParam channelParam = new MTrkChannelParam(); 329 | channelParam.MidiChannel = midiChannel; 330 | channelParam.BankNumber = 0; 331 | channelParam.ProgramNumber = DEFAULT_PROGNUMBER; 332 | channelParam.NoteNumber = -1; 333 | midiEventMapTo[midiEventIndex] = channelParam; 334 | } 335 | 336 | // dispatch channel message 337 | if (channelMessage.Command == MidiChannelCommand.NoteOff || 338 | (channelMessage.Command == MidiChannelCommand.NoteOn && channelMessage.Parameter2 == 0)) 339 | { 340 | // note off 341 | byte noteNumber = channelMessage.Parameter1; 342 | if (midiNoteOn[midiChannel, noteNumber] > 0) 343 | { 344 | // deactivate existing note 345 | midiNoteOn[midiChannel, noteNumber]--; 346 | 347 | // check if all notes are off 348 | bool allNotesOff = true; 349 | for (int note = 0; note < MaxNotes; note++) 350 | { 351 | if (midiNoteOn[midiChannel, note] != 0) 352 | { 353 | allNotesOff = false; 354 | break; 355 | } 356 | } 357 | 358 | // save the trigger timing 359 | if (allNotesOff) 360 | { 361 | // find the next channel message of the same channel 362 | boundaryEventIndex[midiChannel] = midiEventIndex + 1 + midiEventListIn.Skip(midiEventIndex + 1).TakeWhile(ev => 363 | !(ev.Message is MidiChannelMessage && ((MidiChannelMessage)ev.Message).MidiChannel == midiChannel)).Count(); 364 | } 365 | } 366 | } 367 | else if (channelMessage.Command == MidiChannelCommand.NoteOn) 368 | { 369 | // note on: activate note 370 | byte noteNumber = channelMessage.Parameter1; 371 | midiNoteOn[midiChannel, noteNumber]++; 372 | 373 | if (currentProgramNumber[midiChannel] == -1) 374 | { 375 | currentProgramNumber[midiChannel] = DEFAULT_PROGNUMBER; 376 | } 377 | 378 | if (lastTrackMapIndex[midiChannel] == -1) 379 | { 380 | lastTrackMapIndex[midiChannel] = firstChannelEventIndex[midiChannel]; 381 | } 382 | 383 | if (percussion) 384 | { 385 | // check the most recent marker 386 | MTrkChannelParam channelParam = midiEventMapTo[lastTrackMapIndex[midiChannel]]; 387 | if (channelParam.NoteNumber == -1) 388 | { 389 | channelParam.NoteNumber = noteNumber; 390 | } 391 | else if (channelParam.NoteNumber != noteNumber) 392 | { 393 | channelParam = new MTrkChannelParam(); 394 | channelParam.MidiChannel = midiChannel; 395 | channelParam.BankNumber = currentBankNumber[midiChannel]; 396 | channelParam.ProgramNumber = currentProgramNumber[midiChannel]; 397 | channelParam.NoteNumber = noteNumber; 398 | 399 | lastTrackMapIndex[midiChannel] = boundaryEventIndex[midiChannel]; 400 | } 401 | 402 | try 403 | { 404 | midiEventMapTo.Add(lastTrackMapIndex[midiChannel], channelParam); 405 | } 406 | catch (ArgumentException e) 407 | { 408 | // debug output and fallback 409 | int key = lastTrackMapIndex[midiChannel]; 410 | Console.WriteLine(e); 411 | Console.WriteLine(" " + key + " => " + midiEventMapTo[key]); 412 | Console.WriteLine(" is overwritten by " + channelParam); 413 | midiEventMapTo[key] = channelParam; 414 | } 415 | } 416 | 417 | // find the next channel message of the same channel 418 | boundaryEventIndex[midiChannel] = midiEventIndex + 1 + midiEventListIn.Skip(midiEventIndex + 1).TakeWhile(ev => 419 | !(ev.Message is MidiChannelMessage && ((MidiChannelMessage)ev.Message).MidiChannel == midiChannel)).Count(); 420 | } 421 | else if (channelMessage.Command == MidiChannelCommand.ProgramChange) 422 | { 423 | // program change 424 | byte programNumber = channelMessage.Parameter1; 425 | if (currentProgramNumber[midiChannel] != programNumber || 426 | currentBankNumber[midiChannel] != newBankNumber[midiChannel]) 427 | { 428 | currentBankNumber[midiChannel] = newBankNumber[midiChannel]; 429 | currentProgramNumber[midiChannel] = programNumber; 430 | 431 | // determine the output track 432 | MTrkChannelParam channelParam; 433 | if (lastTrackMapIndex[midiChannel] == -1) 434 | { 435 | // update the first checkpoint 436 | channelParam = midiEventMapTo[firstChannelEventIndex[midiChannel]]; 437 | channelParam.BankNumber = currentBankNumber[midiChannel]; 438 | channelParam.ProgramNumber = currentProgramNumber[midiChannel]; 439 | 440 | midiEventMapTo[firstChannelEventIndex[midiChannel]] = channelParam; 441 | lastTrackMapIndex[midiChannel] = firstChannelEventIndex[midiChannel]; 442 | } 443 | else 444 | { 445 | // put new checkpoint 446 | channelParam = new MTrkChannelParam(); 447 | channelParam.MidiChannel = midiChannel; 448 | channelParam.BankNumber = currentBankNumber[midiChannel]; 449 | channelParam.ProgramNumber = currentProgramNumber[midiChannel]; 450 | channelParam.NoteNumber = -1; // will be filled later 451 | 452 | try 453 | { 454 | midiEventMapTo.Add(boundaryEventIndex[midiChannel], channelParam); 455 | } 456 | catch (ArgumentException e) 457 | { 458 | // debug output and fallback 459 | int key = boundaryEventIndex[midiChannel]; 460 | Console.WriteLine(e); 461 | Console.WriteLine(" " + key + " => " + midiEventMapTo[key]); 462 | Console.WriteLine(" is overwritten by " + channelParam); 463 | midiEventMapTo[key] = channelParam; 464 | } 465 | lastTrackMapIndex[midiChannel] = boundaryEventIndex[midiChannel]; 466 | } 467 | 468 | // update the trigger timing 469 | // find the next channel message of the same channel 470 | boundaryEventIndex[midiChannel] = midiEventIndex + 1 + midiEventListIn.Skip(midiEventIndex + 1).TakeWhile(ev => 471 | !(ev.Message is MidiChannelMessage && ((MidiChannelMessage)ev.Message).MidiChannel == midiChannel)).Count(); 472 | } 473 | } 474 | else if (channelMessage.Command == MidiChannelCommand.ControlChange) 475 | { 476 | MidiControllerMessage controllerMessage = midiEvent.Message as MidiControllerMessage; 477 | 478 | // dispatch bank select 479 | if (controllerMessage.ControllerType == MidiControllerType.BankSelect) 480 | { 481 | newBankNumber[midiChannel] &= ~(127 << 7); 482 | newBankNumber[midiChannel] |= controllerMessage.Value << 7; 483 | } 484 | else if (controllerMessage.ControllerType == MidiControllerType.BankSelectFine) 485 | { 486 | newBankNumber[midiChannel] &= ~127; 487 | newBankNumber[midiChannel] |= controllerMessage.Value; 488 | } 489 | } 490 | } 491 | } 492 | 493 | // prepare for midi event writing 494 | IDictionary trackAssociatedWith = new Dictionary(); 495 | List trackInfos = new List(); 496 | if (midiEventMapTo.Count > 0) 497 | { 498 | // erase redundant items 499 | MTrkChannelParam? lastChannelParam = null; 500 | for (int midiEventIndex = 0; midiEventIndex < midiEventListIn.Count; midiEventIndex++) 501 | { 502 | if (midiEventMapTo.ContainsKey(midiEventIndex)) 503 | { 504 | // remove if item is exactly identical to previos one 505 | if (lastChannelParam.HasValue && midiEventMapTo[midiEventIndex].Equals(lastChannelParam.Value)) 506 | { 507 | midiEventMapTo.Remove(midiEventIndex); 508 | } 509 | else 510 | { 511 | lastChannelParam = midiEventMapTo[midiEventIndex]; 512 | } 513 | } 514 | } 515 | 516 | // create output tracks 517 | foreach (KeyValuePair aMidiEventMapTo in midiEventMapTo) 518 | { 519 | if (verbose) 520 | { 521 | Console.WriteLine(String.Format("[MidiEventMap] Index: {0} => MidiChannel: {1}, BankNumber: {2}, ProgramNumber: {3}, NoteNumber: {4}", 522 | aMidiEventMapTo.Key, aMidiEventMapTo.Value.MidiChannel, aMidiEventMapTo.Value.BankNumber, aMidiEventMapTo.Value.ProgramNumber, aMidiEventMapTo.Value.NoteNumber)); 523 | } 524 | 525 | if (!trackAssociatedWith.ContainsKey(aMidiEventMapTo.Value)) 526 | { 527 | MTrkChunkWithInfo trackInfo = new MTrkChunkWithInfo(); 528 | trackInfo.Track = new MTrkChunk(); 529 | trackInfo.Track.Events = new List(); 530 | trackInfo.Channel = aMidiEventMapTo.Value; 531 | trackInfo.SortIndex = trackInfos.Count; 532 | trackInfos.Add(trackInfo); 533 | trackAssociatedWith[aMidiEventMapTo.Value] = trackInfo; 534 | } 535 | } 536 | 537 | // sort by channel number 538 | trackInfos.Sort((a, b) => { 539 | if (a.Channel.MidiChannel != b.Channel.MidiChannel) 540 | { 541 | return a.Channel.MidiChannel - b.Channel.MidiChannel; 542 | } 543 | else 544 | { 545 | return a.SortIndex - b.SortIndex; 546 | } 547 | }); 548 | } 549 | else 550 | { 551 | // special case: track does not have any channel messages 552 | MTrkChunkWithInfo trackInfo = new MTrkChunkWithInfo(); 553 | trackInfo.Track = new MTrkChunk(); 554 | trackInfo.Track.Events = new List(); 555 | trackInfos.Add(trackInfo); 556 | } 557 | 558 | // initialize controller slots 559 | MidiChannelStatus[] status = new MidiChannelStatus[MaxChannels]; 560 | for (int channel = 0; channel < MaxChannels; channel++) 561 | { 562 | status[channel] = new MidiChannelStatus(); 563 | } 564 | 565 | // start copying midi events 566 | IDictionary currentOutputTrackInfo = new Dictionary(); 567 | Queue[,] notesAssociatedWithTrack = new Queue[MaxChannels, MaxNotes]; 568 | for (int midiEventIndex = 0; midiEventIndex < midiEventListIn.Count; midiEventIndex++) 569 | { 570 | MidiFileEvent midiEvent = midiEventListIn[midiEventIndex]; 571 | MTrkChunk targetTrack = null; 572 | bool broadcastToAllTracks = false; 573 | 574 | if (verbose) 575 | { 576 | Console.Write("[MidiEvent] Index: " + midiEventIndex); 577 | Console.Write(", AbsoluteTime: " + midiEvent.AbsoluteTime); 578 | Console.Write(", MessageType: " + midiEvent.Message.GetType().Name); 579 | if (midiEvent.Message is MidiChannelMessage) 580 | { 581 | MidiChannelMessage channelMessage = midiEvent.Message as MidiChannelMessage; 582 | Console.Write(", MidiChannel: " + channelMessage.MidiChannel); 583 | Console.Write(", Command: " + channelMessage.Command); 584 | Console.Write(", Parameter1: " + channelMessage.Parameter1); 585 | Console.Write(", Parameter2: " + channelMessage.Parameter2); 586 | } 587 | Console.WriteLine(); 588 | } 589 | 590 | // switch output track if necessary 591 | if (midiEventMapTo.ContainsKey(midiEventIndex)) 592 | { 593 | MTrkChannelParam aMidiEventMapTo = midiEventMapTo[midiEventIndex]; 594 | MTrkChunkWithInfo newTrackInfo = trackAssociatedWith[aMidiEventMapTo]; 595 | int channel = aMidiEventMapTo.MidiChannel; 596 | 597 | MTrkChunkWithInfo oldTrackInfo = null; 598 | if (currentOutputTrackInfo.ContainsKey(channel)) 599 | { 600 | oldTrackInfo = currentOutputTrackInfo[channel]; 601 | } 602 | 603 | if (oldTrackInfo != newTrackInfo) 604 | { 605 | // switch output track 606 | currentOutputTrackInfo[channel] = newTrackInfo; 607 | 608 | // copy separated controller values 609 | if (copySeparatedControllers) 610 | { 611 | // readahead initialization events for new track (read until the first note on) 612 | MidiChannelStatus initStatus = new MidiChannelStatus(); 613 | initStatus.DataEntryForRPN = status[channel].DataEntryForRPN; 614 | initStatus.ParseMidiEvents(midiEventListIn.Skip(midiEventIndex).TakeWhile(ev => 615 | !(ev.Message is MidiChannelMessage && ((MidiChannelMessage)ev.Message).Command == MidiChannelCommand.NoteOn)), (byte)channel); 616 | 617 | status[channel].AddUpdatedMidiEvents(newTrackInfo.Track, newTrackInfo.Status, midiEvent.AbsoluteTime, channel, initStatus); 618 | } 619 | 620 | // save current controller values 621 | if (oldTrackInfo != null) 622 | { 623 | oldTrackInfo.Status = new MidiChannelStatus(status[channel]); 624 | } 625 | } 626 | } 627 | 628 | // dispatch message 629 | if (midiEvent.Message is MidiChannelMessage) 630 | { 631 | MidiChannelMessage channelMessage = midiEvent.Message as MidiChannelMessage; 632 | byte midiChannel = channelMessage.MidiChannel; 633 | 634 | // determine output track 635 | targetTrack = currentOutputTrackInfo[midiChannel].Track; 636 | 637 | // dispatch note on/off 638 | if (channelMessage.Command == MidiChannelCommand.NoteOff || 639 | (channelMessage.Command == MidiChannelCommand.NoteOn && channelMessage.Parameter2 == 0)) 640 | { 641 | // note off 642 | byte noteNumber = channelMessage.Parameter1; 643 | if (notesAssociatedWithTrack[midiChannel, noteNumber] != null && 644 | notesAssociatedWithTrack[midiChannel, noteNumber].Count != 0) 645 | { 646 | targetTrack = notesAssociatedWithTrack[midiChannel, noteNumber].Dequeue(); 647 | } 648 | } 649 | else if (channelMessage.Command == MidiChannelCommand.NoteOn) 650 | { 651 | // note on 652 | byte noteNumber = channelMessage.Parameter1; 653 | if (notesAssociatedWithTrack[midiChannel, noteNumber] == null) 654 | { 655 | // allocate a queue if not available 656 | notesAssociatedWithTrack[midiChannel, noteNumber] = new Queue(); 657 | } 658 | // remember the output track 659 | notesAssociatedWithTrack[midiChannel, noteNumber].Enqueue(targetTrack); 660 | } 661 | 662 | // update channel status 663 | status[midiChannel].ParseMidiEvents(midiEventListIn.Skip(midiEventIndex).Take(1), midiChannel); 664 | } 665 | else 666 | { 667 | targetTrack = trackInfos[0].Track; 668 | 669 | if (midiEvent.Message is MidiMetaMessage) 670 | { 671 | MidiMetaMessage metaMessage = midiEvent.Message as MidiMetaMessage; 672 | if ((byte)metaMessage.MetaType == 0x21) // Unofficial port select 673 | { 674 | broadcastToAllTracks = true; 675 | } 676 | } 677 | } 678 | 679 | // add event to the list, if it's not end of track 680 | if (!(midiEvent.Message is MidiMetaMessage) || 681 | (midiEvent.Message as MidiMetaMessage).MetaType != MidiMetaType.EndOfTrack) 682 | { 683 | if (broadcastToAllTracks) 684 | { 685 | foreach (MTrkChunkWithInfo trackInfo in trackInfos) 686 | { 687 | IList targetEventList = trackInfo.Track.Events as IList; 688 | targetEventList.Add(midiEvent); 689 | } 690 | } 691 | else 692 | { 693 | IList targetEventList = targetTrack.Events as IList; 694 | targetEventList.Add(midiEvent); 695 | } 696 | } 697 | } 698 | 699 | // construct the plain track list 700 | IList tracks = new List(); 701 | foreach (MTrkChunkWithInfo trackInfo in trackInfos) 702 | { 703 | tracks.Add(trackInfo.Track); 704 | } 705 | 706 | // fix some conversion problems 707 | foreach (MTrkChunk track in tracks) 708 | { 709 | // fixup delta time artifically... 710 | MidiFileEvent midiLastEvent = null; 711 | foreach (MidiFileEvent midiEvent in track.Events) 712 | { 713 | midiEvent.DeltaTime = midiEvent.AbsoluteTime - (midiLastEvent != null ? midiLastEvent.AbsoluteTime : 0); 714 | midiLastEvent = midiEvent; 715 | } 716 | 717 | // add end of track manually 718 | MidiFileEvent endOfTrack = new MidiFileEvent(); 719 | endOfTrack.AbsoluteTime = absoluteEndTime; 720 | endOfTrack.DeltaTime = absoluteEndTime - (midiLastEvent != null ? midiLastEvent.AbsoluteTime : 0); 721 | endOfTrack.Message = new MidiMetaMessage(MidiMetaType.EndOfTrack, new byte[] { }); 722 | (track.Events as IList).Add(endOfTrack); 723 | } 724 | 725 | return tracks; 726 | } 727 | 728 | static MidiFileData ReadMidiFile(string filePath) 729 | { 730 | MidiFileData data = new MidiFileData(); 731 | FileChunkReader reader = FileReaderFactory.CreateReader(filePath); 732 | 733 | data.Header = reader.ReadNextChunk() as MThdChunk; 734 | 735 | List tracks = new List(); 736 | 737 | for (int i = 0; i < data.Header.NumberOfTracks; i++) 738 | { 739 | try 740 | { 741 | var track = reader.ReadNextChunk() as MTrkChunk; 742 | 743 | if (track != null) 744 | { 745 | tracks.Add(track); 746 | } 747 | else 748 | { 749 | Console.WriteLine(String.Format("Track '{0}' was not read successfully.", i + 1)); 750 | } 751 | } 752 | catch (Exception e) 753 | { 754 | reader.SkipCurrentChunk(); 755 | 756 | ConsoleColor prevConsoleColor = Console.ForegroundColor; 757 | Console.ForegroundColor = ConsoleColor.Red; 758 | Console.WriteLine("Failed to read track: " + (i + 1)); 759 | Console.WriteLine(e); 760 | Console.ForegroundColor = prevConsoleColor; 761 | } 762 | } 763 | 764 | data.Tracks = tracks; 765 | return data; 766 | } 767 | } 768 | } 769 | --------------------------------------------------------------------------------