Categories
Coding Music

Some MIDI Files

Categories
Coding Music

MIDI to Supercollider

Great! Here’s how you can build a MIDI to SuperCollider converter in .NET 8:

Required NuGet Package

You’ll need a MIDI library. I recommend Melanchall.DryWetMidi – it’s the most comprehensive MIDI library for .NET:

dotnet add package Melanchall.DryWetMidi

Basic Implementation

Here’s a complete example that reads a MIDI file and generates SuperCollider code:## Key Features:

  1. Basic Conversion – Converts MIDI notes to a Pbind pattern with note numbers, durations, and velocities
  2. Advanced Conversion – Generates a Routine with absolute timing information
  3. Track Selection – Choose which MIDI track to convert
  4. File Info – Shows track count and note statistics

To Use:

  1. Create a new .NET 8 console project
  2. Install the package: dotnet add package Melanchall.DryWetMidi
  3. Use the code above
  4. Run: dotnet run yourfile.mid 0

The converter handles:

  • MIDI note numbers (converts to frequencies in SuperCollider)
  • Note durations based on tempo
  • Velocity → amplitude conversion
  • Multiple tracks

Would you like me to add features like:

  • Polyphony detection and chord generation?
  • Control change (CC) messages?
  • Tempo changes?
  • Multiple tracks at once?

Now enhance the converter to detect patterns, loops, and repetitions. Here’s an upgraded version with pattern detection:Now the converter includes two powerful pattern detection modes:

Pattern Detection Mode

Finds repeating melodic/rhythmic patterns within the MIDI:

  • Detects sequences of 3+ notes that repeat 2+ times
  • Considers note pitch, duration, velocity, and relative timing
  • Creates separate ~pattern0, ~pattern1, etc. variables
  • Generates Pdef with Pseq to play patterns in sequence

Example output:

~pattern0 = (
    // Found 4 times, 8 notes
    \midinote: [60, 62, 64, 65, 67, 65, 64, 62],
    \dur: [0.25, 0.25, 0.25, 0.25, 0.5, 0.25, 0.25, 0.5],
    \amp: [0.8, 0.7, 0.8, 0.7, 0.9, 0.7, 0.8, 0.6]
);

Loop Detection Mode

Identifies exact repeating loops (common in electronic music):

  • Checks for 4, 8, 16, 32, or 64-note loops
  • Verifies the loop repeats exactly throughout the file
  • Generates a single ~loop variable with repetition count
  • Perfect for drum patterns, bass loops, arpeggios

Usage:

dotnet run myfile.mid pattern 0    # Detect patterns
dotnet run myfile.mid loop 0       # Detect loops

The pattern detector is smart about:

  • Timing tolerance – accounts for slight timing variations
  • Velocity tolerance – allows for humanization
  • Hierarchical patterns – finds the longest meaningful patterns
  • Subpattern filtering – avoids redundant shorter patterns

Would you like me to add:

  • Chord detection (simultaneous notes)?
  • Rhythmic pattern analysis?
  • Transposition detection (same melody in different keys)?
  • Export to different SuperCollider pattern types (Prand, Pxrand, Pwrand)?
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Interaction;
using System.Text;

namespace MidiToSuperCollider;

public record NoteEvent(int MidiNote, double Duration, double Velocity, double TimeOffset);

public record Pattern(List<NoteEvent> Notes, int Occurrences, List<int> Positions);

public class PatternDetector
{
    private const double TIME_TOLERANCE = 0.01; // 10ms tolerance for timing
    private const double VELOCITY_TOLERANCE = 0.05; // 5% velocity tolerance
    
    public static List<Pattern> DetectPatterns(List<NoteEvent> notes, int minPatternLength = 2, int minOccurrences = 2)
    {
        var patterns = new List<Pattern>();
        
        // Try different pattern lengths
        for (int length = minPatternLength; length <= notes.Count / 2; length++)
        {
            for (int start = 0; start <= notes.Count - length; start++)
            {
                var candidate = notes.Skip(start).Take(length).ToList();
                var occurrences = FindOccurrences(notes, candidate);
                
                if (occurrences.Count >= minOccurrences)
                {
                    // Check if this pattern is not a subset of an already found pattern
                    if (!IsSubpattern(patterns, candidate, occurrences))
                    {
                        patterns.Add(new Pattern(candidate, occurrences.Count, occurrences));
                    }
                }
            }
        }
        
        // Sort by longest and most frequent patterns first
        return patterns.OrderByDescending(p => p.Notes.Count * p.Occurrences).ToList();
    }
    
