Skip to main content Skip to navigation

Behind the Code

Our Code:

This code assigns each ....... (Again leaving this for Sarah to yap on)

#Importing modules
import cv2
import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np
import pandas as pd
import IPython.display as ipd
import librosa
!pip install midiutil
from midiutil import MIDIFile
import random
!pip install pedalboard
!pip install pretty_midi
from pedalboard import Pedalboard, Chorus, Reverb
from pedalboard.io import AudioFile

from google.colab import drive
drive.mount('/content/drive')

# ==============================
# CELL 1: Import Required Modules
# ==============================

# Image Processing
from PIL import Image
import numpy as np
import colorsys
import cv2

# Music / Notation
from music21 import stream, note, chord, metadata, key, tempo, dynamics, instrument, expressions, meter

# MIDI + Audio Rendering
import pretty_midi
import soundfile as sf

# Audio Effects
from pedalboard import Pedalboard, Reverb, Chorus
from pedalboard.io import AudioFile

# Utility
import random
import copy

# ======================================
# CELL 2: USER CONFIGURATION PANEL
# ======================================

# --- IMAGE ---
IMAGE_PATH = "/content/drive/MyDrive/sunflowers.jpg" # Name of file you want an image of

# --- OUTPUT FILES ---
OUTPUT_XML = "sunflowers.musicxml" # Name of musicXML file which can then be opened in Musescore or other reading software
OUTPUT_MIDI = "sunflowersmidi.mid"
OUTPUT_WAV = "sunflowerswav.wav"
OUTPUT_WAV_EFFECTS = "sunflowerseffectswav.wav"

# --- PIECE NAME ---
PIECE_NAME = "sunflowers"

# --- MUSICAL STYLE ---
STYLE = "cinematic" # User interface - change to either "classical" or "cinematic"

# --- CADENCE SETTINGS ---
USE_SUSPENSION = True

# --- MUSICAL SETTINGS ---
KEY_SIGNATURE = "Am" # User interface - change
TEMPO_BPM = 70 # User interface - change
# time signature included here?

# --- STRUCTURE ---
NO_OF_SLICES = 150 # User interface - change - affects length of piece
REPEAT_AABA = True # something to do with structure - doesn't currently work

# --- MELODY SETTINGS ---
BASE_OCTAVE = 4 # User interface - change - octave you're centered around - middle C is 4
REGISTER_SPAN = 2 # User interface - change

# --- INSTRUMENT SELECTION ---
selected_melody_instrument = instrument.Flute() # List: Violin, Viola, Flute, Oboe, Clarinet, Bassoon, Cello, Harp
selected_counter_instrument = instrument.Oboe()
selected_harmony_instrument = instrument.Piano()
selected_bass_instrument = instrument.Violoncello()

# --- AUDIO SETTINGS ---
ADD_EFFECTS = True
REVERB_AMOUNT = 0.4
CHORUS_AMOUNT = 0.3

# ======================================
# CELL 3: Instrument Ranges + Utilities
# ======================================

INSTRUMENT_RANGES = {
"Violin": (55, 103),
"Viola": (48, 88),
"Violoncello": (36, 76),
"Flute": (60, 96),
"Oboe": (58, 91),
"Clarinet": (50, 94),
"Bassoon": (34, 75),
"Piano": (21, 108)
}

def clamp_to_range(pitch, instrument_obj):
# ensures pitch stays playable
name = instrument_obj.instrumentName
low, high = INSTRUMENT_RANGES.get(name, (21,108)) # retrieving the allowed range

while pitch.midi < low:
pitch.octave += 1
while pitch.midi > high:
pitch.octave -= 1

return pitch

# ======================================
# CELL 4: IMAGE ANALYSIS: Extract Structured Colour Data
# ======================================

def analyse_image(image_path):

img = Image.open(image_path).convert("RGB") # open and convert to RGB, each pixel has three values
width, height = img.size
pixels = np.array(img)

NUMBER_OF_SLICES = min(NO_OF_SLICES, width)
slice_width = width // NUMBER_OF_SLICES # dividing the image vertically into slices

rgb_data = []
brightness = []
saturation = []
vertical_positions = []

total_r = 0
total_g = 0
total_b = 0 # used to determine overall dominant colour

for i in range(NUMBER_OF_SLICES):

x_start = i * slice_width
x_end = x_start + slice_width # determines boundaries of slices

