2025最强MIDI文件处理指南:从读取到创建的完整工作流

2025最强MIDI文件处理指南:从读取到创建的完整工作流

【免费下载链接】NAudio Audio and MIDI library for .NET 【免费下载链接】NAudio 项目地址: https://gitcode.com/gh_mirrors/na/NAudio

引言:告别MIDI处理的9大痛点

你是否还在为以下MIDI(Musical Instrument Digital Interface,音乐设备数字接口)处理问题而困扰?

  • 无法准确解析复杂MIDI文件结构,导致音轨丢失或事件错乱
  • 创建MIDI文件时不知如何组织多音轨事件,时序控制困难
  • 处理大型MIDI文件时性能低下,事件排序耗时严重
  • 不理解MIDI事件格式,无法正确生成Note On/Off等关键事件
  • 多音轨同步问题导致播放时节奏混乱
  • 元数据处理不当,无法正确设置 tempo(速度)和调号
  • 导出文件时格式错误,导致在专业音乐软件中无法打开
  • 对MIDI 1.0规范理解不深入,兼容性问题频发
  • 缺乏完整的工作流示例,从理论到实践困难重重

本文将通过12个实战案例7个完整代码示例,系统讲解如何使用NAudio库处理MIDI文件,从基础读取到高级创建,构建专业级MIDI文件处理能力。

一、MIDI文件核心概念与NAudio架构

1.1 MIDI文件格式解析

MIDI文件本质是一种事件序列,而非音频数据。NAudio支持三种主要MIDI文件格式:

格式类型描述适用场景最大音轨数
Type 0单音轨格式,所有事件在一个音轨中简单旋律、铃声、单个乐器序列1
Type 1多音轨同步格式,所有音轨同步播放歌曲创作、多乐器编排任意
Type 2多音轨异步格式,音轨独立播放多声部独立作品(罕见)任意

技术细节:MIDI文件以"chunk(块)"组织,包含一个"MThd"头块和多个"MTrk"音轨块。头块定义文件格式、音轨数和时间分辨率(ticks per quarter note)。

1.2 NAudio MIDI处理核心类架构

mermaid

1.3 关键时间单位解析

MIDI使用两种时间表示方式:

  • Delta Time:事件之间的相对时间,存储为可变长度整数(VarInt)
  • Absolute Time:从文件开始的绝对时间,NAudio自动计算和维护

时间分辨率由DeltaTicksPerQuarterNote(每四分音符的 ticks 数)决定,常见值为96、120、240或480。例如,在96 ticks/四分音符的设置下:

  • 八分音符 = 48 ticks
  • 十六分音符 = 24 ticks
  • 附点四分音符 = 144 ticks

二、MIDI文件读取与解析实战

2.1 基础读取:获取MIDI文件信息

using NAudio.Midi;

// 读取MIDI文件
var midiFile = new MidiFile("input.mid");

// 输出基本信息
Console.WriteLine($"MIDI File Information:");
Console.WriteLine($"Format: {midiFile.FileFormat}");
Console.WriteLine($"Tracks: {midiFile.Tracks}");
Console.WriteLine($"Resolution: {midiFile.DeltaTicksPerQuarterNote} ticks/quarter note");

// 遍历所有音轨事件
for (int track = 0; track < midiFile.Tracks; track++)
{
    Console.WriteLine($"\nTrack {track + 1} events:");
    foreach (var midiEvent in midiFile.Events[track])
    {
        Console.WriteLine($"{midiEvent.AbsoluteTime}ms: {midiEvent}");
    }
}

性能提示:对于大型MIDI文件(>10000事件),使用strictChecking: false构造函数参数可跳过严格验证,提高加载速度。

2.2 事件类型识别与过滤

MIDI事件类型繁多,NAudio将其封装为不同的类。以下代码演示如何识别和处理常见事件类型:

