@opendaw/studio-core 0.0.105 → 0.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/Engine.d.ts +2 -0
  2. package/dist/Engine.d.ts.map +1 -1
  3. package/dist/capture/RecordAutomation.d.ts.map +1 -1
  4. package/dist/capture/RecordAutomation.js +5 -2
  5. package/dist/{project/polyfill.d.ts.map → polyfill.d.ts.map} +1 -1
  6. package/dist/{project/polyfill.js → polyfill.js} +6 -0
  7. package/dist/processors.js +1 -1
  8. package/dist/processors.js.map +3 -3
  9. package/dist/project/Project.d.ts +3 -1
  10. package/dist/project/Project.d.ts.map +1 -1
  11. package/dist/project/Project.js +7 -2
  12. package/dist/project/ProjectApi.d.ts +1 -2
  13. package/dist/project/ProjectApi.d.ts.map +1 -1
  14. package/dist/project/ProjectApi.js +1 -11
  15. package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.d.ts.map +1 -1
  16. package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.js +1 -1
  17. package/dist/ui/index.d.ts +1 -0
  18. package/dist/ui/index.d.ts.map +1 -1
  19. package/dist/ui/index.js +1 -0
  20. package/dist/ui/timeline/RegionClipResolver.d.ts.map +1 -1
  21. package/dist/ui/timeline/RegionClipResolver.js +47 -7
  22. package/dist/ui/transfer/TransferRegions.d.ts +12 -0
  23. package/dist/ui/transfer/TransferRegions.d.ts.map +1 -0
  24. package/dist/ui/transfer/TransferRegions.js +81 -0
  25. package/dist/ui/transfer/TransferRegions.test.d.ts +2 -0
  26. package/dist/ui/transfer/TransferRegions.test.d.ts.map +1 -0
  27. package/dist/ui/transfer/TransferRegions.test.js +242 -0
  28. package/dist/ui/transfer/index.d.ts +2 -0
  29. package/dist/ui/transfer/index.d.ts.map +1 -0
  30. package/dist/ui/transfer/index.js +1 -0
  31. package/package.json +16 -16
  32. package/dist/project/ProjectApi.test.d.ts +0 -2
  33. package/dist/project/ProjectApi.test.d.ts.map +0 -1
  34. package/dist/project/ProjectApi.test.js +0 -109
  35. /package/dist/{project/polyfill.d.ts → polyfill.d.ts} +0 -0
