From 6de2c21fcccaf670cf4718d4a58b90ea52059774 Mon Sep 17 00:00:00 2001 From: Blackened Date: Wed, 14 Jan 2026 15:38:33 -0500 Subject: [PATCH] Fixed Special Character being mangled by the reading/writing --- MidiFileReader.cs | 59 ++++++++-- MidiFileWriter.cs | 269 +++++++++++++++++++++++++--------------------- 2 files changed, 196 insertions(+), 132 deletions(-) diff --git a/MidiFileReader.cs b/MidiFileReader.cs index 6276e98..b407155 100644 --- a/MidiFileReader.cs +++ b/MidiFileReader.cs @@ -9,7 +9,29 @@ namespace MidiCS { public class MidiFileReader { - public static MidiFile FromBytes(byte[] bytes) + private static string DecodeText(byte[] bytes) + { + // Try strict UTF-8 first (throws on invalid sequences). + try + { + var utf8 = new System.Text.UTF8Encoding(false, true); + return utf8.GetString(bytes); + } + catch (System.ArgumentException) + { + // Invalid UTF-8 -> fall back to Windows-1252 (typical single-byte MIDI text) + try + { + return System.Text.Encoding.GetEncoding(1252).GetString(bytes); + } + catch + { + // Last resort: 28591 (Latin1) to preserve raw byte->codepoint mapping + return System.Text.Encoding.GetEncoding(28591).GetString(bytes); + } + } + } + public static MidiFile FromBytes(byte[] bytes) { using (var s = new MemoryStream(bytes)) return FromStream(s); @@ -122,19 +144,40 @@ public static IMidiMessage readMessage(Stream s) throw new InvalidDataException("Sequence number events must have 2 bytes of data; this one has " + length); return new SequenceNumber(deltaTime, s.ReadUInt16BE()); case MetaEventType.TextEvent: - return new TextEvent(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new TextEvent(deltaTime, DecodeText(data)); + } case MetaEventType.CopyrightNotice: - return new CopyrightNotice(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new CopyrightNotice(deltaTime, DecodeText(data)); + } case MetaEventType.TrackName: - return new TrackName(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new TrackName(deltaTime, DecodeText(data)); + } case MetaEventType.InstrumentName: - return new InstrumentName(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new InstrumentName(deltaTime, DecodeText(data)); + } case MetaEventType.Lyric: - return new Lyric(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new Lyric(deltaTime, DecodeText(data)); + } case MetaEventType.Marker: - return new Marker(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new Marker(deltaTime, DecodeText(data)); + } case MetaEventType.CuePoint: - return new CuePoint(deltaTime, Encoding.ASCII.GetString(s.ReadBytes(length))); + { + var data = s.ReadBytes(length); + return new CuePoint(deltaTime, DecodeText(data)); + } case MetaEventType.ChannelPrefix: if (length != 1) throw new InvalidDataException("Channel prefix events must have 1 byte of data; this one has " + length); diff --git a/MidiFileWriter.cs b/MidiFileWriter.cs index dc4dac5..c3326b9 100644 --- a/MidiFileWriter.cs +++ b/MidiFileWriter.cs @@ -30,129 +30,150 @@ public static void WriteSMF(MidiFile f, Stream s) } } - static int tick = 0; - static byte running_status = 0; - static void writeMessage(IMidiMessage m, Stream s) - { - s.WriteMidiMultiByte(m.DeltaTime); - switch (m) - { - case IMidiEvent e: - var status = (byte)(e.Channel | (byte)e.Type); - if(status != running_status) - { - s.WriteByte(status); - running_status = status; - } - switch(e) - { - case NoteOffEvent x: - s.WriteByte(x.Key); - s.WriteByte(x.Velocity); - break; - case NoteOnEvent x: - s.WriteByte(x.Key); - s.WriteByte(x.Velocity); - break; - case NotePressureEvent x: - s.WriteByte(x.Key); - s.WriteByte(x.Pressure); - break; - case ControllerEvent x: - s.WriteByte(x.Controller); - s.WriteByte(x.Value); - break; - case ProgramChgEvent x: - s.WriteByte(x.Program); - break; - case ChannelPressureEvent x: - s.WriteByte(x.Pressure); - break; - case PitchBendEvent x: - s.WriteBE(x.Bend); - break; - } - break; - case MetaEvent e: - // cancel running status - running_status = 0; - s.WriteByte(0xFF); - s.WriteByte((byte)e.MetaType); - switch(e) - { - case SequenceNumber x: - s.WriteMidiMultiByte(2); - s.WriteBE(x.Number); - break; - case TextEvent x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case CopyrightNotice x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case TrackName x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case InstrumentName x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case Lyric x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case Marker x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case CuePoint x: - s.WriteMidiMultiByte((uint)x.Text.Length); - s.Write(System.Text.Encoding.ASCII.GetBytes(x.Text), 0, x.Text.Length); - break; - case ChannelPrefix x: - s.WriteMidiMultiByte(1); - s.WriteByte(x.Channel); - break; - case EndOfTrackEvent x: - s.WriteMidiMultiByte(0); - break; - case TempoEvent x: - s.WriteMidiMultiByte(3); - s.WriteUInt24BE(x.MicrosPerQn); - break; - case SmtpeOffset x: - s.WriteMidiMultiByte(5); - var sdata = new byte[5] { x.Hours, x.Minutes, x.Seconds, x.Frames, x.FrameHundredths }; - s.Write(sdata, 0, 5); - break; - case TimeSignature x: - s.WriteMidiMultiByte(4); - var tdata = new byte[4] { x.Numerator, x.Denominator, x.ClocksPerTick, x.ThirtySecondNotesPer24Clocks }; - s.Write(tdata, 0, 4); - break; - case KeySignature x: - s.WriteMidiMultiByte(2); - s.WriteByte(x.Sharps); - s.WriteByte(x.Tonality); - break; - case SequencerSpecificEvent x: - s.WriteMidiMultiByte((uint)x.Data.Length); - s.Write(x.Data, 0, x.Data.Length); - break; - } - break; - case SysexEvent e: - // cancel running status - running_status = 0; - s.WriteByte(0xF0); - s.WriteMidiMultiByte((uint)e.Data.Length); - s.Write(e.Data, 0, e.Data.Length); - break; - } - } + static int tick = 0; + static byte running_status = 0; + static void writeMessage(IMidiMessage m, Stream s) + { + s.WriteMidiMultiByte(m.DeltaTime); + switch (m) + { + case IMidiEvent e: + var status = (byte)(e.Channel | (byte)e.Type); + if (status != running_status) + { + s.WriteByte(status); + running_status = status; + } + switch (e) + { + case NoteOffEvent x: + s.WriteByte(x.Key); + s.WriteByte(x.Velocity); + break; + case NoteOnEvent x: + s.WriteByte(x.Key); + s.WriteByte(x.Velocity); + break; + case NotePressureEvent x: + s.WriteByte(x.Key); + s.WriteByte(x.Pressure); + break; + case ControllerEvent x: + s.WriteByte(x.Controller); + s.WriteByte(x.Value); + break; + case ProgramChgEvent x: + s.WriteByte(x.Program); + break; + case ChannelPressureEvent x: + s.WriteByte(x.Pressure); + break; + case PitchBendEvent x: + s.WriteBE(x.Bend); + break; + } + break; + case MetaEvent e: + // cancel running status + running_status = 0; + s.WriteByte(0xFF); + s.WriteByte((byte)e.MetaType); + switch (e) + { + case SequenceNumber x: + s.WriteMidiMultiByte(2); + s.WriteBE(x.Number); + break; + case TextEvent x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case CopyrightNotice x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case TrackName x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case InstrumentName x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case Lyric x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case Marker x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case CuePoint x: + { + var bytes = System.Text.Encoding.UTF8.GetBytes(x.Text ?? ""); + s.WriteMidiMultiByte((uint)bytes.Length); + s.Write(bytes, 0, bytes.Length); + break; + } + case ChannelPrefix x: + s.WriteMidiMultiByte(1); + s.WriteByte(x.Channel); + break; + case EndOfTrackEvent x: + s.WriteMidiMultiByte(0); + break; + case TempoEvent x: + s.WriteMidiMultiByte(3); + s.WriteUInt24BE(x.MicrosPerQn); + break; + case SmtpeOffset x: + s.WriteMidiMultiByte(5); + var sdata = new byte[5] { x.Hours, x.Minutes, x.Seconds, x.Frames, x.FrameHundredths }; + s.Write(sdata, 0, 5); + break; + case TimeSignature x: + s.WriteMidiMultiByte(4); + var tdata = new byte[4] { x.Numerator, x.Denominator, x.ClocksPerTick, x.ThirtySecondNotesPer24Clocks }; + s.Write(tdata, 0, 4); + break; + case KeySignature x: + s.WriteMidiMultiByte(2); + s.WriteByte(x.Sharps); + s.WriteByte(x.Tonality); + break; + case SequencerSpecificEvent x: + s.WriteMidiMultiByte((uint)x.Data.Length); + s.Write(x.Data, 0, x.Data.Length); + break; + } + break; + case SysexEvent e: + // cancel running status + running_status = 0; + s.WriteByte(0xF0); + s.WriteMidiMultiByte((uint)e.Data.Length); + s.Write(e.Data, 0, e.Data.Length); + break; + } + } - } + } }