foreach (var midiEvent in midiFile.Events[0])
{
    switch (midiEvent.CommandCode)
    {
        case MidiCommandCode.NoteOn:
            var noteOn = (NoteOnEvent)midiEvent;
            Console.WriteLine($"Note On: Channel {noteOn.Channel}, Note {noteOn.NoteNumber} " +
                             $"({MidiEvent.NoteNumberToFrequency(noteOn.NoteNumber):F2}Hz), " +
                             $"Velocity {noteOn.Velocity}");
            break;
            
        case MidiCommandCode.ControlChange:
            var cc = (ControlChangeEvent)midiEvent;
            Console.WriteLine($"Control Change: Channel {cc.Channel}, " +
                             $"Controller {cc.Controller} ({cc.ControllerToString()}), " +
                             $"Value {cc.ControlValue}");
            break;
            
        case MidiCommandCode.MetaEvent:
            var meta = (MetaEvent)midiEvent;
            if (meta.MetaEventType == MetaEventType.Tempo)
            {
                int bpm = 60000000 / meta.AbsoluteTime; // 计算BPM
                Console.WriteLine($"Tempo Change: {bpm} BPM");
            }
            break;
    }
}

技术细节:Note On事件速度为0时,在MIDI规范中等同于Note Off事件,NAudio通过MidiEvent.IsNoteOff()方法统一识别。

2.3 实战案例:提取MIDI文件信息

以下代码实现一个完整的MIDI文件分析器,提取关键音乐信息:

public class MidiAnalyzer
{
    public static void Analyze(string filePath)
    {
        using (var midiFile = new MidiFile(filePath, strictChecking: false))
        {
            Console.WriteLine($"MIDI File Analysis: {Path.GetFileName(filePath)}");
            Console.WriteLine("=".PadRight(50, '='));
            Console.WriteLine($"Format Type: {midiFile.FileFormat}");
            Console.WriteLine($"Track Count: {midiFile.Tracks}");
            Console.WriteLine($"Resolution: {midiFile.DeltaTicksPerQuarterNote} ticks/quarter note");
            
            for (int track = 0; track < midiFile.Tracks; track++)
            {
                var trackEvents = midiFile.Events[track];
                var stats = new TrackStats();
                
                foreach (var evt in trackEvents)
                {
                    stats.TotalEvents++;
                    
                    if (evt is NoteEvent noteEvent)
                    {
                        if (MidiEvent.IsNoteOn(noteEvent)) stats.NoteOnCount++;
                        if (MidiEvent.IsNoteOff(noteEvent)) stats.NoteOffCount++;
                        
                        stats.MinNote = Math.Min(stats.MinNote, noteEvent.NoteNumber);
                        stats.MaxNote = Math.Max(stats.MaxNote, noteEvent.NoteNumber);
                    }
                    else if (evt is MetaEvent metaEvent)
                    {
                        switch (metaEvent.MetaEventType)
                        {
                            case MetaEventType.Text:
                            case MetaEventType.TrackName:
                                stats.TrackName = metaEvent.Text;
                                break;
                            case MetaEventType.Tempo:
                                stats.TempoChanges++;
                                break;
                            case MetaEventType.TimeSignature:
                                stats.TimeSignatures++;
                                break;
                        }
                    }
                }
                
                Console.WriteLine($"\nTrack {track + 1}: {stats.TrackName ?? "Unnamed"}");
                Console.WriteLine($"  Events: {stats.TotalEvents}");
                Console.WriteLine($"  Notes: {stats.NoteOnCount} notes, Range: {stats.MinNote}~{stats.MaxNote}");
                Console.WriteLine($"  Tempo Changes: {stats.TempoChanges}, Time Signatures: {stats.TimeSignatures}");
            }
        }
    }
    
    private class TrackStats
    {
        public string TrackName { get; set; }
        public int TotalEvents { get; set; }
        public int NoteOnCount { get; set; }
        public int NoteOffCount { get; set; }
        public int MinNote { get; set; } = 127;
        public int MaxNote { get; set; } = 0;
        public int TempoChanges { get; set; }
        public int TimeSignatures { get; set; }
    }
}

// 使用方法
MidiAnalyzer.Analyze("example.mid");

二、MIDI事件处理高级技术

2.1 事件时间转换与量化

音乐时间与MIDI ticks转换是核心技能:

public static class MidiTimeConverter
{
    // 将小节-拍-滴答转换为绝对ticks
    public static long MeasureBeatToTicks(int measure, int beat, int tick, 
                                         int beatsPerMeasure, int ticksPerBeat, 
                                         long startTick = 0)
    {
        return startTick + (measure - 1) * beatsPerMeasure * ticksPerBeat + 
                          (beat - 1) * ticksPerBeat + tick;
    }
    