@@ -0,0 +1,242 @@
1
+ import "../../polyfill";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { Terminable, UUID } from "@opendaw/lib-std";
4
+ import { AudioUnitBox, CaptureAudioBox, TrackBox, ValueEventCollectionBox, ValueRegionBox } from "@opendaw/studio-boxes";
5
+ import { AudioUnitType } from "@opendaw/studio-enums";
6
+ import { ProjectSkeleton, TrackType } from "@opendaw/studio-adapters";
7
+ import { Project } from "../../project";
8
+ import { TransferRegions } from "./TransferRegions";
9
+ const createEnv = () => ({
10
+ audioContext: {},
11
+ audioWorklets: {},
12
+ sampleManager: {
13
+ getOrCreate: () => ({}),
14
+ record: () => { },
15
+ remove: () => { },
16
+ invalidate: () => { },
17
+ register: () => Terminable.Empty
18
+ },
19
+ soundfontManager: {
20
+ getOrCreate: () => ({}),
21
+ remove: () => { },
22
+ invalidate: () => { }
23
+ }
24
+ });
25
+ const createProject = () => {
26
+ const skeleton = ProjectSkeleton.empty({ createDefaultUser: true, createOutputCompressor: false });
27
+ return Project.fromSkeleton(createEnv(), skeleton, false);
28
+ };
29
+ const createTrackWithRegion = (project, position = 100, duration = 200) => {
30
+ const { boxGraph } = project;
31
+ const captureBox = CaptureAudioBox.create(boxGraph, UUID.generate());
32
+ const audioUnitBox = AudioUnitBox.create(boxGraph, UUID.generate(), box => {
33
+ box.type.setValue(AudioUnitType.Instrument);
34
+ box.collection.refer(project.rootBox.audioUnits);
35
+ box.output.refer(project.masterBusBox.input);
36
+ box.capture.refer(captureBox);
37
+ box.index.setValue(1);
38
+ });
39
+ const trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
40
+ box.type.setValue(TrackType.Value);
41
+ box.tracks.refer(audioUnitBox.tracks);
42
+ box.target.refer(audioUnitBox);
43
+ box.index.setValue(0);
44
+ });
45
+ const collectionBox = ValueEventCollectionBox.create(boxGraph, UUID.generate());
46
+ const regionBox = ValueRegionBox.create(boxGraph, UUID.generate(), box => {
47
+ box.position.setValue(position);
48
+ box.duration.setValue(duration);
49
+ box.loopDuration.setValue(duration);
50
+ box.hue.setValue(42);
51
+ box.events.refer(collectionBox.owners);
52
+ box.regions.refer(trackBox.regions);
53
+ });
54
+ return { audioUnitBox, trackBox, regionBox, collectionBox };
55
+ };
56
+ describe("TransferRegions.transfer", () => {
57
+ let project;
58
+ beforeEach(() => { project = createProject(); });
59
+ afterEach(() => project.terminate());
60
+ describe("same graph", () => {
61
+ it("copies region to same track at new position", () => {
62
+ const { boxGraph } = project;
63
+ boxGraph.beginTransaction();
64
+ const { trackBox, regionBox } = createTrackWithRegion(project);
65
+ boxGraph.endTransaction();
66
+ boxGraph.beginTransaction();
67
+ const copied = TransferRegions.transfer(regionBox, trackBox, 500, false);
68
+ boxGraph.endTransaction();
69
+ expect(copied.position.getValue()).toBe(500);
70
+ expect(copied.graph).toBe(boxGraph);
71
+ expect(trackBox.regions.pointerHub.incoming().length).toBe(2);
72
+ boxGraph.verifyPointers();
73
+ });
74
+ it("copies region to different track", () => {
75
+ const { boxGraph } = project;
76
+ boxGraph.beginTransaction();
77
+ const { trackBox: trackA, regionBox, audioUnitBox } = createTrackWithRegion(project);
78
+ const trackB = TrackBox.create(boxGraph, UUID.generate(), box => {
79
+ box.type.setValue(TrackType.Value);
80
+ box.tracks.refer(audioUnitBox.tracks);
81
+ box.target.refer(audioUnitBox);
82
+ box.index.setValue(1);
83
+ });
84
+ boxGraph.endTransaction();
85
+ boxGraph.beginTransaction();
86
+ const copied = TransferRegions.transfer(regionBox, trackB, 300, false);
87
+ boxGraph.endTransaction();
88
+ expect(copied.position.getValue()).toBe(300);
89
+ const targetUuid = copied.regions.targetVertex.unwrap().box.address.uuid;
90
+ expect(UUID.equals(targetUuid, trackB.address.uuid)).toBe(true);
91
+ expect(trackB.regions.pointerHub.incoming().length).toBe(1);
92
+ expect(trackA.regions.pointerHub.incoming().length).toBe(1);
93
+ boxGraph.verifyPointers();
94
+ });
95
+ it("creates new UUID for copied region", () => {
96
+ const { boxGraph } = project;
97
+ boxGraph.beginTransaction();
98
+ const { trackBox, regionBox } = createTrackWithRegion(project);
99
+ boxGraph.endTransaction();
100
+ boxGraph.beginTransaction();
101
+ const copied = TransferRegions.transfer(regionBox, trackBox, 500, false);
102
+ boxGraph.endTransaction();
103
+ expect(UUID.equals(copied.address.uuid, regionBox.address.uuid)).toBe(false);
104
+ });
105
+ it("preserves region properties", () => {
106
+ const { boxGraph } = project;
107
+ boxGraph.beginTransaction();
108
+ const { trackBox, regionBox } = createTrackWithRegion(project);
109
+ boxGraph.endTransaction();
110
+ boxGraph.beginTransaction();
111
+ const copied = TransferRegions.transfer(regionBox, trackBox, 500, false);
112
+ boxGraph.endTransaction();
113
+ expect(copied.duration.getValue()).toBe(200);
114
+ expect(copied.loopDuration.getValue()).toBe(200);
115
+ expect(copied.hue.getValue()).toBe(42);
116
+ });
117
+ it("deletes source when deleteSource is true", () => {
118
+ const { boxGraph } = project;
119
+ boxGraph.beginTransaction();
120
+ const { trackBox, regionBox } = createTrackWithRegion(project);
121
+ boxGraph.endTransaction();
122
+ boxGraph.beginTransaction();
123
+ TransferRegions.transfer(regionBox, trackBox, 500);
124
+ boxGraph.endTransaction();
125
+ expect(regionBox.isAttached()).toBe(false);
126
+ expect(trackBox.regions.pointerHub.incoming().length).toBe(1);
127
+ boxGraph.verifyPointers();
128
+ });
129
+ it("keeps source when deleteSource is false", () => {
130
+ const { boxGraph } = project;
131
+ boxGraph.beginTransaction();
132
+ const { trackBox, regionBox } = createTrackWithRegion(project);
133
+ boxGraph.endTransaction();
134
+ boxGraph.beginTransaction();
135
+ TransferRegions.transfer(regionBox, trackBox, 500, false);
136
+ boxGraph.endTransaction();
137
+ expect(regionBox.isAttached()).toBe(true);
138
+ expect(trackBox.regions.pointerHub.incoming().length).toBe(2);
139
+ });
140
+ it("copies event collection dependency with new UUID", () => {
141
+ const { boxGraph } = project;
142
+ boxGraph.beginTransaction();
143
+ const { trackBox, regionBox, collectionBox } = createTrackWithRegion(project);
144
+ boxGraph.endTransaction();
145
+ boxGraph.beginTransaction();
146
+ const copied = TransferRegions.transfer(regionBox, trackBox, 500, false);
147
+ boxGraph.endTransaction();
148
+ const copiedEventsVertex = copied.events.targetVertex.unwrap();
149
+ const copiedCollectionUuid = copiedEventsVertex.box.address.uuid;
150
+ expect(UUID.equals(copiedCollectionUuid, collectionBox.address.uuid)).toBe(false);
151
+ boxGraph.verifyPointers();
152
+ });
153
+ });
154
+ describe("cross graph", () => {
155
+ let sourceProject;
156
+ beforeEach(() => { sourceProject = createProject(); });
157
+ afterEach(() => sourceProject.terminate());
158
+ it("copies region to target graph at given position", () => {
159
+ const sourceGraph = sourceProject.boxGraph;
160
+ sourceGraph.beginTransaction();
161
+ const { regionBox: sourceRegion } = createTrackWithRegion(sourceProject);
162
+ sourceGraph.endTransaction();
163
+ const { boxGraph } = project;
164
+ boxGraph.beginTransaction();
165
+ const { trackBox: targetTrack } = createTrackWithRegion(project);
166
+ boxGraph.endTransaction();
167
+ const regionsBefore = targetTrack.regions.pointerHub.incoming().length;
168
+ boxGraph.beginTransaction();
169
+ const copied = TransferRegions.transfer(sourceRegion, targetTrack, 500, false);
170
+ boxGraph.endTransaction();
171
+ expect(copied.position.getValue()).toBe(500);
172
+ expect(copied.graph).toBe(boxGraph);
173
+ expect(copied.graph).not.toBe(sourceGraph);
174
+ expect(targetTrack.regions.pointerHub.incoming().length).toBe(regionsBefore + 1);
175
+ boxGraph.verifyPointers();
176
+ sourceGraph.verifyPointers();
177
+ });
178
+ it("creates new UUID for copied region", () => {
179
+ const sourceGraph = sourceProject.boxGraph;
180
+ sourceGraph.beginTransaction();
181
+ const { regionBox: sourceRegion } = createTrackWithRegion(sourceProject);
182
+ sourceGraph.endTransaction();
183
+ const { boxGraph } = project;
184
+ boxGraph.beginTransaction();
185
+ const { trackBox: targetTrack } = createTrackWithRegion(project);
186
+ boxGraph.endTransaction();
187
+ boxGraph.beginTransaction();
188
+ const copied = TransferRegions.transfer(sourceRegion, targetTrack, 500, false);
189
+ boxGraph.endTransaction();
190
+ expect(UUID.equals(copied.address.uuid, sourceRegion.address.uuid)).toBe(false);
191
+ });
192
+ it("preserves region properties", () => {
193
+ const sourceGraph = sourceProject.boxGraph;
194
+ sourceGraph.beginTransaction();
195
+ const { regionBox: sourceRegion } = createTrackWithRegion(sourceProject, 100, 300);
196
+ sourceGraph.endTransaction();
197
+ const { boxGraph } = project;
198
+ boxGraph.beginTransaction();
199
+ const { trackBox: targetTrack } = createTrackWithRegion(project);
200
+ boxGraph.endTransaction();
201
+ boxGraph.beginTransaction();
202
+ const copied = TransferRegions.transfer(sourceRegion, targetTrack, 500, false);
203
+ boxGraph.endTransaction();
204
+ expect(copied.duration.getValue()).toBe(300);
205
+ expect(copied.loopDuration.getValue()).toBe(300);
206
+ expect(copied.hue.getValue()).toBe(42);
207
+ });
208
+ it("keeps source in source graph when deleteSource is false", () => {
209
+ const sourceGraph = sourceProject.boxGraph;
210
+ sourceGraph.beginTransaction();
211
+ const { regionBox: sourceRegion } = createTrackWithRegion(sourceProject);
212
+ sourceGraph.endTransaction();
213
+ const { boxGraph } = project;
214
+ boxGraph.beginTransaction();
215
+ const { trackBox: targetTrack } = createTrackWithRegion(project);
216
+ boxGraph.endTransaction();
217
+ boxGraph.beginTransaction();
218
+ TransferRegions.transfer(sourceRegion, targetTrack, 500, false);
219
+ boxGraph.endTransaction();
220
+ expect(sourceRegion.isAttached()).toBe(true);
221
+ sourceGraph.verifyPointers();
222
+ });
223
+ it("deletes source when deleteSource is true", () => {
224
+ const sourceGraph = sourceProject.boxGraph;
225
+ sourceGraph.beginTransaction();
226
+ const { regionBox: sourceRegion } = createTrackWithRegion(sourceProject);
227
+ sourceGraph.endTransaction();
228
+ const { boxGraph } = project;
229
+ boxGraph.beginTransaction();
230
+ const { trackBox: targetTrack } = createTrackWithRegion(project);
231
+ boxGraph.endTransaction();
232
+ sourceGraph.beginTransaction();
233
+ boxGraph.beginTransaction();
234
+ TransferRegions.transfer(sourceRegion, targetTrack, 500);
235
+ boxGraph.endTransaction();
236
+ sourceGraph.endTransaction();
237
+ expect(sourceRegion.isAttached()).toBe(false);
238
+ boxGraph.verifyPointers();
239
+ sourceGraph.verifyPointers();
240
+ });
241
+ });
242
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./TransferRegions";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/transfer/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA"}
@@ -0,0 +1 @@
1
+ export * from "./TransferRegions";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendaw/studio-core",
3
- "version": "0.0.105",
3
+ "version": "0.0.106",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -33,17 +33,17 @@
33
33
  "test": "vitest run --config vitest.config.ts"
34
34
  },