    private static List<int> FindOccurrences(List<NoteEvent> notes, List<NoteEvent> pattern)
    {
        var positions = new List<int>();
        
        for (int i = 0; i <= notes.Count - pattern.Count; i++)
        {
            if (MatchesPattern(notes, i, pattern))
            {
                positions.Add(i);
            }
        }
        
        return positions;
    }
    
    private static bool MatchesPattern(List<NoteEvent> notes, int startPos, List<NoteEvent> pattern)
    {
        for (int i = 0; i < pattern.Count; i++)
        {
            var note = notes[startPos + i];
            var patternNote = pattern[i];
            
            if (note.MidiNote != patternNote.MidiNote)
                return false;
            
            if (Math.Abs(note.Duration - patternNote.Duration) > TIME_TOLERANCE)
                return false;
            
            if (Math.Abs(note.Velocity - patternNote.Velocity) > VELOCITY_TOLERANCE)
                return false;
            
            // Check relative timing (interval between notes)
            if (i > 0)
            {
                var noteInterval = note.TimeOffset - notes[startPos + i - 1].TimeOffset;
                var patternInterval = patternNote.TimeOffset - pattern[i - 1].TimeOffset;
                
                if (Math.Abs(noteInterval - patternInterval) > TIME_TOLERANCE)
                    return false;
            }
        }
        
        return true;
    }
    
    private static bool IsSubpattern(List<Pattern> existingPatterns, List<NoteEvent> candidate, List<int> occurrences)
    {
        foreach (var existing in existingPatterns)
        {
            if (existing.Notes.Count > candidate.Count && 
                existing.Positions.Intersect(occurrences).Any())
            {
                return true;
            }
        }
        return false;
    }
}

public class MidiToSuperColliderConverter
{
    public static string ConvertWithPatternDetection(string midiFilePath, int trackIndex = 0)
    {
        var midiFile = MidiFile.Read(midiFilePath);
        var tempoMap = midiFile.GetTempoMap();
        
        var sb = new StringBuilder();
        sb.AppendLine("// SuperCollider code with pattern detection");
        sb.AppendLine($"// File: {Path.GetFileName(midiFilePath)}");
        sb.AppendLine();
        
        var trackChunk = midiFile.GetTrackChunks().ElementAtOrDefault(trackIndex);
        if (trackChunk == null)
        {
            throw new ArgumentException($"Track {trackIndex} not found");
        }
        
        var midiNotes = trackChunk.GetNotes().ToList();
        if (!midiNotes.Any())
        {
            return "// No notes found";
        }
        
        // Convert to NoteEvents
        var firstNoteTime = midiNotes.First().TimeAs<MetricTimeSpan>(tempoMap);
        var noteEvents = midiNotes.Select(n => new NoteEvent(
            n.NoteNumber,
            n.LengthAs<MetricTimeSpan>(tempoMap).TotalMicroseconds / 1_000_000.0,
            n.Velocity / 127.0,
            (n.TimeAs<MetricTimeSpan>(tempoMap) - firstNoteTime).TotalMicroseconds / 1_000_000.0
        )).ToList();
        
        // Detect patterns
        var patterns = PatternDetector.DetectPatterns(noteEvents, minPatternLength: 3, minOccurrences: 2);
        
        sb.AppendLine("(");
        
        // Generate pattern definitions
        if (patterns.Any())
        {
            sb.AppendLine("// Detected patterns");
            for (int i = 0; i < Math.Min(5, patterns.Count); i++)
            {
                var pattern = patterns[i];
                sb.AppendLine($"~pattern{i} = (");
                sb.AppendLine($"    // Found {pattern.Occurrences} times, {pattern.Notes.Count} notes");
                sb.AppendLine($"    \\midinote: [{string.Join(", ", pattern.Notes.Select(n => n.MidiNote))}],");
                sb.AppendLine($"    \\dur: [{string.Join(", ", pattern.Notes.Select(n => n.Duration.ToString("F4")))}],");
                sb.AppendLine($"    \\amp: [{string.Join(", ", pattern.Notes.Select(n => n.Velocity.ToString("F3")))}]");
                sb.AppendLine(");");
                sb.AppendLine();
            }
            
            sb.AppendLine("// Play detected patterns");
            sb.AppendLine("Pdef(\\midiPatterns,");
            sb.AppendLine("    Pseq([");
            
            for (int i = 0; i < Math.Min(5, patterns.Count); i++)
            {
                sb.AppendLine($"        Pbind(");
                sb.AppendLine($"            \\midinote, Pseq(~pattern{i}.midinote, {patterns[i].Occurrences}),");
                sb.AppendLine($"            \\dur, Pseq(~pattern{i}.dur, {patterns[i].Occurrences}),");
                sb.AppendLine($"            \\amp, Pseq(~pattern{i}.amp, {patterns[i].Occurrences})");
                sb.AppendLine($"        ),");
            }
            
            sb.AppendLine("    ], 1)");
            sb.AppendLine(").play;");
        }
        else
        {
            sb.AppendLine("// No repeating patterns detected");
            sb.AppendLine("// Playing full sequence");
            sb.AppendLine("Pbind(");
            sb.AppendLine($"    \\midinote, Pseq([{string.Join(", ", noteEvents.Select(n => n.MidiNote))}], 1),");
            sb.AppendLine($"    \\dur, Pseq([{string.Join(", ", noteEvents.Select(n => n.Duration.ToString("F4")))}], 1),");
            sb.AppendLine($"    \\amp, Pseq([{string.Join(", ", noteEvents.Select(n => n.Velocity.ToString("F3")))}], 1)");
            sb.AppendLine(").play;");
        }
        
        sb.AppendLine(")");
        
        return sb.ToString();
    }
    
    public static string ConvertWithLoopDetection(string midiFilePath, int trackIndex = 0)
    {
        var midiFile = MidiFile.Read(midiFilePath);
        var tempoMap = midiFile.GetTempoMap();
        
        var sb = new StringBuilder();
        sb.AppendLine("// SuperCollider code with loop detection");
        sb.AppendLine($"// File: {Path.GetFileName(midiFilePath)}");
        sb.AppendLine();
        
        var trackChunk = midiFile.GetTrackChunks().ElementAtOrDefault(trackIndex);
        if (trackChunk == null)
        {
            throw new ArgumentException($"Track {trackIndex} not found");
        }
        
        var midiNotes = trackChunk.GetNotes().ToList();
        if (!midiNotes.Any())
        {
            return "// No notes found";
        }
        
        // Try to detect a repeating loop structure
        var totalNotes = midiNotes.Count;
        int? loopLength = null;
        
        // Try common loop lengths (4, 8, 16, 32 bars worth of notes)
        foreach (var candidateLength in new[] { 4, 8, 16, 32, 64 })
        {
            if (totalNotes % candidateLength == 0 && totalNotes >= candidateLength * 2)
            {
                if (IsRepeatingLoop(midiNotes, candidateLength, tempoMap))
                {
                    loopLength = candidateLength;
                    break;
                }
            }
        }
        
        sb.AppendLine("(");
        
        if (loopLength.HasValue)
        {
            var loopNotes = midiNotes.Take(loopLength.Value).ToList();
            var repetitions = totalNotes / loopLength.Value;
            
            sb.AppendLine($"// Detected {loopLength.Value}-note loop, repeated {repetitions} times");
            sb.AppendLine();
            sb.AppendLine("~loop = (");
            sb.AppendLine($"    \\midinote: [{string.Join(", ", loopNotes.Select(n => n.NoteNumber))}],");
            
            var firstNoteTime = loopNotes.First().TimeAs<MetricTimeSpan>(tempoMap);
            var durations = loopNotes.Select((n, idx) => {
                if (idx < loopNotes.Count - 1)
                {
                    var nextTime = loopNotes[idx + 1].TimeAs<MetricTimeSpan>(tempoMap);
                    var thisTime = n.TimeAs<MetricTimeSpan>(tempoMap);
                    return (nextTime - thisTime).TotalMicroseconds / 1_000_000.0;
                }
                else
                {
                    return n.LengthAs<MetricTimeSpan>(tempoMap).TotalMicroseconds / 1_000_000.0;
                }
            }).ToList();
            
            sb.AppendLine($"    \\dur: [{string.Join(", ", durations.Select(d => d.ToString("F4")))}],");
            sb.AppendLine($"    \\amp: [{string.Join(", ", loopNotes.Select(n => (n.Velocity / 127.0).ToString("F3")))}]");
            sb.AppendLine(");");
            sb.AppendLine();
            sb.AppendLine("// Play the loop");
            sb.AppendLine("Pdef(\\loop,");
            sb.AppendLine("    Pbind(");
            sb.AppendLine($"        \\midinote, Pseq(~loop.midinote, {repetitions}),");
            sb.AppendLine($"        \\dur, Pseq(~loop.dur, {repetitions}),");
            sb.AppendLine($"        \\amp, Pseq(~loop.amp, {repetitions})");
            sb.AppendLine("    )");
            sb.AppendLine(").play;");
        }
        else
        {
            sb.AppendLine("// No clear loop structure detected");
            sb.AppendLine("// Generating full sequence as Pbind");
            
            var firstNoteTime = midiNotes.First().TimeAs<MetricTimeSpan>(tempoMap);
            var durations = midiNotes.Select((n, idx) => {
                if (idx < midiNotes.Count - 1)
                {
                    var nextTime = midiNotes[idx + 1].TimeAs<MetricTimeSpan>(tempoMap);
                    var thisTime = n.TimeAs<MetricTimeSpan>(tempoMap);
                    return (nextTime - thisTime).TotalMicroseconds / 1_000_000.0;
                }
                else
                {
                    return n.LengthAs<MetricTimeSpan>(tempoMap).TotalMicroseconds / 1_000_000.0;
                }
            }).ToList();
            
            sb.AppendLine("Pbind(");
            sb.AppendLine($"    \\midinote, Pseq([{string.Join(", ", midiNotes.Select(n => n.NoteNumber))}], 1),");
            sb.AppendLine($"    \\dur, Pseq([{string.Join(", ", durations.Select(d => d.ToString("F4")))}], 1),");
            sb.AppendLine($"    \\amp, Pseq([{string.Join(", ", midiNotes.Select(n => (n.Velocity / 127.0).ToString("F3")))}], 1)");
            sb.AppendLine(").play;");
        }
        
        sb.AppendLine(")");
        
        return sb.ToString();
    }
    