    // 将ticks转换为毫秒(基于速度)
    public static double TicksToMilliseconds(long ticks, int ticksPerQuarterNote, int bpm)
    {
        double quarterNoteMs = 60000.0 / bpm;  // 四分音符毫秒数
        double tickMs = quarterNoteMs / ticksPerQuarterNote;  // 每个tick的毫秒数
        return ticks * tickMs;
    }
    
    // 量化事件时间(修正演奏误差)
    public static void QuantizeEvents(IList<MidiEvent> events, int quantizationValue, 
                                     int ticksPerQuarterNote)
    {
        foreach (var evt in events)
        {
            if (evt.CommandCode != MidiCommandCode.MetaEvent)
            {
                long quantizedTime = (evt.AbsoluteTime / quantizationValue) * quantizationValue;
                evt.AbsoluteTime = quantizedTime;
            }
        }
    }
}

应用示例:将事件量化为八分音符(假设ticksPerQuarterNote=96): MidiTimeConverter.QuantizeEvents(trackEvents, 48, 96);

2.2 多音轨事件同步与合并

处理Type 1 MIDI文件时,保持音轨同步至关重要:

public static MidiEventCollection MergeMidiFiles(params string[] filePaths)
{
    if (filePaths.Length == 0) return null;
    
    // 加载第一个文件作为基础
    var baseFile = new MidiFile(filePaths[0]);
    var mergedEvents = new MidiEventCollection(1, baseFile.DeltaTicksPerQuarterNote);
    
    // 添加基础文件的所有音轨
    for (int i = 0; i < baseFile.Tracks; i++)
    {
        mergedEvents.AddTrack(baseFile.Events[i]);
    }
    
    // 合并其他文件
    foreach (var filePath in filePaths.Skip(1))
    {
        var midiFile = new MidiFile(filePath);
        
        // 确保时间分辨率一致
        if (midiFile.DeltaTicksPerQuarterNote != mergedEvents.DeltaTicksPerQuarterNote)
        {
            throw new InvalidOperationException("Cannot merge MIDI files with different resolutions");
        }
        
        // 添加所有音轨,偏移绝对时间以避免重叠
        long timeOffset = GetMaxAbsoluteTime(mergedEvents) + mergedEvents.DeltaTicksPerQuarterNote * 4;
        
        for (int i = 0; i < midiFile.Tracks; i++)
        {
            var newTrack = mergedEvents.AddTrack();
            foreach (var evt in midiFile.Events[i])
            {
                var clonedEvent = evt.Clone();
                clonedEvent.AbsoluteTime += timeOffset;
                newTrack.Add(clonedEvent);
            }
        }
    }
    
    mergedEvents.PrepareForExport();
    return mergedEvents;
}

private static long GetMaxAbsoluteTime(MidiEventCollection events)
{
    long maxTime = 0;
    foreach (var track in events)
    {
        if (track.Count == 0) continue;
        maxTime = Math.Max(maxTime, track[track.Count - 1].AbsoluteTime);
    }
    return maxTime;
}

三、MIDI文件创建完整工作流

3.1 从零开始创建MIDI文件

以下是创建包含多音轨、多种事件的完整MIDI文件示例:

public static void CreateSampleMidiFile(string outputPath)
{
    // 创建事件集合(Type 1格式,96 ticks/四分音符)
    var events = new MidiEventCollection(1, 96);
    
    // 添加速度和元数据音轨(音轨0)
    var metaTrack = events.AddTrack();
    AddMetaEvents(metaTrack, events.DeltaTicksPerQuarterNote);
    
    // 添加钢琴音轨(音轨1)
    var pianoTrack = events.AddTrack();
    AddPianoMelody(pianoTrack, events.DeltaTicksPerQuarterNote);
    
    // 添加贝司音轨(音轨2)
    var bassTrack = events.AddTrack();
    AddBassLine(bassTrack, events.DeltaTicksPerQuarterNote);
    
    // 准备导出(排序事件、添加结束事件)
    events.PrepareForExport();
    
    // 导出为MIDI文件
    MidiFile.Export(outputPath, events);
}