35
35
  "dependencies": {
36
- "@opendaw/lib-box": "^0.0.72",
37
- "@opendaw/lib-dawproject": "^0.0.57",
38
- "@opendaw/lib-dom": "^0.0.73",
39
- "@opendaw/lib-dsp": "^0.0.71",
40
- "@opendaw/lib-fusion": "^0.0.77",
41
- "@opendaw/lib-runtime": "^0.0.69",
42
- "@opendaw/lib-std": "^0.0.68",
36
+ "@opendaw/lib-box": "^0.0.73",
37
+ "@opendaw/lib-dawproject": "^0.0.58",
38
+ "@opendaw/lib-dom": "^0.0.74",
39
+ "@opendaw/lib-dsp": "^0.0.72",
40
+ "@opendaw/lib-fusion": "^0.0.78",
41
+ "@opendaw/lib-runtime": "^0.0.70",
42
+ "@opendaw/lib-std": "^0.0.69",
43
43
  "@opendaw/nam-wasm": "^1.0.3",
44
- "@opendaw/studio-adapters": "^0.0.83",
45
- "@opendaw/studio-boxes": "^0.0.74",
46
- "@opendaw/studio-enums": "^0.0.63",
44
+ "@opendaw/studio-adapters": "^0.0.84",
45
+ "@opendaw/studio-boxes": "^0.0.75",
46
+ "@opendaw/studio-enums": "^0.0.64",
47
47
  "dropbox": "^10.34.0",
48
48
  "y-websocket": "^1.4.5",
49
49
  "yjs": "^13.6.27",
@@ -57,10 +57,10 @@
57
57
  "@ffmpeg/ffmpeg": "^0.12.15",
58
58
  "@ffmpeg/util": "^0.12.2",
59
59
  "@opendaw/eslint-config": "^0.0.27",
60
- "@opendaw/studio-core-processors": "^0.0.86",
61
- "@opendaw/studio-core-workers": "^0.0.78",
62
- "@opendaw/studio-forge-boxes": "^0.0.74",
63
- "@opendaw/typescript-config": "^0.0.28"
60
+ "@opendaw/studio-core-processors": "^0.0.87",
61
+ "@opendaw/studio-core-workers": "^0.0.79",
62
+ "@opendaw/studio-forge-boxes": "^0.0.75",
63
+ "@opendaw/typescript-config": "^0.0.29"
64
64
  },
