Skip to content

Commit aa15d3c

Browse files
committed
refactor: add equals/hashCode to time classes and improve TimeContext copy behavior
- Add equals() and hashCode() to MeasureMeterPair, MeterMap, and TempoMap for proper equality comparison - Add TimeBase.isBeatBased() helper method to identify beat-based time formats - Add TimeContext.hasSameMusicalContext() to compare tempo/meter maps between contexts - Fix TimeContext copy constructor to create detached snapshot instead of sharing ProjectProperties reference - Update ScoreObjectCopy to include time context
1 parent dc3af08 commit aa15d3c

File tree

17 files changed

+427
-43
lines changed

17 files changed

+427
-43
lines changed

blue-core/src/main/java/blue/time/MeasureMeterPair.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ public MeasureMeterPair withMeter(Meter newMeter) {
6868
return new MeasureMeterPair(this.measureNumber, newMeter);
6969
}
7070

71+
@Override
72+
public boolean equals(Object obj) {
73+
if (this == obj) return true;
74+
if (!(obj instanceof MeasureMeterPair other)) return false;
75+
return measureNumber == other.measureNumber && meter.equals(other.meter);
76+
}
77+
78+
@Override
79+
public int hashCode() {
80+
return Long.hashCode(measureNumber) * 31 + meter.hashCode();
81+
}
82+
7183
/**
7284
* Save MeasureMeterPair to XML.
7385
*/

blue-core/src/main/java/blue/time/MeterMap.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,18 @@ public TimePosition.BBFTime beatsToBBF(double csoundBeats) {
365365
return TimePosition.bbf(bar, wholeBeat, fraction);
366366
}
367367

368+
@Override
369+
public boolean equals(Object obj) {
370+
if (this == obj) return true;
371+
if (!(obj instanceof MeterMap other)) return false;
372+
return entries.equals(other.entries);
373+
}
374+
375+
@Override
376+
public int hashCode() {
377+
return entries.hashCode();
378+
}
379+
368380
/**
369381
* Save MeterMap to XML.
370382
*/

blue-core/src/main/java/blue/time/TempoMap.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,18 @@ public String toString() {
603603
return buffer.toString();
604604
}
605605

606+
@Override
607+
public boolean equals(Object obj) {
608+
if (this == obj) return true;
609+
if (!(obj instanceof TempoMap other)) return false;
610+
return enabled == other.enabled && points.equals(other.points);
611+
}
612+
613+
@Override
614+
public int hashCode() {
615+
return 31 * points.hashCode() + Boolean.hashCode(enabled);
616+
}
617+
606618
// ========== XML Serialization ==========
607619

608620
/**

blue-core/src/main/java/blue/time/TimeBase.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,13 @@ public enum TimeBase {
5353
SMPTE,
5454

5555
/** Audio Sample Frame Number **/
56-
FRAME,
56+
FRAME;
57+
58+
/**
59+
* Returns true if this TimeBase is beat-based (BEATS, BBT, BBST, BBF).
60+
* Beat-based values depend on tempo/meter context for wall-clock conversion.
61+
*/
62+
public boolean isBeatBased() {
63+
return this == BEATS || this == BBT || this == BBST || this == BBF;
64+
}
5765
}

blue-core/src/main/java/blue/time/TimeContext.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@ public TimeContext(TimeContext tc) {
6868
this.smpteFrameRate = tc.smpteFrameRate;
6969
this.meterMap = new MeterMap(tc.meterMap);
7070
this.tempoMap = new TempoMap(tc.tempoMap);
71-
if (tc.projectProperties != null) {
72-
this.projectProperties = tc.projectProperties;
73-
this.projectProperties.addPropertyChangeListener(sampleRateListener);
74-
}
7571
}
7672