private static void AddMetaEvents(IList<MidiEvent> track, int ticksPerQuarterNote)
{
    long absoluteTime = 0;
    
    // 添加轨道名称
    track.Add(new TextEvent("Metadata Track", MetaEventType.TrackName, absoluteTime));
    
    // 设置速度(120 BPM)
    // TempoEvent的构造函数参数是微秒每四分音符
    int microsecondsPerQuarterNote = 60000000 / 120; // 120 BPM
    track.Add(new TempoEvent(microsecondsPerQuarterNote, absoluteTime));
    
    // 设置拍号(4/4拍)
    track.Add(new TimeSignatureEvent(4, 4, 24, 8, absoluteTime));
    
    // 设置调号(C大调)
    track.Add(new KeySignatureEvent(0, false, absoluteTime));
}

private static void AddPianoMelody(IList<MidiEvent> track, int ticksPerQuarterNote)
{
    // 添加轨道名称
    track.Add(new TextEvent("Piano", MetaEventType.TrackName, 0));
    
    // 设置乐器(Acoustic Grand Piano)
    track.Add(new PatchChangeEvent(0, 1, 0)); // 通道1,音色0
    
    long time = 0;
    int[] melodyNotes = { 60, 62, 64, 65, 67, 69, 71, 72 }; // C大调音阶
    
    // 添加上行音阶
    foreach (var note in melodyNotes)
    {
        // 添加Note On事件(力度80)
        track.Add(new NoteOnEvent(time, 1, note, 80, ticksPerQuarterNote / 2));
        
        // 移动到下一个八分音符
        time += ticksPerQuarterNote / 2;
    }
    
    // 添加下行音阶(带附点四分音符)
    for (int i = melodyNotes.Length - 1; i >= 0; i--)
    {
        int duration = (i % 2 == 0) ? ticksPerQuarterNote * 3 / 2 : ticksPerQuarterNote / 2;
        track.Add(new NoteOnEvent(time, 1, melodyNotes[i], 80, duration));
        time += duration;
    }
    
    // 添加和弦(C大三和弦)
    track.Add(new NoteOnEvent(time, 1, 60, 75, ticksPerQuarterNote * 2)); // C4
    track.Add(new NoteOnEvent(time, 1, 64, 75, ticksPerQuarterNote * 2)); // E4
    track.Add(new NoteOnEvent(time, 1, 67, 75, ticksPerQuarterNote * 2)); // G4
}

private static void AddBassLine(IList<MidiEvent> track, int ticksPerQuarterNote)
{
    // 添加轨道名称
    track.Add(new TextEvent("Bass", MetaEventType.TrackName, 0));
    
    // 设置乐器(Acoustic Bass)
    track.Add(new PatchChangeEvent(0, 2, 33)); // 通道2,音色33
    
    long time = 0;
    int[] bassNotes = { 36, 36, 38, 36, 41, 38, 36, 36 }; // 低音线
    
    foreach (var note in bassNotes)
    {
        track.Add(new NoteOnEvent(time, 2, note, 70, ticksPerQuarterNote));
        time += ticksPerQuarterNote; // 每个音符持续一个四分音符
    }
}

3.2 MIDI文件创建流程图

mermaid

3.3 高级应用:程序生成音乐

以下示例生成一个随机但和谐的音乐片段:

public static void GenerateRandomMusic(string outputPath, int measureCount = 16)
{
    var rng = new Random();
    var events = new MidiEventCollection(1, 96);
    
    // 添加元数据
    var metaTrack = events.AddTrack();
    metaTrack.Add(new TempoEvent(60000000 / 110, 0)); // 110 BPM
    metaTrack.Add(new TimeSignatureEvent(4, 4, 24, 8, 0));
    metaTrack.Add(new KeySignatureEvent(0, false, 0));
    
    // 创建和弦进行(I-IV-V-vi进行)
    int[] chordProgression = { 0, 5, 7, 3 }; // C, F, G, Am
    
    // 添加钢琴和弦轨
    var pianoTrack = events.AddTrack();
    pianoTrack.Add(new PatchChangeEvent(0, 1, 0)); // 钢琴音色
    
    // 添加旋律轨
    var melodyTrack = events.AddTrack();
    melodyTrack.Add(new PatchChangeEvent(0, 2, 5)); // 小提琴音色
    
    long time = 0;
    int ticksPerMeasure = 96 * 4; // 4/4拍,每小节4个四分音符
    
    for (int measure = 0; measure < measureCount; measure++)
    {
        // 当前和弦(C大调音阶)
        int chordRoot = chordProgression[measure % chordProgression.Length];
        int[] chordNotes = GetChordNotes(chordRoot);
        
        // 添加和弦
        AddChord(pianoTrack, time, chordNotes, 1, 96 * 4); // 持续整个小节
        
        // 添加旋律
        AddRandomMelody(melodyTrack, time, chordNotes, rng);
        
        time += ticksPerMeasure;
    }
    
    events.PrepareForExport();
    MidiFile.Export(outputPath, events);
}