section = pixels[:, x_start:x_end, :] # taking the vertical slice

avg_color = section.mean(axis=(0,1))
r, g, b = avg_color

rgb_data.append((r, g, b)) # storing the average colour of each slice

# Accumulate totals for overall colour detection
total_r += r
total_g += g
total_b += b

# Brightness
brightness.append(np.mean(avg_color) / 255) # brightness is the average of RGB values, then normalised

# Saturation
r_n, g_n, b_n = r/255, g/255, b/255 # normalising RGB values
_, sat, _ = colorsys.rgb_to_hsv(r_n, g_n, b_n) # converting to HSV, this extracts colour intensity
saturation.append(sat)

# Y position for melodic contour
vertical_profile = section.mean(axis=2).mean(axis=1)
y_index = np.argmax(vertical_profile)
vertical_positions.append(y_index)

return rgb_data, brightness, saturation, vertical_positions, height, total_r, total_g, total_b

# ======================================
# CELL 5: OVERALL COLOUR + CADENCE LOGIC
# ======================================

def determine_cadence_from_image(total_r, total_g, total_b):

# store totals in dictionary for sorting
totals = {"Red": total_r, "Green": total_g, "Blue": total_b}

# sort colours by dominance
sorted_colours = sorted(totals.items(), key=lambda x: x[1], reverse=True)

# extracting these rankings
dominant_colour = sorted_colours[0][0]
second_colour = sorted_colours[1][0]
weakest_colour = sorted_colours[2][0]

# extracting corresponding strength value of each colour
dominant_value = sorted_colours[0][1]
second_value = sorted_colours[1][1]
weakest_value = sorted_colours[2][1]

# detect interrupted cadence case - 2 strong colours and one significantly weaker
if abs(dominant_value - second_value) < 0.05 * dominant_value:
cadence_type = "Interrupted (V-VI)"
cadence_degrees = [5, 6]

# otherwise, we select the type of cadence based on most dominant colour
else:
if dominant_colour == "Red":
cadence_type = "Perfect (V-I)"
cadence_degrees = [5, 1]

elif dominant_colour == "Blue":
cadence_type = "Plagal (IV-I)"
cadence_degrees = [4, 1]

else: # Green dominant
cadence_type = "Imperfect (ends on V)"
cadence_degrees = [1, 5]

print("Overall dominant colour:", dominant_colour)
print("Cadence selected:", cadence_type)

return cadence_type, cadence_degrees

# ======================================
# CELL 6: RGB → Tonal Scale Degree
# ======================================

def rgb_to_degree(r, g, b):

total = r + g + b + 1e-6 # calculate proportional weight of each colour
r_w = r / total
g_w = g / total
b_w = b / total

if STYLE == "classical":
# Strong tonal hierarchy
if r_w > 0.45:
return 1 # tonic
elif g_w > 0.45:
return 3 # mediant
elif b_w > 0.45:
return 5 # dominant
else:
return random.choice([2,4,6,7])

elif STYLE == "cinematic":
# More modal / open feeling
return random.choice([1,2,3,5,6])

# ======================================
# CELL 7: Generate Score (with Cadence)
# ======================================

def generate_score():

# --- Analyse Image ---
(rgb_data, brightness, saturation, vertical_positions, height, total_r, total_g, total_b) = analyse_image(IMAGE_PATH)

cadence_type, cadence_degrees = determine_cadence_from_image(total_r, total_g, total_b)

# --- Initialise score and Metadata ---

score = stream.Score()
score.insert(0, metadata.Metadata())
score.insert(0, tempo.MetronomeMark(number=TEMPO_BPM))
score.insert(0, key.Key(KEY_SIGNATURE))
score.insert(0, meter.TimeSignature("4/4"))

tonal_key = key.Key(KEY_SIGNATURE)
scale = tonal_key.getScale() # generating the scale for the chosen key
scale_pitches = [scale.pitchFromDegree(d) for d in range(1, 8)] # stores the pitches of the scale

score.metadata.title = PIECE_NAME

# ---------------------------
# PARTS SETUP
# ---------------------------
melody = stream.Part()
melody.insert(0, selected_melody_instrument)

counter = stream.Part()
counter.insert(0, selected_counter_instrument)

harmony = stream.Part()
harmony.insert(0, selected_harmony_instrument)

