hexachords_docker / optimize.py
pachet's picture
Make OR-Tools optional
5269783
Raw
History Blame Contribute Delete
2.97 kB
from smallmuse.temporals import NoteList, TemporalNote
# Parameters
QUANTIZATION = 8 # positions per bar
TICKS_PER_BEAT = 2
STEPS_PER_BAR = QUANTIZATION
MAX_TIME_STEP = 32
BAR_LIMIT = .5 # ±1 bar displacement allowed
DISSONANT_INTERVALS = {6, 13}
def _load_cp_model():
try:
from ortools.sat.python import cp_model
except ImportError as exc:
raise ImportError(
"OR-Tools is required for temporal note optimization. "
"Install it with `python -m pip install ortools`."
) from exc
return cp_model
# Note class
# class Note:
# def __init__(self, pitch, start_time, duration):
# self.pitch = pitch
# self.start_time = start_time
# self.duration = duration
# Generate notes
# notes = []
# for i in range(NUM_NOTES):
# pitch = random.randint(48, 72)
# start_time = i * 0.5 # uniform spacing (beats)
# duration = 0.5
# notes.append(Note(pitch, start_time, duration))
def optimize(note_list):
cp_model = _load_cp_model()
# Model
notes = note_list.notes
NUM_NOTES = len(notes)
model = cp_model.CpModel()
original_steps = [int(note.start_time * TICKS_PER_BEAT) for note in notes]
starts = []
for i in range(NUM_NOTES):
lower = max(0, original_steps[i] - int(BAR_LIMIT * STEPS_PER_BAR))
upper = min(MAX_TIME_STEP - 1, original_steps[i] + int(BAR_LIMIT * STEPS_PER_BAR))
var = model.NewIntVar(lower, upper, f'start_{i}')
starts.append(var)
# Order constraint
for i in range(NUM_NOTES - 1):
model.Add(starts[i] <= starts[i + 1])
# Dissonance constraint
for i in range(NUM_NOTES):
for j in range(i + 1, NUM_NOTES):
interval = abs(notes[i].pitch - notes[j].pitch)
if interval in DISSONANT_INTERVALS:
model.Add(starts[i] != starts[j])
# Objective: minimize squared pitch difference
# interval_terms = [(notes[i + 1].pitch - notes[i].pitch) ** 2 for i in range(NUM_NOTES - 1)]
# model.Minimize(sum(interval_terms))
# Objective: minimize total displacement from original start times
displacements = []
for i in range(NUM_NOTES):
diff = model.NewIntVar(0, MAX_TIME_STEP, f'diff_{i}')
model.AddAbsEquality(diff, starts[i] - original_steps[i])
displacements.append(diff)
model.Minimize(sum(displacements))
# Solve
solver = cp_model.CpSolver()
status = solver.Solve(model)
# Output
result = NoteList()
if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
for i in range(NUM_NOTES):
result.add_note(TemporalNote(notes[i].pitch, int(solver.Value(starts[i]) / 2), notes[i].duration()))
# print(f"Note {i}: pitch={notes[i].pitch}, "
# f"original={original_steps[i]}, "
# f"quantized={solver.Value(starts[i])}")
else:
print("No solution found.")
result.join_notes()
return result