private static int[] GetChordNotes(int root)
{
    // 返回C大调音阶上的三和弦
    // 根音 + 三音 + 五音
    return new[] { root, root + 4, root + 7 };
}

private static void AddChord(IList<MidiEvent> track, long time, int[] notes, 
                            int channel, int duration)
{
    foreach (var note in notes)
    {
        // 将和弦音高移至中低音区
        int midiNote = note + 60; // C4及以上
        track.Add(new NoteOnEvent(time, channel, midiNote, 75, duration));
    }
}

private static void AddRandomMelody(IList<MidiEvent> track, long time, 
                                   int[] chordNotes, Random rng)
{
    int[] rhythmPattern = { 24, 24, 24, 24, 48, 48 }; // 十六分音符和八分音符组合
    int channel = 2;
    
    foreach (var duration in rhythmPattern)
    {
        // 从和弦音中随机选择一个音
        int noteIndex = rng.Next(chordNotes.Length);
        int octaveShift = rng.Next(2) == 0 ? 12 : 24; // 高八度或两个八度
        int midiNote = chordNotes[noteIndex] + 60 + octaveShift;
        
        // 随机力度
        int velocity = rng.Next(60, 90);
        
        track.Add(new NoteOnEvent(time, channel, midiNote, velocity, duration));
        time += duration;
    }
}

四、性能优化与高级应用

4.1 大型MIDI文件处理优化

处理包含数千音轨和数百万事件的大型MIDI文件时,需注意性能优化:

public static class MidiPerformanceOptimizations
{
    // 高效合并多个MIDI文件
    public static MidiEventCollection MergeLargeMidiFiles(IEnumerable<string> filePaths)
    {
        // 1. 首先收集所有文件的基本信息,避免同时打开所有文件
        var fileInfos = new List<(int Resolution, long MaxTime)>();
        int commonResolution = -1;
        
        foreach (var path in filePaths)
        {
            using (var tempFile = new MidiFile(path))
            {
                if (commonResolution == -1)
                {
                    commonResolution = tempFile.DeltaTicksPerQuarterNote;
                }
                else if (tempFile.DeltaTicksPerQuarterNote != commonResolution)
                {
                    throw new InvalidOperationException("All files must have the same resolution");
                }
                
                long maxTime = 0;
                for (int i = 0; i < tempFile.Tracks; i++)
                {
                    if (tempFile.Events[i].Count > 0)
                    {
                        maxTime = Math.Max(maxTime, tempFile.Events[i].Last().AbsoluteTime);
                    }
                }
                
                fileInfos.Add((commonResolution, maxTime));
            }
        }
        
        // 2. 创建合并后的事件集合
        var mergedEvents = new MidiEventCollection(1, commonResolution);
        long currentOffset = 0;
        
        // 3. 逐个加载并合并文件,释放资源后再加载下一个
        int fileIndex = 0;
        foreach (var path in filePaths)
        {
            using (var midiFile = new MidiFile(path))
            {
                for (int i = 0; i < midiFile.Tracks; i++)
                {
                    var newTrack = mergedEvents.AddTrack();
                    foreach (var evt in midiFile.Events[i])
                    {
                        var clonedEvent = evt.Clone();
                        clonedEvent.AbsoluteTime += currentOffset;
                        newTrack.Add(clonedEvent);
                    }
                }
            }
            
            // 更新偏移量
            currentOffset += fileInfos[fileIndex].MaxTime + commonResolution * 4; // 添加4拍间隔
            fileIndex++;
        }
        
        mergedEvents.PrepareForExport();
        return mergedEvents;
    }
    