65
- "gitHead": "b39d786e60cee02599e0f3d7106e635a870c47c6"
65
+ "gitHead": "60447f48d38d7cd08ef6a74bb011700dffff68f7"
66
66
  }
@@ -1,2 +0,0 @@
1
- import "./polyfill";
2
- //# sourceMappingURL=ProjectApi.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ProjectApi.test.d.ts","sourceRoot":"","sources":["../../src/project/ProjectApi.test.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAA"}
@@ -1,109 +0,0 @@
1
- import "./polyfill";
2
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
- import { asInstanceOf, Terminable, UUID } from "@opendaw/lib-std";
4
- import { AudioUnitBox, CaptureAudioBox, TrackBox, ValueEventCollectionBox, ValueRegionBox } from "@opendaw/studio-boxes";
5
- import { AudioUnitType } from "@opendaw/studio-enums";
6
- import { ProjectSkeleton, TrackType } from "@opendaw/studio-adapters";
7
- import { Project } from "./Project";
8
- const createEnv = () => ({
9
- audioContext: {},
10
- audioWorklets: {},
11
- sampleManager: {
12
- getOrCreate: () => ({}),
13
- record: () => { },
14
- remove: () => { },
15
- invalidate: () => { },
16
- register: () => Terminable.Empty
17
- },
18
- soundfontManager: {
19
- getOrCreate: () => ({}),
20
- remove: () => { },
21
- invalidate: () => { }
22
- }
23
- });
24
- describe("ProjectApi.copyRegionTo", () => {
25
- let project;
26
- beforeEach(() => {
27
- const skeleton = ProjectSkeleton.empty({ createDefaultUser: true, createOutputCompressor: false });
28
- project = Project.fromSkeleton(createEnv(), skeleton, false);
29
- });
30
- afterEach(() => project.terminate());
31
- const createTrackWithRegion = (project) => {
32
- const { boxGraph } = project;
33
- const captureBox = CaptureAudioBox.create(boxGraph, UUID.generate());
34
- const audioUnitBox = AudioUnitBox.create(boxGraph, UUID.generate(), box => {
35
- box.type.setValue(AudioUnitType.Instrument);
36
- box.collection.refer(project.rootBox.audioUnits);
37
- box.output.refer(project.masterBusBox.input);
38
- box.capture.refer(captureBox);
39
- box.index.setValue(1);
40
- });
41
- const trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
42
- box.type.setValue(TrackType.Value);
43
- box.tracks.refer(audioUnitBox.tracks);
44
- box.target.refer(audioUnitBox);
45
- box.index.setValue(0);
46
- });
47
- const collectionBox = ValueEventCollectionBox.create(boxGraph, UUID.generate());
48
- const regionBox = ValueRegionBox.create(boxGraph, UUID.generate(), box => {
49
- box.position.setValue(100);
50
- box.duration.setValue(200);
51
- box.loopDuration.setValue(200);
52
- box.hue.setValue(0);
53
- box.events.refer(collectionBox.owners);
54
- box.regions.refer(trackBox.regions);
55
- });
56
- return { audioUnitBox, trackBox, regionBox, collectionBox };
57
- };
58
- it("same graph: moves region to target track and updates position", () => {
59
- const { boxGraph } = project;
60
- boxGraph.beginTransaction();
61
- const { trackBox: trackA, regionBox } = createTrackWithRegion(project);
62
- const trackB = TrackBox.create(boxGraph, UUID.generate(), box => {
63
- box.type.setValue(TrackType.Value);
64
- box.tracks.refer(asInstanceOf(trackA.tracks.targetVertex.unwrap().box, AudioUnitBox).tracks);
65
- box.target.refer(trackA.tracks.targetVertex.unwrap().box);
66
- box.index.setValue(1);
67
- });
68
- boxGraph.endTransaction();
69
- boxGraph.beginTransaction();
70
- project.api.copyRegionTo(regionBox, trackB, 500);
71
- boxGraph.endTransaction();
72
- boxGraph.verifyPointers();
73
- expect(regionBox.position.getValue()).toBe(500);
74
- const targetUuid = regionBox.regions.targetVertex.unwrap().box.address.uuid;
75
- expect(UUID.equals(targetUuid, trackB.address.uuid)).toBe(true);
76
- expect(trackB.regions.pointerHub.incoming().length).toBe(1);
77
- expect(trackA.regions.pointerHub.incoming().length).toBe(0);
78
- });
79
- it("different graph: copies region to target track at given position", () => {
80
- const sourceSkeleton = ProjectSkeleton.empty({ createDefaultUser: true, createOutputCompressor: false });
81
- const sourceProject = Project.fromSkeleton(createEnv(), sourceSkeleton, false);
82
- const sourceGraph = sourceProject.boxGraph;
83
- sourceGraph.beginTransaction();
84
- const { regionBox: sourceRegion } = createTrackWithRegion(sourceProject);
85
- sourceGraph.endTransaction();
86
- const { boxGraph } = project;
87
- boxGraph.beginTransaction();
88
- const { trackBox: targetTrack } = createTrackWithRegion(project);
89
- boxGraph.endTransaction();
90
- const regionsBefore = targetTrack.regions.pointerHub.incoming().length;
91
- boxGraph.beginTransaction();
92
- project.api.copyRegionTo(sourceRegion, targetTrack, 500);
93
- boxGraph.endTransaction();
94
- const regionsAfter = targetTrack.regions.pointerHub.incoming();
95
- expect(regionsAfter.length).toBe(regionsBefore + 1);
96
- const copiedRegion = regionsAfter.find(vertex => {
97
- const box = vertex.box;
98
- return box.position.getValue() === 500;
99
- });
100
- expect(copiedRegion).toBeDefined();
101
- const copiedBox = copiedRegion.box;
102
- expect(copiedBox.duration.getValue()).toBe(200);
103
- expect(copiedBox.loopDuration.getValue()).toBe(200);
104
- expect(UUID.equals(copiedBox.address.uuid, sourceRegion.address.uuid)).toBe(false);
105
- boxGraph.verifyPointers();
106
- sourceGraph.verifyPointers();
107
- sourceProject.terminate();
108
- });
109
- });
File without changes