1 | package org.apollo.meldex;
|
---|
2 |
|
---|
3 | /**
|
---|
4 | * An implementation of an approximate string matching algorithm described by Marcel Mongeau
|
---|
5 | * and David Sankoff ("Comparison of Musical Sequences", Computers and the Humanities, Vol 24
|
---|
6 | * (1990), p161 - 175) and modified by Rodger McNab for music matching (REF).
|
---|
7 | * <p>To do: Test fragmentation and consolidation.
|
---|
8 | */
|
---|
9 | public class McNabMongeauSankoffAlgorithm extends DynamicProgrammingAlgorithm
|
---|
10 | {
|
---|
11 | private static final boolean doFragmentationConsolidation = false;
|
---|
12 |
|
---|
13 | /** Constant values used by this algorithm */
|
---|
14 | private final double K1_FACTOR = 112.0;
|
---|
15 |
|
---|
16 |
|
---|
17 | /**
|
---|
18 | * Creates a new McNabMongeauSankoffAlgorithm and initialises it for use.
|
---|
19 | */
|
---|
20 | public McNabMongeauSankoffAlgorithm()
|
---|
21 | {
|
---|
22 | }
|
---|
23 |
|
---|
24 |
|
---|
25 | /**
|
---|
26 | * Returns whether this algorithm has special functionality that is enabled.
|
---|
27 | *
|
---|
28 | * @return whether this algorithm has special functionality that is enabled.
|
---|
29 | */
|
---|
30 | protected boolean hasSpecialFunctionality()
|
---|
31 | {
|
---|
32 | return doFragmentationConsolidation;
|
---|
33 | }
|
---|
34 |
|
---|
35 |
|
---|
36 | /**
|
---|
37 | * Lets the algorithm perform its special functionality (fragmentation and consolidation).
|
---|
38 | *
|
---|
39 | * @param D The distance matrix at the current point in time.
|
---|
40 | * @param i The current x position in the distance matrix.
|
---|
41 | * @param j The current y position in the distance matrix.
|
---|
42 | * @param T The melody that is being matched to the query pattern.
|
---|
43 | * @param queryEvent The melody event at position j-1 in the query pattern.
|
---|
44 | * @param matchEvent The melody event at position i-1 in the match pattern.
|
---|
45 | */
|
---|
46 | protected void doAlgorithmSpecifics(int[][] D, int i, int j, Melody P, Melody T,
|
---|
47 | MelodyEvent queryEvent, MelodyEvent matchEvent)
|
---|
48 | {
|
---|
49 | if (doFragmentationConsolidation) {
|
---|
50 | int pitchType = T.getPitchType();
|
---|
51 |
|
---|
52 | // Do fragmentations (one query event matches many match events)
|
---|
53 | int fragPitch = matchEvent.getPitch();
|
---|
54 | double queryDuration = queryEvent.getDuration();
|
---|
55 | double totalDuration = matchEvent.getDuration();
|
---|
56 |
|
---|
57 | // Subsequent pitches must be the same
|
---|
58 | int requiredPitch = 0;
|
---|
59 | if (pitchType == Melody.ABSOLUTE_PITCH)
|
---|
60 | requiredPitch = fragPitch;
|
---|
61 |
|
---|
62 | // Sum the fragmented weights of the previous k match events
|
---|
63 | int k = 2;
|
---|
64 | while ((k <= i) && (fragPitch == requiredPitch)) {
|
---|
65 | // Total fragmentation length should not exceed query event length
|
---|
66 | if (totalDuration > queryDuration)
|
---|
67 | break;
|
---|
68 |
|
---|
69 | // All fragments must have the same pitch
|
---|
70 | MelodyEvent fragEvent = T.getEvent(i-k);
|
---|
71 | fragPitch = fragEvent.getPitch();
|
---|
72 | if (pitchType == Melody.ABSOLUTE_PITCH && fragPitch != requiredPitch)
|
---|
73 | break;
|
---|
74 |
|
---|
75 | // Calculate the new pitch and duration cost for this fragmentation
|
---|
76 | double wPitch = weightPitch(queryEvent.getPitch(), fragPitch);
|
---|
77 | totalDuration += fragEvent.getDuration();
|
---|
78 | double wDuration = weightDuration(queryDuration, totalDuration);
|
---|
79 | double shortestDuration = (queryDuration < totalDuration ? queryDuration : totalDuration);
|
---|
80 |
|
---|
81 | // Calculate the total cost for this fragmentation
|
---|
82 | int fragWeight = (int) ((wPitch * shortestDuration) + (K1_FACTOR * wDuration));
|
---|
83 |
|
---|
84 | if (debugOutput) {
|
---|
85 | System.out.println("Frag: k = " + k + "\tDi-k,j-1 = " + D[i-k][j-1] + "\tPitch weight: " + wPitch + "\tDuration weight: " + wDuration + "\tTotal weight: " + fragWeight);
|
---|
86 | }
|
---|
87 |
|
---|
88 | // Case 4: Many events are equivalent to one (fragmentation)
|
---|
89 | D[i][j] = Math.min(D[i][j], (D[i-k][j-1] + fragWeight));
|
---|
90 | k++;
|
---|
91 | }
|
---|
92 |
|
---|
93 | // Do consolidations (one match event matches many query events)
|
---|
94 | int consPitch = queryEvent.getPitch();
|
---|
95 | double matchDuration = matchEvent.getDuration();
|
---|
96 | totalDuration = queryEvent.getDuration();
|
---|
97 |
|
---|
98 | // Subsequent pitches must be the same
|
---|
99 | requiredPitch = 0;
|
---|
100 | if (pitchType == Melody.ABSOLUTE_PITCH)
|
---|
101 | requiredPitch = consPitch;
|
---|
102 |
|
---|
103 | // Sum the fragmented weights of the previous k query events
|
---|
104 | k = 2;
|
---|
105 | while ((k <= j) && (consPitch == requiredPitch)) {
|
---|
106 | // Total consolidation length should not exceed match event length
|
---|
107 | if (totalDuration > matchDuration)
|
---|
108 | break;
|
---|
109 |
|
---|
110 | // All fragments must have the same pitch
|
---|
111 | MelodyEvent consEvent = P.getEvent(j-k);
|
---|
112 | consPitch = consEvent.getPitch();
|
---|
113 | if (pitchType == Melody.ABSOLUTE_PITCH && consPitch != requiredPitch)
|
---|
114 | break;
|
---|
115 |
|
---|
116 | // Calculate the new pitch and duration cost for this consolidation
|
---|
117 | double wPitch = weightPitch(matchEvent.getPitch(), consPitch);
|
---|
118 | totalDuration += consEvent.getDuration();
|
---|
119 | double wDuration = weightDuration(matchDuration, totalDuration);
|
---|
120 | double shortestDuration = (matchDuration < totalDuration ? matchDuration : totalDuration);
|
---|
121 |
|
---|
122 | // Calculate the total cost for this consolidation
|
---|
123 | int consWeight = (int) ((wPitch * shortestDuration) + (K1_FACTOR * wDuration));
|
---|
124 |
|
---|
125 | if (debugOutput) {
|
---|
126 | System.out.println("Cons: k = " + k + "\tDi-1,j-k = " + D[i-1][j-k] + "\tPitch weight: " + wPitch + "\tDuration weight: " + wDuration + "\tTotal weight: " + consWeight);
|
---|
127 | }
|
---|
128 |
|
---|
129 | // Case 5: Many events are equivalent to one (consolidation)
|
---|
130 | D[i][j] = Math.min(D[i][j], (D[i-1][j-k] + consWeight));
|
---|
131 | k++;
|
---|
132 | }
|
---|
133 | }
|
---|
134 | }
|
---|
135 |
|
---|
136 |
|
---|
137 | /**
|
---|
138 | * Returns a distance measure signifying the distance between the query and match events.
|
---|
139 | *
|
---|
140 | * @param queryEvent The event to compare from the query pattern.
|
---|
141 | * @param matchEvent The event to compare from the match pattern.
|
---|
142 | *
|
---|
143 | * @return a distance measure signifying the distance between the query and match events.
|
---|
144 | */
|
---|
145 | protected int weightFunction(MelodyEvent queryEvent, MelodyEvent matchEvent)
|
---|
146 | {
|
---|
147 | // Calculate the pitch weighting
|
---|
148 | int queryPitch = ((queryEvent == null) ? 0 : queryEvent.getPitch());
|
---|
149 | int matchPitch = ((matchEvent == null) ? 0 : matchEvent.getPitch());
|
---|
150 | double wPitch = weightPitch(queryPitch, matchPitch);
|
---|
151 |
|
---|
152 | return (int)wPitch;
|
---|
153 |
|
---|
154 | /*
|
---|
155 |
|
---|
156 | // Calculate the duration weighting
|
---|
157 | double queryLength = ((queryEvent == null) ? 0.0 : queryEvent.getDuration());
|
---|
158 | double matchLength = ((matchEvent == null) ? 0.0 : matchEvent.getDuration());
|
---|
159 | double wDuration = weightDuration(queryLength, matchLength);
|
---|
160 | double shortestLength = (queryLength < matchLength ? queryLength : matchLength);
|
---|
161 |
|
---|
162 | // Calculate the final weighting (linear combination of pitch and duration)
|
---|
163 | return (int) ((wPitch * shortestLength) + (K1_FACTOR * wDuration));
|
---|
164 | */
|
---|
165 |
|
---|
166 | }
|
---|
167 |
|
---|
168 |
|
---|
169 | /**
|
---|
170 | * Returns a measure signifying the distance between the query and match pitches.
|
---|
171 | *
|
---|
172 | * @param queryPitch The pitch of the query event.
|
---|
173 | * @param matchPitch The pitch of the match event.
|
---|
174 | *
|
---|
175 | * @return a measure signifying the distance between the query and match pitches.
|
---|
176 | */
|
---|
177 | private double weightPitch(int queryPitch, int matchPitch)
|
---|
178 | {
|
---|
179 | // If either of the events is a wild then the pitch weight is 0
|
---|
180 | if ((queryPitch == MelodyEvent.WILD_EVENT) || (matchPitch == MelodyEvent.WILD_EVENT))
|
---|
181 | return 0.0;
|
---|
182 |
|
---|
183 | // If one or other of the events is a rest, use a special pitch factor of 80.0
|
---|
184 | if ((queryPitch == MelodyEvent.REST_EVENT) ^ (matchPitch == MelodyEvent.REST_EVENT))
|
---|
185 | return 80.0;
|
---|
186 |
|
---|
187 | // Otherwise calculate a pitch factor based on the pitch difference
|
---|
188 | int pitchDifference = Math.abs((queryPitch - matchPitch));
|
---|
189 |
|
---|
190 | // Special case absolute pitch
|
---|
191 | /* if (P.getPitchType() == Melody.ABSOLUTE_PITCH) {
|
---|
192 | pitchDifference = pitchDifference % 12;
|
---|
193 | if (pitchDifference > 6)
|
---|
194 | pitchDifference = 12 - pitchDifference;
|
---|
195 | } */
|
---|
196 |
|
---|
197 | return (pitchDifference * 32.0);
|
---|
198 | }
|
---|
199 |
|
---|
200 |
|
---|
201 | /**
|
---|
202 | * Returns a measure signifying the distance between the query and match durations.
|
---|
203 | *
|
---|
204 | * @param queryLength The duration of the query event.
|
---|
205 | * @param matchLength The duration of the match event.
|
---|
206 | *
|
---|
207 | * @return a measure signifying the distance between the query and match durations.
|
---|
208 | */
|
---|
209 | private double weightDuration(double queryLength, double matchLength)
|
---|
210 | {
|
---|
211 | // The duration weight is directly proportional to the distance between the two lengths
|
---|
212 | return Math.abs((queryLength - matchLength));
|
---|
213 | }
|
---|
214 | }
|
---|