    // 事件过滤与压缩
    public static void FilterAndCompressEvents(MidiEventCollection events, 
                                             int minVelocity = 20, 
                                             int quantization = 12)
    {
        foreach (var track in events)
        {
            // 创建新事件列表,避免修改正在迭代的集合
            var newEvents = new List<MidiEvent>();
            
            foreach (var evt in track)
            {
                // 过滤弱音事件
                if (evt is NoteEvent noteEvent && noteEvent.Velocity < minVelocity)
                {
                    continue;
                }
                
                // 量化非元事件
                if (evt.CommandCode != MidiCommandCode.MetaEvent)
                {
                    evt.AbsoluteTime = (evt.AbsoluteTime / quantization) * quantization;
                }
                
                newEvents.Add(evt);
            }
            
            // 替换原事件列表
            track.Clear();
            foreach (var evt in newEvents)
            {
                track.Add(evt);
            }
        }
    }
}

4.2 MIDI文件与音乐理论结合应用

使用NAudio实现音乐理论功能:

public static class MidiMusicTheory
{
    // 计算音程
    public static int CalculateInterval(int note1, int note2)
    {
        int interval = Math.Abs(note1 - note2) % 12;
        return interval switch
        {
            0 => 1,   // 同度
            1 => 2,   // 小二度
            2 => 3,   // 大二度
            3 => 4,   // 小三度
            4 => 5,   // 大三度
            5 => 6,   // 纯四度
            6 => 7,   // 增四度/减五度
            7 => 8,   // 纯五度
            8 => 9,   // 小六度
            9 => 10,  // 大六度
            10 => 11, // 小七度
            11 => 12, // 大七度
            _ => 1    // 不可能发生
        };
    }
    
    // 分析和弦进行
    public static List<string> AnalyzeChordProgression(MidiFile midiFile)
    {
        var chordProgression = new List<string>();
        var key = DetermineKey(midiFile);
        
        // 实现复杂的和弦识别逻辑...
        
        return chordProgression;
    }
    
    // 确定MIDI文件的调式
    public static KeySignature DetermineKey(MidiFile midiFile)
    {
        // 实现调式识别算法...
        return new KeySignature(0, false); // 默认C大调
    }
    
    // 和弦结构类
    public class KeySignature
    {
        public int SharpsFlats { get; }
        public bool IsMinor { get; }
        
        public KeySignature(int sharpsFlats, bool isMinor)
        {
            SharpsFlats = sharpsFlats;
            IsMinor = isMinor;
        }
    }
}

五、实战案例:MIDI文件批量处理器

以下是一个完整的MIDI文件批量处理工具,支持转格式、变调、变速等功能:

public class MidiBatchProcessor
{
    public enum ProcessingAction
    {
        ConvertToType0,
        Transpose,
        ChangeTempo,
        ExtractPianoTracks,
        Quantize
    }
    
    public class ProcessingOptions
    {
        public ProcessingAction Action { get; set; }
        public int TransposeSemitones { get; set; } = 0;
        public double TempoMultiplier { get; set; } = 1.0;
        public int QuantizationValue { get; set; } = 24; // 十六分音符
    }
    
    public void ProcessFiles(IEnumerable<string> inputPaths, string outputDirectory, 
                           ProcessingOptions options)
    {
        foreach (var inputPath in inputPaths)
        {
            try
            {
                ProcessSingleFile(inputPath, outputDirectory, options);
                Console.WriteLine($"Processed: {Path.GetFileName(inputPath)}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error processing {Path.GetFileName(inputPath)}: {ex.Message}");
            }
        }
    }
    
    private void ProcessSingleFile(string inputPath, string outputDirectory, 
                                 ProcessingOptions options)
    {
        using (var midiFile = new MidiFile(inputPath))
        {
            // 创建新的事件集合
            var newEvents = new MidiEventCollection(
                options.Action == ProcessingOptions.ProcessingAction.ConvertToType0 ? 0 : midiFile.FileFormat,
                midiFile.DeltaTicksPerQuarterNote);
            
            // 复制并处理事件
            for (int track = 0; track < midiFile.Tracks; track++)
            {
                var originalTrack = midiFile.Events[track];
                
                // 根据选项决定是否处理此音轨
                if (options.Action == ProcessingOptions.ProcessingAction.ExtractPianoTracks)
                {
                    if (!IsPianoTrack(originalTrack))
                        continue;
                }
                
                var newTrack = newEvents.AddTrack();
                
                foreach (var evt in originalTrack)
                {
                    var processedEvent = ProcessEvent(evt, options, midiFile.DeltaTicksPerQuarterNote);
                    if (processedEvent != null)
                    {
                        newTrack.Add(processedEvent);
                    }
                }
            }
            
            newEvents.PrepareForExport();
            
            // 创建输出路径
            string fileName = Path.GetFileNameWithoutExtension(inputPath);
            string outputPath = Path.Combine(outputDirectory, $"{fileName}_processed.mid");
            
            // 导出处理后的文件
            MidiFile.Export(outputPath, newEvents);
        }
    }
    
