File size: 2,973 Bytes
135ad48
 
 
 
 
 
 
 
 
 
5269783
 
 
 
 
 
 
 
 
 
 
135ad48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5269783
135ad48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1da61d4
 
 
135ad48
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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