< Summary

Information
Class: MusicTheory.Theory.Midi.MidiConductorHelper
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Midi/MidiFileWriter.cs
Line coverage
100%
Covered lines: 15
Uncovered lines: 0
Coverable lines: 15
Total lines: 281
Line coverage: 100%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExtractConductor(...)100%44100%
ConsolidateAndStrip(...)100%1818100%

File(s)

/home/runner/work/MusicTheory/MusicTheory/Theory/Midi/MidiFileWriter.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using MusicTheory.Theory.Time;
 6
 7namespace MusicTheory.Theory.Midi;
 8
 9/// <summary>
 10/// シンプルな SMF (Standard MIDI File) Writer。
 11/// Format0 (単一) と Format1 (複数同期トラック) をサポート。
 12/// 対応メタイベント: Tempo(FF51), TimeSignature(FF58), KeySignature(FF59), Text/Name/Marker (FF01/03/06)。
 13/// </summary>
 14public static class MidiFileWriter
 15{
 16    /// <summary>
 17    /// トラックを SMF format0 バイト列として書き出す。Tempo/TimeSignature メタイベントは track.MetaEvents から収集。
 18    /// </summary>
 19    public static byte[] WriteSingleTrack(MidiTrack track, int division = Duration.TicksPerQuarter, bool useRunningStatu
 20    {
 21        if (division <= 0) throw new ArgumentOutOfRangeException(nameof(division));
 22        track.Sort();
 23        var events = new List<(long tick, byte[] data, bool channel)>();
 24
 25        // Meta events
 26        foreach (var me in track.MetaEvents)
 27        {
 28            switch (me)
 29            {
 30                case TempoEvent te:
 31                    // FF 51 03 tttttt
 32                    int us = te.Tempo.MicrosecondsPerQuarter;
 33                    events.Add((te.Tick, new byte[]{0xFF,0x51,0x03,(byte)((us>>16)&0xFF),(byte)((us>>8)&0xFF),(byte)(us&
 34                    break;
 35                case TimeSignatureEvent tse:
 36                    // FF 58 04 nn dd cc bb
 37                    byte nn = (byte)tse.Signature.Numerator;
 38                    // dd = log2(denominator)
 39                    byte dd = (byte)Math.Log2(tse.Signature.Denominator);
 40                    byte cc = 24; // メトロノームクリック (デフォルト)
 41                    byte bb = 8;  // 32分音符数 (四分音符あたり)
 42                    events.Add((tse.Tick, new byte[]{0xFF,0x58,0x04, nn, dd, cc, bb}, false));
 43                    break;
 44                case KeySignatureEvent kse:
 45                    // FF 59 02 sf mi (sf=-7..7 flats/sharps, mi=0 major 1 minor)
 46                    sbyte sf = (sbyte)kse.SharpsFlats;
 47                    byte mi = (byte)(kse.IsMinor ? 1:0);
 48                    events.Add((kse.Tick, new byte[]{0xFF,0x59,0x02,(byte)sf, mi}, false));
 49                    break;
 50                case TextEvent txt:
 51                    events.Add((txt.Tick, BuildTextMeta(0x01, txt.Text), false));
 52                    break;
 53                case TrackNameEvent tname:
 54                    events.Add((tname.Tick, BuildTextMeta(0x03, tname.Name), false));
 55                    break;
 56                case MarkerEvent mark:
 57                    events.Add((mark.Tick, BuildTextMeta(0x06, mark.Label), false));
 58                    break;
 59            }
 60        }
 61
 62        // Note events
 63        foreach (var ne in track.Notes)
 64        {
 65            int statusBase = ne.IsNoteOn ? 0x90 : 0x80; // 0x9x / 0x8x
 66            byte status = (byte)(statusBase | (ne.Channel & 0x0F));
 67            byte pitch = (byte)Math.Clamp(ne.Pitch, 0, 127);
 68            byte velocity = (byte)Math.Clamp(ne.Velocity, 0, 127);
 69            events.Add((ne.Tick, new byte[]{status, pitch, velocity}, true));
 70        }
 71
 72        // End Of Track (tick = 最後のイベント tick と同じ or 0)
 73        long endTick = events.Count > 0 ? events.Max(e => e.tick) : 0;
 74        events.Add((endTick, new byte[]{0xFF,0x2F,0x00}, false));
 75
 76        events.Sort((a,b)=> a.tick.CompareTo(b.tick));
 77
 78        var trackData = new MemoryStream();
 79        long prevTick = 0;
 80        byte? lastStatus = null;
 81        foreach (var ev in events)
 82        {
 83            long delta = ev.tick - prevTick;
 84            WriteVarLen(trackData, delta);
 85            prevTick = ev.tick;
 86            if (useRunningStatus && ev.channel)
 87            {
 88                byte status = ev.data[0];
 89                if (lastStatus.HasValue && lastStatus.Value == status)
 90                {
 91                    // omit status
 92                    trackData.Write(ev.data, 1, ev.data.Length-1);
 93                }
 94                else
 95                {
 96                    trackData.Write(ev.data, 0, ev.data.Length);
 97                    lastStatus = status;
 98                }
 99            }
 100            else
 101            {
 102                trackData.Write(ev.data, 0, ev.data.Length); if (ev.channel) lastStatus = ev.data[0];
 103            }
 104        }
 105
 106        byte[] trackBytes = trackData.ToArray();
 107
 108        // Header (MThd)
 109        var ms = new MemoryStream();
 110        using (var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen:true))
 111        {
 112            bw.Write(new byte[]{(byte)'M',(byte)'T',(byte)'h',(byte)'d'});
 113            bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(6))); // length
 114            bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((short)0))); // format 0
 115            bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((short)1))); // ntrks=1
 116            bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((short)division))); // division
 117
 118            // Track chunk
 119            bw.Write(new byte[]{(byte)'M',(byte)'T',(byte)'r',(byte)'k'});
 120            bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(trackBytes.Length)));
 121            bw.Write(trackBytes);
 122        }
 123        return ms.ToArray();
 124    }
 125
 126    /// <summary>複数トラック (Format1)。各 MidiTrack は独立にソートされ、先頭テンポ/拍子は最初のトラック(インデックス0)に集約することを推奨。</summary>
 127    public static byte[] WriteMultipleTracks(IReadOnlyList<MidiTrack> tracks, int division = Duration.TicksPerQuarter, b
 128    {
 129        if (tracks==null || tracks.Count==0) throw new ArgumentException("tracks empty");
 130        if (consolidateConductor && tracks.Count>1)
 131        {
 132            // 先頭トラックに Tempo/TimeSig/KeySig を集約 (tick=0 のもの優先) し他トラックから除去
 133            var primary = tracks[0];
 134            var others = tracks.Skip(1);
 135            var conductorTypes = new HashSet<Type>{ typeof(TempoEvent), typeof(TimeSignatureEvent), typeof(KeySignatureE
 136            foreach (var ot in others)
 137            {
 138                var moving = ot.MetaEvents.Where(m=>conductorTypes.Contains(m.GetType())).ToList();
 139                foreach (var m in moving)
 140                {
 141                    // 既に primary に同種 tick==m.Tick が無ければ追加
 142                    if (!primary.MetaEvents.Any(x=> x.GetType()==m.GetType() && x.Tick==m.Tick)) primary.AddMeta(m);
 143                }
 144                // 除去 (Tempo/TimeSig/KeySig のみ)
 145                RemoveMetaInternal(ot, conductorTypes);
 146            }
 147        }
 148        foreach (var t in tracks) t.Sort();
 149        var trackChunks = new List<byte[]>();
 150        foreach (var track in tracks)
 151            trackChunks.Add(BuildTrackChunk(track, useRunningStatus));
 152
 153        var ms = new MemoryStream();
 154        using var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen:true);
 155        // Header
 156        bw.Write(new byte[]{(byte)'M',(byte)'T',(byte)'h',(byte)'d'});
 157        bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(6)));
 158        bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((short)1))); // format1
 159        bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((short)trackChunks.Count))); // ntrks
 160        bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((short)division)));
 161        // Tracks
 162        foreach (var chunk in trackChunks)
 163            bw.Write(chunk);
 164        return ms.ToArray();
 165    }
 166
 167    private static byte[] BuildTrackChunk(MidiTrack track, bool useRunningStatus)
 168    {
 169        var events = new List<(long tick, byte[] data, bool channel)>();
 170        foreach (var me in track.MetaEvents)
 171        {
 172            switch (me)
 173            {
 174                case TempoEvent te:
 175                    int us = te.Tempo.MicrosecondsPerQuarter;
 176                    events.Add((te.Tick, new byte[]{0xFF,0x51,0x03,(byte)((us>>16)&0xFF),(byte)((us>>8)&0xFF),(byte)(us&
 177                    break;
 178                case TimeSignatureEvent tse:
 179                    byte nn = (byte)tse.Signature.Numerator; byte dd = (byte)Math.Log2(tse.Signature.Denominator); byte 
 180                    events.Add((tse.Tick, new byte[]{0xFF,0x58,0x04, nn, dd, cc, bb}, false));
 181                    break;
 182                case KeySignatureEvent kse:
 183                    sbyte sf = (sbyte)kse.SharpsFlats; byte mi = (byte)(kse.IsMinor?1:0);
 184                    events.Add((kse.Tick, new byte[]{0xFF,0x59,0x02,(byte)sf, mi}, false));
 185                    break;
 186                case TextEvent txt:
 187                    events.Add((txt.Tick, BuildTextMeta(0x01, txt.Text), false));
 188                    break;
 189                case TrackNameEvent tname:
 190                    events.Add((tname.Tick, BuildTextMeta(0x03, tname.Name), false));
 191                    break;
 192                case MarkerEvent mark:
 193                    events.Add((mark.Tick, BuildTextMeta(0x06, mark.Label), false));
 194                    break;
 195            }
 196        }
 197        foreach (var ne in track.Notes)
 198        {
 199            int statusBase = ne.IsNoteOn ? 0x90 : 0x80; byte status = (byte)(statusBase | (ne.Channel & 0x0F));
 200            byte pitch = (byte)Math.Clamp(ne.Pitch, 0, 127); byte velocity = (byte)Math.Clamp(ne.Velocity, 0, 127);
 201            events.Add((ne.Tick, new byte[]{status, pitch, velocity}, true));
 202        }
 203        long endTick = events.Count>0 ? events.Max(e=>e.tick) : 0;
 204        events.Add((endTick, new byte[]{0xFF,0x2F,0x00}, false));
 205        events.Sort((a,b)=> a.tick.CompareTo(b.tick));
 206        var ms = new MemoryStream();
 207        long prev=0; byte? lastStatus=null; foreach (var ev in events){ long d=ev.tick-prev; WriteVarLen(ms,d); prev=ev.
 208        var data = ms.ToArray();
 209        var chunk = new MemoryStream();
 210        using var bw = new BinaryWriter(chunk, System.Text.Encoding.ASCII, leaveOpen:true);
 211        bw.Write(new byte[]{(byte)'M',(byte)'T',(byte)'r',(byte)'k'});
 212        bw.Write(BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(data.Length)));
 213        bw.Write(data);
 214        return chunk.ToArray();
 215    }
 216
 217    private static byte[] BuildTextMeta(byte type, string text)
 218    {
 219        var bytes = System.Text.Encoding.UTF8.GetBytes(text ?? "");
 220        var ms = new MemoryStream();
 221        ms.WriteByte(0xFF); ms.WriteByte(type); WriteVarLen(ms, bytes.Length); ms.Write(bytes,0,bytes.Length);
 222        return ms.ToArray();
 223    }
 224
 225    private static void WriteVarLen(Stream s, long value)
 226    {
 227        // 最大 4 バイト想定
 228        uint buffer = (uint)value & 0x7F;
 229        while ((value >>= 7) > 0)
 230        {
 231            buffer <<= 8;
 232            buffer |= (uint)(((uint)value & 0x7F) | 0x80);
 233        }
 234        while (true)
 235        {
 236            s.WriteByte((byte)buffer);
 237            if ((buffer & 0x80) != 0) buffer >>= 8; else break;
 238        }
 239    }
 240    private static void RemoveMetaInternal(MidiTrack track, HashSet<Type> types)
 241    {
 242        var keep = track.MetaEvents.Where(m=>!types.Contains(m.GetType())).ToList();
 243        var field = typeof(MidiTrack).GetField("_meta", System.Reflection.BindingFlags.NonPublic|System.Reflection.Bindi
 244        if (field!=null && field.GetValue(track) is List<MetaEvent> list)
 245        {
 246            list.Clear(); list.AddRange(keep);
 247        }
 248    }
 249}
 250
 251/// <summary>コンダクタートラック生成 / 集約ヘルパ。</summary>
 252public static class MidiConductorHelper
 253{
 254    public static (MidiTrack conductor, IList<MidiTrack> others) ExtractConductor(IReadOnlyList<MidiTrack> tracks)
 255    {
 7256        if (tracks.Count==0) throw new ArgumentException("tracks empty");
 3257        var conductor = new MidiTrack();
 21258        foreach (var m in tracks[0].MetaEvents) conductor.AddMeta(m);
 3259        return (conductor, tracks.Skip(1).ToList());
 260    }
 261    public static MidiTrack ConsolidateAndStrip(IList<MidiTrack> tracks)
 262    {
 9263        if (tracks.Count==0) throw new ArgumentException("tracks empty");
 264        // consolidate by calling writer helper indirectly not exposed; replicate logic here
 5265        var conductor = new MidiTrack();
 5266        var types = new HashSet<Type>{ typeof(TempoEvent), typeof(TimeSignatureEvent), typeof(KeySignatureEvent) };
 28267        foreach (var t in tracks)
 268        {
 20269            var moving = t.MetaEvents.Where(m=>types.Contains(m.GetType())).ToList();
 38270            foreach (var m in moving)
 24271                if (!conductor.MetaEvents.Any(x=>x.GetType()==m.GetType() && x.Tick==m.Tick)) conductor.AddMeta(m);
 272            // strip
 9273            var field = typeof(MidiTrack).GetField("_meta", System.Reflection.BindingFlags.NonPublic|System.Reflection.B
 9274            if (field!=null && field.GetValue(t) is List<MetaEvent> list)
 275            {
 20276                list.RemoveAll(m=>types.Contains(m.GetType()));
 277            }
 278        }
 5279        return conductor;
 280    }
 281}