contentTypes, final MixerRole mixerRole,
38 | final double volume, final double pan) {
39 | final Track track = new Track();
40 | track.channel = new Channel();
41 | track.name = name;
42 | track.channel.volume = createRealParameter(Unit.LINEAR, volume);
43 | track.channel.pan = createRealParameter(Unit.NORMALIZED, pan);
44 | track.contentType = contentTypes.toArray(new ContentType[]{});
45 | track.channel.role = mixerRole;
46 | return track;
47 | }
48 |
49 | /**
50 | * Creates an audio media file.
51 | *
52 | * @param relativePath
53 | * The relative path to the audio file
54 | * @param sampleRate
55 | * The sample rate
56 | * @param channels
57 | * The number of channels
58 | * @param duration
59 | * The duration in seconds
60 | * @return The audio instance
61 | */
62 | public static Audio createAudio(final String relativePath, final int sampleRate, final int channels,
63 | final double duration) {
64 | final var audio = new Audio();
65 | audio.timeUnit = TimeUnit.SECONDS;
66 | audio.file = new FileReference();
67 | audio.file.path = relativePath;
68 | audio.file.external = Boolean.FALSE;
69 | audio.sampleRate = sampleRate;
70 | audio.channels = channels;
71 | audio.duration = duration;
72 | return audio;
73 | }
74 |
75 | /**
76 | * Creates a warp.
77 | *
78 | * @param time
79 | * The warped time
80 | * @param contentTime
81 | * The time of the content
82 | * @return The warp instance
83 | */
84 | public static Warp createWarp(final double time, final double contentTime) {
85 | final var warp = new Warp();
86 | warp.time = time;
87 | warp.contentTime = contentTime;
88 | return warp;
89 | }
90 |
91 | /**
92 | * Creates a clip.
93 | *
94 | * @param content
95 | * The content of the clip
96 | * @param time
97 | * The time position of the clip
98 | * @param duration
99 | * The duration of the clip
100 | * @return The clip instance
101 | */
102 | public static Clip createClip(final Timeline content, final double time, final double duration) {
103 | final var clip = new Clip();
104 | clip.content = content;
105 | clip.time = time;
106 | clip.duration = Double.valueOf(duration);
107 | return clip;
108 | }
109 |
110 | /**
111 | * Creates a Clips object from several clips.
112 | *
113 | * @param clips
114 | * The clips to wrap
115 | * @return The Clips instance
116 | */
117 | public static Clips createClips(final Clip... clips) {
118 | final var timeline = new Clips();
119 | Collections.addAll(timeline.clips, clips);
120 | return timeline;
121 | }
122 |
123 | /**
124 | * Creates a marker.
125 | *
126 | * @param time
127 | * The time position of the marker
128 | * @param name
129 | * The name of the marker
130 | * @return The marker instance
131 | */
132 | public static Marker createMarker(final double time, final String name) {
133 | final var markerEvent = new Marker();
134 | markerEvent.time = time;
135 | markerEvent.name = name;
136 | return markerEvent;
137 | }
138 |
139 | /**
140 | * Creates a point for an automation envelope.
141 | *
142 | * @param time
143 | * The time position of the point
144 | * @param value
145 | * The value of the point
146 | * @param interpolation
147 | * The interpolation to use
148 | * @return The point instance
149 | */
150 | public static RealPoint createPoint(final double time, final double value, final Interpolation interpolation) {
151 | final var point = new RealPoint();
152 | point.time = Double.valueOf(time);
153 | point.value = Double.valueOf(value);
154 | point.interpolation = interpolation;
155 | return point;
156 | }
157 |
158 | /**
159 | * Create a real parameter instance.
160 | *
161 | * @param unit
162 | * The unit of the parameter
163 | * @param value
164 | * The value of the parameter
165 | * @return The parameter
166 | */
167 | public static RealParameter createRealParameter(final Unit unit, final double value) {
168 | final RealParameter param = new RealParameter();
169 | param.unit = unit;
170 | param.value = Double.valueOf(value);
171 | return param;
172 | }
173 |
174 | /**
175 | * Create a real parameter instance.
176 | *
177 | * @param unit
178 | * The unit of the parameter
179 | * @param min
180 | * The minimum value of the parameter
181 | * @param max
182 | * The maximum value of the parameter
183 | * @param value
184 | * The value of the parameter
185 | * @return The parameter
186 | */
187 | public static RealParameter createRealParameter(final Unit unit, final double min, final double max,
188 | final double value) {
189 | final RealParameter param = createRealParameter(unit, value);
190 | param.min = Double.valueOf(min);
191 | param.max = Double.valueOf(max);
192 | return param;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/AuPlugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /** The Apple AU plug-in format. */
6 | @XmlRootElement(name = "AuPlugin")
7 | public class AuPlugin extends Plugin {
8 | // Intentionally empty
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/BuiltinDevice.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 | import jakarta.xml.bind.annotation.XmlSeeAlso;
5 |
6 | /** The built-in plug-in format. */
7 | @XmlRootElement(name = "BuiltinDevice")
8 | @XmlSeeAlso({Equalizer.class, Compressor.class, NoiseGate.class, Limiter.class})
9 | public class BuiltinDevice extends Device {
10 | // Intentionally empty
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/ClapPlugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /**
6 | * A CLAP Plug-in instance.
7 | *
8 | *
9 | * The CLAP plug-in state should be stored in .clap-preset format.
10 | */
11 | @XmlRootElement(name = "ClapPlugin")
12 | public class ClapPlugin extends Plugin {
13 | // Intentionally empty
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Compressor.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlElement;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | import com.bitwig.dawproject.BoolParameter;
7 | import com.bitwig.dawproject.RealParameter;
8 |
9 | /** A generic 'built-in' compressor. */
10 | @XmlRootElement(name = "Compressor")
11 | public class Compressor extends BuiltinDevice {
12 | /** The threshold setting of the compressor in dB. */
13 | @XmlElement(name = "Threshold")
14 | public RealParameter threshold;
15 |
16 | /** The ratio setting of the compressor in percent (0-100). */
17 | @XmlElement(name = "Ratio")
18 | public RealParameter ratio;
19 |
20 | /** The attack setting of the compressor in seconds. */
21 | @XmlElement(name = "Attack")
22 | public RealParameter attack;
23 |
24 | /** The release setting of the compressor in seconds. */
25 | @XmlElement(name = "Release")
26 | public RealParameter release;
27 |
28 | /** Pre-compression gain stage. (input/gain/drive) in dB. */
29 | @XmlElement(name = "InputGain")
30 | public RealParameter inputGain;
31 |
32 | /** Post-compression gain stage. (output/makeup gain) in dB. */
33 | @XmlElement(name = "OutputGain")
34 | public RealParameter outputGain;
35 |
36 | /** Should auto makeup be applied? */
37 | @XmlElement(name = "AutoMakeup")
38 | public BoolParameter autoMakeup;
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Device.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlAttribute;
7 | import jakarta.xml.bind.annotation.XmlElement;
8 | import jakarta.xml.bind.annotation.XmlElementRef;
9 | import jakarta.xml.bind.annotation.XmlElementWrapper;
10 | import jakarta.xml.bind.annotation.XmlRootElement;
11 | import jakarta.xml.bind.annotation.XmlSeeAlso;
12 |
13 | import com.bitwig.dawproject.BoolParameter;
14 | import com.bitwig.dawproject.FileReference;
15 | import com.bitwig.dawproject.Parameter;
16 | import com.bitwig.dawproject.Referenceable;
17 |
18 | /** Either a Plug-in or native Device with in a DAW. */
19 | @XmlRootElement(name = "Device")
20 | @XmlSeeAlso({Vst2Plugin.class, Vst3Plugin.class, ClapPlugin.class, BuiltinDevice.class, AuPlugin.class,
21 | Parameter.class})
22 | public class Device extends Referenceable {
23 | /** This device is enabled (as in not bypassed). */
24 | @XmlElement(name = "Enabled")
25 | public BoolParameter enabled;
26 |
27 | /** Role of this device/plug-in. */
28 | @XmlAttribute(required = true)
29 | public DeviceRole deviceRole;
30 |
31 | /** If this device/plug-in is loaded/active of not. */
32 | @XmlAttribute
33 | public Boolean loaded = Boolean.TRUE;
34 |
35 | /** Name of the device/plugin */
36 | @XmlAttribute(required = true)
37 | public String deviceName;
38 |
39 | /**
40 | * Unique identifier of device/plug-in.
41 | * Standards which use UUID as an identifier use the canonical textual
42 | * representation of the UUID (8-4-4-4-12 with no braces) (VST3)
43 | * Standards which use an integer as an identifier use the value in decimal
44 | * form. (base-10 unsigned) (VST2)
45 | * Text-based identifiers are used as-is. (CLAP)
46 | */
47 | @XmlAttribute
48 | public String deviceID;
49 |
50 | /** Vendor name of the device/plugin */
51 | @XmlAttribute
52 | public String deviceVendor;
53 |
54 | /**
55 | * Path to a file representing the device / plug-in state in its native format.
56 | *
57 | *
58 | * This file must be embedded inside the container ZIP and have the
59 | * FileReference configured with (external=false).
60 | */
61 | @XmlElement(name = "State", required = false)
62 | public FileReference state;
63 |
64 | /**
65 | * Parameters for this device, which is required for automated parameters in
66 | * order to provide an ID.
67 | * Note: If the automated parameter is already present like the BuiltinDevice
68 | * parameters, it should not be included here as well.
69 | */
70 | @XmlElementWrapper(name = "Parameters", required = false)
71 | @XmlElementRef
72 | public List automatedParameters = new ArrayList<>();
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/DeviceRole.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 | import jakarta.xml.bind.annotation.XmlEnumValue;
5 |
6 | /** The role of a device. */
7 | @XmlEnum
8 | public enum DeviceRole {
9 | /** An instrument device. */
10 | @XmlEnumValue("instrument")
11 | INSTRUMENT,
12 |
13 | /** A note/MIDI effect device. */
14 | @XmlEnumValue("noteFX")
15 | NOTE_FX,
16 |
17 | /** An audio effect device. */
18 | @XmlEnumValue("audioFX")
19 | AUDIO_FX,
20 |
21 | /** An analyzer device. */
22 | @XmlEnumValue("analyzer")
23 | ANALYZER
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/EqBand.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlElement;
5 |
6 | import com.bitwig.dawproject.BoolParameter;
7 | import com.bitwig.dawproject.RealParameter;
8 |
9 | /** The parameters of a band of a generic 'built-in' equalizer. */
10 | public class EqBand {
11 | /** The frequency setting of the band. */
12 | @XmlElement(name = "Freq", required = true)
13 | public RealParameter freq;
14 |
15 | /** The gain setting of the band. */
16 | @XmlElement(name = "Gain")
17 | public RealParameter gain;
18 |
19 | /** The Q setting of the band. */
20 | @XmlElement(name = "Q")
21 | public RealParameter Q;
22 |
23 | /** The enabled state of the band. */
24 | @XmlElement(name = "Enabled")
25 | public BoolParameter enabled;
26 |
27 | /** The filter type of the band. */
28 | @XmlAttribute(required = true)
29 | public EqBandType type;
30 |
31 | /** The index of the band. */
32 | @XmlAttribute
33 | public Integer order;
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/EqBandType.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 | import jakarta.xml.bind.annotation.XmlEnumValue;
5 |
6 | /** A type of a band of a generic 'built-in' equalizer. */
7 | @XmlEnum
8 | public enum EqBandType {
9 | /** A high pass filter. */
10 | @XmlEnumValue("highPass")
11 | HIGH_PASS,
12 | /** A low pass filter. */
13 | @XmlEnumValue("lowPass")
14 | LOW_PASS,
15 | /** A band pass filter. */
16 | @XmlEnumValue("bandPass")
17 | BAND_PASS,
18 | /** A high shelf filter. */
19 | @XmlEnumValue("highShelf")
20 | HIGH_SHELF,
21 | /** A low shelf filter. */
22 | @XmlEnumValue("lowShelf")
23 | LOW_SHELF,
24 | /** A bell filter. */
25 | @XmlEnumValue("bell")
26 | BELL,
27 | /** A notch filter. */
28 | @XmlEnumValue("notch")
29 | NOTCH
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Equalizer.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlElement;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | import com.bitwig.dawproject.RealParameter;
10 |
11 | /** A generic 'built-in' equalizer. */
12 | @XmlRootElement(name = "Equalizer")
13 | public class Equalizer extends BuiltinDevice {
14 | /** The bands of the equalizer. */
15 | @XmlElement(name = "Band")
16 | public List bands = new ArrayList<>();
17 |
18 | /** The input gain of the equalizer in dB. */
19 | @XmlElement(name = "InputGain")
20 | public RealParameter inputGain;
21 |
22 | /** The output gain of the equalizer in dB. */
23 | @XmlElement(name = "OutputGain")
24 | public RealParameter outputGain;
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Limiter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlElement;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | import com.bitwig.dawproject.RealParameter;
7 |
8 | /** A generic 'built-in' limiter. */
9 | @XmlRootElement(name = "Limiter")
10 | public class Limiter extends BuiltinDevice {
11 | /** The threshold setting of the limiter in dB. */
12 | @XmlElement(name = "Threshold")
13 | public RealParameter threshold;
14 |
15 | /** The input gain of the limiter in dB. */
16 | @XmlElement(name = "InputGain")
17 | public RealParameter inputGain;
18 |
19 | /** The output gain of the limiter in dB. */
20 | @XmlElement(name = "OutputGain")
21 | public RealParameter outputGain;
22 |
23 | /** The attack setting of the limiter in seconds. */
24 | @XmlElement(name = "Attack")
25 | public RealParameter attack;
26 |
27 | /** The release setting of the limiter in seconds. */
28 | @XmlElement(name = "Release")
29 | public RealParameter release;
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/NoiseGate.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlElement;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | import com.bitwig.dawproject.RealParameter;
7 |
8 | /** A generic 'built-in' noise gate. */
9 | @XmlRootElement(name = "NoiseGate")
10 | public class NoiseGate extends BuiltinDevice {
11 | /** The threshold of the noise gate in dB. */
12 | @XmlElement(name = "Threshold")
13 | public RealParameter threshold;
14 |
15 | /** The ratio of the noise gate in percent (0-100). */
16 | @XmlElement(name = "Ratio")
17 | public RealParameter ratio;
18 |
19 | /** The attack of the noise gate in seconds. */
20 | @XmlElement(name = "Attack")
21 | public RealParameter attack;
22 |
23 | /** The release of the noise gate in seconds. */
24 | @XmlElement(name = "Release")
25 | public RealParameter release;
26 |
27 | /** Range or amount of maximum gain reduction. Possible range [-inf to 0] */
28 | @XmlElement(name = "Range")
29 | public RealParameter range;
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Plugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 |
5 | /** Abstract base class for all plug-in formats. */
6 | public abstract class Plugin extends Device {
7 | /** Version of the plug-in */
8 | @XmlAttribute
9 | public String pluginVersion;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Vst2Plugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /**
6 | * A VST2 Plug-in instance.
7 | *
8 | *
9 | * The VST2 plug-in state should be stored in FXB or FXP format.
10 | */
11 | @XmlRootElement(name = "Vst2Plugin")
12 | public class Vst2Plugin extends Plugin {
13 | // Intentionally empty
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/device/Vst3Plugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /**
6 | * A VST3 Plug-in instance.
7 | *
8 | *
9 | * The VST3 plug-in state should be stored in .vstpreset format.
10 | */
11 | @XmlRootElement(name = "Vst3Plugin")
12 | public class Vst3Plugin extends Plugin {
13 | // Intentionally empty
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Audio.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /**
7 | * Representation of an audio file as a timeline. Duration should be the entire
8 | * length of the file, any clipping should be done by placing the Audio element
9 | * within a Clip element. The timeUnit attribute should always be set to
10 | * seconds.
11 | */
12 | @XmlRootElement(name = "Audio")
13 | public class Audio extends MediaFile {
14 | /** Sample-rate of audio-file. */
15 | @XmlAttribute(required = true)
16 | public int sampleRate;
17 |
18 | /** Number of channels of audio-file (1=mono, 2=stereo...). */
19 | @XmlAttribute(required = true)
20 | public int channels;
21 |
22 | /** Playback algorithm used to warp audio (vendor-specific). */
23 | @XmlAttribute(required = false)
24 | public String algorithm;
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/AutomationTarget.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlIDREF;
5 |
6 | import com.bitwig.dawproject.ExpressionType;
7 | import com.bitwig.dawproject.Parameter;
8 |
9 | /**
10 | * Defines the target of automation or expression, usually used within a Points
11 | * element.
12 | *
13 | *
14 | * Either it points directly ot a parameter or an expression, and in the
15 | * expression case it can either be monophonic (such as MIDI CCs) or
16 | * per-note/polyphonic (such as poly pressure)
17 | */
18 | public class AutomationTarget {
19 | /** Parameter to automate. */
20 | @XmlIDREF
21 | @XmlAttribute(required = false)
22 | public Parameter parameter;
23 |
24 | /** Expression type to control. */
25 | @XmlAttribute(required = false)
26 | public ExpressionType expression;
27 |
28 | /** MIDI channel */
29 | @XmlAttribute(required = false)
30 | public Integer channel;
31 |
32 | /**
33 | * MIDI key.
34 | *
35 | *
36 | * Used when expression="polyPressure".
37 | */
38 | @XmlAttribute(required = false)
39 | public Integer key;
40 |
41 | /**
42 | * MIDI Channel Controller Number (0 based index).
43 | *
44 | *
45 | * Used when expression="channelController".
46 | */
47 | @XmlAttribute(required = false)
48 | public Integer controller;
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/BoolPoint.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /** A single automation point for a boolean value. */
7 | @XmlRootElement(name = "BoolPoint")
8 | public class BoolPoint extends Point {
9 | /** Boolean value of this point (true/false). */
10 | @XmlAttribute(required = true)
11 | public Boolean value;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Clip.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlElementRef;
5 | import jakarta.xml.bind.annotation.XmlIDREF;
6 | import jakarta.xml.bind.annotation.XmlRootElement;
7 |
8 | import com.bitwig.dawproject.Nameable;
9 |
10 | /**
11 | * A Clip provides a clipped view on to a Timeline, and is used either on a
12 | * Clips timeline (typically for arrangements) or inside a ClipSlot element (for
13 | * clip launcher Scenes). A Clip must either have a child-element inheriting
14 | * from Timeline or provide a ID reference to a timeline somewhere else (for
15 | * linked/alias clips).
16 | */
17 | @XmlRootElement(name = "Clip")
18 | public class Clip extends Nameable {
19 | /** Time on the parent timeline where this clips starts playing. */
20 | @XmlAttribute(required = true)
21 | public double time;
22 |
23 | /**
24 | * Duration on the parent timeline of this clip.
25 | * If duration is omitted, it should be inferred from the playStop - playStart
26 | * instead.
27 | * This is particularity useful when timeUnit and contentTimeUnit are different,
28 | * like when placing an audio clip with content length defined in seconds onto
29 | * an arrangement defined in beats.
30 | */
31 | @XmlAttribute(required = false)
32 | public Double duration;
33 |
34 | /**
35 | * The TimeUnit used by the scope inside this timeline. This affects the
36 | * content/reference, playStart, playStop, loopStart, loopEnd but not time and
37 | * duration which are using the TimeUnit of the parent scope.
38 | */
39 | @XmlAttribute(required = false)
40 | public TimeUnit contentTimeUnit;
41 |
42 | /**
43 | * Time inside the content timeline (or reference) where the clip starts
44 | * playing.
45 | */
46 | @XmlAttribute(required = false)
47 | public Double playStart;
48 |
49 | /**
50 | * Time inside the content timeline (or reference) where the clip stops playing.
51 | */
52 | @XmlAttribute(required = false)
53 | public Double playStop;
54 |
55 | /**
56 | * Time inside the content timeline (or reference) where the clip loop starts.
57 | */
58 | @XmlAttribute(required = false)
59 | public Double loopStart;
60 |
61 | /** Time inside the content timeline (or reference) where the clip loop ends. */
62 | @XmlAttribute(required = false)
63 | public Double loopEnd;
64 |
65 | /** The TimeUnit used by the fadeInTime and fadeOutTime. */
66 | @XmlAttribute(required = false)
67 | public TimeUnit fadeTimeUnit;
68 |
69 | /**
70 | * Duration of fade-in.
71 | *
72 | *
73 | * To create cross-fade, use a negative value which will make this Clip start at
74 | * t = time - abs(fadeInTime)
75 | */
76 | @XmlAttribute(required = false)
77 | public Double fadeInTime;
78 |
79 | /** Duration of fade-out. */
80 | @XmlAttribute(required = false)
81 | public Double fadeOutTime;
82 |
83 | /** Whether this clip should be played back. Default value is true. */
84 | @XmlAttribute(required = false)
85 | public Boolean enable;
86 |
87 | /** Content Timeline this clip is playing. */
88 | @XmlElementRef(required = false)
89 | public Timeline content;
90 |
91 | /**
92 | * Reference to a Content Timeline this clip is playing, in case of linked/alias
93 | * clips. You can use either content or reference for one clip, but not both.
94 | */
95 | @XmlAttribute(required = false)
96 | @XmlIDREF
97 | public Timeline reference;
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/ClipSlot.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlElement;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 |
7 | /**
8 | * Represent a clip launcher slot within a Scene which can contain a Clip. It is
9 | * generally set to a specific track.
10 | */
11 | @XmlRootElement(name = "ClipSlot")
12 | public class ClipSlot extends Timeline {
13 | /**
14 | * Whether launching this slot should stop the track playback when this slot is
15 | * empty.
16 | */
17 | @XmlAttribute(required = false)
18 | public Boolean hasStop;
19 |
20 | /** Contained clip. */
21 | @XmlElement(name = "Clip", required = false)
22 | public Clip clip;
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Clips.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlElement;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | /**
10 | * Represents a timeline of clips. Each contained Clip have its time and
11 | * duration that defines its location on this timeline (defined by timeUnit of
12 | * the Clips element).
13 | */
14 | @XmlRootElement(name = "Clips")
15 | public class Clips extends Timeline {
16 | /** Clips of this timeline. */
17 | @XmlElement(name = "Clip")
18 | public List clips = new ArrayList<>();
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/EnumPoint.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /** A single automation point for an enumerated value. */
7 | @XmlRootElement(name = "EnumPoint")
8 | public class EnumPoint extends Point {
9 | /** Integer value of the Enum index for this point. */
10 | @XmlAttribute(required = true)
11 | public Integer value;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/IntegerPoint.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /** A single automation point for an integer value. */
7 | @XmlRootElement(name = "IntegerPoint")
8 | public class IntegerPoint extends Point {
9 | /** Integer value of this point. */
10 | @XmlAttribute(required = true)
11 | public Integer value;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Lanes.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlElementRef;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | /**
10 | * The Lanes element provides the ability to contain multiple parallel timelines
11 | * inside it, and is the main layering element of the format. It is also a
12 | * natural fit for defining the scope of contained timelines to a specific
13 | * track.
14 | */
15 | @XmlRootElement(name = "Lanes")
16 | public class Lanes extends Timeline {
17 | /** Lanes representing nested content. */
18 | @XmlElementRef
19 | public List lanes = new ArrayList<>();
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Marker.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | import com.bitwig.dawproject.Nameable;
7 |
8 | /** A single cue-marker. */
9 | @XmlRootElement(name = "Marker")
10 | public class Marker extends Nameable {
11 | /** Time on the parent timeline of this marker. */
12 | @XmlAttribute(required = true)
13 | public double time;
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Markers.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlElement;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | /** Represents a timeline of cue-markers. */
10 | @XmlRootElement
11 | public class Markers extends Timeline {
12 | /** Markers of this timeline. */
13 | @XmlElement(required = true, name = "Marker")
14 | public List markers = new ArrayList<>();
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/MediaFile.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlElement;
5 |
6 | import com.bitwig.dawproject.FileReference;
7 |
8 | /**
9 | * A media file. (audio or video).
10 | *
11 | *
12 | * The duration attribute is intended to be provide the file length (and not be
13 | * interpreted as a playback parameter, use a Clip or Warps element for that).
14 | */
15 | public class MediaFile extends Timeline {
16 | /** The media file. */
17 | @XmlElement(name = "File", required = true)
18 | public FileReference file = new FileReference();
19 |
20 | /** Duration in seconds of the media file (as stored). */
21 | @XmlAttribute(required = true)
22 | public double duration;
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Note.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlElementRef;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 | import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
7 |
8 | import com.bitwig.dawproject.DoubleAdapter;
9 |
10 | /**
11 | * A single Note (MIDI-style). It can additionally contain child timelines to
12 | * hold per-note expression.
13 | */
14 | @XmlRootElement(name = "Note")
15 | public final class Note {
16 | /** Time on the parent timeline where this note starts playing. */
17 | @XmlAttribute(required = true)
18 | @XmlJavaTypeAdapter(DoubleAdapter.class)
19 | public Double time;
20 |
21 | /** Duration on the parent timeline of this note. */
22 | @XmlAttribute(required = true)
23 | @XmlJavaTypeAdapter(DoubleAdapter.class)
24 | public Double duration;
25 |
26 | /** MIDI channel of this note. */
27 | @XmlAttribute(required = false)
28 | public int channel;
29 |
30 | /** MIDI key of this note. */
31 | @XmlAttribute(required = true)
32 | public int key;
33 |
34 | /** Note On Velocity of this note. (normalized) */
35 | @XmlAttribute(name = "vel")
36 | @XmlJavaTypeAdapter(DoubleAdapter.class)
37 | public Double velocity;
38 |
39 | /** Note Off Velocity of this note. (normalized) */
40 | @XmlAttribute(name = "rel")
41 | @XmlJavaTypeAdapter(DoubleAdapter.class)
42 | public Double releaseVelocity;
43 |
44 | /** Per-note expressions can be stored within the note object as timelines. */
45 | @XmlElementRef(name = "Content", required = false)
46 | public Timeline content;
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Notes.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlElement;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | /** Timeline containing Notes (MIDI-style) */
10 | @XmlRootElement(name = "Notes")
11 | public class Notes extends Timeline {
12 | /** Contained notes. */
13 | @XmlElement(name = "Note")
14 | public List notes = new ArrayList<>();
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Point.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 | import jakarta.xml.bind.annotation.XmlSeeAlso;
6 | import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
7 |
8 | import com.bitwig.dawproject.DoubleAdapter;
9 |
10 | /** A single automation point (abstract class). */
11 | @XmlRootElement(name = "Point")
12 | @XmlSeeAlso({RealPoint.class, EnumPoint.class, BoolPoint.class, IntegerPoint.class, TimeSignaturePoint.class})
13 | public abstract class Point {
14 | /** Time (within enclosing Points timeline) of this event */
15 | @XmlJavaTypeAdapter(DoubleAdapter.class)
16 | @XmlAttribute(required = true)
17 | public Double time;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Points.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlAttribute;
7 | import jakarta.xml.bind.annotation.XmlElement;
8 | import jakarta.xml.bind.annotation.XmlElementRef;
9 | import jakarta.xml.bind.annotation.XmlRootElement;
10 | import jakarta.xml.bind.annotation.XmlType;
11 |
12 | import com.bitwig.dawproject.Unit;
13 |
14 | /**
15 | * A timeline of points for automation or expression.
16 | *
17 | *
18 | * All the points should be of the same element-type and match the target.
19 | */
20 | @XmlRootElement(name = "Points")
21 | @XmlType(propOrder = {"target", "points", "unit"})
22 | public class Points extends Timeline {
23 | /** The parameter or expression this timeline should target. */
24 | @XmlElement(name = "Target", required = true)
25 | public AutomationTarget target = new AutomationTarget();
26 |
27 | /**
28 | * The contained points. They should all be of the same type and match the
29 | * target parameter.
30 | */
31 | @XmlElementRef(required = true)
32 | public List points = new ArrayList<>();
33 |
34 | /** A unit should be provided for when used with RealPoint elements. */
35 | @XmlAttribute(required = false)
36 | public Unit unit;
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/RealPoint.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 | import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
6 |
7 | import com.bitwig.dawproject.DoubleAdapter;
8 | import com.bitwig.dawproject.Interpolation;
9 |
10 | /**
11 | * A point with a double resolution and additional interpolation information.
12 | */
13 | @XmlRootElement(name = "RealPoint")
14 | public class RealPoint extends Point {
15 | /** The value of the point. */
16 | @XmlJavaTypeAdapter(DoubleAdapter.class)
17 | @XmlAttribute(required = true)
18 | public Double value;
19 |
20 | /**
21 | * Interpolation mode used for the segment starting at this point. Default to
22 | * 'hold' when unspecified.
23 | */
24 | @XmlAttribute(required = false)
25 | public Interpolation interpolation;
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/TimeSignaturePoint.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /** A single automation point for a time-signature value. */
7 | @XmlRootElement(name = "TimeSignaturePoint")
8 | public class TimeSignaturePoint extends Point {
9 | /** Numerator of the time-signature. (3/4 → 3, 4/4 → 4) */
10 | @XmlAttribute(required = true)
11 | public Integer numerator;
12 |
13 | /** Denominator of the time-signature. (3/4 → 4, 7/8 → 8) */
14 | @XmlAttribute(required = true)
15 | public Integer denominator;
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/TimeUnit.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 | import jakarta.xml.bind.annotation.XmlEnumValue;
5 |
6 | /** The unit of time. */
7 | @XmlEnum
8 | public enum TimeUnit {
9 | /** Time is represented in beats (quarter-notes). */
10 | @XmlEnumValue("beats")
11 | BEATS,
12 | /** Time is represented in seconds. */
13 | @XmlEnumValue("seconds")
14 | SECONDS;
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Timeline.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlIDREF;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 | import jakarta.xml.bind.annotation.XmlSeeAlso;
7 |
8 | import com.bitwig.dawproject.Referenceable;
9 | import com.bitwig.dawproject.Track;
10 |
11 | /** Abstract base class for all timeline structures. */
12 | @XmlRootElement(name = "Timeline")
13 | @XmlSeeAlso({Note.class, Notes.class, Lanes.class, Clip.class, Clips.class, ClipSlot.class, Marker.class, Markers.class,
14 | Warps.class, Audio.class, Video.class, Point.class, Points.class})
15 | public abstract class Timeline extends Referenceable {
16 | /** When present, the timeline is local to this track. */
17 | @XmlAttribute(required = false)
18 | @XmlIDREF
19 | public Track track;
20 |
21 | /**
22 | * The TimeUnit used by this and nested timelines. If no TimeUnit is provided by
23 | * this or the parent scope then 'beats' will be used.
24 | */
25 | @XmlAttribute(required = false)
26 | public TimeUnit timeUnit;
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Video.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /**
7 | * Representation of a video file as a timeline. Duration should be the entire
8 | * length of the file, any clipping should be done by placing the Audio element
9 | * within a Clip element. The timeUnit attribute should always be set to
10 | * seconds.
11 | */
12 | @XmlRootElement(name = "Video")
13 | public class Video extends MediaFile {
14 | /** Sample-rate of audio (if present). */
15 | @XmlAttribute(required = false)
16 | public int sampleRate;
17 |
18 | /** Number of channels of audio (1=mono..., if present). */
19 | @XmlAttribute(required = false)
20 | public int channels;
21 |
22 | /** Playback algorithm used to warp audio (vendor-specific, if present). */
23 | @XmlAttribute(required = false)
24 | public String algorithm;
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Warp.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /**
7 | * A single warp event, which defines the time both on the outer scope (time)
8 | * and the inner scope (contentTime). The time range between the Warp events are
9 | * assumed to be linearly interpolated.
10 | */
11 | @XmlRootElement(name = "Warp")
12 | public class Warp {
13 | /**
14 | * The time this point represent to the 'outside' of the Warps element. The
15 | * TimeUnit is defined by the parent Warps element timeUnit attribute or
16 | * inherited from the parent element of the Warps container
17 | */
18 | @XmlAttribute(required = true)
19 | public double time;
20 |
21 | /**
22 | * The time this point represent to the 'inside' of the Warps element. The
23 | * TimeUnit is defined by the parent Warps element contentTimeUnit attribute
24 | */
25 | @XmlAttribute(required = true)
26 | public double contentTime;
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/bitwig/dawproject/timeline/Warps.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import jakarta.xml.bind.annotation.XmlAttribute;
7 | import jakarta.xml.bind.annotation.XmlElement;
8 | import jakarta.xml.bind.annotation.XmlElementRef;
9 | import jakarta.xml.bind.annotation.XmlRootElement;
10 |
11 | /**
12 | * Warps the time of nested content as defined by a list of Warp points.
13 | *
14 | *
15 | * A typical use case would be to warp an audio-file (contentTimeUnit = seconds)
16 | * onto a timeline defined in beats (timeUnit = beats) as defined by a set of
17 | * Warp events.
18 | *
19 | *
20 | * At least two Warp events need to present in order to define a usable
21 | * beats/seconds conversion. For a plain fixed-speed mapping, provide two event:
22 | * One at (0,0) and a second event with the desired beat-time length along with
23 | * the length of the contained Audio file in seconds.
24 | *
25 | *
{@code
26 | *
27 | *
28 | *
29 | *
30 | *
31 | *
32 | *
33 | *
34 | *
35 | *
36 | * }
37 | */
38 | @XmlRootElement(name = "Warps")
39 | public class Warps extends Timeline {
40 | /** Warp events defining the transformation (minimum 2). */
41 | @XmlElement(required = true, name = "Warp")
42 | public List events = new ArrayList<>();
43 |
44 | /** Content timeline to be warped. */
45 | @XmlElementRef(name = "Content", required = true)
46 | public Timeline content;
47 |
48 | /**
49 | * The TimeUnit used by the content (nested) timeline and the contentTime
50 | * attribute of the Warp events.
51 | */
52 | @XmlAttribute(required = true)
53 | public TimeUnit contentTimeUnit;
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | /** Module definition for dawproject. */
2 | module com.bitwig.dawproject
3 | {
4 | requires jakarta.xml.bind;
5 | requires org.apache.commons.io;
6 |
7 |
8 | opens com.bitwig.dawproject to jakarta.xml.bind;
9 | opens com.bitwig.dawproject.device to jakarta.xml.bind;
10 | opens com.bitwig.dawproject.timeline to jakarta.xml.bind;
11 |
12 |
13 | exports com.bitwig.dawproject;
14 | exports com.bitwig.dawproject.device;
15 | exports com.bitwig.dawproject.timeline;
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/java/com/bitwig/dawproject/DawProjectTest.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.ArrayList;
6 | import java.util.EnumSet;
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.Set;
10 | import java.util.function.BiConsumer;
11 |
12 | import com.bitwig.dawproject.device.Device;
13 | import com.bitwig.dawproject.device.DeviceRole;
14 | import com.bitwig.dawproject.device.Vst3Plugin;
15 | import com.bitwig.dawproject.timeline.Clip;
16 | import com.bitwig.dawproject.timeline.Clips;
17 | import com.bitwig.dawproject.timeline.Lanes;
18 | import com.bitwig.dawproject.timeline.Markers;
19 | import com.bitwig.dawproject.timeline.Note;
20 | import com.bitwig.dawproject.timeline.Notes;
21 | import com.bitwig.dawproject.timeline.Points;
22 | import com.bitwig.dawproject.timeline.TimeUnit;
23 | import com.bitwig.dawproject.timeline.Warps;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | /** Several tests for the reading/writing DAWproject files. */
28 | public class DawProjectTest {
29 | private enum Features {
30 | CUE_MARKERS, CLIPS, AUDIO, NOTES, AUTOMATION, ALIAS_CLIPS, PLUGINS
31 | }
32 |
33 | private enum AudioScenario {
34 | WARPED, RAW_BEATS, RAW_SECONDS, FILE_WITH_ABSOLUTE_PATH, FILE_WITH_RELATIVE_PATH
35 | }
36 |
37 | private final Set simpleFeatures = EnumSet.of(Features.CLIPS, Features.NOTES, Features.AUDIO);
38 |
39 | /**
40 | * Validate a project.
41 | *
42 | * @throws IOException
43 | * Could not validate the project
44 | */
45 | @Test
46 | public void validateDawProject() throws IOException {
47 | final Project project = createDummyProject(3, this.simpleFeatures);
48 | DawProject.validate(project);
49 | }
50 |
51 | /**
52 | * Validate a complex project.
53 | *
54 | * @throws IOException
55 | * Could not validate the project
56 | */
57 | @Test
58 | public void validateComplexDawProject() throws IOException {
59 | final Project project = createDummyProject(3, EnumSet.allOf(Features.class));
60 | DawProject.validate(project);
61 | }
62 |
63 | /**
64 | * Test storing a project.
65 | *
66 | * @throws IOException
67 | * Could not store the project
68 | */
69 | @Test
70 | public void saveDawProject() throws IOException {
71 | final Project project = createDummyProject(3, this.simpleFeatures);
72 | final MetaData metadata = new MetaData();
73 |
74 | final Map embeddedFiles = new HashMap<>();
75 | final File file = new File("target/test.dawproject");
76 | DawProject.save(project, metadata, embeddedFiles, file);
77 | DawProject.saveXML(project, new File("target/test.dawproject.xml"));
78 | Assert.assertTrue(file.exists());
79 | }
80 |
81 | /**
82 | * Test storing and loading a project.
83 | *
84 | * @throws IOException
85 | * Could not load the project
86 | */
87 | @Test
88 | public void saveAndLoadDawProject() throws IOException {
89 | final Project project = createDummyProject(5, this.simpleFeatures);
90 | final MetaData metadata = new MetaData();
91 |
92 | final var file = File.createTempFile("testfile", ".dawproject");
93 | final Map embeddedFiles = new HashMap<>();
94 | DawProject.save(project, metadata, embeddedFiles, file);
95 |
96 | final var loadedProject = DawProject.loadProject(file);
97 |
98 | Assert.assertEquals(project.structure.size(), loadedProject.structure.size());
99 | Assert.assertEquals(project.scenes.size(), loadedProject.scenes.size());
100 | }
101 |
102 | /**
103 | * Test storing a complex project.
104 | *
105 | * @throws IOException
106 | * Could not store the project
107 | */
108 | @Test
109 | public void saveComplexDawProject() throws IOException {
110 | final Project project = createDummyProject(3, EnumSet.allOf(Features.class));
111 | final MetaData metadata = new MetaData();
112 |
113 | final Map embeddedFiles = new HashMap<>();
114 | final File file = new File("target/test-complex.dawproject");
115 | DawProject.save(project, metadata, embeddedFiles, file);
116 | DawProject.saveXML(project, new File("target/test-complex.dawproject.xml"));
117 | Assert.assertTrue(file.exists());
118 | }
119 |
120 | /**
121 | * Test storing and loading a complex project.
122 | *
123 | * @throws IOException
124 | * Could not store or load the project
125 | */
126 | @Test
127 | public void saveAndLoadComplexDawProject() throws IOException {
128 | final Project project = createDummyProject(5, EnumSet.allOf(Features.class));
129 | final MetaData metadata = new MetaData();
130 |
131 | final Map embeddedFiles = new HashMap<>();
132 | final var file = File.createTempFile("testfile2", ".dawproject");
133 | DawProject.save(project, metadata, embeddedFiles, file);
134 |
135 | final var loadedProject = DawProject.loadProject(file);
136 |
137 | Assert.assertEquals(project.structure.size(), loadedProject.structure.size());
138 | Assert.assertEquals(project.scenes.size(), loadedProject.scenes.size());
139 | Assert.assertEquals(project.arrangement.lanes.getClass(), loadedProject.arrangement.lanes.getClass());
140 | Assert.assertEquals(project.arrangement.markers.getClass(), loadedProject.arrangement.markers.getClass());
141 | }
142 |
143 | /**
144 | * Test creating the metadata XML schema.
145 | *
146 | * @throws IOException
147 | * Could not create the XML schema
148 | */
149 | @Test
150 | public void writeMetadataSchema() throws IOException {
151 | final File file = new File("MetaData.xsd");
152 | DawProject.exportSchema(file, MetaData.class);
153 | Assert.assertTrue(file.exists());
154 | }
155 |
156 | /**
157 | * Test creating the project XML schema.
158 | *
159 | * @throws IOException
160 | * Could not create the XML schema
161 | */
162 | @Test
163 | public void writeProjectSchema() throws IOException {
164 | final File file = new File("Project.xsd");
165 | DawProject.exportSchema(file, Project.class);
166 | Assert.assertTrue(file.exists());
167 | }
168 |
169 | /**
170 | * Test audio clips with offsets and fades.
171 | *
172 | * @throws IOException
173 | * Could not create
174 | */
175 | @Test
176 | public void createAudioExample() throws IOException {
177 | for (AudioScenario scenario : AudioScenario.values()) {
178 | createAudioExample(0, 0, scenario, false);
179 | if (shouldTestOffsetAndFades(scenario)) {
180 | createAudioExample(0, 0, scenario, true);
181 | createAudioExample(1, 0, scenario, false);
182 | createAudioExample(0, 1, scenario, false);
183 | }
184 | }
185 | }
186 |
187 | /**
188 | * Test MIDI automation envelopes.
189 | *
190 | * @throws IOException
191 | * Could not create
192 | */
193 | @Test
194 | public void createMIDIAutomationInClipsExample() throws IOException {
195 | createMIDIAutomationExample("MIDI-CC1-AutomationOnTrack", false, false);
196 | createMIDIAutomationExample("MIDI-CC1-AutomationInClips", true, false);
197 | createMIDIAutomationExample("MIDI-PitchBend-AutomationOnTrack", false, true);
198 | createMIDIAutomationExample("MIDI-PitchBend-AutomationInClips", true, true);
199 | }
200 |
201 | /**
202 | * Test the double adapter for infinity constants.
203 | *
204 | * @throws Exception
205 | * Could not parse the infinity constants
206 | */
207 | @Test
208 | public void testDoubleAdapter() throws Exception {
209 | final var adapter = new DoubleAdapter();
210 | Assert.assertEquals(adapter.unmarshal("-inf").doubleValue(), Double.NEGATIVE_INFINITY, 0);
211 | Assert.assertEquals(adapter.unmarshal("inf").doubleValue(), Double.POSITIVE_INFINITY, 0);
212 | Assert.assertEquals("inf", adapter.marshal(Double.valueOf(Double.POSITIVE_INFINITY)));
213 | Assert.assertEquals("-inf", adapter.marshal(Double.valueOf(Double.NEGATIVE_INFINITY)));
214 | }
215 |
216 | private static boolean shouldTestOffsetAndFades(final AudioScenario scenario) {
217 | return switch (scenario) {
218 | case WARPED -> true;
219 | case RAW_BEATS -> true;
220 | case RAW_SECONDS -> true;
221 | default -> false;
222 | };
223 | }
224 |
225 | private static void createAudioExample(final double playStartOffset, final double clipTime,
226 | final AudioScenario scenario, final boolean withFades) throws IOException {
227 | String name = "Audio" + scenario.name();
228 | if (withFades)
229 | name += "WithFades";
230 | if (playStartOffset != 0)
231 | name += "Offset";
232 | if (clipTime != 0)
233 | name += "Clipstart";
234 |
235 | final Project project = createEmptyProject();
236 | final Track masterTrack = Utility.createTrack("Master", EnumSet.noneOf(ContentType.class), MixerRole.MASTER, 1,
237 | 0.5);
238 | final var audioTrack = Utility.createTrack("Audio", EnumSet.of(ContentType.AUDIO), MixerRole.REGULAR, 1, 0.5);
239 | audioTrack.channel.destination = masterTrack.channel;
240 |
241 | project.structure.add(masterTrack);
242 | project.structure.add(audioTrack);
243 |
244 | project.arrangement = new Arrangement();
245 | project.transport = new Transport();
246 | project.transport.tempo = Utility.createRealParameter(Unit.BPM, 155.0);
247 | final var arrangementLanes = new Lanes();
248 | project.arrangement.lanes = arrangementLanes;
249 | final var arrangementIsInSeconds = scenario == AudioScenario.RAW_SECONDS;
250 | project.arrangement.lanes.timeUnit = arrangementIsInSeconds ? TimeUnit.SECONDS : TimeUnit.BEATS;
251 |
252 | final var sample = "white-glasses.wav";
253 | Clip audioClip;
254 | final var sampleDuration = 3.097;
255 | final var audio = Utility.createAudio(sample, 44100, 2, sampleDuration);
256 |
257 | if (scenario == AudioScenario.FILE_WITH_ABSOLUTE_PATH) {
258 | audio.file.external = Boolean.TRUE;
259 | audio.file.path = new File("test-data", sample).getAbsolutePath();
260 | } else if (scenario == AudioScenario.FILE_WITH_RELATIVE_PATH) {
261 | audio.file.external = Boolean.TRUE;
262 | audio.file.path = "../test-data/" + sample;
263 | }
264 |
265 | if (scenario == AudioScenario.WARPED) {
266 | final var warps = new Warps();
267 | warps.content = audio;
268 | warps.contentTimeUnit = TimeUnit.SECONDS;
269 | warps.events.add(Utility.createWarp(0, 0));
270 | warps.events.add(Utility.createWarp(8, sampleDuration));
271 | audioClip = Utility.createClip(warps, clipTime, 8);
272 | audioClip.contentTimeUnit = TimeUnit.BEATS;
273 | audioClip.playStart = Double.valueOf(playStartOffset);
274 | if (withFades) {
275 | audioClip.fadeTimeUnit = TimeUnit.BEATS;
276 | audioClip.fadeInTime = Double.valueOf(0.25);
277 | audioClip.fadeOutTime = Double.valueOf(0.25);
278 | }
279 | } else {
280 | audioClip = Utility.createClip(audio, clipTime, arrangementIsInSeconds ? sampleDuration : 8);
281 | audioClip.contentTimeUnit = TimeUnit.SECONDS;
282 | audioClip.playStart = Double.valueOf(playStartOffset);
283 | audioClip.playStop = Double.valueOf(sampleDuration);
284 | if (withFades) {
285 | audioClip.fadeTimeUnit = TimeUnit.SECONDS;
286 | audioClip.fadeInTime = Double.valueOf(0.1);
287 | audioClip.fadeOutTime = Double.valueOf(0.1);
288 | }
289 | }
290 |
291 | final var clips = Utility.createClips(audioClip);
292 | clips.track = audioTrack;
293 | arrangementLanes.lanes.add(clips);
294 |
295 | saveTestProject(project, name, (meta, files) -> files.put(new File("test-data/" + sample), sample));
296 | }
297 |
298 | private static void createMIDIAutomationExample(final String name, final boolean inClips, final boolean isPitchBend)
299 | throws IOException {
300 | final Project project = createEmptyProject();
301 | final Track masterTrack = Utility.createTrack("Master", EnumSet.noneOf(ContentType.class), MixerRole.MASTER, 1,
302 | 0.5);
303 | final var instrumentTrack = Utility.createTrack("Notes", EnumSet.of(ContentType.NOTES), MixerRole.REGULAR, 1,
304 | 0.5);
305 | instrumentTrack.channel.destination = masterTrack.channel;
306 |
307 | project.structure.add(masterTrack);
308 | project.structure.add(instrumentTrack);
309 |
310 | project.arrangement = new Arrangement();
311 | project.transport = new Transport();
312 | project.transport.tempo = Utility.createRealParameter(Unit.BPM, 123.0);
313 | final var arrangementLanes = new Lanes();
314 | project.arrangement.lanes = arrangementLanes;
315 | project.arrangement.lanes.timeUnit = TimeUnit.BEATS;
316 |
317 | // Create some mod-wheel or pitch-bend automation
318 | final var automation = new Points();
319 | automation.unit = Unit.NORMALIZED;
320 | if (isPitchBend) {
321 | automation.target.expression = ExpressionType.PITCH_BEND;
322 | automation.target.channel = Integer.valueOf(0);
323 | } else {
324 | automation.target.expression = ExpressionType.CHANNEL_CONTROLLER;
325 | automation.target.channel = Integer.valueOf(0);
326 | automation.target.controller = Integer.valueOf(1);
327 | }
328 | automation.points.add(Utility.createPoint(0, 0.0, Interpolation.LINEAR));
329 | automation.points.add(Utility.createPoint(1, 0.0, Interpolation.LINEAR));
330 | automation.points.add(Utility.createPoint(2, 0.5, Interpolation.LINEAR));
331 | automation.points.add(Utility.createPoint(3, 0.5, Interpolation.LINEAR));
332 | automation.points.add(Utility.createPoint(4, 1.0, Interpolation.LINEAR));
333 | automation.points.add(Utility.createPoint(5, 1.0, Interpolation.LINEAR));
334 | automation.points.add(Utility.createPoint(6, 0.5, Interpolation.LINEAR));
335 | automation.points.add(Utility.createPoint(7, 1, Interpolation.HOLD));
336 | automation.points.add(Utility.createPoint(8, 0.5, Interpolation.HOLD));
337 |
338 | if (inClips) {
339 | final var noteClip = Utility.createClip(automation, 0, 8);
340 | final var clips = Utility.createClips(noteClip);
341 | clips.track = instrumentTrack;
342 | arrangementLanes.lanes.add(clips);
343 | } else {
344 | automation.track = instrumentTrack;
345 | arrangementLanes.lanes.add(automation);
346 | }
347 |
348 | saveTestProject(project, name, null);
349 | }
350 |
351 | private static void saveTestProject(final Project project, final String name,
352 | final BiConsumer> configurer) throws IOException {
353 | final MetaData metadata = new MetaData();
354 | final Map embeddedFiles = new HashMap<>();
355 |
356 | if (configurer != null)
357 | configurer.accept(metadata, embeddedFiles);
358 |
359 | DawProject.save(project, metadata, embeddedFiles, new File("target/" + name + ".dawproject"));
360 | DawProject.saveXML(project, new File("target/" + name + ".xml"));
361 | DawProject.validate(project);
362 | }
363 |
364 | private static Project createDummyProject(final int numTracks, final Set features) {
365 | final Project project = createEmptyProject();
366 |
367 | final Track masterTrack = Utility.createTrack("Master", EnumSet.noneOf(ContentType.class), MixerRole.MASTER, 1,
368 | 0.5);
369 | project.structure.add(masterTrack);
370 |
371 | if (features.contains(Features.PLUGINS)) {
372 | final Device device = new Vst3Plugin();
373 | device.deviceName = "Limiter";
374 | device.deviceRole = DeviceRole.AUDIO_FX;
375 | device.state = new FileReference();
376 | device.state.path = "plugin-states/12323545.vstpreset";
377 |
378 | if (masterTrack.channel.devices == null)
379 | masterTrack.channel.devices = new ArrayList<>();
380 |
381 | masterTrack.channel.devices.add(device);
382 | }
383 |
384 | project.arrangement = new Arrangement();
385 | final var arrangementLanes = new Lanes();
386 | arrangementLanes.timeUnit = TimeUnit.BEATS;
387 | project.arrangement.lanes = arrangementLanes;
388 |
389 | if (features.contains(Features.CUE_MARKERS)) {
390 | final var cueMarkers = new Markers();
391 | project.arrangement.markers = cueMarkers;
392 | cueMarkers.markers.add(Utility.createMarker(0, "Verse"));
393 | cueMarkers.markers.add(Utility.createMarker(24, "Chorus"));
394 | }
395 |
396 | for (int i = 0; i < numTracks; i++) {
397 | final var track = Utility.createTrack("Track " + (i + 1), EnumSet.of(ContentType.NOTES), MixerRole.REGULAR,
398 | 1, 0.5);
399 | project.structure.add(track);
400 | track.color = "#" + i + i + i + i + i + i;
401 | track.channel.destination = masterTrack.channel;
402 |
403 | final var trackLanes = new Lanes();
404 | trackLanes.track = track;
405 | arrangementLanes.lanes.add(trackLanes);
406 |
407 | if (features.contains(Features.CLIPS)) {
408 | final var clips = new Clips();
409 |
410 | trackLanes.lanes.add(clips);
411 |
412 | final var clip = new Clip();
413 | clip.name = "Clip " + i;
414 | clip.time = 8 * i;
415 | clip.duration = Double.valueOf(4.0);
416 | clips.clips.add(clip);
417 |
418 | final var notes = new Notes();
419 | clip.content = notes;
420 |
421 | for (int j = 0; j < 8; j++) {
422 | final var note = new Note();
423 | note.key = 36 + 12 * (j % (1 + i));
424 | note.velocity = Double.valueOf(0.8);
425 | note.releaseVelocity = Double.valueOf(0.5);
426 | note.time = Double.valueOf(0.5 * j);
427 | note.duration = Double.valueOf(0.5);
428 | notes.notes.add(note);
429 | }
430 |
431 | if (features.contains(Features.ALIAS_CLIPS)) {
432 | final var clip2 = new Clip();
433 | clip2.name = "Alias Clip " + i;
434 | clip2.time = 32 + 8 * i;
435 | clip2.duration = Double.valueOf(4.0);
436 | clips.clips.add(clip2);
437 | clip2.reference = notes;
438 | }
439 |
440 | if (i == 0 && features.contains(Features.AUTOMATION)) {
441 | final var points = new Points();
442 | points.target.parameter = track.channel.volume;
443 | trackLanes.lanes.add(points);
444 |
445 | // fade-in over 8 quarter notes
446 | points.points.add(Utility.createPoint(0.0, 0.0, Interpolation.LINEAR));
447 | points.points.add(Utility.createPoint(8.0, 1.0, Interpolation.LINEAR));
448 | }
449 | }
450 | }
451 |
452 | return project;
453 | }
454 |
455 | private static Project createEmptyProject() {
456 | Referenceable.setAutoID(true);
457 |
458 | final Project project = new Project();
459 | project.application.name = "Test";
460 | project.application.version = "1.0";
461 | return project;
462 | }
463 | }
464 |
--------------------------------------------------------------------------------
/src/test/java/com/bitwig/dawproject/GenerateDocumentationTest.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import static j2html.TagCreator.a;
4 | import static j2html.TagCreator.b;
5 | import static j2html.TagCreator.body;
6 | import static j2html.TagCreator.br;
7 | import static j2html.TagCreator.div;
8 | import static j2html.TagCreator.h1;
9 | import static j2html.TagCreator.h2;
10 | import static j2html.TagCreator.h3;
11 | import static j2html.TagCreator.head;
12 | import static j2html.TagCreator.html;
13 | import static j2html.TagCreator.link;
14 | import static j2html.TagCreator.p;
15 | import static j2html.TagCreator.rawHtml;
16 | import static j2html.TagCreator.span;
17 | import static j2html.TagCreator.table;
18 | import static j2html.TagCreator.td;
19 | import static j2html.TagCreator.text;
20 | import static j2html.TagCreator.th;
21 | import static j2html.TagCreator.title;
22 | import static j2html.TagCreator.tr;
23 |
24 | import java.io.File;
25 | import java.io.IOException;
26 | import java.lang.annotation.Annotation;
27 | import java.lang.reflect.Field;
28 | import java.lang.reflect.Modifier;
29 | import java.lang.reflect.ParameterizedType;
30 | import java.nio.charset.StandardCharsets;
31 | import java.nio.file.Files;
32 | import java.util.ArrayList;
33 | import java.util.Arrays;
34 | import java.util.Collections;
35 | import java.util.Comparator;
36 | import java.util.List;
37 | import java.util.Optional;
38 | import java.util.stream.Collectors;
39 |
40 | import jakarta.xml.bind.annotation.XmlAttribute;
41 | import jakarta.xml.bind.annotation.XmlElement;
42 | import jakarta.xml.bind.annotation.XmlElementRef;
43 | import jakarta.xml.bind.annotation.XmlElementWrapper;
44 | import jakarta.xml.bind.annotation.XmlEnumValue;
45 | import jakarta.xml.bind.annotation.XmlIDREF;
46 | import jakarta.xml.bind.annotation.XmlRootElement;
47 |
48 | import com.bitwig.dawproject.device.AuPlugin;
49 | import com.bitwig.dawproject.device.BuiltinDevice;
50 | import com.bitwig.dawproject.device.ClapPlugin;
51 | import com.bitwig.dawproject.device.Compressor;
52 | import com.bitwig.dawproject.device.Device;
53 | import com.bitwig.dawproject.device.EqBand;
54 | import com.bitwig.dawproject.device.Equalizer;
55 | import com.bitwig.dawproject.device.Limiter;
56 | import com.bitwig.dawproject.device.NoiseGate;
57 | import com.bitwig.dawproject.device.Plugin;
58 | import com.bitwig.dawproject.device.Vst2Plugin;
59 | import com.bitwig.dawproject.device.Vst3Plugin;
60 | import com.bitwig.dawproject.timeline.Audio;
61 | import com.bitwig.dawproject.timeline.AutomationTarget;
62 | import com.bitwig.dawproject.timeline.BoolPoint;
63 | import com.bitwig.dawproject.timeline.Clip;
64 | import com.bitwig.dawproject.timeline.ClipSlot;
65 | import com.bitwig.dawproject.timeline.Clips;
66 | import com.bitwig.dawproject.timeline.EnumPoint;
67 | import com.bitwig.dawproject.timeline.IntegerPoint;
68 | import com.bitwig.dawproject.timeline.Lanes;
69 | import com.bitwig.dawproject.timeline.Marker;
70 | import com.bitwig.dawproject.timeline.Markers;
71 | import com.bitwig.dawproject.timeline.MediaFile;
72 | import com.bitwig.dawproject.timeline.Note;
73 | import com.bitwig.dawproject.timeline.Notes;
74 | import com.bitwig.dawproject.timeline.Point;
75 | import com.bitwig.dawproject.timeline.Points;
76 | import com.bitwig.dawproject.timeline.RealPoint;
77 | import com.bitwig.dawproject.timeline.TimeSignaturePoint;
78 | import com.bitwig.dawproject.timeline.Timeline;
79 | import com.bitwig.dawproject.timeline.Video;
80 | import com.bitwig.dawproject.timeline.Warp;
81 | import com.bitwig.dawproject.timeline.Warps;
82 | import com.github.therapi.runtimejavadoc.ClassJavadoc;
83 | import com.github.therapi.runtimejavadoc.Comment;
84 | import com.github.therapi.runtimejavadoc.CommentFormatter;
85 | import com.github.therapi.runtimejavadoc.FieldJavadoc;
86 | import com.github.therapi.runtimejavadoc.RuntimeJavadoc;
87 | import j2html.rendering.IndentedHtml;
88 | import j2html.tags.DomContent;
89 | import j2html.tags.specialized.DivTag;
90 | import j2html.tags.specialized.HtmlTag;
91 | import j2html.tags.specialized.SpanTag;
92 | import j2html.tags.specialized.TableTag;
93 | import j2html.tags.specialized.TdTag;
94 | import j2html.tags.specialized.TrTag;
95 | import org.junit.Assert;
96 | import org.junit.Test;
97 | import org.reflections.Reflections;
98 |
99 | /** Not really a test but generates the HTML documentation. */
100 | public class GenerateDocumentationTest {
101 | private static final CommentFormatter formatter = new CommentFormatter();
102 |
103 | private Reflections mReflections = new Reflections("com.bitwig.dawproject");
104 |
105 | /**
106 | * Create a HTML class summary.
107 | *
108 | * @throws IOException
109 | * Could not write the HTML file
110 | */
111 | @Test
112 | public void createClassSummary() throws IOException {
113 | final var htmlFile = new File("Reference.html");
114 |
115 | final var title = "DAWPROJECT XML Reference";
116 | final var html = createDocument(title);
117 |
118 | Files.write(htmlFile.toPath(), Collections.singleton(html.render(IndentedHtml.inMemory())),
119 | StandardCharsets.UTF_8);
120 |
121 | Assert.assertTrue(htmlFile.exists());
122 | }
123 |
124 | private HtmlTag createDocument(final String title) {
125 | final var toc = div().withClass("toc");
126 | toc.with(b("Table of Contents"));
127 | return html(head(title(title), link().withRel("stylesheet").withHref("style.css")), body(h1(title),
128 | div(a(rawHtml("↑")).withHref("#top")).withClass("goto-toc"), toc,
129 | createClassesSummary(toc, "Root", new Class[]{Project.class, MetaData.class,}),
130 | createClassesSummary(toc, "Other",
131 | new Class[]{Application.class, FileReference.class, Transport.class,}),
132 | createClassesSummary(toc, "Mixer", new Class[]{Track.class, Channel.class, Send.class,}),
133 | createClassesSummary(toc, "Timeline",
134 | new Class[]{Arrangement.class, Scene.class, ClipSlot.class, Timeline.class, Lanes.class,
135 | Clips.class, Clip.class, Notes.class, Note.class, Audio.class, Video.class, Warps.class,
136 | Warp.class, Markers.class, Marker.class,}),
137 | createClassesSummary(toc, "Parameters",
138 | new Class[]{Parameter.class, BoolParameter.class, EnumParameter.class, IntegerParameter.class,
139 | RealParameter.class, TimeSignatureParameter.class,}),
140 | createClassesSummary(toc, "Automation",
141 | new Class[]{Points.class, AutomationTarget.class, Point.class, RealPoint.class, BoolPoint.class,
142 | EnumPoint.class, IntegerPoint.class, TimeSignaturePoint.class,}),
143 | createClassesSummary(toc, "Device",
144 | new Class[]{Device.class, AuPlugin.class, ClapPlugin.class, Plugin.class, Vst2Plugin.class,
145 | Vst3Plugin.class, BuiltinDevice.class, Compressor.class, Equalizer.class, EqBand.class,
146 | Limiter.class, NoiseGate.class,}),
147 | createClassesSummary(toc, "Abstract",
148 | new Class[]{Nameable.class, Referenceable.class, MediaFile.class,})));
149 | }
150 |
151 | private DomContent createClassesSummary(final DivTag toc, final String label, final Class>[] classes) {
152 | final var content = new ArrayList();
153 |
154 | content.add(h2(label + " Elements"));
155 |
156 | final var tocP = p(label + " Elements");
157 | final var tocDiv = div();
158 | toc.with(tocP, tocDiv);
159 |
160 | for (final var cls : classes) {
161 | content.add(createClassSummary(cls));
162 | tocDiv.with(createElementLink(cls));
163 | }
164 |
165 | return new SpanTag().with(content).withClass("elements-block");
166 | }
167 |
168 | private DomContent createClassSummary(final Class> cls) {
169 | final var content = span().withClass("element-block");
170 | final var elementName = getElementNameForClass(cls);
171 | content.with(h3(bracketize(elementName)).withId(elementName).withClass("element-title"));
172 |
173 | ClassJavadoc classDoc = RuntimeJavadoc.getJavadoc(cls);
174 |
175 | if (!classDoc.isEmpty()) {
176 | content.with(p(formatJavadocComment(classDoc.getComment())));
177 | }
178 |
179 | var superClass = cls.getSuperclass();
180 |
181 | if (superClass != Object.class) {
182 | final var p = p("Inherits from").withClass("bubble");
183 |
184 | while (superClass != Object.class) {
185 | p.with(createElementLink(superClass));
186 | superClass = superClass.getSuperclass();
187 | }
188 | content.with(p);
189 | }
190 |
191 | final var subTypes = this.mReflections.getSubTypesOf(cls);
192 |
193 | if (!subTypes.isEmpty()) {
194 | final var p = p("Implementations").withClass("bubble");
195 |
196 | subTypes.stream().sorted(Comparator.comparing(Class::getName)).forEach(c -> p.with(createElementLink(c)));
197 |
198 | content.with(p);
199 | }
200 |
201 | if (Modifier.isAbstract(cls.getModifiers())) {
202 | content.with(p("\nThis element is abstract in the DOM and cannot be used as an XML element directly."));
203 | }
204 |
205 | final var table = table().withClass("detail-table");
206 |
207 | content.with(table);
208 | createAttributeTable(table, cls);
209 | createElementsTable(table, cls);
210 |
211 | return content;
212 | }
213 |
214 | private static void createAttributeTable(final TableTag table, final Class> cls) {
215 | final var content = new ArrayList();
216 |
217 | content.add(tr(th("Attribute name").withStyle("text-align:center;"), th("Description"),
218 | th("Type").withStyle("text-align:center;"), th("Required").withStyle("text-align:center;")));
219 |
220 | for (final var field : cls.getFields()) {
221 | final var fieldJavadoc = RuntimeJavadoc.getJavadoc(field);
222 | createAttributeTableRow(field, fieldJavadoc, field.getDeclaringClass() == cls).ifPresent(content::add);
223 | }
224 |
225 | if (content.size() > 1)
226 | table.with(content);
227 | }
228 |
229 | private static Optional createAttributeTableRow(final Field field, final FieldJavadoc javadoc,
230 | final boolean isDeclaredInThisClass) {
231 | for (Annotation annotation : field.getAnnotations()) {
232 | if (annotation instanceof XmlAttribute attribute) {
233 | final var comment = getComment(field, javadoc);
234 | var name = attribute.name();
235 | if (name.startsWith("#"))
236 | name = field.getName();
237 |
238 | var tr = tr(td(name).withStyle("text-align:center;"), td(comment),
239 | td(getType(field)).withStyle("text-align:center;"),
240 | td(attribute.required() ? "yes" : "no").withStyle("text-align:center;"));
241 |
242 | if (!isDeclaredInThisClass)
243 | tr = tr.withClass("inherited");
244 |
245 | return Optional.of(tr);
246 | }
247 | }
248 | return Optional.empty();
249 | }
250 |
251 | private void createElementsTable(final TableTag table, final Class> cls) {
252 | final var content = new ArrayList();
253 |
254 | content.add(tr(th("Element name").withStyle("text-align:center;"), th("Description"),
255 | th("Element Type").withStyle("text-align:center;"), th("Required").withStyle("text-align:center;")));
256 |
257 | for (final var field : cls.getFields()) {
258 | final var fieldJavadoc = RuntimeJavadoc.getJavadoc(field);
259 | createElementTableRow(field, fieldJavadoc, field.getDeclaringClass() == cls).ifPresent(content::add);
260 | }
261 |
262 | if (content.size() > 1)
263 | table.with(content);
264 | }
265 |
266 | private static boolean isDynamicType(final Field field) {
267 | return field.getAnnotation(XmlElementRef.class) != null;
268 | }
269 |
270 | private static boolean isRequired(final Field field) {
271 | for (Annotation annotation : field.getAnnotations()) {
272 | if (annotation instanceof XmlElementWrapper e && e.required())
273 | return true;
274 | if (annotation instanceof XmlElement e && e.required())
275 | return true;
276 | if (annotation instanceof XmlElementRef e && e.required())
277 | return true;
278 | }
279 | return false;
280 | }
281 |
282 | private static boolean isElement(final Field field) {
283 | for (Annotation annotation : field.getAnnotations()) {
284 | if (annotation instanceof XmlElementWrapper)
285 | return true;
286 | if (annotation instanceof XmlElement)
287 | return true;
288 | if (annotation instanceof XmlElementRef)
289 | return true;
290 | }
291 | return false;
292 | }
293 |
294 | boolean isFieldList(final Field field) {
295 | final Class> type = field.getType();
296 | return type == List.class;
297 | }
298 |
299 | Class> getListGenericType(final Field field) {
300 | if (field.getGenericType() instanceof ParameterizedType pt && pt.getActualTypeArguments().length == 1) {
301 | final var listType = pt.getActualTypeArguments()[0];
302 | if (listType instanceof Class cls)
303 | return cls;
304 | }
305 | return null;
306 | }
307 |
308 | private static DomContent formatJavadocComment(Comment comment) {
309 | return rawHtml(formatter.format(comment));
310 | }
311 |
312 | private Optional createElementTableRow(final Field field, final FieldJavadoc javadoc,
313 | final boolean isDeclaredInThisClass) {
314 | if (!isElement(field))
315 | return Optional.empty();
316 |
317 | final var comment = getComment(field, javadoc);
318 | var name = getFieldName(field);
319 |
320 | final var isRequired = isRequired(field);
321 | final boolean isList = isFieldList(field);
322 |
323 | if (isDynamicType(field)) {
324 | final var elementName = td(text("from Type"));
325 | if (isList)
326 | elementName.with(br(), text("(multiple)"));
327 | TdTag typeCell = createElementLinksToSubclasses(field);
328 |
329 | var tr = tr(elementName.withStyle("text-align:center;"), td(comment),
330 | typeCell.withStyle("text-align:center;"),
331 | td(isRequired ? "yes" : "no").withStyle("text-align:center;"));
332 | if (!isDeclaredInThisClass)
333 | tr = tr.withClass("inherited");
334 |
335 | return Optional.of(tr);
336 | }
337 |
338 | var typeCell = td(createElementLink(isList ? getListGenericType(field) : field.getType()));
339 | final var elementName = td("<" + name + ">");
340 | if (isList)
341 | elementName.with(br(), text("(multiple)"));
342 |
343 | var tr = tr(elementName.withStyle("text-align:center;"), td(comment), typeCell.withStyle("text-align:center;"),
344 | td(isRequired ? "yes" : "no").withStyle("text-align:center;"));
345 | if (!isDeclaredInThisClass)
346 | tr = tr.withClass("inherited");
347 |
348 | return Optional.of(tr);
349 | }
350 |
351 | private TdTag createElementLinksToSubclasses(final Field field) {
352 | final var td = td();
353 | final boolean isList = isFieldList(field);
354 |
355 | final var isCollection = isList && field.getGenericType() instanceof final ParameterizedType pt
356 | && pt.getActualTypeArguments().length == 1;
357 |
358 | final Class> type = isCollection ? getListGenericType(field) : field.getType();
359 | this.mReflections.getSubTypesOf(type).stream().sorted(Comparator.comparing(Class::getName)).forEach(t -> {
360 | td.with(createElementLink(t));
361 | td.with(br());
362 | });
363 |
364 | return td;
365 | }
366 |
367 | private static DomContent getComment(final Field field, final FieldJavadoc javadoc) {
368 | final var comment = javadoc != null ? formatJavadocComment(javadoc.getComment()) : text("");
369 |
370 | final Class> type = field.getType();
371 |
372 | final var enumClass = type.isEnum()
373 | ? type
374 | : type.isArray() ? type.getComponentType().isEnum() ? type.getComponentType() : null : null;
375 |
376 | if (enumClass != null) {
377 | final var choices = "Possible values: "
378 | + Arrays.stream(enumClass.getFields()).map(f -> getFieldName(f)).collect(Collectors.joining(", "));
379 | return span(comment, p(choices));
380 | }
381 |
382 | return comment;
383 | }
384 |
385 | private static String getFieldName(final Field field) {
386 | for (Annotation annotation : field.getAnnotations()) {
387 | if (annotation instanceof XmlElementWrapper wrapper) {
388 | if (!wrapper.name().startsWith("#"))
389 | return wrapper.name();
390 | } else if (annotation instanceof XmlElement element) {
391 | if (!element.name().startsWith("#"))
392 | return element.name();
393 | } else if (annotation instanceof XmlElementRef element) {
394 | if (!element.name().startsWith("#"))
395 | return element.name();
396 | } else if (annotation instanceof XmlEnumValue element) {
397 | if (!element.value().startsWith("#"))
398 | return element.value();
399 | }
400 | }
401 |
402 | return field.getName();
403 | }
404 |
405 | private static String getType(final Field field) {
406 | final var xmlIDREF = field.getAnnotation(XmlIDREF.class);
407 |
408 | if (xmlIDREF != null)
409 | return "ID";
410 |
411 | final Class> type = field.getType();
412 |
413 | if (type.isEnum())
414 | return "Enum";
415 |
416 | if (type.isArray() && type.getComponentType().isEnum())
417 | return "Enum,...";
418 |
419 | return type.getSimpleName();
420 | }
421 |
422 | private static String getElementNameForClass(Class> cls) {
423 | final var rootElement = cls.getDeclaredAnnotation(XmlRootElement.class);
424 | if (rootElement instanceof XmlRootElement re && !re.name().startsWith("#"))
425 | return re.name();
426 | return cls.getSimpleName();
427 | }
428 |
429 | private static DomContent createElementLink(Class> cls) {
430 | if (cls == String.class)
431 | return text("text");
432 |
433 | if (cls.isPrimitive())
434 | return text(cls.getSimpleName());
435 |
436 | final var name = getElementNameForClass(cls);
437 | return a("<" + name + ">").withHref("#" + name).withClass("element-link");
438 | }
439 |
440 | private static String bracketize(final String s) {
441 | return "<" + s + ">";
442 | }
443 | }
444 |
--------------------------------------------------------------------------------
/src/test/java/com/bitwig/dawproject/LoadDawProjectTest.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.nio.file.FileVisitResult;
6 | import java.nio.file.FileVisitor;
7 | import java.nio.file.Files;
8 | import java.nio.file.Path;
9 | import java.nio.file.attribute.BasicFileAttributes;
10 | import java.util.ArrayList;
11 | import java.util.Collection;
12 | import java.util.List;
13 |
14 | import org.junit.Assert;
15 | import org.junit.Test;
16 | import org.junit.runner.RunWith;
17 | import org.junit.runners.Parameterized;
18 |
19 | /** Parameterized class to test loading a metadata and project files. */
20 | @RunWith(Parameterized.class)
21 | public class LoadDawProjectTest {
22 | private final File mFile;
23 |
24 | /**
25 | * Constructor.
26 | *
27 | * @param file
28 | * The file to load
29 | * @param name
30 | * Unused
31 | */
32 | public LoadDawProjectTest(final File file, @SuppressWarnings("unused") final Object name) {
33 | this.mFile = file;
34 | }
35 |
36 | /**
37 | * Get all files.
38 | *
39 | * @return The files
40 | * @throws IOException
41 | * Could not load the files
42 | */
43 | @Parameterized.Parameters(name = "{1}")
44 | public static Collection getFiles() throws IOException {
45 | final List result = new ArrayList<>();
46 |
47 | final File testDataDir = new File("src/test-data");
48 |
49 | if (testDataDir.exists() && testDataDir.isDirectory()) {
50 | final Path rootPath = testDataDir.toPath();
51 | Files.walkFileTree(rootPath, new FileVisitor<>() {
52 | @Override
53 | public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) {
54 | return FileVisitResult.CONTINUE;
55 | }
56 |
57 | @Override
58 | public FileVisitResult visitFile(final Path path, final BasicFileAttributes attrs) {
59 | final File file = path.toFile();
60 |
61 | if (file.getAbsolutePath().toLowerCase().endsWith("." + DawProject.FILE_EXTENSION)) {
62 | final Object[] args = {file, rootPath.relativize(path).toString()};
63 | result.add(args);
64 | }
65 | return FileVisitResult.CONTINUE;
66 | }
67 |
68 | @Override
69 | public FileVisitResult visitFileFailed(final Path file, final IOException exc) {
70 | return FileVisitResult.CONTINUE;
71 | }
72 |
73 | @Override
74 | public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) {
75 | return FileVisitResult.CONTINUE;
76 | }
77 | });
78 | }
79 |
80 | return result;
81 | }
82 |
83 | /**
84 | * Load the project.
85 | *
86 | * @throws IOException
87 | * Could not load the project
88 | */
89 | @Test
90 | public void loadProject() throws IOException {
91 | final Project project = DawProject.loadProject(this.mFile);
92 | Assert.assertNotNull(project);
93 | }
94 |
95 | /**
96 | * Load the metadata.
97 | *
98 | * @throws IOException
99 | * Could not load the metadata
100 | */
101 | @Test
102 | public void loadMetadata() throws IOException {
103 | final MetaData metadata = DawProject.loadMetadata(this.mFile);
104 | Assert.assertNotNull(metadata);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,200;8..144,300;8..144,400&display=swap');
2 |
3 | body {
4 | margin: 24px;
5 | font-family: "Roboto Flex", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
6 | font-size: 1rem;
7 | font-weight: 400;
8 | line-height: 1.25;
9 | color: #212529;
10 | text-align: left;
11 | background-color: #fff;
12 | margin-bottom: 80px;
13 | }
14 |
15 | th, td {
16 | padding: 8px;
17 | padding-inline: 12px;
18 | line-height: 1.25;
19 | }
20 |
21 | table {
22 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
23 | margin-top: 16px;
24 | margin-bottom: 16px;
25 | border-collapse:collapse;
26 | }
27 |
28 | th {
29 | background-color: #999;
30 | color: #fff;
31 | font-weight: 500;
32 | font-size: 0.8em;
33 | text-align: left;
34 | }
35 |
36 | h1 {
37 | font-weight: 100;
38 | font-size: 2.6em;
39 | }
40 |
41 | h2 {
42 | font-synthesis: none;
43 | color: #777;
44 | font-weight: 300;
45 | font-size: 2.1em;
46 | font-style: italic;
47 | padding-top: 16px;
48 | border-top: 1px solid #cccccc;
49 | }
50 |
51 | .element-title {
52 | font-weight: 200;
53 | font-size: 1.8em;
54 | letter-spacing: 2px;
55 | }
56 |
57 | tbody tr {
58 | border-bottom: 1px solid #dddddd;
59 | }
60 |
61 | tbody tr:nth-of-type(even) {
62 | background-color: #f3f3f3;
63 | }
64 |
65 | .detail-table {
66 | width: 100%;
67 | }
68 |
69 | .inherited {
70 | font-style: italic;
71 | font-weight: 250;
72 | color: #555;
73 | }
74 |
75 | .element-link {
76 | color: #FF5A00;
77 | font-size: 1em;
78 | font-weight: 300;
79 | margin-inline-start: 8px;
80 | text-decoration: none;
81 | letter-spacing: 1px;
82 | transition: color 0.25s;
83 | margin-bottom: 8px;
84 | }
85 |
86 | .element-link:hover {
87 | color: #2E9CE6;
88 | }
89 |
90 | .bubble {
91 | /*outline: 2px solid #bbb;
92 | border-radius: 8px;
93 | padding: 4px 12px 4px 12px;*/
94 | }
95 |
96 | .toc {
97 | /*outline: 2px solid #bbb;*/
98 | border-radius: 8px;
99 | padding: 20px;
100 | background-color: #eee;
101 | }
102 |
103 | .goto-toc {
104 | border-radius: 8px;
105 | padding: 4px 12px 4px 12px;
106 | background-color: #eee;
107 | position:fixed;
108 | bottom: 24px;
109 | right: 24px
110 | }
111 |
--------------------------------------------------------------------------------
/test-data/white-glasses.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitwig/dawproject/f016fb017292eb481d9afbfbafc0afa49ac6b89a/test-data/white-glasses.wav
--------------------------------------------------------------------------------