    private MidiEvent ProcessEvent(MidiEvent evt, ProcessingOptions options, int ticksPerQuarterNote)
    {
        var clonedEvent = evt.Clone();
        
        // 变调处理
        if (options.Action == ProcessingOptions.ProcessingAction.Transpose && 
            options.TransposeSemitones != 0 && evt is NoteEvent noteEvent)
        {
            int newNoteNumber = noteEvent.NoteNumber + options.TransposeSemitones;
            if (newNoteNumber >= 0 && newNoteNumber <= 127)
            {
                noteEvent.NoteNumber = newNoteNumber;
            }
        }
        
        // 变速处理
        if (options.Action == ProcessingOptions.ProcessingAction.ChangeTempo && 
            Math.Abs(options.TempoMultiplier - 1.0) > 0.001)
        {
            if (evt is TempoEvent tempoEvent)
            {
                // 调整速度事件
                int newTempo = (int)(tempoEvent.MicrosecondsPerQuarterNote / options.TempoMultiplier);
                return new TempoEvent(newTempo, evt.AbsoluteTime);
            }
            else
            {
                // 调整所有事件的时间
                clonedEvent.AbsoluteTime = (long)(evt.AbsoluteTime / options.TempoMultiplier);
            }
        }
        
        // 量化处理
        if (options.Action == ProcessingOptions.ProcessingAction.Quantize && 
            evt.CommandCode != MidiCommandCode.MetaEvent)
        {
            clonedEvent.AbsoluteTime = (clonedEvent.AbsoluteTime / options.QuantizationValue) * 
                                      options.QuantizationValue;
        }
        
        return clonedEvent;
    }
    
    private bool IsPianoTrack(IList<MidiEvent> track)
    {
        // 判断是否为钢琴音轨(简单实现)
        foreach (var evt in track)
        {
            if (evt is PatchChangeEvent patchEvent)
            {
                // 0-8通常是钢琴类乐器
                return patchEvent.Patch >= 0 && patchEvent.Patch <= 8;
            }
        }
        return false;
    }
}

// 使用示例
var processor = new MidiBatchProcessor();
var options = new MidiBatchProcessor.ProcessingOptions
{
    Action = MidiBatchProcessor.ProcessingAction.Transpose,
    TransposeSemitones = 2 // 升高2个半音
};

processor.ProcessFiles(new[] { "song1.mid", "song2.mid" }, "processed_output", options);

结语:掌握MIDI文件处理的下一步

通过本文的学习,您已经掌握了使用NAudio处理MIDI文件的核心技术。要进一步提升技能,建议:

  1. 深入学习MIDI 1.0规范:理解所有事件类型和元数据格式
  2. 研究音乐理论:了解和弦结构、调式和音乐形式对MIDI事件组织的影响
  3. 探索高级应用:如MIDI到音频的合成、实时MIDI处理、音乐信息检索等
  4. 分析复杂MIDI文件:研究专业音乐软件生成的MIDI文件结构和事件组织

NAudio库为.NET开发者提供了强大的MIDI处理能力,结合音乐理论知识,可以构建从简单MIDI播放器到专业音乐创作软件的各种应用。

扩展资源

  • NAudio官方文档:https://github.com/naudio/NAudio
  • MIDI 1.0规范:https://www.midi.org/specifications-old/item/the-midi-1-0-specification
  • Music Theory for Programmers:探索音乐理论与代码结合的高级主题

希望本文能帮助您构建专业级MIDI文件处理应用,释放音乐创造力!

如果您觉得本文有价值,请点赞、收藏并关注作者,获取更多音频处理技术分享。下一篇将探讨"NAudio实时音频流与MIDI合成技术",敬请期待!

【免费下载链接】NAudio Audio and MIDI library for .NET 【免费下载链接】NAudio 项目地址: https://gitcode.com/gh_mirrors/na/NAudio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值