bass = stream.Part()
bass.insert(0, selected_bass_instrument)


# ---------------------------
# RHYTHM ENGINE
# ---------------------------
rhythm_patterns = [
[1, 0.5, 0.5],
[0.75, 0.25, 0.5],
[0.5, 0.5, 0.5, 0.5],
[1.5, 0.5],
[0.25, 0.25, 0.5, 1],
[0.5, 1, 0.5],
[0.25, 0.75, 0.5]
]

current_pattern = random.choice(rhythm_patterns)
pattern_index = 0
elapsed_time = 0
current_dynamic = None
current_bar = 1 # tracks harmonic bars

# harmony_interval = 4
# next_harmony_time = harmony_interval

# ---------------------------
# MAIN COMPOSITION LOOP
# ---------------------------
for i in range(NO_OF_SLICES):

r, g, b = rgb_data[i] # average colour of the slice

# pitch derived from RGB proportions
degree = rgb_to_degree(r, g, b) # converts colour to degree of scale
pitch = scale.pitchFromDegree(degree) # converts degree to pitch

# melodic contour from vertical positions
octave_shift = int((1 - vertical_positions[i] / height) * REGISTER_SPAN)
pitch.octave = BASE_OCTAVE + octave_shift
pitch = clamp_to_range(pitch, selected_melody_instrument) # ensures notes are within instrument range

# ---- Rhythm selection ----
duration = current_pattern[pattern_index]
pattern_index += 1
if pattern_index >= len(current_pattern):
current_pattern = random.choice(rhythm_patterns)
pattern_index = 0

# ---- Melody ----
if random.random() < 0.12:
m_obj = note.Rest(quarterLength=duration)
else:
m_obj = note.Note(pitch, quarterLength=duration)
melody.append(m_obj)

# ---- Countermelody (3rd below) ----
counter_degree = (degree - 2 - 1) % 7 + 1
counter_pitch = scale.pitchFromDegree(counter_degree)
counter_pitch.octave = pitch.octave
if counter_pitch.midi >= pitch.midi:
counter_pitch.octave -= 1
counter_note = note.Note(counter_pitch, quarterLength=duration)
counter.append(counter_note)

# ---- Dynamics ----
if brightness[i] < 0.3:
dyn_mark = "p"
elif brightness[i] < 0.5:
dyn_mark = "mp"
elif brightness[i] < 0.7:
dyn_mark = "mf"
else:
dyn_mark = "f"

if dyn_mark != current_dynamic:
melody.insert(melody.highestTime, dynamics.Dynamic(dyn_mark))
counter.insert(counter.highestTime, dynamics.Dynamic(dyn_mark))
harmony.insert(harmony.highestTime, dynamics.Dynamic(dyn_mark))
bass.insert(bass.highestTime, dynamics.Dynamic(dyn_mark))
current_dynamic = dyn_mark

elapsed_time += duration

# ---------------------------
# HARMONY
# ---------------------------

if elapsed_time >= current_bar * 4:

sat = saturation[i]

if sat < 0.3:
chord_degrees = [degree, (degree+2-1)%7+1, (degree+4-1)%7+1]

elif sat < 0.6:
chord_degrees = [degree, (degree+2-1)%7+1, (degree+4-1)%7+1, (degree+6-1)%7+1]

else:
chord_degrees = [degree, (degree+2-1)%7+1, (degree+4-1)%7+1, (degree+6-1)%7+1]

chord_pitches = [scale.pitchFromDegree(d) for d in chord_degrees]


# First chord: dotted minim (3 beats)
harm1 = chord.Chord(chord_pitches)
harm1.quarterLength = 3
harmony.append(harm1)

# Second chord: same harmony, crotchet (1 beat)
harm2 = chord.Chord(chord_pitches)
harm2.quarterLength = 1
harmony.append(harm2)

# Bass mirrors harmony root
root_pitch = scale.pitchFromDegree(degree)
root_pitch.octave = 2
bass1 = note.Note(root_pitch, quarterLength=3)
bass2 = note.Note(root_pitch, quarterLength=1)
bass.append(bass1)
bass.append(bass2)

current_bar += 1 # advance harmonic bar



# ---------------------------------
# ALIGN BEFORE CADENCE
# ---------------------------------
parts = [melody, counter, harmony, bass]
max_time = max(p.highestTime for p in parts)