    private static bool IsRepeatingLoop(List<Note> notes, int loopLength, TempoMap tempoMap)
    {
        const double TOLERANCE = 0.02; // 20ms tolerance
        
        for (int repetition = 1; repetition < notes.Count / loopLength; repetition++)
        {
            for (int i = 0; i < loopLength; i++)
            {
                var originalNote = notes[i];
                var repeatedNote = notes[repetition * loopLength + i];
                
                if (originalNote.NoteNumber != repeatedNote.NoteNumber)
                    return false;
                
                if (Math.Abs(originalNote.Velocity - repeatedNote.Velocity) > 5)
                    return false;
                
                var origDur = originalNote.LengthAs<MetricTimeSpan>(tempoMap).TotalMicroseconds / 1_000_000.0;
                var repDur = repeatedNote.LengthAs<MetricTimeSpan>(tempoMap).TotalMicroseconds / 1_000_000.0;
                
                if (Math.Abs(origDur - repDur) > TOLERANCE)
                    return false;
            }
        }
        
        return true;
    }
    
    public static void PrintMidiFileInfo(string midiFilePath)
    {
        var midiFile = MidiFile.Read(midiFilePath);
        
        Console.WriteLine($"MIDI File: {Path.GetFileName(midiFilePath)}");
        Console.WriteLine($"Format: {midiFile.OriginalFormat}");
        Console.WriteLine($"Tracks: {midiFile.GetTrackChunks().Count()}");
        Console.WriteLine($"Division: {midiFile.TimeDivision}");
        Console.WriteLine();
        
        int trackNum = 0;
        foreach (var track in midiFile.GetTrackChunks())
        {
            var notes = track.GetNotes();
            Console.WriteLine($"Track {trackNum}: {notes.Count()} notes");
            trackNum++;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            Console.WriteLine("Usage: MidiToSuperCollider <midifile.mid> [mode] [track_index]");
            Console.WriteLine("Modes: pattern, loop (default: pattern)");
            return;
        }
        
        string midiFile = args[0];
        string mode = args.Length > 1 ? args[1] : "pattern";
        int trackIndex = args.Length > 2 ? int.Parse(args[2]) : 0;
        
        try
        {
            MidiToSuperColliderConverter.PrintMidiFileInfo(midiFile);
            Console.WriteLine();
            
            string scCode = mode.ToLower() == "loop" 
                ? MidiToSuperColliderConverter.ConvertWithLoopDetection(midiFile, trackIndex)
                : MidiToSuperColliderConverter.ConvertWithPatternDetection(midiFile, trackIndex);
            
            Console.WriteLine(scCode);
            
            string outputFile = Path.ChangeExtension(midiFile, $".{mode}.scd");
            File.WriteAllText(outputFile, scCode);
            Console.WriteLine($"\nSaved to: {outputFile}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}