Last active
February 2, 2025 16:26
-
-
Save kmorrill/121a664160dede43afa06d5373201a27 to your computer and use it in GitHub Desktop.
Mozaic OP-XY Melody Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Description | |
Melodic Generator v2.0 | |
Uses four knobs to control melodic generation: | |
- Melodic Contour: Smooth ↔ Angular intervals | |
- Rhythm: Steady ↔ Syncopated patterns | |
- Register: Narrow ↔ Wide pitch range | |
- Harmony: Sparse ↔ Rich note choices | |
Adjust any knob to generate a new melody. | |
@End | |
@OnLoad | |
// Initialize layout and labels | |
ShowLayout 4 | |
SetShortName {MelGen} | |
LabelKnobs {Melodic Generator} | |
LabelKnob 0, {Contour} | |
LabelKnob 1, {Rhythm} | |
LabelKnob 2, {Register} | |
LabelKnob 3, {Harmony} | |
// Initialize musical parameters | |
SetMetroPPQN 4 | |
PresetScale {Major} | |
SetRootNote 0 // C | |
// Default parameters | |
if Unassigned init | |
init = YES | |
baseNote = 60 // Middle C | |
lastNote = baseNote | |
currentStep = 0 | |
// Set initial knob values | |
SetKnobValue 0, 64 // Contour | |
SetKnobValue 1, 64 // Rhythm | |
SetKnobValue 2, 64 // Register | |
SetKnobValue 3, 64 // Harmony | |
// Initialize arrays for sequence storage | |
FillArray melodyPitches, -1, 64 | |
FillArray melodyDurations, 4, 64 // Default to quarter notes | |
endif | |
Call @GenerateMelody | |
@End | |
@OnMetroPulse | |
if HostRunning | |
Call @PlayCurrentStep | |
endif | |
@End | |
@PlayCurrentStep | |
if melodyPitches[currentStep] > 0 // Normal note | |
// Stop any currently playing note | |
SendMIDICC 0, 123, 0 // All notes off | |
// Play new note with its duration | |
noteToPlay = melodyPitches[currentStep] | |
noteDuration = melodyDurations[currentStep] | |
SendMIDINoteOn 0, noteToPlay, 100 | |
SendMIDINoteOff 0, noteToPlay, 0, (QuarterNote/4) * noteDuration | |
endif | |
// Skip if it's -1 (rest) or -2 (continuation of previous note) | |
// Advance sequence | |
inc currentStep | |
if currentStep >= 64 | |
currentStep = 0 | |
endif | |
@End | |
@OnKnobChange | |
Log {=== Knob }, LastKnob, { changed to }, GetKnobValue LastKnob | |
Call @GenerateMelody | |
@End | |
@GenerateMelody | |
// Get current knob values | |
contourVal = GetKnobValue 0 | |
rhythmVal = GetKnobValue 1 | |
registerVal = GetKnobValue 2 | |
harmonyVal = GetKnobValue 3 | |
Log {=== New Melody Generation ===} | |
Log {Controls:} | |
Log { Contour: }, contourVal | |
Log { Rhythm: }, rhythmVal | |
Log { Register: }, registerVal | |
Log { Harmony: }, harmonyVal | |
currentStep = 0 | |
lastNote = baseNote | |
step = 0 | |
// Pre-analyze rhythm characteristics | |
if rhythmVal < 33 | |
Log {Pattern: Steady rhythm} | |
elseif rhythmVal < 66 | |
Log {Pattern: Mixed rhythm} | |
else | |
Log {Pattern: Complex rhythm} | |
endif | |
registerRange = Round (TranslateScale registerVal, 0, 127, 3, 24) | |
Log {Range: }, (NoteName (baseNote - registerRange)), { to }, (NoteName (baseNote + registerRange)) | |
while step < 64 | |
// Determine rhythm first | |
rhythmValue = TranslateScale rhythmVal, 0, 127, 0, 100 | |
// Duration patterns based on rhythm value | |
duration = 4 // Default duration | |
if rhythmValue < 33 | |
// Steady rhythm - mostly quarter notes | |
duration = 4 | |
restChance = 10 | |
elseif rhythmValue < 66 | |
// Mixed rhythm - eighths and quarters with some rests | |
durationChoice = Random 0, 100 | |
if durationChoice < 50 | |
duration = 4 // quarter | |
elseif durationChoice < 80 | |
duration = 2 // eighth | |
else | |
duration = 6 // dotted quarter | |
endif | |
restChance = 25 | |
else | |
// Complex rhythm - varied lengths and more rests | |
durationChoice = Random 0, 100 | |
if durationChoice < 30 | |
duration = 1 // sixteenth | |
elseif durationChoice < 50 | |
duration = 2 // eighth | |
elseif durationChoice < 70 | |
duration = 3 // dotted eighth | |
elseif durationChoice < 85 | |
duration = 4 // quarter | |
else | |
duration = 6 // dotted quarter | |
endif | |
restChance = 40 | |
endif | |
// Handle rests - check BEFORE storing the note | |
if Random 0, 100 < restChance | |
for i = 0 to duration - 1 | |
if (step + i) < 64 | |
melodyPitches[step + i] = -1 | |
melodyDurations[step + i] = 1 | |
endif | |
endfor | |
step = step + duration | |
Continue | |
endif | |
// Determine interval based on contour | |
// Ensure at least some movement even at low contour values | |
minStep = 1 // Minimum step size | |
maxStep = Round (TranslateScale contourVal, 0, 127, 2, 12) // Max interval range | |
// Generate interval with bias towards smaller intervals at low contour values | |
if contourVal < 30 | |
interval = Random -2, 2 // Small steps | |
if interval = 0 | |
interval = 1 // Ensure some movement | |
endif | |
elseif contourVal < 70 | |
interval = Random -maxStep, maxStep | |
// Bias towards smaller intervals | |
if Abs interval > 3 and Random 0, 100 < 70 | |
interval = interval / 2 | |
endif | |
else | |
interval = Random -maxStep, maxStep | |
// Allow larger jumps more frequently | |
endif | |
// Calculate new note | |
newNote = lastNote + interval | |
// Apply register constraints | |
registerRange = Round (TranslateScale registerVal, 0, 127, 3, 24) | |
rangeMin = baseNote - registerRange | |
rangeMax = baseNote + registerRange | |
newNote = Clip newNote, rangeMin, rangeMax | |
// Quantize to scale based on harmony setting | |
harmonyThreshold = TranslateScale harmonyVal, 0, 127, 95, 20 | |
if Random 0, 100 > harmonyThreshold | |
newNote = ScaleQuantize newNote | |
else | |
// Add chromatic notes for tension when harmony is high | |
if Random 0, 100 < 50 | |
newNote = newNote + 1 | |
endif | |
endif | |
// Store note and duration | |
if step < 64 | |
melodyPitches[step] = newNote | |
// Fill duration slots with -2 to mark them as part of a longer note | |
for i = 1 to duration - 1 | |
if (step + i) < 64 | |
melodyPitches[step + i] = -2 | |
melodyDurations[step + i] = 0 | |
endif | |
endfor | |
melodyDurations[step] = duration | |
lastNote = newNote | |
endif | |
step = step + duration | |
endwhile | |
// Log the complete melody | |
Log {=== Melody Sequence ===} | |
melodyStep = 0 | |
while melodyStep < 64 | |
if melodyPitches[melodyStep] = -1 | |
Log {Step }, melodyStep, {: Rest (}, melodyDurations[melodyStep], {)} | |
elseif melodyPitches[melodyStep] = -2 | |
Log {Step }, melodyStep, {: Note continuation} | |
else | |
Log {Step }, melodyStep, {: }, (NoteName melodyPitches[melodyStep]), { (}, melodyDurations[melodyStep], {)} | |
endif | |
inc melodyStep | |
endwhile | |
// Count notes and rests | |
quarterNotes = 0 | |
eighthNotes = 0 | |
sixteenthNotes = 0 | |
dottedNotes = 0 | |
rests = 0 | |
for i = 0 to 63 | |
if melodyPitches[i] = -1 | |
inc rests | |
elseif melodyPitches[i] > 0 // Don't count continuations (-2) | |
if melodyDurations[i] = 4 | |
inc quarterNotes | |
elseif melodyDurations[i] = 2 | |
inc eighthNotes | |
elseif melodyDurations[i] = 1 | |
inc sixteenthNotes | |
else | |
inc dottedNotes | |
endif | |
endif | |
endfor | |
Log {=== Pattern Stats ===} | |
Log {Quarter: }, quarterNotes, { Eighth: }, eighthNotes, { 16th: }, sixteenthNotes, { Dotted: }, dottedNotes | |
Log {Notes: }, (64 - rests), { Rests: }, rests | |
Log {==================} | |
@End | |
@OnHostStart | |
currentStep = 0 | |
@End | |
@OnHostStop | |
SendMIDICC 0, 123, 0 // All notes off | |
@End |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment