Categories
Uncategorized

Hannah Peel

some very interesting and varied things

Categories
Uncategorized

Midi to SuperCollider

https://github.com/noisyvoltuk/MidiToSuperCollider

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}");
        }
    }
}

Categories
Music Theory

Euphoria

Electronic dance music achieves that euphoric quality through several musical and psychological elements:

Harmonic choices: EDM often uses major keys and uplifting chord progressions that naturally evoke positive emotions. Progressions like I-V-vi-IV create a sense of resolution and emotional lift. Many tracks also feature key changes or modulations that give a fresh burst of energy.

The build and drop structure: The signature tension-and-release pattern is crucial. Producers build anticipation through rising pitch, filtering, drum rolls, and removing elements, then release all that energy in the “drop” – this mimics emotional catharsis and triggers dopamine release in listeners.

Layered synths and frequencies: Bright, shimmering synth leads in higher frequencies, often with slight detuning or chorus effects, create that sparkly, uplifting sound. Supersaws (multiple saw waves layered together) are particularly popular for this euphoric quality.

Tempo and rhythm: Most euphoric EDM sits around 128-140 BPM, which aligns well with elevated heart rates during excitement or dancing. The four-on-the-floor kick drum pattern creates a driving, hypnotic pulse.

Reverb and space: Generous reverb and delay create a sense of vastness and atmosphere, making the music feel transcendent or otherworldly.

Melodic hooks: Simple, memorable melodies that repeat and evolve throughout the track create familiarity while building emotional connection.

The combination of these elements in a club or festival setting, paired with the social experience and physical movement of dancing, amplifies the euphoric effect even further.

Categories
Music Theory

Gypsy Woman (Shes Homeless)

“Gypsy Woman (She’s Homeless)” by Crystal Waters (1991) is a deceptively simple but emotionally powerful track, largely because of how its chord structure, transitions, and repetition create both hypnotic groove and emotional depth. Let’s break that down:


🎹 Chord Structure and Transitions

The song is built around a repetitive, looping chord progression — most analyses agree it cycles through something like:

Am – G – F – G

(or variations with added color tones, e.g. Am7 – G – Fmaj7 – G)

This is a descending sequence, moving stepwise down the scale from A to G to F — a classic pop and dance music device that gives a sense of falling or lamentation. Then the G brings it back up slightly, looping endlessly.

What that means musically:

  • A minor establishes a moody, introspective tonality — the emotional “core” of the song.
  • G brings a gentle lift — it softens the sadness and adds a touch of brightness.
  • F deepens the melancholy; it’s warmer, but heavier.
  • Returning to G resets the loop, providing circular continuity.

This creates a push-pull between tension and release, minor sadness and gentle uplift, that feels both yearning and hypnotic.


💫 Transitions and Repetition

Because the chords repeat almost unchanged throughout the song, the transitions are smooth and cyclical. There are no big key changes or dramatic shifts — instead, it’s all about steady momentum, like a dancer’s trance or a city rhythm that never stops.

This repetition mirrors the lyrical theme — the endless cycle of homelessness and invisibility (“she’s just like you and me, but she’s homeless…”). The music’s constancy makes the listener feel that same loop of daily struggle and persistence.


💔 Emotional Feelings and Atmosphere

ElementEmotional Effect
Minor tonality (A minor)Evokes empathy, melancholy, reflection.
Descending motion (Am → G → F)Suggests falling, loss, or resignation.
Steady 4/4 rhythm with house beatContrasts with the sadness — gives it energy, defiance, and life.
Looping progressionFeels hypnotic; conveys cycles of poverty and resilience.
Crystal Waters’ vocal deliverySoulful but detached; a haunting balance of storytelling and groove.

The result is a song that feels sad but danceable, detached yet deeply human — a rare balance that made it stand out in early ’90s house music.


🧠 In summary:

The chords in “Gypsy Woman” move in a gently descending loop that reflects emotional descent and resilience. Their repetition turns sorrow into rhythm — transforming empathy into motion, and hardship into a danceable truth.


Categories
Finds

Joe Meek

Categories
Finds

Fairlight

Categories
Finds

Synclavier

Categories
Finds

Love Hulten