# Force cadence to start at next full bar
remainder = max_time % 4
if remainder != 0:
padding = 4 - remainder
else:
padding = 0

for p in parts:
if p.highestTime < max_time:
p.append(note.Rest(quarterLength=max_time - p.highestTime))
if padding > 0:
p.append(note.Rest(quarterLength=padding))
# ---------------------------------
# FINAL CADENCE
# ---------------------------------

base_tempo = TEMPO_BPM
cadence_start = max(p.highestTime for p in [melody, counter, harmony, bass])
melody.insert(cadence_start, expressions.TextExpression("rit."))

# ---- Melody anticipation ----
anticipation = note.Note(scale_pitches[4]) # Dominant
anticipation.quarterLength = 4
melody.append(anticipation)

final_melody = note.Note(scale_pitches[4]) # Tonic
final_melody.quarterLength = 4
final_melody.expressions.append(expressions.Fermata())
melody.append(final_melody)

# ---- Countermelody resolution ----

final_counter = note.Note(scale_pitches[3])
final_counter.quarterLength = 4
counter.append(final_counter)


step1 = note.Note(scale_pitches[3])
step1.quarterLength = 1
step2 = note.Note(scale_pitches[2])
step2.quarterLength = 1
step3 = note.Note(scale_pitches[1])
step3.quarterLength = 1
resolution = note.Note(scale_pitches[2])
resolution.quarterLength = 1
resolution.expressions.append(expressions.Fermata())

counter.append(step1)
counter.append(step2)
counter.append(step3)
counter.append(resolution)

current_time = cadence_start

for i, d in enumerate(cadence_degrees):

# Gradual tempo reduction
rit_value = base_tempo * (0.9 ** (i + 1))
score.insert(current_time, tempo.MetronomeMark(number=rit_value))

root = scale.pitchFromDegree(d)
final_chord = chord.Chord([
root,
scale.pitchFromDegree((d+2-1)%7+1),
scale.pitchFromDegree((d+4-1)%7+1)
])

final_chord.quarterLength = 4
if d == 1:
final_chord.expressions.append(expressions.Fermata())
harmony.append(final_chord)


bass_note = note.Note(root)
bass_note.octave = 2
bass_note.quarterLength = 4
if d == 1:
bass_note.expressions.append(expressions.Fermata())
bass.append(bass_note)


# ---------------------------
# FINAL ALIGNMENT CHECK
# ---------------------------

parts = [melody, counter, harmony, bass]
max_time = max(p.highestTime for p in parts)

for p in parts:
remaining = max_time - p.highestTime
if remaining > 0:
p.append(note.Rest(quarterLength=remaining))

# ---------------------------
# ADD PARTS TO SCORE
# ---------------------------
score.append(melody)
score.append(counter)
score.append(harmony)
score.append(bass)

return score

# ======================================
# CELL 8: Export Symbolic Files
# ======================================

score = generate_score()

score.write("musicxml", OUTPUT_XML)
score.write("midi", OUTPUT_MIDI)

print("MusicXML and MIDI successfully created.")

# ======================================
# CELL 9: Convert MIDI → WAV
# ======================================

# load MIDI
midi_data = pretty_midi.PrettyMIDI(OUTPUT_MIDI)

# assign proper General MIDI programs
for inst in midi_data.instruments:
try:
inst.program = pretty_midi.instrument_name_to_program(inst.name)
except:
inst.program = 0

audio = midi_data.synthesize(fs=44100, wave=np.sin)

Audio = audio / np.max(np.abs(audio)) #normalise

sf.write(OUTPUT_WAV, Audio, 44100)

print("Audio rendering complete.")

# ======================================
# CELL 10: Add Reverb / Chorus
# ======================================

if ADD_EFFECTS:

board = Pedalboard([
Chorus(depth=CHORUS_AMOUNT),
Reverb(room_size=REVERB_AMOUNT)
])

with AudioFile(OUTPUT_WAV) as f:
audio = f.read(f.frames)
effected = board(audio, f.samplerate)

with AudioFile(OUTPUT_WAV_EFFECTS, 'w', f.samplerate, effected.shape[0]) as f:
f.write(effected)

print("Effects applied.")

Flowchart explaining code:

This is an explanation of how the code works (will be replaced by actual flowchart when I have it from Sarah, Sarah yap here please)

Flowchart

Let us know you agree to cookies