Categories
Category: Coding Music
Categories
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:
- Basic Conversion – Converts MIDI notes to a
Pbindpattern with note numbers, durations, and velocities - Advanced Conversion – Generates a
Routinewith absolute timing information - Track Selection – Choose which MIDI track to convert
- File Info – Shows track count and note statistics
To Use:
- Create a new .NET 8 console project
- Install the package:
dotnet add package Melanchall.DryWetMidi - Use the code above
- 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
PdefwithPseqto 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
~loopvariable 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}");
}
}
}