From 99ef135730af70298b7ac8b87d1f7cd4d3541997 Mon Sep 17 00:00:00 2001 From: Moritz Surwehme Date: Wed, 8 Jul 2026 17:37:39 +0200 Subject: [PATCH] Added an optional Gain parameter to Clip for per-clip audio gain. The gain is a RealParameter, consistent with channel volume, send level and the device gain stages, so the unit is self-describing and the parameter can be used as an automation target. It is applied in addition to the fade-in/fade-out envelope and independently of the channel volume; unity gain is the implied default when the element is omitted. Regenerated Project.xsd and Reference.html and added a save/load round-trip test. --- Project.xsd | 1 + Reference.html | 19 ++++++++ .../com/bitwig/dawproject/timeline/Clip.java | 11 +++++ .../com/bitwig/dawproject/DawProjectTest.java | 43 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/Project.xsd b/Project.xsd index ef40559..929d7d7 100644 --- a/Project.xsd +++ b/Project.xsd @@ -517,6 +517,7 @@ + diff --git a/Reference.html b/Reference.html index 86ca83d..0823771 100644 --- a/Reference.html +++ b/Reference.html @@ -2725,6 +2725,25 @@

Required + + + <Gain> + + + Gain applied to the audio content of this clip, in addition to the + fade-in/fade-out envelope and independently of the volume of the channel this + clip plays on. Unity gain is the implied default when this element is + omitted. + + + + <RealParameter> + + + + no + + from Type diff --git a/src/main/java/com/bitwig/dawproject/timeline/Clip.java b/src/main/java/com/bitwig/dawproject/timeline/Clip.java index 536a13b..e1e2d43 100644 --- a/src/main/java/com/bitwig/dawproject/timeline/Clip.java +++ b/src/main/java/com/bitwig/dawproject/timeline/Clip.java @@ -1,11 +1,13 @@ package com.bitwig.dawproject.timeline; import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementRef; import jakarta.xml.bind.annotation.XmlIDREF; import jakarta.xml.bind.annotation.XmlRootElement; import com.bitwig.dawproject.Nameable; +import com.bitwig.dawproject.RealParameter; /** * A Clip provides a clipped view on to a Timeline, and is used either on a @@ -80,6 +82,15 @@ public class Clip extends Nameable { @XmlAttribute(required = false) public Double fadeOutTime; + /** + * Gain applied to the audio content of this clip, in addition to the + * fade-in/fade-out envelope and independently of the volume of the channel this + * clip plays on. Unity gain is the implied default when this element is + * omitted. + */ + @XmlElement(name = "Gain", required = false) + public RealParameter gain; + /** Whether this clip should be played back. Default value is true. */ @XmlAttribute(required = false) public Boolean enable; diff --git a/src/test/java/com/bitwig/dawproject/DawProjectTest.java b/src/test/java/com/bitwig/dawproject/DawProjectTest.java index 596c540..ac239f4 100644 --- a/src/test/java/com/bitwig/dawproject/DawProjectTest.java +++ b/src/test/java/com/bitwig/dawproject/DawProjectTest.java @@ -99,6 +99,49 @@ public void saveAndLoadDawProject() throws IOException { Assert.assertEquals(project.scenes.size(), loadedProject.scenes.size()); } + /** + * Test storing and loading a clip with gain. + * + * @throws IOException + * Could not store or load the project + */ + @Test + public void saveAndLoadClipGain() throws IOException { + final Project project = createEmptyProject(); + final Track masterTrack = Utility.createTrack("Master", EnumSet.noneOf(ContentType.class), MixerRole.MASTER, 1, + 0.5); + final var audioTrack = Utility.createTrack("Audio", EnumSet.of(ContentType.AUDIO), MixerRole.REGULAR, 1, 0.5); + audioTrack.channel.destination = masterTrack.channel; + project.structure.add(masterTrack); + project.structure.add(audioTrack); + + project.arrangement = new Arrangement(); + final var arrangementLanes = new Lanes(); + arrangementLanes.timeUnit = TimeUnit.BEATS; + project.arrangement.lanes = arrangementLanes; + + final var audio = Utility.createAudio("white-glasses.wav", 44100, 2, 3.097); + final var audioClip = Utility.createClip(audio, 0, 8); + audioClip.contentTimeUnit = TimeUnit.SECONDS; + audioClip.gain = Utility.createRealParameter(Unit.LINEAR, 0.5); + + final var clips = Utility.createClips(audioClip); + clips.track = audioTrack; + arrangementLanes.lanes.add(clips); + + DawProject.validate(project); + + final var file = File.createTempFile("testfile-clip-gain", ".dawproject"); + DawProject.save(project, new MetaData(), new HashMap<>(), file); + final var loadedProject = DawProject.loadProject(file); + + final var loadedLanes = (Lanes) loadedProject.arrangement.lanes; + final var loadedClips = (Clips) loadedLanes.lanes.get(0); + final var loadedGain = loadedClips.clips.get(0).gain; + Assert.assertEquals(Unit.LINEAR, loadedGain.unit); + Assert.assertEquals(0.5, loadedGain.value.doubleValue(), 0.0); + } + /** * Test storing a complex project. *