7773
/**
@@ -143,6 +139,20 @@ public void setTempoMap(TempoMap tempoMap) {
143139
this.tempoMap = tempoMap;
144140
}
145141

142+
/**
143+
* Returns true if this context has the same musical context
144+
* (tempo map and meter map) as the other context. Used to determine
145+
* whether beat-based values need conversion when pasting between contexts.
146+
*
147+
* @param other the other TimeContext to compare
148+
* @return true if tempo and meter maps are equal
149+
*/
150+
public boolean hasSameMusicalContext(TimeContext other) {
151+
if (other == null) return false;
152+
if (this == other) return true;
153+
return tempoMap.equals(other.tempoMap) && meterMap.equals(other.meterMap);
154+
}
155+
146156
/**
147157
* Save TimeContext to XML.
148158
*/
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* blue - object composition environment for csound
3+
* Copyright (c) 2023 Steven Yi (stevenyi@gmail.com)
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published
7+
* by the Free Software Foundation; either version 2 of the License or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful, but
11+
* WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program; see the file COPYING.LIB. If not, write to
17+
* the Free Software Foundation Inc., 59 Temple Place - Suite 330,
18+
* Boston, MA 02111-1307 USA
19+
*/
20+
package blue.time;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
import org.junit.jupiter.api.Test;
24+
25+
class MeasureMeterPairTest {
26+
27+
@Test
28+
void shouldBeEqualForSameMeasureAndMeter() {
29+
var a = new MeasureMeterPair(1, new Meter(4, 4));
30+
var b = new MeasureMeterPair(1, new Meter(4, 4));
31+
assertEquals(a, b);
32+
assertEquals(a.hashCode(), b.hashCode());
33+
}
34+
35+
@Test
36+
void shouldNotBeEqualForDifferentMeasure() {
37+
var a = new MeasureMeterPair(1, new Meter(4, 4));
38+
var b = new MeasureMeterPair(2, new Meter(4, 4));
39+
assertNotEquals(a, b);
40+
}
41+
42+
@Test
43+
void shouldNotBeEqualForDifferentMeter() {
44+
var a = new MeasureMeterPair(1, new Meter(4, 4));
45+
var b = new MeasureMeterPair(1, new Meter(3, 4));
46+
assertNotEquals(a, b);
47+
}
48+
49+
@Test
50+
void shouldNotBeEqualToNull() {
51+
var a = new MeasureMeterPair(1, new Meter(4, 4));
52+
assertNotEquals(null, a);
53+
}
54+
55+
@Test
56+
void shouldBeEqualToSelf() {
57+
var a = new MeasureMeterPair(1, new Meter(4, 4));
58+
assertEquals(a, a);
59+
}
60+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* blue - object composition environment for csound
3+
* Copyright (c) 2023 Steven Yi (stevenyi@gmail.com)
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published
7+
* by the Free Software Foundation; either version 2 of the License or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful, but
11+
* WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program; see the file COPYING.LIB. If not, write to
17+
* the Free Software Foundation Inc., 59 Temple Place - Suite 330,
18+
* Boston, MA 02111-1307 USA
19+
*/
20+
package blue.time;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
import org.junit.jupiter.api.Test;
24+
25+
class TimeBaseTest {
26+
27+
@Test
28+
void shouldIdentifyBeatBasedTimeBases() {
29+
assertTrue(TimeBase.BEATS.isBeatBased());
30+
assertTrue(TimeBase.BBT.isBeatBased());
31+
assertTrue(TimeBase.BBST.isBeatBased());
32+
assertTrue(TimeBase.BBF.isBeatBased());
33+
}
34+
35+
@Test
36+
void shouldIdentifyNonBeatBasedTimeBases() {
37+
assertFalse(TimeBase.TIME.isBeatBased());
38+
assertFalse(TimeBase.SMPTE.isBeatBased());
39+
assertFalse(TimeBase.FRAME.isBeatBased());
40+
}
41+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* blue - object composition environment for csound
3+
* Copyright (c) 2023 Steven Yi (stevenyi@gmail.com)
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published
7+
* by the Free Software Foundation; either version 2 of the License or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful, but
11+
* WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program; see the file COPYING.LIB. If not, write to
17+
* the Free Software Foundation Inc., 59 Temple Place - Suite 330,
18+
* Boston, MA 02111-1307 USA
19+
*/
20+
package blue.time;
21+
22+
import blue.ProjectProperties;
23+
import static org.junit.jupiter.api.Assertions.*;
24+
import org.junit.jupiter.api.Test;
25+
26+
27+
class TimeContextEqualityTest {
28+
29+
// ===== MeterMap equals =====
30+
31+
@Test
32+
void shouldConsiderDefaultMeterMapsEqual() {
33+
var a = new MeterMap();
34+
var b = new MeterMap();
35+
assertEquals(a, b);
36+
assertEquals(a.hashCode(), b.hashCode());
37+
}
38+
39+
@Test
40+
void shouldConsiderMeterMapsWithSameEntriesEqual() {
41+
var a = new MeterMap();
42+
a.add(new MeasureMeterPair(5, new Meter(3, 4)));
43+
var b = new MeterMap();
44+
b.add(new MeasureMeterPair(5, new Meter(3, 4)));
45+
assertEquals(a, b);
46+
}
47+
48+
@Test
49+
void shouldConsiderMeterMapsWithDifferentEntriesNotEqual() {
50+
var a = new MeterMap();
51+
var b = new MeterMap();
52+
b.add(new MeasureMeterPair(5, new Meter(6, 8)));
53+
assertNotEquals(a, b);
54+
}
55+
56+
// ===== TempoMap equals =====
57+
58+
@Test
59+
void shouldConsiderDefaultTempoMapsEqual() {
60+
var a = new TempoMap();
61+
var b = new TempoMap();
62+
assertEquals(a, b);
63+
assertEquals(a.hashCode(), b.hashCode());
64+
}
65+
66+
@Test
67+
void shouldConsiderTempoMapsWithDifferentEnabledNotEqual() {
68+
var a = new TempoMap();
69+
a.setEnabled(true);
70+
var b = new TempoMap();
71+
b.setEnabled(false);
72+
assertNotEquals(a, b);
73+
}
74+
75+
@Test
76+
void shouldConsiderTempoMapsWithDifferentPointsNotEqual() {
77+
var a = new TempoMap();
78+
var b = new TempoMap();
79+
b.addTempoPoint(new TempoPoint(4.0, 140.0));
80+
assertNotEquals(a, b);
81+
}
82+
83+
@Test
84+
void shouldConsiderCopiedTempoMapEqual() {
85+
var a = new TempoMap();
86+
a.setEnabled(true);
87+
a.addTempoPoint(new TempoPoint(8.0, 100.0));
88+
var b = new TempoMap(a);
89+
assertEquals(a, b);
90+
assertEquals(a.hashCode(), b.hashCode());
91+
}
92+
93+
// ===== TimeContext.hasSameMusicalContext =====
94+
95+
@Test
96+
void shouldDetectSameMusicalContext() {
97+
var a = new TimeContext();
98+
var b = new TimeContext();
99+
assertTrue(a.hasSameMusicalContext(b));
100+
}
101+
102+
@Test
103+
void shouldDetectSameMusicalContextForCopy() {
104+
var a = new TimeContext();
105+
a.getTempoMap().setEnabled(true);
106+
a.getMeterMap().add(new MeasureMeterPair(3, new Meter(6, 8)));
107+
var b = new TimeContext(a);
108+
assertTrue(a.hasSameMusicalContext(b));
109+
}
110+
111+
@Test
112+
void shouldDetectDifferentMusicalContextWhenTempoMapDiffers() {
113+
var a = new TimeContext();
114+
var b = new TimeContext();
115+
b.getTempoMap().setEnabled(true);
116+
assertFalse(a.hasSameMusicalContext(b));
117+
}
118+
119+
@Test
120+
void shouldDetectDifferentMusicalContextWhenMeterMapDiffers() {
121+
var a = new TimeContext();
122+
var b = new TimeContext();
123+
b.getMeterMap().add(new MeasureMeterPair(5, new Meter(7, 8)));
124+
assertFalse(a.hasSameMusicalContext(b));
125+
}
126+
127+
@Test
128+
void shouldReturnFalseForNullContext() {
129+
var a = new TimeContext();
130+
assertFalse(a.hasSameMusicalContext(null));
131+
}
132+
133+
@Test
134+
void shouldReturnTrueForSelf() {
135+
var a = new TimeContext();
136+
assertTrue(a.hasSameMusicalContext(a));
137+
}
138+
139+
@Test
140+
void shouldCopySampleRateAsDetachedSnapshot() {
141+
ProjectProperties props = new ProjectProperties();
142+
props.setSampleRate("48000");
143+
144+
TimeContext original = new TimeContext();
145+
original.setProjectProperties(props);
146+
147+
TimeContext copy = new TimeContext(original);
148+
149+
// mutate source after copy; snapshot should stay fixed
150+
props.setSampleRate("96000");
151+
152+
assertEquals(96000L, original.getSampleRate());
153+
assertEquals(48000L, copy.getSampleRate());
154+
}
155+
}

blue-projects/src/main/java/blue/projects/BlueProjectManager.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import blue.settings.ProjectDefaultsSettings;
3232
import blue.settings.RealtimeRenderSettings;
3333
import blue.soundObject.PolyObject;
34-
import blue.time.TimeContext;
3534
import blue.time.TimeContextManager;
3635
import blue.ui.utilities.FileChooserManager;
3736
import blue.undo.BlueUndoManager;
@@ -206,20 +205,7 @@ public void setCurrentProject(BlueProject project) {
206205

207206
new Thread(() -> {
208207
try {
209-
// Set TimeContext for this thread's operation
210-
TimeContext context = project.getData().getScore().getTimeContext();
211-
TimeContext previousContext = TimeContextManager.hasContext() ? TimeContextManager.getContext() : null;
212-
TimeContextManager.setContext(context);
213-
214-
try {
215-
score.processOnLoad();
216-
} finally {
217-
if (previousContext != null) {
218-
TimeContextManager.setContext(previousContext);
219-
} else {
220-
TimeContextManager.clearContext();
221-
}
222-
}
208+
score.processOnLoad();
223209
} catch (Exception ex) {
224210
Exceptions.printStackTrace(ex);
225211
}

0 commit comments

Comments
 (0)