├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── DawProjectTest.java
├── LICENSE
├── MetaData.xsd
├── Project.xsd
├── README.md
├── Tonn_and_DAWProject_Tutorial.md
├── classes
├── application.py
├── arrangement.py
├── audio.py
├── automationTarget.py
├── boolParameter.py
├── builtInDevice.py
├── channel.py
├── clip.py
├── clips.py
├── compressor.py
├── contentType.py
├── dawProject.py
├── device.py
├── deviceRole.py
├── doubleAdapter.py
├── eqBand.py
├── eqBandType.py
├── equalizer.py
├── expressionType.py
├── fileReference.py
├── interpolation.py
├── lane.py
├── lanes.py
├── marker.py
├── markers.py
├── mediaFile.py
├── metaData.py
├── mixerRole.py
├── nameable.py
├── note.py
├── notes.py
├── parameter.py
├── point.py
├── points.py
├── project.py
├── realParameter.py
├── realPoint.py
├── referenceable.py
├── scene.py
├── send.py
├── sendType.py
├── timeSignatureParameter.py
├── timeUnit.py
├── timeline.py
├── track.py
├── transport.py
├── unit.py
├── utility.py
├── warp.py
└── warps.py
├── examples
├── audio_in
│ └── audio.MD
├── createBitwigProject.py
├── initial_multitrack_mix_payload.json
└── roex_daw_project_export.py
├── java_classes
├── Application.java
├── Arrangement.java
├── BoolParameter.java
├── Channel.java
├── ContentType.java
├── DawProject.java
├── DoubleAdapter.java
├── EnumParameter.java
├── ExpressionType.java
├── FileReference.java
├── IntegerParameter.java
├── Interpolation.java
├── Lane.java
├── MetaData.java
├── MixerRole.java
├── Nameable.java
├── Parameter.java
├── Project.java
├── RealParameter.java
├── Referenceable.java
├── Scene.java
├── Send.java
├── SendType.java
├── TimeSignatureParameter.java
├── Track.java
├── Transport.java
├── Unit.java
├── Utility.java
├── device
│ ├── AuPlugin.java
│ ├── BuiltinDevice.java
│ ├── ClapPlugin.java
│ ├── Compressor.java
│ ├── Device.java
│ ├── DeviceRole.java
│ ├── EqBand.java
│ ├── EqBandType.java
│ ├── Equalizer.java
│ ├── Limiter.java
│ ├── NoiseGate.java
│ ├── Plugin.java
│ ├── Vst2Plugin.java
│ └── Vst3Plugin.java
└── timeline
│ ├── Audio.java
│ ├── AutomationTarget.java
│ ├── BoolPoint.java
│ ├── Clip.java
│ ├── ClipSlot.java
│ ├── Clips.java
│ ├── EnumPoint.java
│ ├── IntegerPoint.java
│ ├── Lanes.java
│ ├── Marker.java
│ ├── Markers.java
│ ├── MediaFile.java
│ ├── Note.java
│ ├── Notes.java
│ ├── Point.java
│ ├── Points.java
│ ├── RealPoint.java
│ ├── TimeSignaturePoint.java
│ ├── TimeUnit.java
│ ├── Timeline.java
│ ├── Video.java
│ ├── Warp.java
│ └── Warps.java
├── requirements.txt
└── target
└── RoEx_Automix.xml
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 RoEx LTD
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MetaData.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🎵 DAWProject-Py
2 | *Python code for working with DAWProject files — enabling DAW interoperability.*
3 |
4 | [](LICENSE)
5 | [](https://github.com/roex-audio/dawproject-py/stargazers)
6 |
7 | ## 📖 About
8 |
9 | DAWProject is an **open XML-based file format** designed for **seamless project exchange between DAWs**. It allows music producers, engineers, and developers to share **full session data** between different DAWs without losing important information.
10 |
11 | The original **DAWProject** repository, developed by **Bitwig**, was written in **Java**. However, at **RoEx**, we primarily develop in **Python and C++**, so we converted the core classes to Python to integrate with our systems and allow more developers to build upon it.
12 |
13 | We **love the idea of DAWProject** and want to see it in **every DAW**. The more people building on it, the better—so we're making our **Python version publicly available**. If anyone wants to **turn it into a pip package**, extend it, or modify it further, feel free!
14 |
15 | For reference, we’ve **kept the original Java classes** and implementations for posterity.
16 |
17 | ---
18 |
19 | ## 📦 Installation
20 |
21 | Since this is a **source-only library**, you can clone the repository and use it directly in your Python project:
22 |
23 | ```sh
24 | git clone https://github.com/roex-audio/dawproject-py.git
25 | cd dawproject-py
26 | ```
27 |
28 | Then, import it into your Python project:
29 |
30 | ```python
31 | from classes.dawProject import DawProject
32 | ```
33 |
34 | ---
35 |
36 | ## 🚀 Quick Start
37 |
38 | ### **Loading a DAWProject file (XML-based)**
39 | ```python
40 | from classes.dawProject import DawProject
41 |
42 | # Load a DAWProject file (XML format)
43 | project = DawProject.load("example.dawproject")
44 |
45 | # Print project metadata
46 | print(f"Project Name: {project.name}")
47 | print(f"Tracks: {len(project.structure)}")
48 | ```
49 |
50 | ### **Creating an Empty DAWProject**
51 | ```python
52 | from classes.project import Project
53 | from classes.application import Application
54 |
55 | # Initialize an empty project
56 | project = Project()
57 | project.application = Application(name="RoEx Automix", version="1.0")
58 |
59 | # Save the project as XML
60 | DawProject.save_xml(project, "new_project.dawproject")
61 | ```
62 |
63 | ### **Creating a Project with Audio Tracks**
64 | ```python
65 | from classes.utility import Utility
66 | from classes.mixerRole import MixerRole
67 | from classes.contentType import ContentType
68 |
69 | # Initialize a project
70 | project = Project()
71 |
72 | # Create a master track
73 | master_track = Utility.create_track(name="Master", content_types=set(), mixer_role=MixerRole.MASTER, pan=0.5, volume=1.0)
74 | project.structure.append(master_track)
75 |
76 | # Create an audio track
77 | audio_track = Utility.create_track(name="Lead Synth", content_types={ContentType.AUDIO}, mixer_role=MixerRole.REGULAR, pan=0.2, volume=0.8)
78 | audio_track.channel.destination = master_track.channel
79 |
80 | # Add the track to the project
81 | project.structure.append(audio_track)
82 |
83 | # Save the project as XML
84 | DawProject.save_xml(project, "audio_project.dawproject")
85 | ```
86 |
87 | ---
88 | ## 🏗 Examples
89 |
90 | An examples folder is included in this repository, demonstrating how to use DAWProject-Py in real-world scenarios. One example shows how to use the Tonn API to obtain multitrack mix settings for a DAWProject file.
91 |
92 | ___
93 |
94 | ## 📜 DAWProject Format
95 |
96 | DAWProject is an **open XML-based format** designed to enable **cross-DAW compatibility**. Instead of exporting audio stems, `.dawproject` files allow projects to be shared across DAWs while preserving:
97 |
98 | - 🎚 **Track names, volume, and panning**
99 | - 🎛 **Effects and automation data**
100 | - 🎵 **MIDI and audio region placements**
101 | - 🕒 **Tempo and time signature changes**
102 |
103 | For the full specification, visit [DAWProject on GitHub](https://github.com/bitwig/dawproject).
104 |
105 | ---
106 |
107 | ## 🛠 Development & Contributions
108 |
109 | We **welcome contributions!** If you’d like to extend **DAWProject-Py**, whether by improving existing functionality or turning it into a **pip package**, feel free to contribute!
110 |
111 | ### **Clone the Repository**
112 | ```sh
113 | git clone https://github.com/roex-audio/dawproject-py.git
114 | cd dawproject-py
115 | ```
116 |
117 | ### **Contributing**
118 | - Fork the repository
119 | - Create a feature branch (`git checkout -b feature-name`)
120 | - Commit your changes (`git commit -m "Add feature XYZ"`)
121 | - Push to GitHub (`git push origin feature-name`)
122 | - Open a **Pull Request** 🚀
123 |
124 | ---
125 |
126 | ## 📜 License
127 | This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
128 |
129 | ---
130 |
131 | ## 🌎 Stay Connected
132 | 📢 Have ideas or feedback? Open an issue or start a discussion!
133 |
134 | 🔗 **Website:** [www.roexaudio.com](https://dawproject.com)
135 | 💬 **Socials:** [@roexaudio](https://twitter.com/dawproject)
136 | 📢 **GitHub Discussions:** [DAWProject-Py Discussions](https://github.com/roex-audio/dawproject-py/discussions)
137 | ⭐ **Star this repo** if you find it useful!
138 |
139 | ---
140 |
141 | 🎶 **DAWProject-Py – Enabling DAW interoperability!** 🚀
142 |
--------------------------------------------------------------------------------
/Tonn_and_DAWProject_Tutorial.md:
--------------------------------------------------------------------------------
1 | # **Automating Multitrack Mixing with Tonn API and DAWProject: A Guide for Developers**
2 |
3 | ## **Introduction**
4 |
5 | In modern music production, automation is key. Whether you’re developing a DAW-integrated tool, running a music tech startup, or streamlining workflows for independent producers, automating the mixing process can save countless hours. This tutorial demonstrates how to use the **Tonn API** to retrieve a mix preview, download stems, apply processing, and format them for **Bitwig Studio and PreSonus Studio One**—all programmatically.
6 |
7 | What makes this particularly powerful is how **Tonn API integrates seamlessly with** [DAWProject-py](https://github.com/bitwig/dawproject-py), an open-source project that allows for structured export to DAWs. This means that, in just a few steps, you can take your audio, apply mix settings like **EQ, Compression, Gain, and Panning**, and generate a DAW session file that loads everything into your preferred environment.
8 |
9 | This is a **simple example**, but it highlights the potential for fully automated, API-driven mixing workflows—something that can **revolutionize** audio production tools. For a more detailed example check out the GitHub repository.
10 |
11 | ---
12 |
13 | ## **What You’ll Learn**
14 |
15 | - How to **retrieve a mix preview** from the Tonn API
16 | - How to **download and process multitrack audio files**
17 | - How to **apply gain, EQ, and compression settings** programmatically
18 | - How to **export directly to DAWs** using **DAWProject-py**
19 | - How to **handle network errors and retries gracefully**
20 |
21 | ---
22 |
23 | ## **Prerequisites**
24 |
25 | Before diving into the code, ensure you have:
26 |
27 | - Python 3 installed
28 | - The following dependencies installed via `pip`:
29 | ```sh
30 | pip install requests soundfile numpy
31 | ```
32 | - An **API key** from [Tonn Portal](https://tonn-portal.roexaudio.com)
33 | - Bitwig Studio or PreSonus Studio One (optional, but recommended for testing project files)
34 | - **Access to a cloud storage bucket to store demo audio files** (Tonn API requires audio files to be stored in a bucket before processing)
35 |
36 | ---
37 |
38 | ## **1. Setting Up API Communication**
39 |
40 | The Tonn API acts as the backbone of this process, allowing us to send **multitrack audio** for automated mixing and retrieve the processed output. To start, let’s define some constants for interacting with the API:
41 |
42 | ```python
43 | import time
44 | import requests
45 | import json
46 | import os
47 | import soundfile as sf
48 |
49 | # Base API endpoint for Tonn
50 | BASE_URL = "https://tonn.roexaudio.com"
51 |
52 | # Retry settings for API calls
53 | MAX_RETRIES = 3
54 | RETRY_DELAY = 2 # seconds
55 |
56 | API_KEY = "go to https://tonn-portal.roexaudio.com to get one"
57 | ```
58 |
59 | We’ll use `requests` to interact with the API, ensuring that we handle failures gracefully with **retry mechanisms**.
60 |
61 | ---
62 |
63 | ## **2. Downloading Multitrack Audio Files**
64 |
65 | Before processing, we need to fetch the **original stems** that will be used for the mix. Our function downloads these files while handling errors and retries.
66 |
67 | ```python
68 | def download_audio(file_name, url, output_dir="audio_in"):
69 | """
70 | Downloads an audio file from the given URL and saves it locally.
71 | """
72 | os.makedirs(output_dir, exist_ok=True)
73 | local_filename = os.path.join(output_dir, file_name)
74 |
75 | for attempt in range(MAX_RETRIES):
76 | try:
77 | print(f"Downloading {file_name} (Attempt {attempt + 1})...")
78 | response = requests.get(url, stream=True, timeout=10)
79 | response.raise_for_status()
80 |
81 | with open(local_filename, "wb") as file:
82 | for chunk in response.iter_content(chunk_size=8192):
83 | file.write(chunk)
84 |
85 | print(f"Successfully downloaded {file_name}")
86 | return local_filename
87 | except requests.exceptions.RequestException as e:
88 | print(f"Error downloading {file_name}: {e}")
89 | if attempt < MAX_RETRIES - 1:
90 | time.sleep(RETRY_DELAY ** (attempt + 1))
91 | else:
92 | print(f"Failed to download {file_name} after {MAX_RETRIES} attempts.")
93 | return None
94 | ```
95 | We apply exponential backoff ```(RETRY_DELAY ** (attempt + 1))``` to avoid overloading the API with repeated requests.
96 |
97 | ---
98 |
99 | ## **3. Retrieving and Polling the Mix Preview**
100 |
101 | Once we submit our multitrack audio to the Tonn API, we must poll for the preview mix until it’s ready. The following function repeatedly checks the API and retrieves the processed mix when available:
102 |
103 | ```python
104 | def poll_preview_mix(task_id, headers, max_attempts=30, poll_interval=5):
105 | """
106 | Polls the API until the preview mix is ready.
107 | """
108 | retrieve_url = f"{BASE_URL}/retrievepreviewmix"
109 | retrieve_payload = {"multitrackData": {"multitrackTaskId": task_id, "retrieveFXSettings": True}}
110 |
111 | print("Polling for the preview mix...")
112 | for attempt in range(max_attempts):
113 | response = requests.post(retrieve_url, json=retrieve_payload, headers=headers)
114 |
115 | if response.status_code == 200:
116 | results = response.json().get("previewMixTaskResults", {})
117 | if results.get("status") == "MIX_TASK_PREVIEW_COMPLETED":
118 | print("Preview mix is ready!")
119 | return results
120 | elif response.status_code == 202:
121 | print(f"Attempt {attempt + 1}: Still processing...")
122 | else:
123 | print("Unexpected error while polling.")
124 | return None
125 |
126 | time.sleep(poll_interval)
127 |
128 | print("Mix preview was not available after polling. Try again later.")
129 | return None
130 | ```
131 |
132 | ---
133 |
134 | ## **4. Processing Audio: Gain, EQ, and Compression**
135 |
136 | Once we have the mix output settings, we apply **gain adjustments, equalization, and compression**.
137 |
138 | ```python
139 | def format_audio_tracks_for_daw(mix_output_settings, downloaded_files):
140 | """
141 | Applies mix settings (gain, EQ, compression) to each track.
142 | """
143 | audio_tracks = []
144 |
145 | for track_name, settings in mix_output_settings.items():
146 | file_path = downloaded_files.get(track_name)
147 | if not file_path:
148 | continue
149 |
150 | audio_x, sr = sf.read(file_path, dtype="float32")
151 | audio_x *= settings["gain_settings"]["initial_gain"]
152 | sf.write(file_path, audio_x, sr)
153 |
154 | audio_tracks.append({
155 | "file_path": file_path,
156 | "gain": settings["gain_settings"]["gain_amplitude"],
157 | "eq_settings": settings.get("eq_settings", []),
158 | "compressor_settings": settings.get("drc_settings", {})
159 | })
160 |
161 | return audio_tracks
162 | ```
163 |
164 | ---
165 |
166 | ## **5. Exporting to Bitwig Studio and PreSonus Studio One**
167 |
168 | We format our processed tracks and **automatically create a DAW project** using DAWProject-py.
169 |
170 | ```python
171 | import createBitwigProject
172 |
173 | def create_bitwig_project(audio_tracks):
174 | createBitwigProject.create_project_with_audio_tracks(audio_tracks)
175 | ```
176 | This step ensures seamless DAW integration, making our automated mixing process immediately usable for further production and tweaking.
177 |
178 | ---
179 |
180 | ## **6. Additional Resources**
181 |
182 | To help you get started, we've provided a **demo repository** containing the full source code and example payloads. You can find it in the same directory.
183 |
184 | For more on DAW session exports, check out **[DAWProject-py](https://github.com/bitwig/dawproject-py)**.
185 |
186 | ---
187 |
188 | ## **Conclusion**
189 |
190 | By leveraging the **Tonn API**, we’ve automated a core part of music production: mixing multitrack audio programmatically.
191 |
192 | 🚀 Ready to build the future of music production? Get your API key from [Tonn Portal](https://tonn-portal.roexaudio.com) and start automating today! Also, be sure to check out the **[Demo Repository - Link TBD]** for example implementations!
193 |
--------------------------------------------------------------------------------
/classes/application.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 |
3 |
4 | class Application:
5 | def __init__(self, name=None, version=None):
6 | self.name = name
7 | self.version = version
8 |
9 | def to_xml(self):
10 | application_elem = ET.Element(
11 | "Application", name=self.name, version=self.version
12 | )
13 |
14 | if self.name:
15 | application_elem.set("name", self.name)
16 | if self.version:
17 | application_elem.set("version", self.version)
18 |
19 | return application_elem
20 |
21 | @classmethod
22 | def from_xml(cls, element):
23 | name = element.get("name")
24 | version = element.get("version")
25 |
26 | return cls(name, version)
27 |
--------------------------------------------------------------------------------
/classes/arrangement.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.lanes import Lanes
3 | from classes.referenceable import Referenceable
4 | from classes.markers import Markers
5 | from classes.points import Points
6 |
7 |
8 | class Arrangement(Referenceable):
9 | def __init__(
10 | self,
11 | time_signature_automation=None,
12 | tempo_automation=None,
13 | markers=None,
14 | lanes=None,
15 | ):
16 | self.time_signature_automation = time_signature_automation
17 | self.tempo_automation = tempo_automation
18 | self.markers = markers
19 | self.lanes = lanes
20 |
21 | def to_xml(self):
22 | arrangement_elem = ET.Element("Arrangement")
23 |
24 | if self.time_signature_automation:
25 | ts_automation_elem = ET.SubElement(
26 | arrangement_elem, "TimeSignatureAutomation"
27 | )
28 | ts_automation_elem.append(self.time_signature_automation.to_xml())
29 | if self.tempo_automation:
30 | tempo_automation_elem = ET.SubElement(arrangement_elem, "TempoAutomation")
31 | tempo_automation_elem.append(self.tempo_automation.to_xml())
32 | if self.lanes:
33 | lanes_elem = ET.SubElement(arrangement_elem, "Lanes")
34 | lanes_elem.append(self.lanes.to_xml())
35 | if self.markers:
36 | markers_elem = ET.SubElement(arrangement_elem, "Markers")
37 | for marker in self.markers.markers:
38 | markers_elem.append(marker.to_xml())
39 |
40 | return arrangement_elem
41 |
42 | @classmethod
43 | def from_xml(cls, element):
44 | ts_automation_elem = element.find("TimeSignatureAutomation")
45 | time_signature_automation = (
46 | Points.from_xml(ts_automation_elem)
47 | if ts_automation_elem is not None
48 | else None
49 | )
50 |
51 | tempo_automation_elem = element.find("TempoAutomation")
52 | tempo_automation = (
53 | Points.from_xml(tempo_automation_elem)
54 | if tempo_automation_elem is not None
55 | else None
56 | )
57 |
58 | markers_elem = element.find("Markers")
59 | markers = Markers.from_xml(markers_elem) if markers_elem is not None else None
60 |
61 | lanes_elem = element.find("Lanes")
62 | lanes = Lanes.from_xml(lanes_elem) if lanes_elem is not None else None
63 |
64 | return cls(time_signature_automation, tempo_automation, markers, lanes)
65 |
--------------------------------------------------------------------------------
/classes/audio.py:
--------------------------------------------------------------------------------
1 | from classes.mediaFile import MediaFile
2 | from lxml import etree as ET
3 | from classes.fileReference import FileReference
4 | from classes.timeUnit import TimeUnit
5 |
6 |
7 | class Audio(MediaFile):
8 | def __init__(
9 | self,
10 | sample_rate,
11 | channels,
12 | duration,
13 | algorithm=None,
14 | file=None,
15 | name=None,
16 | time_unit="seconds",
17 | ):
18 | super().__init__(file=file, duration=duration, name=name)
19 | self.sample_rate = sample_rate
20 | self.channels = channels
21 | self.algorithm = algorithm
22 | self.time_unit = time_unit
23 |
24 | def to_xml(self):
25 | # Create the root element specifically as "Audio"
26 | audio_elem = ET.Element("Audio")
27 |
28 | # Call MediaFile's to_xml to add the common elements and attributes
29 | file_elem = self.file.to_xml()
30 | file_elem.tag = "File"
31 | audio_elem.append(file_elem)
32 |
33 | audio_elem.set("duration", str(self.duration))
34 |
35 | # Set Audio-specific attributes
36 | audio_elem.set("sampleRate", str(self.sample_rate))
37 | audio_elem.set("channels", str(self.channels))
38 | if self.algorithm:
39 | audio_elem.set("algorithm", self.algorithm)
40 | # Convert time_unit to a string if it's an instance of a class
41 | if isinstance(self.time_unit, TimeUnit):
42 | audio_elem.set(
43 | "timeUnit", str(self.time_unit.value)
44 | ) # Assuming 'value' is the correct attribute
45 | else:
46 | audio_elem.set(
47 | "timeUnit", str(self.time_unit)
48 | ) # Directly use if it's already a string
49 |
50 | return audio_elem
51 |
52 | @classmethod
53 | def from_xml(cls, element):
54 | sample_rate = int(element.get("sampleRate"))
55 | channels = int(element.get("channels"))
56 | algorithm = element.get("algorithm")
57 | file_elem = element.find("File")
58 | file = (
59 | FileReference.from_xml(file_elem)
60 | if file_elem is not None
61 | else FileReference()
62 | )
63 | duration = float(element.get("duration"))
64 | time_unit = element.get("timeUnit", "seconds")
65 | return cls(
66 | sample_rate,
67 | channels,
68 | duration,
69 | algorithm=algorithm,
70 | file=file,
71 | time_unit=time_unit,
72 | )
73 |
--------------------------------------------------------------------------------
/classes/automationTarget.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 |
3 |
4 | class AutomationTarget:
5 | def __init__(
6 | self, parameter=None, expression=None, channel=None, key=None, controller=None
7 | ):
8 | self.parameter = parameter
9 | self.expression = expression
10 | self.channel = channel
11 | self.key = key
12 | self.controller = controller
13 |
14 | def to_xml(self):
15 | target_elem = ET.Element("Target")
16 | if self.parameter:
17 | # Ensure parameter is a string or get its ID
18 | parameter_id = getattr(self.parameter, "id", None) or getattr(
19 | self.parameter, "parameterID", None
20 | )
21 | if parameter_id is not None:
22 | target_elem.set("parameter", str(parameter_id))
23 | else:
24 | raise TypeError(
25 | f"Expected parameter to be a string or an object with 'id' or 'parameterID', got {type(self.parameter).__name__}"
26 | )
27 |
28 | if self.expression:
29 | target_elem.set("expression", self.expression)
30 | if self.channel is not None:
31 | target_elem.set("channel", str(self.channel))
32 | if self.key is not None:
33 | target_elem.set("key", str(self.key))
34 | if self.controller is not None:
35 | target_elem.set("controller", str(self.controller))
36 | return target_elem
37 |
38 | @classmethod
39 | def from_xml(cls, element):
40 | parameter = element.get("parameter")
41 | expression = element.get("expression")
42 | channel = element.get("channel")
43 | channel = int(channel) if channel else None
44 | key = element.get("key")
45 | key = int(key) if key else None
46 | controller = element.get("controller")
47 | controller = int(controller) if controller else None
48 | return cls(parameter, expression, channel, key, controller)
49 |
--------------------------------------------------------------------------------
/classes/boolParameter.py:
--------------------------------------------------------------------------------
1 | from classes.parameter import Parameter
2 |
3 |
4 | class BoolParameter(Parameter):
5 | def __init__(
6 | self, value=None, parameter_id=None, name=None, color=None, comment=None
7 | ):
8 | super().__init__(parameter_id, name, color, comment)
9 | self.value = value
10 |
11 | def to_xml(self):
12 | bool_param_elem = super().to_xml() # Inherits the XML creation from Parameter
13 | bool_param_elem.tag = "BoolParameter"
14 | if self.value is not None:
15 | # Ensure that value is set as a string 'true' or 'false'
16 | bool_param_elem.set("value", str(self.value).lower())
17 | return bool_param_elem
18 |
19 | @classmethod
20 | def from_xml(cls, element):
21 | instance = super().from_xml(element)
22 | value = element.get("value")
23 | instance.value = value.lower() == "true" if value else None
24 | return instance
25 |
--------------------------------------------------------------------------------
/classes/builtInDevice.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.device import Device
3 |
4 |
5 | class BuiltInDevice(Device):
6 | def __init__(self, device_type=None):
7 | self.device_type = device_type # This could be one of Equalizer, Compressor, NoiseGate, Limiter
8 |
9 | def to_xml(self):
10 | # Create the root element for BuiltinDevice
11 | device_elem = ET.Element("BuiltinDevice")
12 |
13 | # Depending on the device_type, serialize it to XML
14 | if self.device_type:
15 | device_elem.append(self.device_type.to_xml())
16 |
17 | return device_elem
18 |
19 | @classmethod
20 | def from_xml(cls, element):
21 | # Lazy import to avoid circular dependency
22 | from classes.equalizer import Equalizer
23 | from classes.compressor import Compressor
24 |
25 | # Import NoiseGate and Limiter here if they exist
26 |
27 | # Logic to determine the actual device type
28 | device_type = None
29 | if element.find("Equalizer") is not None:
30 | device_type = Equalizer.from_xml(element.find("Equalizer"))
31 | elif element.find("Compressor") is not None:
32 | device_type = Compressor.from_xml(element.find("Compressor"))
33 | # Add other device types similarly
34 |
35 | return cls(device_type=device_type)
36 |
--------------------------------------------------------------------------------
/classes/channel.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.lane import Lane
3 | from classes.realParameter import RealParameter
4 | from classes.boolParameter import BoolParameter
5 | from classes.device import Device
6 |
7 |
8 | class Channel(Lane):
9 | def __init__(
10 | self,
11 | role=None,
12 | audio_channels=2,
13 | volume=None,
14 | pan=None,
15 | mute=None,
16 | solo=None,
17 | destination=None,
18 | sends=None,
19 | devices=None,
20 | name=None,
21 | color=None,
22 | comment=None,
23 | ):
24 | super().__init__(name, color, comment)
25 | self.role = role
26 | self.audio_channels = audio_channels
27 | self.volume = volume
28 | self.pan = pan
29 | self.mute = mute
30 | self.solo = solo
31 | self.destination = destination
32 | self.sends = sends if sends else []
33 | self.devices = devices if devices else []
34 |
35 | def to_xml(self):
36 | # Inherit XML generation from Lane and modify tag
37 | channel_elem = super().to_xml()
38 | channel_elem.tag = "Channel"
39 |
40 | # Set attributes for the Channel element
41 | if self.role is not None:
42 | channel_elem.set("role", str(self.role)) # Ensure role is a string
43 | if self.audio_channels is not None:
44 | channel_elem.set(
45 | "audioChannels", str(self.audio_channels)
46 | ) # Convert int to string
47 | if self.solo is not None:
48 | channel_elem.set(
49 | "solo", str(self.solo).lower()
50 | ) # Convert boolean to lowercase string
51 | if self.destination:
52 | channel_elem.set(
53 | "destination", str(self.destination.id)
54 | ) # Assuming destination has an id attribute
55 |
56 | # Append complex elements if they exist
57 | if self.devices:
58 | devices_elem = ET.SubElement(channel_elem, "Devices")
59 | for device in self.devices:
60 | devices_elem.append(device.to_xml())
61 | if self.mute:
62 | mute_elem = ET.SubElement(channel_elem, "Mute")
63 | mute_elem.append(self.mute.to_xml())
64 | if self.pan:
65 | pan_elem = ET.SubElement(channel_elem, "Pan")
66 | pan_elem.set(
67 | "value", str(self.pan.value) if self.pan.value is not None else ""
68 | )
69 | pan_elem.set(
70 | "unit", str(self.pan.unit.value) if self.pan.unit is not None else ""
71 | )
72 | if self.sends:
73 | sends_elem = ET.SubElement(channel_elem, "Sends")
74 | for send in self.sends:
75 | send_elem = ET.SubElement(sends_elem, "Send")
76 | send_elem.append(send.to_xml())
77 | if self.volume:
78 | volume_elem = ET.SubElement(channel_elem, "Volume")
79 | volume_elem.set(
80 | "value", str(self.volume.value) if self.volume.value is not None else ""
81 | )
82 | volume_elem.set(
83 | "unit",
84 | str(self.volume.unit.value) if self.volume.unit is not None else "",
85 | )
86 |
87 | return channel_elem
88 |
89 | @classmethod
90 | def from_xml(cls, element):
91 | from classes.send import Send
92 |
93 | instance = super().from_xml(element)
94 |
95 | instance.role = element.get("role")
96 |
97 | audio_channels = element.get("audioChannels")
98 | instance.audio_channels = (
99 | int(audio_channels) if audio_channels is not None else 2
100 | )
101 |
102 | volume_elem = element.find("Volume")
103 | instance.volume = (
104 | RealParameter.from_xml(volume_elem) if volume_elem is not None else None
105 | )
106 |
107 | pan_elem = element.find("Pan")
108 | instance.pan = (
109 | RealParameter.from_xml(pan_elem) if pan_elem is not None else None
110 | )
111 |
112 | mute_elem = element.find("Mute")
113 | instance.mute = (
114 | BoolParameter.from_xml(mute_elem) if mute_elem is not None else None
115 | )
116 |
117 | solo = element.get("solo")
118 | instance.solo = solo.lower() == "true" if solo else None
119 |
120 | destination_id = element.get("destination")
121 | instance.destination = (
122 | Channel.get_channel_by_id(destination_id)
123 | if destination_id is not None
124 | else None
125 | )
126 |
127 | sends = []
128 | for send_elem in element.findall(".//Sends/Send"):
129 | sends.append(Send.from_xml(send_elem))
130 | instance.sends = sends
131 |
132 | devices = []
133 | for device_elem in element.findall(".//Devices/*"):
134 | devices.append(Device.from_xml(device_elem))
135 | instance.devices = devices
136 |
137 | return instance
138 |
139 | @staticmethod
140 | def get_channel_by_id(channel_id):
141 | # Implement logic to retrieve a Channel instance by its ID
142 | pass
143 |
--------------------------------------------------------------------------------
/classes/clip.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.nameable import Nameable
3 | from classes.referenceable import Referenceable
4 | from classes.timeline import Timeline
5 | from classes.timeUnit import TimeUnit
6 |
7 |
8 | class Clip(Nameable):
9 | def __init__(
10 | self,
11 | time,
12 | duration=None,
13 | content_time_unit=None,
14 | play_start=None,
15 | play_stop=None,
16 | loop_start=None,
17 | loop_end=None,
18 | fade_time_unit=None,
19 | fade_in_time=None,
20 | fade_out_time=None,
21 | content=None,
22 | reference=None,
23 | name=None,
24 | color=None,
25 | comment=None,
26 | ):
27 | super().__init__(name, color, comment)
28 | self.time = time
29 | self.duration = duration
30 | self.content_time_unit = content_time_unit
31 | self.play_start = play_start
32 | self.play_stop = play_stop
33 | self.loop_start = loop_start
34 | self.loop_end = loop_end
35 | self.fade_time_unit = fade_time_unit
36 | self.fade_in_time = fade_in_time
37 | self.fade_out_time = fade_out_time
38 | self.content = content
39 | self.reference = reference
40 |
41 | def to_xml(self):
42 | # Create the Clip XML element
43 | clip_elem = ET.Element("Clip")
44 |
45 | # Set common attributes
46 | clip_elem.set("time", str(self.time))
47 | if self.duration is not None:
48 | clip_elem.set("duration", str(self.duration))
49 | if self.content_time_unit is not None:
50 | clip_elem.set("contentTimeUnit", self.content_time_unit.value)
51 | if self.play_start is not None:
52 | clip_elem.set("playStart", str(self.play_start))
53 | if self.play_stop is not None:
54 | clip_elem.set("playStop", str(self.play_stop))
55 | if self.loop_start is not None:
56 | clip_elem.set("loopStart", str(self.loop_start))
57 | if self.loop_end is not None:
58 | clip_elem.set("loopEnd", str(self.loop_end))
59 | if self.fade_time_unit is not None:
60 | clip_elem.set("fadeTimeUnit", self.fade_time_unit.value)
61 | if self.fade_in_time is not None:
62 | clip_elem.set("fadeInTime", str(self.fade_in_time))
63 | if self.fade_out_time is not None:
64 | clip_elem.set("fadeOutTime", str(self.fade_out_time))
65 |
66 | # Append content if present
67 | if self.content is not None:
68 | content_elem = self.content.to_xml()
69 | clip_elem.append(content_elem)
70 |
71 | # Reference handling
72 | if self.reference is not None:
73 | clip_elem.set("reference", self.reference.id)
74 |
75 | # Append inherited attributes
76 | nameable_elem = super().to_xml()
77 | for key, value in nameable_elem.attrib.items():
78 | clip_elem.set(key, value)
79 |
80 | return clip_elem
81 |
82 | @classmethod
83 | def from_xml(cls, element):
84 | # Initialize using parent class method
85 | instance = super().from_xml(element)
86 |
87 | # Extract Clip-specific attributes
88 | instance.time = float(element.get("time"))
89 | instance.duration = (
90 | float(element.get("duration")) if element.get("duration") else None
91 | )
92 | instance.content_time_unit = (
93 | TimeUnit(element.get("contentTimeUnit"))
94 | if element.get("contentTimeUnit")
95 | else None
96 | )
97 | instance.play_start = (
98 | float(element.get("playStart")) if element.get("playStart") else None
99 | )
100 | instance.play_stop = (
101 | float(element.get("playStop")) if element.get("playStop") else None
102 | )
103 | instance.loop_start = (
104 | float(element.get("loopStart")) if element.get("loopStart") else None
105 | )
106 | instance.loop_end = (
107 | float(element.get("loopEnd")) if element.get("loopEnd") else None
108 | )
109 | instance.fade_time_unit = (
110 | TimeUnit(element.get("fadeTimeUnit"))
111 | if element.get("fadeTimeUnit")
112 | else None
113 | )
114 | instance.fade_in_time = (
115 | float(element.get("fadeInTime")) if element.get("fadeInTime") else None
116 | )
117 | instance.fade_out_time = (
118 | float(element.get("fadeOutTime")) if element.get("fadeOutTime") else None
119 | )
120 |
121 | # Handling content and reference
122 | content_elem = element.find("Timeline")
123 | if content_elem is not None:
124 | content_class = globals().get(content_elem.tag)
125 | if content_class and issubclass(content_class, Timeline):
126 | instance.content = content_class.from_xml(content_elem)
127 |
128 | reference_id = element.get("reference")
129 | if reference_id:
130 | instance.reference = Referenceable.get_by_id(reference_id)
131 |
132 | return instance
133 |
--------------------------------------------------------------------------------
/classes/clips.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.timeline import Timeline
3 | from classes.clip import Clip
4 |
5 |
6 | class Clips(Timeline):
7 | XML_TAG = "Clips"
8 |
9 | def __init__(
10 | self,
11 | clips=None,
12 | track=None,
13 | time_unit=None,
14 | name=None,
15 | color=None,
16 | comment=None,
17 | ):
18 | super().__init__(track, time_unit, name, color, comment)
19 | self.clips = clips if clips else []
20 |
21 | def to_xml(self):
22 | # Create the XML element for Clips
23 | clips_elem = ET.Element(self.XML_TAG)
24 |
25 | # Set attributes specific to Clips
26 | if self.time_unit:
27 | clips_elem.set("timeUnit", str(self.time_unit))
28 | if self.track:
29 | clips_elem.set(
30 | "track", str(self.track.id)
31 | ) # Assuming track has an id attribute
32 |
33 | # Append child elements for each clip
34 | for clip in self.clips:
35 | clips_elem.append(clip.to_xml())
36 |
37 | return clips_elem
38 |
39 | @classmethod
40 | def from_xml(cls, element):
41 | # Initialize instance using the parent class's method
42 | instance = super().from_xml(element)
43 |
44 | # Process child elements of type Clip
45 | clips = []
46 | for clip_elem in element.findall("Clip"):
47 | clips.append(Clip.from_xml(clip_elem))
48 | instance.clips = clips
49 |
50 | return instance
51 |
--------------------------------------------------------------------------------
/classes/compressor.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.realParameter import RealParameter
3 | from classes.boolParameter import BoolParameter
4 | from classes.builtInDevice import BuiltInDevice
5 | from classes.unit import Unit # Assuming this is where the Unit enum is defined
6 |
7 |
8 | class Compressor(BuiltInDevice):
9 | def __init__(
10 | self,
11 | device_name=None,
12 | device_role=None,
13 | threshold=None,
14 | ratio=None,
15 | attack=None,
16 | release=None,
17 | input_gain=None,
18 | output_gain=None,
19 | auto_makeup=None,
20 | ):
21 | super().__init__()
22 |
23 | self.device_name = device_name # The required deviceName attribute
24 | self.device_role = device_role # The required deviceRole attribute
25 | # Ensure all parameters are RealParameter instances
26 | self.threshold = (
27 | threshold
28 | if isinstance(threshold, RealParameter)
29 | else RealParameter(threshold)
30 | )
31 | self.ratio = ratio if isinstance(ratio, RealParameter) else RealParameter(ratio)
32 | self.attack = (
33 | attack if isinstance(attack, RealParameter) else RealParameter(attack)
34 | )
35 | self.release = (
36 | release if isinstance(release, RealParameter) else RealParameter(release)
37 | )
38 | self.input_gain = (
39 | input_gain
40 | if isinstance(input_gain, RealParameter)
41 | else RealParameter(input_gain)
42 | )
43 | self.output_gain = (
44 | output_gain
45 | if isinstance(output_gain, RealParameter)
46 | else RealParameter(output_gain)
47 | )
48 | self.auto_makeup = (
49 | auto_makeup
50 | if isinstance(auto_makeup, BoolParameter)
51 | else BoolParameter(auto_makeup)
52 | )
53 |
54 | def to_xml(self):
55 | compressor_elem = ET.Element("Compressor")
56 |
57 | # Set the required deviceName attribute
58 | if self.device_name:
59 | compressor_elem.set("deviceName", self.device_name)
60 | else:
61 | raise ValueError("deviceName attribute is required but not provided.")
62 |
63 | # Set the required deviceRole attribute
64 | if self.device_role:
65 | compressor_elem.set("deviceRole", self.device_role)
66 | else:
67 | raise ValueError("deviceRole attribute is required but not provided.")
68 |
69 | # Define a helper function to add RealParameter elements
70 | def add_real_parameter_elem(parent_elem, tag, real_param, unit):
71 | param_elem = ET.Element(tag)
72 | param_elem.set("id", real_param.id)
73 | param_elem.set("value", str(real_param.value))
74 | param_elem.set("unit", unit)
75 | parent_elem.append(param_elem)
76 |
77 | add_real_parameter_elem(
78 | compressor_elem, "Attack", self.attack, Unit.SECONDS.value
79 | )
80 | # Add BoolParameter element with appropriate tag
81 | auto_makeup_elem = ET.Element("AutoMakeup")
82 | auto_makeup_elem.set("id", self.auto_makeup.id)
83 | auto_makeup_elem.set("value", str(self.auto_makeup.value).lower())
84 | compressor_elem.append(auto_makeup_elem)
85 |
86 | add_real_parameter_elem(
87 | compressor_elem, "InputGain", self.input_gain, Unit.DECIBEL.value
88 | )
89 | add_real_parameter_elem(
90 | compressor_elem, "OutputGain", self.output_gain, Unit.DECIBEL.value
91 | )
92 |
93 | # Add RealParameter elements with the required unit attributes
94 |
95 | add_real_parameter_elem(compressor_elem, "Ratio", self.ratio, Unit.PERCENT.value)
96 |
97 | add_real_parameter_elem(
98 | compressor_elem, "Release", self.release, Unit.SECONDS.value
99 | )
100 |
101 | add_real_parameter_elem(
102 | compressor_elem, "Threshold", self.threshold, Unit.DECIBEL.value
103 | )
104 |
105 | return compressor_elem
106 |
107 | @classmethod
108 | def from_xml(cls, element):
109 | device_name = element.get("deviceName")
110 | if not device_name:
111 | raise ValueError("deviceName attribute is required but missing in the XML.")
112 |
113 | device_role = element.get("deviceRole")
114 | if not device_role:
115 | raise ValueError("deviceRole attribute is required but missing in the XML.")
116 |
117 | threshold = RealParameter.from_xml(element.find("Threshold"))
118 | ratio = RealParameter.from_xml(element.find("Ratio"))
119 | attack = RealParameter.from_xml(element.find("Attack"))
120 | release = RealParameter.from_xml(element.find("Release"))
121 | input_gain = RealParameter.from_xml(element.find("InputGain"))
122 | output_gain = RealParameter.from_xml(element.find("OutputGain"))
123 | auto_makeup = BoolParameter.from_xml(element.find("AutoMakeup"))
124 |
125 | return cls(
126 | device_name=device_name,
127 | device_role=device_role,
128 | threshold=threshold,
129 | ratio=ratio,
130 | attack=attack,
131 | release=release,
132 | input_gain=input_gain,
133 | output_gain=output_gain,
134 | auto_makeup=auto_makeup,
135 | )
136 |
--------------------------------------------------------------------------------
/classes/contentType.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class ContentType(Enum):
5 | AUDIO = "audio"
6 | AUTOMATION = "automation"
7 | NOTES = "notes"
8 | VIDEO = "video"
9 | MARKERS = "markers"
10 | TRACKS = "tracks"
11 |
--------------------------------------------------------------------------------
/classes/dawProject.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from zipfile import ZipFile
3 | from lxml import etree
4 | from io import StringIO, BytesIO
5 | import chardet
6 |
7 | from .project import Project
8 | from .metaData import MetaData
9 | from .application import Application
10 | from .transport import Transport
11 | from .lane import Lane
12 | from .arrangement import Arrangement
13 | from .scene import Scene
14 |
15 |
16 | FORMAT_NAME = "DAWproject exchange format"
17 | FILE_EXTENSION = "dawproject"
18 |
19 | PROJECT_FILE = "project.xml"
20 | METADATA_FILE = "metadata.xml"
21 |
22 |
23 | class DawProject:
24 | FORMAT_NAME = "DAWproject exchange format"
25 | FILE_EXTENSION = "dawproject"
26 | PROJECT_FILE = "project.xml"
27 | METADATA_FILE = "metadata.xml"
28 |
29 | @staticmethod
30 | def export_schema(file, cls):
31 | try:
32 | schema_root = etree.Element(
33 | "xs:schema", nsmap={"xs": "http://www.w3.org/2001/XMLSchema"}
34 | )
35 | schema_tree = etree.ElementTree(schema_root)
36 | with open(file, "wb") as schema_file:
37 | schema_tree.write(
38 | schema_file,
39 | pretty_print=True,
40 | xml_declaration=True,
41 | encoding="UTF-8",
42 | )
43 | except Exception as e:
44 | raise IOError(e)
45 |
46 | @staticmethod
47 | def to_xml(obj):
48 | try:
49 | root = ET.Element(obj.__class__.__name__)
50 | for key, value in vars(obj).items():
51 | if isinstance(
52 | value, list
53 | ): # Handling lists, such as scenes or structure
54 | list_elem = ET.SubElement(root, key.capitalize())
55 | for item in value:
56 | list_elem.append(DawProject.to_xml_element(item))
57 | elif hasattr(
58 | value, "to_xml"
59 | ): # Handle nested objects with a to_xml method
60 | tag_name = getattr(value, "XML_TAG", value.__class__.__name__)
61 | element = value.to_xml()
62 | if element.tag != tag_name: # Only rename if different
63 | element.tag = tag_name
64 | root.append(value.to_xml())
65 | else:
66 | # Handle simple data types, including attributes
67 | if key == "version": # Assuming version should be an attribute
68 | root.set(key, str(value))
69 | else:
70 | child = ET.SubElement(root, key.capitalize())
71 | child.text = str(value) if value is not None else ""
72 | return ET.tostring(root, encoding="utf-8").decode("utf-8")
73 | except Exception as e:
74 | raise IOError(e)
75 |
76 | @staticmethod
77 | def to_xml_element(obj):
78 | if hasattr(obj, "to_xml"):
79 | return obj.to_xml()
80 | else:
81 | # Fallback if no to_xml method; consider handling this case appropriately
82 | element = ET.Element(obj.__class__.__name__)
83 | for key, value in vars(obj).items():
84 | child = ET.SubElement(element, key)
85 | child.text = str(value) if value is not None else ""
86 | return element
87 |
88 | @staticmethod
89 | def create_context(cls):
90 | pass
91 |
92 | @staticmethod
93 | def from_xml(reader, cls):
94 | try:
95 | tree = ET.parse(reader)
96 | root = tree.getroot()
97 | obj = cls()
98 | for child in root:
99 | setattr(obj, child.tag, child.text)
100 | return obj
101 | except Exception as e:
102 | raise IOError(e)
103 |
104 | @staticmethod
105 | def save_xml(project, file):
106 | project_xml = DawProject.to_xml(project)
107 | with open(file, "wb") as file_out:
108 | file_out.write(project_xml.encode("utf-8"))
109 |
110 | @staticmethod
111 | def validate(project):
112 | try:
113 | # Convert the project object to an XML string
114 | project_xml = DawProject.to_xml(project)
115 |
116 | # Load the XML Schema (.xsd file)
117 | with open("../Project.xsd", "r") as schema_file:
118 | schema_doc = etree.parse(schema_file)
119 | schema = etree.XMLSchema(schema_doc)
120 |
121 | # Parse the project XML and validate it against the schema
122 | xml_doc = etree.parse(StringIO(project_xml))
123 | schema.assertValid(xml_doc)
124 | print("Validation successful.")
125 |
126 | except etree.XMLSchemaError as e:
127 | raise IOError(f"Schema validation error: {e}")
128 | except Exception as e:
129 | raise IOError(f"Unexpected error: {e}")
130 |
131 | @staticmethod
132 | def save(project, metadata, embedded_files, file):
133 | metadata_xml = DawProject.to_xml(metadata)
134 | project_xml = DawProject.to_xml(project)
135 | with ZipFile(file, "w") as zos:
136 | DawProject.add_to_zip(
137 | zos, DawProject.METADATA_FILE, metadata_xml.encode("utf-8")
138 | )
139 | DawProject.add_to_zip(
140 | zos, DawProject.PROJECT_FILE, project_xml.encode("utf-8")
141 | )
142 | for path, file_path in embedded_files.items():
143 | DawProject.add_to_zip(zos, file_path, path)
144 |
145 | @staticmethod
146 | def add_to_zip(zos, path, data):
147 | with zos.open(path, "w") as entry:
148 | entry.write(data)
149 |
150 | @staticmethod
151 | def strip_bom(input_stream):
152 | data = input_stream.read()
153 | result = chardet.detect(data)
154 | encoding = result["encoding"]
155 | if encoding is None:
156 | raise IOError("The charset is not supported.")
157 | return StringIO(data.decode(encoding))
158 |
159 | @staticmethod
160 | def load_project(file):
161 | with ZipFile(file, "r") as zip_file:
162 | project_entry = zip_file.open(DawProject.PROJECT_FILE)
163 | return DawProject.from_xml(DawProject.strip_bom(project_entry), Project)
164 |
165 | @staticmethod
166 | def load_metadata(file):
167 | with ZipFile(file, "r") as zip_file:
168 | entry = zip_file.open(DawProject.METADATA_FILE)
169 | return DawProject.from_xml(DawProject.strip_bom(entry), MetaData)
170 |
171 | @staticmethod
172 | def stream_embedded(file, embedded_path):
173 | zip_file = ZipFile(file, "r")
174 | entry = zip_file.open(embedded_path)
175 | return entry
176 |
--------------------------------------------------------------------------------
/classes/device.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.referenceable import Referenceable
3 | from classes.boolParameter import BoolParameter
4 | from classes.deviceRole import DeviceRole
5 | from classes.fileReference import FileReference
6 | from classes.parameter import Parameter
7 |
8 |
9 | class Device(Referenceable):
10 | def __init__(
11 | self,
12 | enabled=None,
13 | device_role=None,
14 | loaded=True,
15 | device_name=None,
16 | device_id=None,
17 | device_vendor=None,
18 | state=None,
19 | automated_parameters=None,
20 | name=None,
21 | color=None,
22 | comment=None,
23 | ):
24 | super().__init__(name, color, comment)
25 | self.enabled = enabled
26 | self.device_role = device_role
27 | self.loaded = loaded
28 | self.device_name = device_name
29 | self.device_id = device_id
30 | self.device_vendor = device_vendor
31 | self.state = state
32 | self.automated_parameters = automated_parameters if automated_parameters else []
33 |
34 | def to_xml(self):
35 | device_elem = super().to_xml()
36 | device_elem.tag = "Device"
37 |
38 | if self.automated_parameters:
39 | parameters_elem = ET.SubElement(device_elem, "Parameters")
40 | for param in self.automated_parameters:
41 | parameters_elem.append(param.to_xml())
42 |
43 | if self.enabled is not None:
44 | ET.SubElement(device_elem, "Enabled")
45 |
46 | if self.device_role is not None:
47 | device_elem.set("deviceRole", self.device_role.value)
48 |
49 | if self.loaded is not None:
50 | device_elem.set("loaded", str(self.loaded).lower())
51 |
52 | if self.device_name is not None:
53 | device_elem.set("deviceName", self.device_name)
54 |
55 | if self.device_id is not None:
56 | device_elem.set("deviceID", self.device_id)
57 |
58 | if self.device_vendor is not None:
59 | device_elem.set("deviceVendor", self.device_vendor)
60 |
61 | if self.state:
62 | device_elem.append(self.state.to_xml())
63 |
64 | return device_elem
65 |
66 | @classmethod
67 | def from_xml(cls, element):
68 | instance = super().from_xml(element)
69 |
70 | enabled_elem = element.find("Enabled")
71 | instance.enabled = (
72 | BoolParameter.from_xml(enabled_elem) if enabled_elem is not None else None
73 | )
74 |
75 | device_role = element.get("deviceRole")
76 | instance.device_role = DeviceRole(device_role) if device_role else None
77 |
78 | loaded = element.get("loaded")
79 | instance.loaded = loaded.lower() == "true" if loaded else True
80 |
81 | instance.device_name = element.get("deviceName")
82 | instance.device_id = element.get("deviceID")
83 | instance.device_vendor = element.get("deviceVendor")
84 |
85 | state_elem = element.find("State")
86 | instance.state = (
87 | FileReference.from_xml(state_elem) if state_elem is not None else None
88 | )
89 |
90 | parameters_elem = element.find("Parameters")
91 | parameters = []
92 | if parameters_elem is not None:
93 | for param_elem in parameters_elem:
94 | param_class = globals().get(param_elem.tag)
95 | if param_class and issubclass(param_class, Parameter):
96 | parameters.append(param_class.from_xml(param_elem))
97 | instance.automated_parameters = parameters
98 |
99 | return instance
100 |
--------------------------------------------------------------------------------
/classes/deviceRole.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class DeviceRole(Enum):
5 | INSTRUMENT = "instrument"
6 | NOTE_FX = "noteFX"
7 | AUDIO_FX = "audioFX"
8 | ANALYZER = "analyzer"
9 |
--------------------------------------------------------------------------------
/classes/doubleAdapter.py:
--------------------------------------------------------------------------------
1 | class DoubleAdapter:
2 | @staticmethod
3 | def to_xml(value: float) -> str:
4 | """
5 | Converts a float value to a string for XML serialization.
6 | Handles special cases: float('inf') becomes 'inf', float('-inf') becomes '-inf'.
7 | """
8 | if value is None:
9 | return None
10 | if value == float("inf"):
11 | return "inf"
12 | elif value == float("-inf"):
13 | return "-inf"
14 | else:
15 | return f"{value:.6f}"
16 |
17 | @staticmethod
18 | def from_xml(value: str) -> float:
19 | """
20 | Converts a string from XML to a float value.
21 | Handles special cases: 'inf' becomes float('inf'), '-inf' becomes float('-inf').
22 | """
23 | if value is None or value in ("null", ""):
24 | return None
25 | if value == "inf":
26 | return float("inf")
27 | elif value == "-inf":
28 | return float("-inf")
29 | else:
30 | return float(value)
31 |
--------------------------------------------------------------------------------
/classes/eqBand.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.realParameter import RealParameter
3 | from classes.boolParameter import BoolParameter
4 | from classes.eqBandType import EqBandType
5 | from classes.unit import Unit # Assuming this is where the Unit enum is defined
6 |
7 |
8 | class EqBand:
9 | def __init__(
10 | self, freq=None, gain=None, q=None, enabled=None, band_type=None, order=None
11 | ):
12 | # Ensure freq, gain, and q are RealParameter instances
13 | self.freq = freq if isinstance(freq, RealParameter) else RealParameter(freq)
14 | self.gain = gain if isinstance(gain, RealParameter) else RealParameter(gain)
15 | self.q = q if isinstance(q, RealParameter) else RealParameter(q)
16 |
17 | self.enabled = (
18 | enabled if isinstance(enabled, BoolParameter) else BoolParameter(enabled)
19 | )
20 | self.band_type = band_type # Expected to be an instance of EqBandType
21 | self.order = order # Expected to be an integer or None
22 |
23 | def to_xml(self):
24 | band_elem = ET.Element("Band")
25 |
26 | # Create specific elements for Freq, Gain, and Q with the required unit attribute from the Unit enum
27 | freq_elem = ET.Element("Freq")
28 | freq_elem.set("id", self.freq.id)
29 | freq_elem.set("value", str(self.freq.value))
30 | freq_elem.set("unit", Unit.HERTZ.value) # Using the Unit enum for frequency
31 |
32 | gain_elem = ET.Element("Gain")
33 | gain_elem.set("id", self.gain.id)
34 | gain_elem.set("value", str(self.gain.value))
35 | gain_elem.set("unit", Unit.DECIBEL.value) # Using the Unit enum for gain
36 |
37 | q_elem = ET.Element("Q")
38 | q_elem.set("id", self.q.id)
39 | q_elem.set("value", str(self.q.value))
40 | q_elem.set(
41 | "unit", Unit.LINEAR.value
42 | ) # Assuming Q is unitless but using a suitable enum value
43 |
44 | # Add these elements to the Band element
45 | band_elem.append(freq_elem)
46 | band_elem.append(gain_elem)
47 | band_elem.append(q_elem)
48 |
49 | # Add BoolParameter element with appropriate tag
50 | enabled_elem = ET.Element("Enabled")
51 | enabled_elem.set("id", self.enabled.id)
52 | enabled_elem.set("value", str(self.enabled.value).lower())
53 | band_elem.append(enabled_elem)
54 |
55 | # Set attributes
56 | if self.band_type is not None:
57 | band_elem.set(
58 | "type", str(self.band_type.value)
59 | ) # Assuming EqBandType has a 'value' attribute
60 | if self.order is not None:
61 | band_elem.set("order", str(self.order))
62 |
63 | return band_elem
64 |
65 | @classmethod
66 | def from_xml(cls, element):
67 | # Parse specific elements Freq, Gain, and Q
68 | freq = RealParameter.from_xml(element.find("Freq"))
69 | gain = RealParameter.from_xml(element.find("Gain"))
70 | q = RealParameter.from_xml(element.find("Q"))
71 |
72 | # Parse BoolParameter element
73 | enabled = BoolParameter.from_xml(element.find("Enabled"))
74 |
75 | # Parse attributes
76 | band_type = EqBandType(element.get("type")) if element.get("type") else None
77 | order = int(element.get("order")) if element.get("order") else None
78 |
79 | return cls(
80 | freq=freq, gain=gain, q=q, enabled=enabled, band_type=band_type, order=order
81 | )
82 |
--------------------------------------------------------------------------------
/classes/eqBandType.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class EqBandType(Enum):
5 | HIGH_PASS = "highPass"
6 | LOW_PASS = "lowPass"
7 | BAND_PASS = "bandPass"
8 | HIGH_SHELF = "highShelf"
9 | LOW_SHELF = "lowShelf"
10 | BELL = "bell"
11 | NOTCH = "notch"
12 |
--------------------------------------------------------------------------------
/classes/equalizer.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.realParameter import RealParameter
3 | from classes.eqBand import EqBand
4 | from classes.unit import Unit # Assuming this is where the Unit enum is defined
5 |
6 |
7 | class Equalizer:
8 | def __init__(
9 | self,
10 | device_name=None,
11 | device_role=None,
12 | bands=None,
13 | input_gain=None,
14 | output_gain=None,
15 | ):
16 | self.device_name = device_name # The required deviceName attribute
17 | self.device_role = device_role # The required deviceRole attribute
18 | self.bands = bands if bands is not None else []
19 |
20 | # Ensure input_gain and output_gain are RealParameter instances
21 | self.input_gain = (
22 | input_gain
23 | if isinstance(input_gain, RealParameter)
24 | else RealParameter(input_gain)
25 | )
26 | self.output_gain = (
27 | output_gain
28 | if isinstance(output_gain, RealParameter)
29 | else RealParameter(output_gain)
30 | )
31 |
32 | def to_xml(self):
33 | eq_elem = ET.Element("Equalizer")
34 |
35 | # Set the required deviceName attribute
36 | if self.device_name:
37 | eq_elem.set("deviceName", self.device_name)
38 | else:
39 | raise ValueError("deviceName attribute is required but not provided.")
40 |
41 | # Set the required deviceRole attribute
42 | if self.device_role:
43 | eq_elem.set("deviceRole", self.device_role)
44 | else:
45 | raise ValueError("deviceRole attribute is required but not provided.")
46 |
47 | # Add bands as child elements
48 | for band in self.bands:
49 | eq_elem.append(band.to_xml())
50 |
51 | # Add InputGain as a child element with the unit attribute
52 | input_gain_elem = ET.Element("InputGain")
53 | input_gain_elem.set(
54 | "unit", Unit.DECIBEL.value
55 | ) # Assuming the unit for InputGain is in decibels
56 | input_gain_elem.extend(self.input_gain.to_xml().getchildren())
57 | eq_elem.append(input_gain_elem)
58 |
59 | # Add OutputGain as a child element with the unit attribute
60 | output_gain_elem = ET.Element("OutputGain")
61 | output_gain_elem.set(
62 | "unit", Unit.DECIBEL.value
63 | ) # Assuming the unit for OutputGain is in decibels
64 | output_gain_elem.extend(self.output_gain.to_xml().getchildren())
65 | eq_elem.append(output_gain_elem)
66 |
67 | return eq_elem
68 |
69 | @classmethod
70 | def from_xml(cls, element):
71 | device_name = element.get("deviceName")
72 | if not device_name:
73 | raise ValueError("deviceName attribute is required but missing in the XML.")
74 |
75 | device_role = element.get("deviceRole")
76 | if not device_role:
77 | raise ValueError("deviceRole attribute is required but missing in the XML.")
78 |
79 | bands = []
80 | for band_elem in element.findall("Band"):
81 | bands.append(EqBand.from_xml(band_elem))
82 |
83 | # Extract the RealParameter from the InputGain and OutputGain elements
84 | input_gain_elem = element.find("InputGain")
85 | input_gain = RealParameter.from_xml(input_gain_elem.find("RealParameter"))
86 |
87 | output_gain_elem = element.find("OutputGain")
88 | output_gain = RealParameter.from_xml(output_gain_elem.find("RealParameter"))
89 |
90 | return cls(
91 | device_name=device_name,
92 | device_role=device_role,
93 | bands=bands,
94 | input_gain=input_gain,
95 | output_gain=output_gain,
96 | )
97 |
--------------------------------------------------------------------------------
/classes/expressionType.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class ExpressionType(Enum):
5 | GAIN = "gain"
6 | PAN = "pan"
7 | TRANSPOSE = "transpose"
8 | TIMBRE = "timbre"
9 | FORMANT = "formant"
10 | PRESSURE = "pressure"
11 | CHANNEL_CONTROLLER = "channelController"
12 | CHANNEL_PRESSURE = "channelPressure"
13 | POLY_PRESSURE = "polyPressure"
14 | PITCH_BEND = "pitchBend"
15 | PROGRAM_CHANGE = "programChange"
16 |
--------------------------------------------------------------------------------
/classes/fileReference.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 |
3 |
4 | class FileReference:
5 | def __init__(self, path, external=False):
6 | if path is None:
7 | raise ValueError("The 'path' attribute is required for FileReference")
8 | self.path = path
9 | self.external = external
10 |
11 | def to_xml(self):
12 | state_elem = ET.Element("State")
13 | state_elem.set("path", self.path)
14 | state_elem.set("external", str(self.external).lower())
15 | return state_elem
16 |
17 | @classmethod
18 | def from_xml(cls, element):
19 | path = element.get("path")
20 | external = element.get("external")
21 | external = external.lower() == "true" if external else False
22 | return cls(path, external)
23 |
--------------------------------------------------------------------------------
/classes/interpolation.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class Interpolation(Enum):
5 | HOLD = "hold"
6 | LINEAR = "linear"
7 |
--------------------------------------------------------------------------------
/classes/lane.py:
--------------------------------------------------------------------------------
1 | from classes.referenceable import Referenceable
2 |
3 |
4 | class Lane(Referenceable):
5 | pass
6 |
--------------------------------------------------------------------------------
/classes/lanes.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.timeline import Timeline
3 |
4 |
5 | class Lanes(Timeline):
6 | XML_TAG = "Lanes"
7 |
8 | def __init__(
9 | self,
10 | lanes=None,
11 | track=None,
12 | time_unit=None,
13 | name=None,
14 | color=None,
15 | comment=None,
16 | ):
17 | super().__init__(track, time_unit, name, color, comment)
18 | self.lanes = lanes if lanes else []
19 |
20 | def to_xml(self):
21 | # Create the XML element for Lanes
22 | lanes_elem = ET.Element(self.XML_TAG)
23 |
24 | # Set attributes specific to Lanes
25 | if self.time_unit:
26 | lanes_elem.set("timeUnit", str(self.time_unit.value))
27 | if self.track:
28 | lanes_elem.set(
29 | "track", str(self.track.id)
30 | ) # Assuming track has an id attribute
31 |
32 | # Append child elements for each lane
33 | for lane in self.lanes:
34 | lanes_elem.append(lane.to_xml())
35 |
36 | return lanes_elem
37 |
38 | @classmethod
39 | def from_xml(cls, element):
40 | # Initialize instance using the parent class's method
41 | instance = super().from_xml(element)
42 |
43 | # Process child elements of type Timeline
44 | lanes = []
45 | for lane_elem in element.findall(".//Timeline"):
46 | lane_type = lane_elem.tag
47 | lane_class = globals().get(lane_type)
48 | if lane_class and issubclass(lane_class, Timeline):
49 | lanes.append(lane_class.from_xml(lane_elem))
50 | instance.lanes = lanes
51 |
52 | return instance
53 |
--------------------------------------------------------------------------------
/classes/marker.py:
--------------------------------------------------------------------------------
1 | from classes.nameable import Nameable
2 |
3 |
4 | class Marker(Nameable):
5 | def __init__(self, time, name=None, color=None, comment=None):
6 | super().__init__(name, color, comment)
7 | self.time = time
8 |
9 | def to_xml(self):
10 | marker_elem = super().to_xml()
11 | marker_elem.tag = "Marker"
12 | marker_elem.set("time", str(self.time))
13 | return marker_elem
14 |
15 | @classmethod
16 | def from_xml(cls, element):
17 | instance = super().from_xml(element)
18 | time = element.get("time")
19 | instance.time = float(time) if time is not None else None
20 | return instance
21 |
--------------------------------------------------------------------------------
/classes/markers.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.timeline import Timeline
3 | from classes.marker import Marker
4 |
5 |
6 | class Markers(Timeline):
7 | XML_TAG = "Markers"
8 |
9 | def __init__(
10 | self,
11 | markers=None,
12 | track=None,
13 | time_unit=None,
14 | name=None,
15 | color=None,
16 | comment=None,
17 | ):
18 | super().__init__(track, time_unit, name, color, comment)
19 | self.markers = markers if markers else []
20 |
21 | def to_xml(self):
22 | markers_elem = ET.Element(self.XML_TAG)
23 |
24 | if self.time_unit:
25 | markers_elem.set("timeUnit", str(self.time_unit))
26 | if self.track:
27 | markers_elem.set(
28 | "track", str(self.track.id)
29 | ) # Assuming track has an id attribute
30 |
31 | for marker in self.markers:
32 | markers_elem.append(marker.to_xml())
33 |
34 | if self.name:
35 | markers_elem.set("name", self.name)
36 | if self.color:
37 | markers_elem.set("color", self.color)
38 | if self.comment:
39 | markers_elem.set("comment", self.comment)
40 |
41 | return markers_elem
42 |
43 | @classmethod
44 | def from_xml(cls, element):
45 | instance = super().from_xml(element)
46 |
47 | markers = []
48 | for marker_elem in element.findall("Marker"):
49 | markers.append(Marker.from_xml(marker_elem))
50 | instance.markers = markers
51 |
52 | return instance
53 |
--------------------------------------------------------------------------------
/classes/mediaFile.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.fileReference import FileReference
3 | from classes.timeline import Timeline
4 |
5 |
6 | class MediaFile(Timeline):
7 | def __init__(self, file=None, duration=0.0, name=None):
8 | super().__init__(name) # Call the base class (Timeline) constructor
9 | self.file = (
10 | file if file else FileReference()
11 | ) # Ensure file is a FileReference instance
12 | self.duration = duration
13 |
14 | def to_xml(self):
15 | # The tag name will be determined by the subclass, e.g., "Audio", "Video"
16 | media_elem = ET.Element(
17 | "MediaFile"
18 | ) # Placeholder, should be overridden by subclass
19 |
20 | # Add the File element
21 | file_elem = self.file.to_xml()
22 | file_elem.tag = "File" # Ensure the tag is "File"
23 | media_elem.append(file_elem)
24 |
25 | # Set the duration attribute
26 | media_elem.set("duration", str(self.duration))
27 |
28 | return media_elem
29 |
30 | @classmethod
31 | def from_xml(cls, element):
32 | # Parse the File element
33 | file_elem = element.find("File")
34 | file = (
35 | FileReference.from_xml(file_elem)
36 | if file_elem is not None
37 | else FileReference()
38 | )
39 |
40 | # Parse the duration attribute
41 | duration = float(element.get("duration", 0.0))
42 |
43 | # Optional name attribute from Timeline
44 | name = element.get("name")
45 |
46 | return cls(file=file, duration=duration, name=name)
47 |
--------------------------------------------------------------------------------
/classes/metaData.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 |
3 |
4 | class MetaData:
5 | def __init__(
6 | self,
7 | title=None,
8 | artist=None,
9 | album=None,
10 | original_artist=None,
11 | composer=None,
12 | songwriter=None,
13 | producer=None,
14 | arranger=None,
15 | year=None,
16 | genre=None,
17 | copyright=None,
18 | website=None,
19 | comment=None,
20 | ):
21 | self.title = title
22 | self.artist = artist
23 | self.album = album
24 | self.original_artist = original_artist
25 | self.composer = composer
26 | self.songwriter = songwriter
27 | self.producer = producer
28 | self.arranger = arranger
29 | self.year = year
30 | self.genre = genre
31 | self.copyright = copyright
32 | self.website = website
33 | self.comment = comment
34 |
35 | def to_xml(self):
36 | root = ET.Element("MetaData")
37 |
38 | if self.title:
39 | title_elem = ET.SubElement(root, "Title")
40 | title_elem.text = self.title
41 | if self.artist:
42 | artist_elem = ET.SubElement(root, "Artist")
43 | artist_elem.text = self.artist
44 | if self.album:
45 | album_elem = ET.SubElement(root, "Album")
46 | album_elem.text = self.album
47 | if self.original_artist:
48 | original_artist_elem = ET.SubElement(root, "OriginalArtist")
49 | original_artist_elem.text = self.original_artist
50 | if self.composer:
51 | composer_elem = ET.SubElement(root, "Composer")
52 | composer_elem.text = self.composer
53 | if self.songwriter:
54 | songwriter_elem = ET.SubElement(root, "Songwriter")
55 | songwriter_elem.text = self.songwriter
56 | if self.producer:
57 | producer_elem = ET.SubElement(root, "Producer")
58 | producer_elem.text = self.producer
59 | if self.arranger:
60 | arranger_elem = ET.SubElement(root, "Arranger")
61 | arranger_elem.text = self.arranger
62 | if self.year:
63 | year_elem = ET.SubElement(root, "Year")
64 | year_elem.text = self.year
65 | if self.genre:
66 | genre_elem = ET.SubElement(root, "Genre")
67 | genre_elem.text = self.genre
68 | if self.copyright:
69 | copyright_elem = ET.SubElement(root, "Copyright")
70 | copyright_elem.text = self.copyright
71 | if self.website:
72 | website_elem = ET.SubElement(root, "Website")
73 | website_elem.text = self.website
74 | if self.comment:
75 | comment_elem = ET.SubElement(root, "Comment")
76 | comment_elem.text = self.comment
77 |
78 | return root
79 |
80 | @classmethod
81 | def from_xml(cls, element):
82 | title = element.findtext("Title")
83 | artist = element.findtext("Artist")
84 | album = element.findtext("Album")
85 | original_artist = element.findtext("OriginalArtist")
86 | composer = element.findtext("Composer")
87 | songwriter = element.findtext("Songwriter")
88 | producer = element.findtext("Producer")
89 | arranger = element.findtext("Arranger")
90 | year = element.findtext("Year")
91 | genre = element.findtext("Genre")
92 | copyright = element.findtext("Copyright")
93 | website = element.findtext("Website")
94 | comment = element.findtext("Comment")
95 |
96 | return cls(
97 | title,
98 | artist,
99 | album,
100 | original_artist,
101 | composer,
102 | songwriter,
103 | producer,
104 | arranger,
105 | year,
106 | genre,
107 | copyright,
108 | website,
109 | comment,
110 | )
111 |
--------------------------------------------------------------------------------
/classes/mixerRole.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class MixerRole(Enum):
5 | REGULAR = "regular"
6 | MASTER = "master"
7 | EFFECT_TRACK = "effectTrack"
8 | SUB_MIX = "subMix"
9 | VCA = "vca"
10 |
--------------------------------------------------------------------------------
/classes/nameable.py:
--------------------------------------------------------------------------------
1 | from abc import ABC
2 | from lxml import etree as ET
3 |
4 |
5 | class Nameable(ABC):
6 | def __init__(self, name=None, color=None, comment=None):
7 | self.name = name
8 | self.color = color
9 | self.comment = comment
10 |
11 | def to_xml(self):
12 | element = ET.Element(self.__class__.__name__)
13 |
14 | if self.name is not None:
15 | element.set("name", self.name)
16 | if self.color is not None:
17 | element.set("color", self.color)
18 | if self.comment is not None:
19 | element.set("comment", self.comment)
20 |
21 | return element
22 |
23 | @classmethod
24 | def from_xml(cls, element):
25 | name = element.get("name")
26 | color = element.get("color")
27 | comment = element.get("comment")
28 |
29 | return cls(name, color, comment)
30 |
--------------------------------------------------------------------------------
/classes/note.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.doubleAdapter import DoubleAdapter
3 | from classes.timeline import Timeline
4 |
5 |
6 | class Note:
7 | def __init__(
8 | self,
9 | time,
10 | duration,
11 | key,
12 | channel=0,
13 | velocity=None,
14 | release_velocity=None,
15 | content=None,
16 | ):
17 | self.time = time
18 | self.duration = duration
19 | self.channel = channel
20 | self.key = key
21 | self.velocity = velocity
22 | self.release_velocity = release_velocity
23 | self.content = content
24 |
25 | def to_xml(self):
26 | note_elem = ET.Element("Note")
27 | note_elem.set("time", DoubleAdapter.to_xml(self.time))
28 | note_elem.set("duration", DoubleAdapter.to_xml(self.duration))
29 | note_elem.set("key", str(self.key))
30 |
31 | if self.channel is not None:
32 | note_elem.set("channel", str(self.channel))
33 | if self.velocity is not None:
34 | note_elem.set("vel", DoubleAdapter.to_xml(self.velocity))
35 | if self.release_velocity is not None:
36 | note_elem.set("rel", DoubleAdapter.to_xml(self.release_velocity))
37 | if self.content is not None:
38 | content_elem = ET.SubElement(note_elem, "Content")
39 | content_elem.append(self.content.to_xml())
40 |
41 | return note_elem
42 |
43 | @classmethod
44 | def from_xml(cls, element):
45 | time = DoubleAdapter.from_xml(element.get("time"))
46 | duration = DoubleAdapter.from_xml(element.get("duration"))
47 | key = int(element.get("key"))
48 | channel = int(element.get("channel")) if element.get("channel") else 0
49 | velocity = (
50 | DoubleAdapter.from_xml(element.get("vel")) if element.get("vel") else None
51 | )
52 | release_velocity = (
53 | DoubleAdapter.from_xml(element.get("rel")) if element.get("rel") else None
54 | )
55 |
56 | content_elem = element.find("Content")
57 | content = None
58 | if content_elem is not None:
59 | content_class = globals().get(content_elem.tag)
60 | if content_class and issubclass(content_class, Timeline):
61 | content = content_class.from_xml(content_elem)
62 |
63 | return cls(time, duration, key, channel, velocity, release_velocity, content)
64 |
--------------------------------------------------------------------------------
/classes/notes.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.timeline import Timeline
3 | from classes.note import Note
4 |
5 |
6 | class Notes(Timeline):
7 | XML_TAG = "Notes"
8 |
9 | def __init__(
10 | self,
11 | notes=None,
12 | track=None,
13 | time_unit=None,
14 | name=None,
15 | color=None,
16 | comment=None,
17 | ):
18 | super().__init__(track, time_unit, name, color, comment)
19 | self.notes = notes if notes else []
20 |
21 | def to_xml(self):
22 | # Create the Notes XML element
23 | notes_elem = ET.Element(self.XML_TAG)
24 |
25 | # Set attributes specific to Notes
26 | if self.time_unit:
27 | notes_elem.set("timeUnit", str(self.time_unit))
28 | if self.track:
29 | notes_elem.set(
30 | "track", str(self.track.id)
31 | ) # Assuming track has an id attribute
32 |
33 | # Append child elements for each note
34 | for note in self.notes:
35 | notes_elem.append(note.to_xml())
36 |
37 | # Append inherited attributes if available
38 | if self.name:
39 | notes_elem.set("name", self.name)
40 | if self.color:
41 | notes_elem.set("color", self.color)
42 | if self.comment:
43 | notes_elem.set("comment", self.comment)
44 |
45 | return notes_elem
46 |
47 | @classmethod
48 | def from_xml(cls, element):
49 | # Initialize using parent class method
50 | instance = super().from_xml(element)
51 |
52 | # Process child elements of type Note
53 | notes = []
54 | for note_elem in element.findall("Note"):
55 | notes.append(Note.from_xml(note_elem))
56 | instance.notes = notes
57 |
58 | return instance
59 |
--------------------------------------------------------------------------------
/classes/parameter.py:
--------------------------------------------------------------------------------
1 | from classes.referenceable import Referenceable
2 |
3 |
4 | class Parameter(Referenceable):
5 | def __init__(self, parameter_id=None, name=None, color=None, comment=None):
6 | super().__init__(name, color, comment)
7 | self.parameter_id = parameter_id
8 |
9 | def to_xml(self):
10 | parameter_elem = (
11 | super().to_xml()
12 | ) # Call the superclass to_xml to get common attributes
13 | if self.parameter_id is not None:
14 | parameter_elem.set(
15 | "parameterID", str(self.parameter_id)
16 | ) # Set the parameterID attribute
17 | return parameter_elem
18 |
19 | @classmethod
20 | def from_xml(cls, element):
21 | instance = super().from_xml(element) # Initialize using the superclass method
22 | parameter_id = element.get("parameterID")
23 | instance.parameter_id = int(parameter_id) if parameter_id is not None else None
24 | return instance
25 |
--------------------------------------------------------------------------------
/classes/point.py:
--------------------------------------------------------------------------------
1 | from abc import ABC
2 | from lxml import etree as ET
3 | from classes.doubleAdapter import DoubleAdapter
4 |
5 |
6 | class Point(ABC):
7 | def __init__(self, time=None):
8 | self.time = time
9 |
10 | def to_xml(self):
11 | point_elem = ET.Element(self.__class__.__name__)
12 | if self.time is not None:
13 | point_elem.set("time", DoubleAdapter.to_xml(self.time))
14 | return point_elem
15 |
16 | @classmethod
17 | def from_xml(cls, element):
18 | time = element.get("time")
19 | time = DoubleAdapter.from_xml(time) if time is not None else None
20 | return cls(time)
21 |
--------------------------------------------------------------------------------
/classes/points.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.timeline import Timeline
3 | from classes.automationTarget import AutomationTarget
4 |
5 |
6 | class Points(Timeline):
7 | XML_TAG = "Points"
8 |
9 | def __init__(
10 | self,
11 | target=None,
12 | points=None,
13 | unit=None,
14 | track=None,
15 | time_unit=None,
16 | name=None,
17 | color=None,
18 | comment=None,
19 | ):
20 | super().__init__(track, time_unit, name, color, comment)
21 | self.target = target if target else AutomationTarget()
22 | self.points = points if points else []
23 | self.unit = unit
24 |
25 | def to_xml(self):
26 | points_elem = ET.Element(self.XML_TAG)
27 |
28 | if self.time_unit:
29 | points_elem.set("timeUnit", str(self.time_unit))
30 | if self.track:
31 | points_elem.set("track", str(self.track.id))
32 | if self.unit:
33 | points_elem.set("unit", self.unit)
34 |
35 | # Create the Target element with attributes directly set
36 | target_elem = self.target.to_xml()
37 | points_elem.append(target_elem)
38 |
39 | for point in self.points:
40 | points_elem.append(point.to_xml())
41 |
42 | if self.name:
43 | points_elem.set("name", self.name)
44 | if self.color:
45 | points_elem.set("color", self.color)
46 | if self.comment:
47 | points_elem.set("comment", self.comment)
48 |
49 | return points_elem
50 |
51 | @classmethod
52 | def from_xml(cls, element):
53 | instance = super().from_xml(element)
54 |
55 | target_elem = element.find("Target")
56 | instance.target = (
57 | AutomationTarget.from_xml(target_elem)
58 | if target_elem is not None
59 | else AutomationTarget()
60 | )
61 |
62 | points = []
63 | for point_elem in element:
64 | if point_elem.tag in [
65 | "Point",
66 | "RealPoint",
67 | "EnumPoint",
68 | "BoolPoint",
69 | "IntegerPoint",
70 | "TimeSignaturePoint",
71 | ]:
72 | point_class = globals().get(point_elem.tag)
73 | if point_class:
74 | points.append(point_class.from_xml(point_elem))
75 | instance.points = points
76 |
77 | instance.unit = element.get("unit")
78 |
79 | return instance
80 |
--------------------------------------------------------------------------------
/classes/project.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.application import Application
3 | from classes.transport import Transport
4 | from classes.lane import Lane
5 | from classes.arrangement import Arrangement
6 | from classes.scene import Scene
7 |
8 |
9 | class Project:
10 | CURRENT_VERSION = "1.0"
11 |
12 | def __init__(
13 | self,
14 | version=None,
15 | application=None,
16 | transport=None,
17 | structure=None,
18 | arrangement=None,
19 | scenes=None,
20 | ):
21 | self.version = version if version else self.CURRENT_VERSION
22 | self.application = application if application else Application()
23 | self.transport = transport
24 | self.structure = structure if structure else []
25 | self.arrangement = arrangement
26 | self.scenes = scenes if scenes else []
27 |
28 | def to_xml(self):
29 | root = ET.Element("Project", version=self.version)
30 | print(f"Creating XML for Project with version: {self.version}")
31 |
32 | # Correctly handling Application element with attributes
33 | app_elem = self.application.to_xml()
34 | root.append(app_elem)
35 | print("Added Application element.")
36 |
37 | if self.transport:
38 | transport_elem = ET.SubElement(root, "Transport")
39 | transport_elem.append(self.transport.to_xml())
40 | print("Added Transport element.")
41 |
42 | if self.structure:
43 | structure_elem = ET.SubElement(root, "Structure")
44 | for lane in self.structure:
45 | structure_elem.append(lane.to_xml())
46 | print("Added Structure element with lanes.")
47 |
48 | if self.arrangement:
49 | arrangement_elem = ET.SubElement(root, "Arrangement")
50 | arrangement_elem.append(self.arrangement.to_xml())
51 | print("Added Arrangement element.")
52 |
53 | if self.scenes:
54 | scenes_elem = ET.SubElement(root, "Scenes")
55 | for scene in self.scenes:
56 | scene_elem = ET.SubElement(scenes_elem, "Scene")
57 | scene_elem.append(scene.to_xml())
58 | print("Added Scenes element with scenes.")
59 |
60 | return root
61 |
62 | @classmethod
63 | def from_xml(cls, element):
64 | version = element.get("version", cls.CURRENT_VERSION)
65 | application = Application.from_xml(element.find("Application"))
66 | transport_elem = element.find("Transport")
67 | transport = (
68 | Transport.from_xml(transport_elem) if transport_elem is not None else None
69 | )
70 |
71 | structure_elem = element.find("Structure")
72 | structure = (
73 | [Lane.from_xml(lane) for lane in structure_elem]
74 | if structure_elem is not None
75 | else []
76 | )
77 |
78 | arrangement_elem = element.find("Arrangement")
79 | arrangement = (
80 | Arrangement.from_xml(arrangement_elem)
81 | if arrangement_elem is not None
82 | else None
83 | )
84 |
85 | scenes_elem = element.find("Scenes")
86 | scenes = (
87 | [Scene.from_xml(scene) for scene in scenes_elem]
88 | if scenes_elem is not None
89 | else []
90 | )
91 |
92 | return cls(version, application, transport, structure, arrangement, scenes)
93 |
--------------------------------------------------------------------------------
/classes/realParameter.py:
--------------------------------------------------------------------------------
1 | from classes.parameter import Parameter
2 | from classes.doubleAdapter import DoubleAdapter
3 | from classes.unit import Unit
4 |
5 |
6 | class RealParameter(Parameter):
7 | def __init__(self, value=None, unit=None, min_value=None, max_value=None):
8 | super().__init__()
9 | self.value = value
10 | self.unit = unit
11 | self.min = min_value
12 | self.max = max_value
13 |
14 | def to_xml(self):
15 | param_elem = super().to_xml()
16 | param_elem.tag = "RealParameter"
17 |
18 | if self.value is not None:
19 | param_elem.set("value", DoubleAdapter.to_xml(self.value))
20 | if self.unit is not None:
21 | param_elem.set("unit", self.unit.name)
22 | if self.min is not None:
23 | param_elem.set("min", DoubleAdapter.to_xml(self.min))
24 | if self.max is not None:
25 | param_elem.set("max", DoubleAdapter.to_xml(self.max))
26 |
27 | return param_elem
28 |
29 | @classmethod
30 | def from_xml(cls, element):
31 | value = DoubleAdapter.from_xml(element.get("value"))
32 | unit = Unit(element.get("unit"))
33 | min_value = DoubleAdapter.from_xml(element.get("min"))
34 | max_value = DoubleAdapter.from_xml(element.get("max"))
35 |
36 | return cls(value=value, unit=unit, min_value=min_value, max_value=max_value)
37 |
--------------------------------------------------------------------------------
/classes/realPoint.py:
--------------------------------------------------------------------------------
1 | from classes.point import Point
2 | from classes.doubleAdapter import DoubleAdapter
3 | from classes.interpolation import Interpolation
4 |
5 |
6 | class RealPoint(Point):
7 | def __init__(self, time, value, interpolation=None):
8 | super().__init__(time)
9 | self.value = value
10 | self.interpolation = interpolation
11 |
12 | def to_xml(self):
13 | real_point_elem = super().to_xml()
14 | real_point_elem.tag = "RealPoint"
15 | real_point_elem.set("value", DoubleAdapter.to_xml(self.value))
16 | if self.interpolation is not None:
17 | real_point_elem.set("interpolation", self.interpolation.value)
18 | return real_point_elem
19 |
20 | @classmethod
21 | def from_xml(cls, element):
22 | instance = super().from_xml(element)
23 | instance.value = DoubleAdapter.from_xml(element.get("value"))
24 | interpolation = element.get("interpolation")
25 | instance.interpolation = Interpolation(interpolation) if interpolation else None
26 | return instance
27 |
--------------------------------------------------------------------------------
/classes/referenceable.py:
--------------------------------------------------------------------------------
1 | from classes.nameable import Nameable
2 |
3 |
4 | class Referenceable(Nameable):
5 | ID = 0
6 | _instances = {}
7 |
8 | @classmethod
9 | def reset_id(cls):
10 | cls.ID = 0
11 | cls._instances = {}
12 |
13 | def __init__(self, name=None, color=None, comment=None):
14 | super().__init__(name, color, comment)
15 | self.id = f"id{Referenceable.ID}"
16 | Referenceable._instances[self.id] = self
17 | Referenceable.ID += 1
18 |
19 | def to_xml(self):
20 | element = super().to_xml()
21 | element.set("id", self.id)
22 | return element
23 |
24 | @classmethod
25 | def from_xml(cls, element):
26 | instance = super().from_xml(element)
27 | instance.id = element.get("id")
28 | cls._instances[instance.id] = instance
29 | return instance
30 |
31 | @classmethod
32 | def get_by_id(cls, id):
33 | return cls._instances.get(id)
34 |
--------------------------------------------------------------------------------
/classes/scene.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.referenceable import Referenceable
3 | from classes.timeline import Timeline
4 |
5 |
6 | class Scene(Referenceable):
7 | def __init__(self, content=None, name=None, color=None, comment=None):
8 | super().__init__(name, color, comment)
9 | self.content = content
10 |
11 | def to_xml(self):
12 | scene_elem = super().to_xml()
13 | scene_elem.tag = "Scene"
14 |
15 | if self.content:
16 | content_elem = ET.SubElement(scene_elem, "Timeline")
17 | content_elem.append(self.content.to_xml())
18 |
19 | return scene_elem
20 |
21 | @classmethod
22 | def from_xml(cls, element):
23 | instance = super().from_xml(element)
24 |
25 | content_elem = element.find("Timeline")
26 | if content_elem is not None:
27 | content_class = globals().get(content_elem.tag)
28 | if content_class and issubclass(content_class, Timeline):
29 | instance.content = content_class.from_xml(content_elem)
30 |
31 | return instance
32 |
--------------------------------------------------------------------------------
/classes/send.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.referenceable import Referenceable
3 | from classes.sendType import SendType
4 | from classes.realParameter import RealParameter
5 |
6 |
7 | class Send(Referenceable):
8 | def __init__(
9 | self,
10 | volume=None,
11 | pan=None,
12 | type=SendType.SendType.POST,
13 | destination=None,
14 | name=None,
15 | color=None,
16 | comment=None,
17 | ):
18 | super().__init__(name, color, comment)
19 | self.volume = volume
20 | self.pan = pan
21 | self.type = type
22 | self.destination = destination
23 |
24 | def to_xml(self):
25 | send_elem = super().to_xml()
26 | send_elem.tag = "Send"
27 |
28 | if self.volume:
29 | volume_elem = ET.SubElement(send_elem, "Volume")
30 | volume_elem.append(self.volume.to_xml())
31 | if self.pan:
32 | pan_elem = ET.SubElement(send_elem, "Pan")
33 | pan_elem.append(self.pan.to_xml())
34 | send_elem.set("type", self.type)
35 | if self.destination:
36 | send_elem.set("destination", self.destination.id)
37 |
38 | return send_elem
39 |
40 | @classmethod
41 | def from_xml(cls, element):
42 | from classes.channel import Channel
43 |
44 | instance = super().from_xml(element)
45 |
46 | volume_elem = element.find("Volume")
47 | instance.volume = (
48 | RealParameter.from_xml(volume_elem) if volume_elem is not None else None
49 | )
50 |
51 | pan_elem = element.find("Pan")
52 | instance.pan = (
53 | RealParameter.from_xml(pan_elem) if pan_elem is not None else None
54 | )
55 |
56 | instance.type = element.get("type")
57 |
58 | destination_id = element.get("destination")
59 | instance.destination = (
60 | Channel.get_channel_by_id(destination_id)
61 | if destination_id is not None
62 | else None
63 | )
64 |
65 | return instance
66 |
--------------------------------------------------------------------------------
/classes/sendType.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class SendType(Enum):
5 | PRE = "pre"
6 | POST = "post"
7 |
--------------------------------------------------------------------------------
/classes/timeSignatureParameter.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 |
3 |
4 | class TimeSignatureParameter:
5 | def __init__(self, numerator=None, denominator=None):
6 | self.numerator = numerator
7 | self.denominator = denominator
8 |
9 | def to_xml(self):
10 | time_signature_elem = ET.Element("TimeSignatureParameter")
11 |
12 | if self.numerator is not None:
13 | time_signature_elem.set("numerator", str(self.numerator))
14 | if self.denominator is not None:
15 | time_signature_elem.set("denominator", str(self.denominator))
16 |
17 | return time_signature_elem
18 |
19 | @classmethod
20 | def from_xml(cls, element):
21 | numerator = element.get("numerator")
22 | numerator = int(numerator) if numerator is not None else None
23 |
24 | denominator = element.get("denominator")
25 | denominator = int(denominator) if denominator is not None else None
26 |
27 | return cls(numerator, denominator)
28 |
--------------------------------------------------------------------------------
/classes/timeUnit.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class TimeUnit(Enum):
5 | BEATS = "beats"
6 | SECONDS = "seconds"
7 |
--------------------------------------------------------------------------------
/classes/timeline.py:
--------------------------------------------------------------------------------
1 | from classes.referenceable import Referenceable
2 |
3 |
4 | class Timeline(Referenceable):
5 | def __init__(self, track=None, time_unit=None, name=None, color=None, comment=None):
6 | super().__init__(name, color, comment)
7 | self.track = track
8 | self.time_unit = time_unit
9 |
10 | def to_xml(self):
11 | raise NotImplementedError("Subclasses should implement this method.")
12 |
13 | @classmethod
14 | def from_xml(cls, element):
15 | raise NotImplementedError("Subclasses should implement this method.")
16 |
--------------------------------------------------------------------------------
/classes/track.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.lane import Lane
3 | from classes.channel import Channel
4 |
5 |
6 | class Track(Lane):
7 | def __init__(
8 | self,
9 | content_type=None,
10 | loaded=None,
11 | channel=None,
12 | tracks=None,
13 | name=None,
14 | color=None,
15 | comment=None,
16 | ):
17 | super().__init__(name, color, comment)
18 | self.content_type = content_type if content_type else []
19 | self.loaded = loaded
20 | self.channel = channel
21 | self.tracks = tracks if tracks else []
22 |
23 | def to_xml(self):
24 | # Inheriting to_xml from Lane and changing tag
25 | track_elem = super().to_xml()
26 | track_elem.tag = "Track"
27 |
28 | # Set content_type as an attribute
29 | if self.content_type:
30 | content_type_str = ",".join(str(ct.value) for ct in self.content_type)
31 | track_elem.set("contentType", content_type_str)
32 |
33 | # Set loaded as an attribute, converting to lowercase string for XML
34 | if self.loaded is not None:
35 | track_elem.set("loaded", str(self.loaded).lower())
36 |
37 | # Append Channel as a nested XML element if present
38 | if self.channel:
39 | channel_elem = ET.SubElement(track_elem, "Channel")
40 | channel_elem.extend(self.channel.to_xml().getchildren())
41 |
42 | # Recursively add nested tracks
43 | for track in self.tracks:
44 | track_elem.append(track.to_xml())
45 |
46 | return track_elem
47 |
48 | @classmethod
49 | def from_xml(cls, element):
50 | # Call the superclass method to initialize inherited attributes
51 | instance = super().from_xml(element)
52 |
53 | # Extract contentType text and split into a list
54 | content_type_elem = element.find("contentType")
55 | if content_type_elem is not None and content_type_elem.text:
56 | instance.content_type = content_type_elem.text.split(",")
57 |
58 | # Parse the loaded attribute, converting 'true'/'false' to Boolean
59 | loaded = element.get("loaded")
60 | instance.loaded = loaded.lower() == "true" if loaded else None
61 |
62 | # Initialize channel using Channel's from_xml method if present
63 | channel_elem = element.find("Channel")
64 | instance.channel = (
65 | Channel.from_xml(channel_elem) if channel_elem is not None else None
66 | )
67 |
68 | # Recursively parse nested Track elements
69 | tracks = []
70 | for track_elem in element.findall("Track"):
71 | tracks.append(Track.from_xml(track_elem))
72 | instance.tracks = tracks
73 |
74 | return instance
75 |
--------------------------------------------------------------------------------
/classes/transport.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.realParameter import RealParameter
3 | from classes.timeSignatureParameter import TimeSignatureParameter
4 | from classes.unit import Unit
5 |
6 |
7 | class Transport:
8 | def __init__(self, tempo=None, time_signature=None):
9 | self.tempo = tempo
10 | self.time_signature = time_signature
11 |
12 | def to_xml(self):
13 | transport_elem = ET.Element("Transport")
14 |
15 | if self.tempo:
16 | tempo_elem = ET.SubElement(transport_elem, "Tempo")
17 | # Assuming self.tempo is an instance of RealParameter with attributes id, value, unit
18 | tempo_elem.set("id", self.tempo.id)
19 | tempo_elem.set("value", str(self.tempo.value))
20 | tempo_elem.set("unit", Unit.BPM.value)
21 |
22 | if self.time_signature:
23 | time_signature_elem = ET.SubElement(transport_elem, "TimeSignature")
24 | time_signature_elem.append(self.time_signature.to_xml())
25 |
26 | return transport_elem
27 |
28 | @classmethod
29 | def from_xml(cls, element):
30 | tempo_elem = element.find("Tempo")
31 | tempo = RealParameter.from_xml(tempo_elem) if tempo_elem is not None else None
32 |
33 | time_signature_elem = element.find("TimeSignature")
34 | time_signature = (
35 | TimeSignatureParameter.from_xml(time_signature_elem)
36 | if time_signature_elem is not None
37 | else None
38 | )
39 |
40 | return cls(tempo, time_signature)
41 |
--------------------------------------------------------------------------------
/classes/unit.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class Unit(Enum):
5 | LINEAR = "linear"
6 | NORMALIZED = "normalized"
7 | PERCENT = "percent"
8 | DECIBEL = "decibel"
9 | HERTZ = "hertz"
10 | SEMITONES = "semitones"
11 | SECONDS = "seconds"
12 | BEATS = "beats"
13 | BPM = "bpm"
14 |
--------------------------------------------------------------------------------
/classes/utility.py:
--------------------------------------------------------------------------------
1 | from typing import Set
2 |
3 | # Assuming the following classes and enums are already defined in your project
4 | from classes.track import Track
5 | from classes.channel import Channel
6 | from classes.realParameter import RealParameter
7 | from classes.audio import Audio
8 | from classes.fileReference import FileReference
9 | from classes.warp import Warp
10 | from classes.clip import Clip
11 | from classes.clips import Clips
12 | from classes.timeUnit import TimeUnit
13 | from classes.contentType import ContentType
14 | from classes.mixerRole import MixerRole
15 | from classes.unit import Unit
16 | from classes.timeline import Timeline
17 |
18 |
19 | class Utility:
20 | @staticmethod
21 | def create_track(
22 | name: str,
23 | content_types: Set[ContentType],
24 | mixer_role: MixerRole,
25 | volume: float,
26 | pan: float,
27 | ) -> Track:
28 | track_channel = Channel(
29 | volume=RealParameter(value=volume, unit=Unit(value="linear")),
30 | pan=RealParameter(value=pan, unit=Unit(value="normalized")),
31 | role=mixer_role,
32 | )
33 | track = Track(name=name, channel=track_channel, content_type=content_types, loaded=True)
34 | return track
35 |
36 | @staticmethod
37 | def create_audio(
38 | relative_path: str, sample_rate: int, channels: int, duration: float
39 | ) -> Audio:
40 | audio = Audio(
41 | time_unit=TimeUnit(value="seconds"),
42 | file=FileReference(path=relative_path, external=False),
43 | sample_rate=sample_rate,
44 | channels=channels,
45 | duration=duration,
46 | )
47 | return audio
48 |
49 | @staticmethod
50 | def create_warp(time: float, content_time: float) -> Warp:
51 | warp = Warp(time=time, content_time=content_time)
52 | return warp
53 |
54 | @staticmethod
55 | def create_clip(content: Timeline, time: float, duration: float) -> Clip:
56 | clip = Clip(content=content, time=time, duration=duration)
57 | return clip
58 |
59 | @staticmethod
60 | def create_clips(*clips: Clip) -> Clips:
61 | clips_timeline = Clips(clips=list(clips))
62 | return clips_timeline
63 |
--------------------------------------------------------------------------------
/classes/warp.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 |
3 |
4 | class Warp:
5 | def __init__(self, time, content_time):
6 | self.time = time
7 | self.content_time = content_time
8 |
9 | def to_xml(self):
10 | warp_elem = ET.Element("Warp")
11 | warp_elem.set("time", str(self.time))
12 | warp_elem.set("contentTime", str(self.content_time))
13 | return warp_elem
14 |
15 | @classmethod
16 | def from_xml(cls, element):
17 | time = float(element.get("time"))
18 | content_time = float(element.get("contentTime"))
19 | return cls(time, content_time)
20 |
--------------------------------------------------------------------------------
/classes/warps.py:
--------------------------------------------------------------------------------
1 | from lxml import etree as ET
2 | from classes.timeline import Timeline
3 | from classes.warp import Warp
4 | from classes.timeUnit import TimeUnit
5 |
6 |
7 | class Warps(Timeline):
8 | def __init__(self, events=None, content=None, content_time_unit=None, **kwargs):
9 | super().__init__(**kwargs) # Inherit from Timeline
10 | self.events = events if events is not None else []
11 | self.content = content # Should be an instance of Timeline or its subclass
12 | self.content_time_unit = content_time_unit # Instance of TimeUnit
13 |
14 | def to_xml(self):
15 | # Create the root element for Warps
16 | warps_elem = ET.Element("Warps")
17 |
18 | # Append the nested content (e.g., another Timeline)
19 | if self.content:
20 | warps_elem.append(self.content.to_xml())
21 |
22 | # Recursively add nested Warp elements
23 | for warp in self.events:
24 | warps_elem.append(warp.to_xml())
25 |
26 | # Set contentTimeUnit as an attribute
27 | if self.content_time_unit:
28 | warps_elem.set("contentTimeUnit", self.content_time_unit.value)
29 |
30 | return warps_elem
31 |
32 | @classmethod
33 | def from_xml(cls, element):
34 | # Parse content (which should be a Timeline or subclass)
35 | content_elem = element.find("Content")
36 | content = Timeline.from_xml(content_elem) if content_elem is not None else None
37 |
38 | # Recursively parse nested Warp elements
39 | events = []
40 | for warp_elem in element.findall("Warp"):
41 | events.append(Warp.from_xml(warp_elem))
42 |
43 | # Parse the contentTimeUnit attribute
44 | content_time_unit = element.get("contentTimeUnit")
45 | content_time_unit = TimeUnit(content_time_unit) if content_time_unit else None
46 |
47 | return cls(events=events, content=content, content_time_unit=content_time_unit)
48 |
--------------------------------------------------------------------------------
/examples/audio_in/audio.MD:
--------------------------------------------------------------------------------
1 | Download your audio here.
--------------------------------------------------------------------------------
/examples/createBitwigProject.py:
--------------------------------------------------------------------------------
1 | import os
2 | from classes.referenceable import Referenceable
3 | from classes.project import Project
4 | from classes.application import Application
5 | from classes.realParameter import RealParameter
6 | from classes.mixerRole import MixerRole
7 | from classes.deviceRole import DeviceRole
8 | from classes.arrangement import Arrangement
9 | from classes.lanes import Lanes
10 | from classes.timeUnit import TimeUnit
11 | from classes.dawProject import DawProject
12 | from classes.metaData import MetaData
13 | from classes.utility import Utility
14 | from classes.contentType import ContentType
15 | from classes.transport import Transport
16 | from classes.unit import Unit
17 | from classes.compressor import Compressor
18 | from classes.equalizer import Equalizer
19 | from classes.eqBand import EqBand
20 | from classes.eqBandType import EqBandType
21 |
22 |
23 | def create_empty_project():
24 | Referenceable.reset_id()
25 | project = Project()
26 | project.application = Application(name="RoEx Automix", version="1.0")
27 | return project
28 |
29 |
30 | def get_audio_file_as_bytes(sample_path):
31 | """
32 | Method to retrieve the audio file content as bytes.
33 | """
34 | with open(sample_path, 'rb') as audio_file:
35 | return audio_file.read()
36 |
37 |
38 | def save_test_project(project, name, configurer=None):
39 | metadata = MetaData()
40 | embedded_files = {}
41 |
42 | if configurer:
43 | configurer(metadata, embedded_files)
44 |
45 | DawProject.save(project, metadata, embedded_files, f"../target/{name}.dawproject")
46 | DawProject.save_xml(project, f"../target/{name}.xml")
47 | DawProject.validate(project)
48 |
49 |
50 | def create_project_with_audio_tracks(audio_tracks):
51 | """
52 | Create a DAW project with the provided audio tracks, each with its gain, panning, EQ, and compressor settings.
53 |
54 | :param audio_tracks: List of dictionaries where each dictionary contains the following keys:
55 | - 'file_path': The location of the audio file.
56 | - 'gain': The gain setting for the track (in dB).
57 | - 'pan': The panning setting for the track (range -1.0 to 1.0).
58 | - 'eq_settings': List of dictionaries with EQ band settings. Each dictionary should contain:
59 | - 'frequency': The frequency of the EQ band (in Hz).
60 | - 'gain': The gain for the EQ band (in dB).
61 | - 'q': The Q factor for the EQ band.
62 | - 'enabled': Boolean to enable/disable the EQ band.
63 | - 'band_type': Type of EQ band (e.g., 'bell', 'high_shelf', etc.).
64 | - 'compressor_settings': Dictionary with compressor settings:
65 | - 'threshold': Threshold (in dB).
66 | - 'ratio': Compression ratio.
67 | - 'attack': Attack time (in seconds).
68 | - 'release': Release time (in seconds).
69 | - 'input_gain': Input gain (in dB).
70 | - 'output_gain': Output gain (in dB).
71 | - 'auto_makeup': Boolean to enable/disable automatic makeup gain.
72 | """
73 | project = create_empty_project()
74 | project.transport = Transport()
75 | project.transport.tempo = RealParameter()
76 | project.transport.tempo.unit = Unit.BPM
77 | project.transport.tempo.value = 120.0
78 |
79 | master_track = Utility.create_track(name="Master", content_types=set(), mixer_role=MixerRole.MASTER, pan=0.5,
80 | volume=1.0)
81 | project.structure.append(master_track)
82 | project.arrangement = Arrangement()
83 | project.arrangement.lanes = Lanes()
84 | project.arrangement.lanes.time_unit = TimeUnit.SECONDS
85 |
86 | embedded_files = {}
87 |
88 | for i, track_info in enumerate(audio_tracks):
89 | # Create audio track
90 | track_name = os.path.basename(track_info['file_path'])
91 | audio_track = Utility.create_track(name=track_name, content_types={ContentType.AUDIO},
92 | mixer_role=MixerRole.REGULAR, pan=track_info['pan'],
93 | volume=track_info['gain'])
94 | audio_track.channel.destination = master_track.channel
95 | project.structure.append(audio_track)
96 |
97 | # Load audio file
98 | sample_path = track_info['file_path']
99 | sample_duration = track_info['sample_duration'] # Assuming this method exists to determine duration
100 | audio = Utility.create_audio(track_name, 44100, 2, sample_duration)
101 | audio.file.external = True
102 | audio.file.path = os.path.abspath(sample_path)
103 |
104 | # Add the audio file to the embedded files
105 | embedded_files[get_audio_file_as_bytes(os.path.abspath(sample_path))] = os.path.basename(sample_path)
106 |
107 | # Create and add clip to the track
108 | audio_clip = Utility.create_clip(audio, 0, sample_duration)
109 | audio_clip.content_time_unit = TimeUnit.SECONDS
110 | audio_clip.play_start = 0
111 |
112 | clips = Utility.create_clips(audio_clip)
113 | clips.track = audio_track
114 |
115 | # Create lanes and add clips to arrangement
116 | # track_lanes = Lanes()
117 | # track_lanes.lanes.append(clips)
118 | project.arrangement.lanes.lanes.append(clips)
119 |
120 | # Apply EQ settings
121 | if 'eq_settings' in track_info:
122 | eq_bands = []
123 | for eq_band_info in track_info['eq_settings']:
124 | eq_band = EqBand(
125 | freq=eq_band_info['frequency'],
126 | gain=eq_band_info['gain'],
127 | q=eq_band_info['q'],
128 | enabled=eq_band_info['enabled'],
129 | band_type=EqBandType[eq_band_info['band_type'].upper()]
130 | )
131 | eq_bands.append(eq_band)
132 | equalizer = Equalizer(device_name=f"Eq_{i + 1}", device_role=DeviceRole.AUDIO_FX.value, bands=eq_bands)
133 | audio_track.channel.devices.append(equalizer)
134 |
135 | # Apply Compressor settings
136 | if 'compressor_settings' in track_info:
137 | comp_info = track_info['compressor_settings']
138 | compressor = Compressor(
139 | device_name=f"Compressor_{i + 1}",
140 | device_role=DeviceRole.AUDIO_FX.value,
141 | threshold=comp_info['threshold'],
142 | ratio=comp_info['ratio'],
143 | attack=comp_info['attack'],
144 | release=comp_info['release'],
145 | input_gain=comp_info['input_gain'],
146 | output_gain=comp_info['output_gain'],
147 | auto_makeup=comp_info['auto_makeup']
148 | )
149 | audio_track.channel.devices.append(compressor)
150 |
151 | # Save the project with embedded files
152 | save_test_project(project, "RoEx_Automix", lambda meta, files: files.update(embedded_files))
153 |
154 |
155 | if __name__ == "__main__":
156 |
157 | # Example usage
158 | audio_tracks = [
159 | {
160 | 'file_path': './audio_in/masks-bass.wav',
161 | 'sample_duration': 30,
162 | 'gain': 0.7,
163 | 'pan': 0.5,
164 | 'eq_settings': [
165 | {'frequency': 100, 'gain': 6, 'q': 1.0, 'enabled': True, 'band_type': 'bell'},
166 | {'frequency': 1000, 'gain': -3, 'q': 1.0, 'enabled': True, 'band_type': 'high_shelf'}
167 | ],
168 | 'compressor_settings': {
169 | 'threshold': -15,
170 | 'ratio': 0.5,
171 | 'attack': 0.01,
172 | 'release': 0.2,
173 | 'input_gain': 0.0,
174 | 'output_gain': 0.0,
175 | 'auto_makeup': True
176 | }
177 | },
178 | {
179 | 'file_path': './audio_in/masks-chord.wav',
180 | 'sample_duration': 30,
181 | 'gain': 0.9,
182 | 'pan': 0.7,
183 | 'eq_settings': [
184 | {'frequency': 100, 'gain': 6, 'q': 1.0, 'enabled': True, 'band_type': 'bell'},
185 | {'frequency': 1000, 'gain': -3, 'q': 1.0, 'enabled': True, 'band_type': 'high_shelf'}
186 | ],
187 | 'compressor_settings': {
188 | 'threshold': -15,
189 | 'ratio': 0.5,
190 | 'attack': 0.01,
191 | 'release': 0.2,
192 | 'input_gain': 0.0,
193 | 'output_gain': 0.0,
194 | 'auto_makeup': True
195 | }
196 | },
197 | {
198 | 'file_path': './audio_in/masks-kick.wav',
199 | 'sample_duration': 30,
200 | 'gain': 0.2,
201 | 'pan': 0.5,
202 | 'eq_settings': [
203 | {'frequency': 100, 'gain': 6, 'q': 1.0, 'enabled': True, 'band_type': 'bell'},
204 | {'frequency': 1000, 'gain': -3, 'q': 1.0, 'enabled': True, 'band_type': 'high_shelf'}
205 | ],
206 | 'compressor_settings': {
207 | 'threshold': -15,
208 | 'ratio': 0.3,
209 | 'attack': 0.01,
210 | 'release': 0.2,
211 | 'input_gain': 0.0,
212 | 'output_gain': 0.0,
213 | 'auto_makeup': True
214 | }
215 | },
216 | {
217 | 'file_path': './audio_in/masks-percussion.wav',
218 | 'sample_duration': 30,
219 | 'gain': 0.7,
220 | 'pan': 0.5,
221 | 'eq_settings': [
222 | {'frequency': 100, 'gain': 6, 'q': 1.0, 'enabled': True, 'band_type': 'bell'},
223 | {'frequency': 1000, 'gain': -3, 'q': 1.0, 'enabled': True, 'band_type': 'high_shelf'}
224 | ],
225 | 'compressor_settings': {
226 | 'threshold': -5,
227 | 'ratio': 0.2,
228 | 'attack': 0.01,
229 | 'release': 0.2,
230 | 'input_gain': 0.0,
231 | 'output_gain': 0.0,
232 | 'auto_makeup': True
233 | }
234 | },
235 | {
236 | 'file_path': './audio_in/masks-synth.wav',
237 | 'sample_duration': 30,
238 | 'gain': 0.4,
239 | 'pan': 0.2,
240 | 'eq_settings': [
241 | {'frequency': 100, 'gain': 6, 'q': 1.0, 'enabled': True, 'band_type': 'bell'},
242 | {'frequency': 1000, 'gain': -3, 'q': 1.0, 'enabled': True, 'band_type': 'high_shelf'}
243 | ],
244 | 'compressor_settings': {
245 | 'threshold': -35,
246 | 'ratio': 0.7,
247 | 'attack': 0.01,
248 | 'release': 0.2,
249 | 'input_gain': 0.0,
250 | 'output_gain': 0.0,
251 | 'auto_makeup': True
252 | }
253 | },
254 | # Add more tracks here
255 | ]
256 |
257 | # Create the DAW project with the specified audio tracks
258 | create_project_with_audio_tracks(audio_tracks)
259 |
--------------------------------------------------------------------------------
/examples/initial_multitrack_mix_payload.json:
--------------------------------------------------------------------------------
1 | {
2 | "multitrackData": {
3 | "trackData": [
4 | {
5 | "trackURL": "https://storage.googleapis.com/test-bucket-api-roex/masks_test/masks-bass.wav",
6 | "instrumentGroup": "BASS_GROUP",
7 | "presenceSetting": "NORMAL",
8 | "panPreference": "CENTRE",
9 | "reverbPreference" : "NONE"
10 | },
11 | {
12 | "trackURL": "https://storage.googleapis.com/test-bucket-api-roex/masks_test/masks-chord.wav",
13 | "instrumentGroup": "SYNTH_GROUP",
14 | "presenceSetting": "LEAD",
15 | "panPreference": "CENTRE",
16 | "reverbPreference" : "NONE"
17 | },
18 | {
19 | "trackURL": "https://storage.googleapis.com/test-bucket-api-roex/masks_test/masks-kick.wav",
20 | "instrumentGroup": "KICK_GROUP",
21 | "presenceSetting": "NORMAL",
22 | "panPreference": "CENTRE",
23 | "reverbPreference" : "NONE"
24 | },
25 | {
26 | "trackURL": "https://storage.googleapis.com/test-bucket-api-roex/masks_test/masks-percssion.wav",
27 | "instrumentGroup": "PERCS_GROUP",
28 | "presenceSetting": "NORMAL",
29 | "panPreference": "CENTRE",
30 | "reverbPreference" : "NONE"
31 | },
32 | {
33 | "trackURL": "https://storage.googleapis.com/test-bucket-api-roex/masks_test/masks-synth.wav",
34 | "instrumentGroup": "SYNTH_GROUP",
35 | "presenceSetting": "BACKGROUND",
36 | "panPreference": "CENTRE",
37 | "reverbPreference" : "NONE"
38 | }
39 | ],
40 | "musicalStyle": "ELECTRONIC",
41 | "webhookURL": "https://us-central1-united-masters.cloudfunctions.net/webhook-test"
42 | }
43 | }
--------------------------------------------------------------------------------
/examples/roex_daw_project_export.py:
--------------------------------------------------------------------------------
1 | import time
2 | import requests
3 | import json
4 | import argparse
5 | import os
6 | import createBitwigProject
7 | import soundfile as sf
8 |
9 | # Base API endpoint for Tonn
10 | BASE_URL = "https://tonn.roexaudio.com"
11 |
12 | # Constants that define how many times to retry network requests and how long to wait between retries
13 | MAX_RETRIES = 3
14 | RETRY_DELAY = 2 # in seconds
15 | API_KEY = "go to https://tonn-portal.roexaudio.com to get one"
16 |
17 |
18 | def download_audio(file_name, url, output_dir="audio_in"):
19 | """
20 | Downloads an audio file from the given URL and saves it locally.
21 |
22 | :param file_name: Name to save the file as.
23 | :param url: URL to download from.
24 | :param output_dir: Directory to save the file.
25 | :return: Local file path if successful, None otherwise.
26 | """
27 | # Ensure the output directory exists
28 | os.makedirs(output_dir, exist_ok=True)
29 | local_filename = os.path.join(output_dir, file_name)
30 |
31 | # Attempt to download up to MAX_RETRIES times
32 | for attempt in range(MAX_RETRIES):
33 | try:
34 | print(f"Downloading {file_name} from {url} (Attempt {attempt + 1})...")
35 | response = requests.get(url, stream=True, timeout=10)
36 | response.raise_for_status() # Raise an exception if the request fails
37 |
38 | # Write the response content in chunks to avoid large memory usage
39 | with open(local_filename, "wb") as file:
40 | for chunk in response.iter_content(chunk_size=8192):
41 | file.write(chunk)
42 |
43 | # Confirm the file was actually created
44 | if os.path.exists(local_filename):
45 | print(f"Successfully downloaded {file_name}")
46 | return local_filename
47 | else:
48 | # If the file doesn't exist after the download, raise an error to trigger a retry
49 | raise FileNotFoundError("File does not exist after download.")
50 |
51 | except requests.exceptions.RequestException as e:
52 | print(f"Error downloading {file_name}: {e}")
53 | # If this wasn't the last attempt, wait before retrying
54 | if attempt < MAX_RETRIES - 1:
55 | time.sleep(RETRY_DELAY ** (attempt + 1))
56 | else:
57 | print(f"Failed to download {file_name} after {MAX_RETRIES} attempts.")
58 | return None
59 |
60 |
61 | def download_audio_files(stems, output_dir="audio_in"):
62 | """
63 | Downloads all audio stems from URLs and saves them locally.
64 |
65 | :param stems: Dictionary where keys are track names and values are download URLs.
66 | :param output_dir: Directory to store downloaded audio files.
67 | :return: Dictionary mapping track names to local file paths.
68 | """
69 | downloaded_files = {}
70 |
71 | # For each track in the stems dictionary, attempt to download
72 | for track_name, url in stems.items():
73 | local_path = download_audio(track_name, url, output_dir)
74 | if local_path:
75 | downloaded_files[track_name] = local_path
76 |
77 | return downloaded_files
78 |
79 |
80 | def download_original_audio(track_data, output_dir="audio_in"):
81 | """
82 | Downloads original multitrack audio files from the provided trackData.
83 |
84 | :param track_data: List of track objects containing 'trackURL' and file names.
85 | :param output_dir: Directory to store the downloaded files.
86 | :return: Dictionary mapping track names to local file paths.
87 | """
88 | downloaded_files = {}
89 |
90 | # Each track entry has a 'trackURL'; we derive the local filename from the URL
91 | for track in track_data:
92 | file_url = track["trackURL"]
93 | track_name = os.path.basename(file_url)
94 | local_path = download_audio(track_name, file_url, output_dir)
95 |
96 | if local_path:
97 | downloaded_files[track_name] = local_path
98 |
99 | return downloaded_files
100 |
101 |
102 | def format_audio_tracks_for_daw(mix_output_settings, downloaded_files):
103 | """
104 | Convert mix output settings and downloaded audio file paths into the format required
105 | by create_project_with_audio_tracks.
106 |
107 | :param mix_output_settings: Dictionary containing mix settings for each track.
108 | :param downloaded_files: Dictionary mapping track names to local file paths.
109 | :return: List of dictionaries formatted for create_project_with_audio_tracks.
110 | """
111 | audio_tracks = []
112 |
113 | # Loop through each track in the mix output settings
114 | for track_name, settings in mix_output_settings.items():
115 | file_path = downloaded_files.get(track_name)
116 | if not file_path:
117 | print(f"Warning: No file found for {track_name}, skipping.")
118 | continue
119 |
120 | try:
121 | # Load the audio file into a NumPy array for processing
122 | audio_x, sr = sf.read(file_path, dtype="float32")
123 | except FileNotFoundError as e:
124 | raise e
125 | except Exception as e:
126 | print("Problem loading audio with exception: " + str(e))
127 | raise e
128 |
129 | # Apply an initial gain multiplier directly to the audio data
130 | audio_x = audio_x * settings["gain_settings"]["initial_gain"]
131 | # Save the modified audio back to the file
132 | sf.write(file_path, audio_x, sr)
133 |
134 | # Build EQ settings from the provided band data (if any)
135 | eq_bands = []
136 | if "eq_settings" in settings:
137 | for band_key, band_values in settings["eq_settings"].items():
138 | eq_bands.append({
139 | "frequency": band_values["centre_freq"],
140 | "gain": band_values["gain"],
141 | "q": band_values["q"],
142 | "enabled": True,
143 | "band_type": "bell" # Default to bell curve
144 | })
145 |
146 | # Format the compressor settings; note that ratio is converted into a percentage
147 | compressor_settings = {
148 | "threshold": settings["drc_settings"]["threshold"],
149 | "ratio": continuous_to_percentage(settings["drc_settings"]["ratio"]),
150 | "attack": settings["drc_settings"]["attack_ms"], # Provided in ms
151 | "release": settings["drc_settings"]["release_ms"],
152 | "input_gain": 0.0,
153 | "output_gain": 0.0,
154 | "auto_makeup": True
155 | }
156 |
157 | # Append a dict describing this track to the list
158 | audio_tracks.append({
159 | "file_path": file_path,
160 | # get_file_duration_in_secs expects channel-first data, so we transpose audio_x.
161 | "sample_duration": get_file_duration_in_secs(audio_x.T, sr),
162 | "gain": settings["gain_settings"]["gain_amplitude"],
163 | # Normalizing panning angle from a -60..+60 range to a -1..+1 range used by the DAW
164 | "pan": normalise_daw_values(settings["panning_settings"]["panning_angle"], -60, 60, -1, 1),
165 | "eq_settings": eq_bands,
166 | "compressor_settings": compressor_settings
167 | })
168 |
169 | return audio_tracks
170 |
171 |
172 | def normalise_daw_values(value, old_min=-60.0, old_max=60.0, new_min=-1.0, new_max=1.0):
173 | """
174 | Rescales a value from the old range (old_min..old_max) to the new range (new_min..new_max).
175 |
176 | :param value: The value in the old range.
177 | :param old_min: The minimum of the old range.
178 | :param old_max: The maximum of the old range.
179 | :param new_min: The minimum of the new range.
180 | :param new_max: The maximum of the new range.
181 | :return: The value rescaled into the new range.
182 | """
183 | ratio = (value - old_min) / (old_max - old_min)
184 | normalized_value = ratio * (new_min - new_max) + new_max
185 | return normalized_value
186 |
187 |
188 | def get_file_duration_in_secs(audio_x, sample_rate):
189 | """
190 | Calculate the duration of the given audio in seconds.
191 |
192 | :param audio_x: NumPy array of the audio data (channels x samples).
193 | :param sample_rate: Sampling rate of the audio file.
194 | :return: Floating-point duration in seconds.
195 | """
196 | return float(audio_x.shape[1] / sample_rate)
197 |
198 |
199 | def continuous_to_percentage(x: float) -> float:
200 | """
201 | Convert a continuous value (>=1) to a percentage based on the
202 | mapping 1->0%, 2->50%, 3->66.6%, ..., 10->90%, 100->100%.
203 |
204 | :param x: A float value indicating the compression ratio (>=1).
205 | :return: A float value indicating the percentage ratio.
206 | """
207 | # Handle ratios below 1 if desired
208 | if x < 1:
209 | return 0.0
210 |
211 | # Main formula: percentage = 100 * (1 - 1/x)
212 | percentage = 100 * (1 - 1 / x)
213 |
214 | # Clamp to 100 at x=100 or above
215 | if x >= 100:
216 | percentage = 100.0
217 |
218 | return percentage
219 |
220 |
221 | def poll_preview_mix(task_id, headers, max_attempts=30, poll_interval=5):
222 | """
223 | Poll the /retrievepreviewmix endpoint until the preview mix is ready.
224 |
225 | This function repeatedly sends a POST request with the task ID to the
226 | /retrievepreviewmix endpoint, waiting poll_interval seconds between attempts.
227 |
228 | :param task_id: The multitrack task ID returned from the preview mix creation.
229 | :param headers: HTTP headers including Content-Type and API key.
230 | :param max_attempts: Maximum number of polling attempts.
231 | :param poll_interval: Seconds to wait between attempts.
232 | :return: The preview mix task results if ready, otherwise None.
233 | """
234 | retrieve_url = f"{BASE_URL}/retrievepreviewmix"
235 | retrieve_payload = {
236 | "multitrackData": {
237 | "multitrackTaskId": task_id,
238 | "retrieveFXSettings": True # Set to False unless FX settings are needed
239 | }
240 | }
241 |
242 | print("Polling for the preview mix URL...")
243 | attempt = 0
244 | while attempt < max_attempts:
245 | try:
246 | # Send a POST request to the /retrievepreviewmix endpoint
247 | retrieve_response = requests.post(retrieve_url, json=retrieve_payload, headers=headers)
248 | except Exception as e:
249 | print("Error during POST request to /retrievepreviewmix:", e)
250 | return None
251 |
252 | try:
253 | # Parse the JSON response
254 | retrieve_data = retrieve_response.json()
255 | except Exception as e:
256 | print("Error parsing JSON response:", e)
257 | return None
258 |
259 | # Check the response status to see if the mix is done
260 | if retrieve_response.status_code == 202:
261 | current_status = retrieve_data.get("status", "Processing")
262 | print(f"Attempt {attempt + 1}/{max_attempts}: Task still processing. Status: {current_status}.")
263 | elif retrieve_response.status_code == 200:
264 | # If the API returns 200, the mix might be completed
265 | results = retrieve_data.get("previewMixTaskResults", {})
266 | status = results.get("status", "")
267 | if status == "MIX_TASK_PREVIEW_COMPLETED":
268 | print("Preview mix is complete.")
269 | return results
270 | else:
271 | print("Received 200 but unexpected status in response:", status)
272 | return None
273 | else:
274 | # Handle any unexpected HTTP status codes
275 | print("Unexpected response code:", retrieve_response.status_code)
276 | print("Response:", retrieve_response.text)
277 | return None
278 |
279 | # Wait before the next attempt
280 | time.sleep(poll_interval)
281 | attempt += 1
282 |
283 | print("Preview mix URL was not available after polling. Please try again later.")
284 | return None
285 |
286 |
287 | def main():
288 | """
289 | Main function to handle preview mix retrieval, download audio stems,
290 | and format them for a Bitwig DAW project.
291 | """
292 | # Set up command-line argument parsing
293 | parser = argparse.ArgumentParser(
294 | description="Retrieve preview mix, download stems, and prepare DAW project."
295 | )
296 | parser.add_argument("payload_file", type=str, help="Path to the JSON file containing the mix preview payload")
297 | args = parser.parse_args()
298 |
299 | # Load the preview payload from the specified JSON file
300 | try:
301 | with open(args.payload_file, "r") as f:
302 | preview_payload = json.load(f)
303 | except Exception as e:
304 | print("Error reading preview payload JSON file:", e)
305 | return
306 |
307 | # Prepare headers for the API calls
308 | headers = {
309 | "Content-Type": "application/json",
310 | "x-api-key": API_KEY
311 | }
312 |
313 | # Extract original track URLs before sending the request (to have them on hand for local use)
314 | track_data = preview_payload["multitrackData"]["trackData"]
315 | original_audio_files = download_original_audio(track_data)
316 |
317 | # Step 1: Send a POST request to initiate the preview mix process
318 | mixpreview_url = f"{BASE_URL}/mixpreview"
319 | print(f"Sending POST request to {mixpreview_url} for preview mix...")
320 | try:
321 | response = requests.post(mixpreview_url, json=preview_payload, headers=headers)
322 | except Exception as e:
323 | print("Error during POST request to /mixpreview:", e)
324 | return
325 |
326 | # Verify the request to start the preview mix was successful
327 | if response.status_code == 200:
328 | try:
329 | data = response.json()
330 | except Exception as e:
331 | print("Error parsing preview mix JSON response:", e)
332 | return
333 |
334 | # Get the task ID required for polling
335 | task_id = data.get("multitrack_task_id")
336 | if not task_id:
337 | print("Error: Task ID not found in response:", data)
338 | return
339 | print("Mix preview task created successfully. Task ID:", task_id)
340 | else:
341 | print("Failed to create preview mix.")
342 | return
343 |
344 | # Step 2: Poll for the mix to complete
345 | preview_results = poll_preview_mix(task_id, headers)
346 | if not preview_results:
347 | return
348 |
349 | # The endpoint returns settings for each track, like gain, EQ, etc.
350 | mix_output_settings = preview_results.get("mix_output_settings", {})
351 |
352 | # Step 3: Format the tracks using the original audio files and the new mix settings
353 | audio_tracks = format_audio_tracks_for_daw(mix_output_settings, original_audio_files)
354 |
355 | # Step 4: Create a Bitwig project with the formatted track data
356 | createBitwigProject.create_project_with_audio_tracks(audio_tracks)
357 |
358 |
359 | if __name__ == "__main__":
360 | main()
361 |
--------------------------------------------------------------------------------
/java_classes/Application.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 |
5 | /** Metadata about the application which saved the DAWPROJECT file. */
6 | public class Application
7 | {
8 | /** Name of the application. */
9 | @XmlAttribute(required = true)
10 | public String name;
11 |
12 | /** Version number of the application. */
13 | @XmlAttribute(required = true)
14 | public String version;
15 | }
16 |
--------------------------------------------------------------------------------
/java_classes/Arrangement.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import com.bitwig.dawproject.timeline.Lanes;
4 | import com.bitwig.dawproject.timeline.Markers;
5 | import com.bitwig.dawproject.timeline.Points;
6 | import jakarta.xml.bind.annotation.XmlElement;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | /** Represents the main Arrangement timeline of a DAW. */
10 |
11 | @XmlRootElement(name = "Arrangement")
12 | public class Arrangement extends Referenceable
13 | {
14 | /** Automation data for time-signature inside this Arrangement.
15 | *
{@code
16 | *
17 | *
18 | *
19 | *
20 | * ...
21 | *
22 | *
23 | * }
24 | * */
25 | @XmlElement(required = false, name = "TimeSignatureAutomation", type = Points.class)
26 | public Points timeSignatureAutomation;
27 |
28 | /** Automation data for tempo inside this Arrangement, which will define the conversion between seconds and beats
29 | * at the root level. */
30 | @XmlElement(required = false, name = "TempoAutomation", type = Points.class)
31 | public Points tempoAutomation;
32 |
33 | /** Cue markers inside this arrangement */
34 | @XmlElement(required = false, name = "Markers", type = Markers.class)
35 | public Markers markers;
36 |
37 | /** The lanes of this arrangement. Generally this would contain another Lanes timeline for (and scoped to) each
38 | * track which would then contain all Note, Audio, and Automation timelines. */
39 | @XmlElement(name = "Lanes", type = Lanes.class)
40 | public Lanes lanes;
41 | }
42 |
--------------------------------------------------------------------------------
/java_classes/BoolParameter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 | /** Represents a parameter which can provide a boolean (true/false) value and be used as an automation target. */
6 | @XmlRootElement(name = "BoolParameter")
7 | public class BoolParameter extends Parameter
8 | {
9 | /** Boolean value for this parameter. */
10 | @XmlAttribute(required = false)
11 | public Boolean value;
12 | }
13 |
--------------------------------------------------------------------------------
/java_classes/Channel.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import java.util.List;
4 |
5 | import com.bitwig.dawproject.device.Device;
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.XmlIDREF;
11 | import jakarta.xml.bind.annotation.XmlRootElement;
12 |
13 | /**
14 | * Represents a mixer channel. It provides the ability to route signals to other channels and can contain
15 | * Device/Plug-in for processing.
16 | */
17 | @XmlRootElement(name = "Channel")
18 | public class Channel extends Lane
19 | {
20 | /** Role of this channel in the mixer. */
21 | @XmlAttribute(required = false)
22 | public MixerRole role;
23 |
24 | /** Number of audio-channels of this mixer channel. (1=mono, 2=stereo…) */
25 | @XmlAttribute(required = false)
26 | public Integer audioChannels = 2;
27 |
28 | /** Channel volume */
29 | @XmlElement(name = "Volume", required = false)
30 | public RealParameter volume;
31 |
32 | /** Channel pan/balance */
33 | @XmlElement(name = "Pan", required = false)
34 | public RealParameter pan;
35 |
36 | /** Channel mute */
37 | @XmlElement(name = "Mute", required = false)
38 | public BoolParameter mute;
39 |
40 | /** Channel solo */
41 | @XmlAttribute(required = false)
42 | public Boolean solo;
43 |
44 | /** Output channel routing */
45 | @XmlIDREF
46 | @XmlAttribute()
47 | public Channel destination;
48 |
49 | /** Send levels & destination */
50 | @XmlElementWrapper(name="Sends")
51 | @XmlElement(name="Send", type = Send.class)
52 | public List sends;
53 |
54 | /** Devices & plug-ins of this channel */
55 | @XmlElementWrapper(name="Devices")
56 | @XmlElementRef
57 | public List devices;
58 | }
59 |
--------------------------------------------------------------------------------
/java_classes/ContentType.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 |
5 | @XmlEnum
6 | public enum ContentType
7 | {
8 | audio,
9 | automation,
10 | notes,
11 | video,
12 | markers,
13 | tracks,
14 | }
15 |
--------------------------------------------------------------------------------
/java_classes/DawProject.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import javax.xml.XMLConstants;
4 | import javax.xml.transform.Result;
5 | import javax.xml.transform.stream.StreamResult;
6 | import javax.xml.validation.Schema;
7 | import javax.xml.validation.SchemaFactory;
8 | import java.io.File;
9 | import java.io.FileInputStream;
10 | import java.io.FileOutputStream;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.io.InputStreamReader;
14 | import java.io.StringReader;
15 | import java.io.StringWriter;
16 | import java.nio.charset.Charset;
17 | import java.nio.charset.StandardCharsets;
18 | import java.util.Map;
19 | import java.util.zip.ZipEntry;
20 | import java.util.zip.ZipFile;
21 | import java.util.zip.ZipOutputStream;
22 |
23 | import org.apache.commons.io.ByteOrderMark;
24 | import org.apache.commons.io.input.BOMInputStream;
25 |
26 | import jakarta.xml.bind.JAXBContext;
27 | import jakarta.xml.bind.JAXBException;
28 | import jakarta.xml.bind.Marshaller;
29 | import jakarta.xml.bind.SchemaOutputResolver;
30 | import org.xml.sax.SAXException;
31 |
32 | public class DawProject
33 | {
34 | public static final String FORMAT_NAME = "DAWproject exchange format";
35 | public static final String FILE_EXTENSION = "dawproject";
36 |
37 | private static final String PROJECT_FILE = "project.xml";
38 | private static final String METADATA_FILE = "metadata.xml";
39 |
40 | public static void exportSchema(File file, Class cls) throws IOException
41 | {
42 | try
43 | {
44 | var context = createContext(cls);
45 |
46 | var resolver = new SchemaOutputResolver()
47 | {
48 | @Override public Result createOutput (String namespaceUri, String suggestedFileName) throws IOException
49 | {
50 | FileOutputStream fileOutputStream = new FileOutputStream(file);
51 | StreamResult result = new StreamResult(fileOutputStream);
52 | result.setSystemId(file.getName());
53 | return result;
54 | }
55 | };
56 |
57 | context.generateSchema(resolver);
58 | }
59 | catch (JAXBException e)
60 | {
61 | throw new IOException(e);
62 | }
63 | }
64 |
65 | private static String toXML(Object object) throws IOException
66 | {
67 | try
68 | {
69 | var context = createContext(object.getClass());
70 |
71 | var marshaller = context.createMarshaller();
72 | marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
73 |
74 | var sw = new StringWriter();
75 | marshaller.marshal(object, sw);
76 |
77 | return sw.toString();
78 | }
79 | catch (Exception e)
80 | {
81 | throw new IOException(e);
82 | }
83 | }
84 |
85 | private static JAXBContext createContext(final Class cls) throws JAXBException
86 | {
87 | return JAXBContext.newInstance(cls);
88 | }
89 |
90 | private static T fromXML(InputStreamReader reader, Class cls) throws IOException
91 | {
92 | try
93 | {
94 | var jaxbContext = JAXBContext.newInstance(cls);
95 |
96 | final var unmarshaller = jaxbContext.createUnmarshaller();
97 |
98 | final var object = cls.cast (unmarshaller.unmarshal(reader));
99 |
100 | return object;
101 | }
102 | catch (JAXBException e)
103 | {
104 | throw new IOException(e);
105 | }
106 | }
107 |
108 | public static void saveXML(Project project, File file) throws IOException
109 | {
110 | String projectXML = toXML(project);
111 | FileOutputStream fileOutputStream = new FileOutputStream(file);
112 | fileOutputStream.write(projectXML.getBytes(StandardCharsets.UTF_8));
113 | fileOutputStream.close();
114 | }
115 |
116 | public static void validate(Project project) throws IOException
117 | {
118 | String projectXML = toXML(project);
119 |
120 | try
121 | {
122 | var context = createContext(Project.class);
123 |
124 | final var schemaFile = File.createTempFile("schema", ".xml");
125 | exportSchema(schemaFile, Project.class);
126 |
127 | SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
128 | Schema schema = sf.newSchema(schemaFile);
129 |
130 | final var unmarshaller = context.createUnmarshaller();
131 | unmarshaller.setSchema(schema);
132 |
133 | unmarshaller.unmarshal(new StringReader(projectXML));
134 | }
135 | catch (JAXBException e)
136 | {
137 | throw new IOException(e);
138 | }
139 | catch (SAXException e)
140 | {
141 | throw new IOException(e);
142 | }
143 | }
144 |
145 | public static void save(Project project, MetaData metadata, Map embeddedFiles, File file) throws IOException
146 | {
147 | String metadataXML = toXML(metadata);
148 | String projectXML = toXML(project);
149 |
150 | final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file));
151 |
152 | addToZip(zos, METADATA_FILE, metadataXML.getBytes(StandardCharsets.UTF_8));
153 | addToZip(zos, PROJECT_FILE, projectXML.getBytes(StandardCharsets.UTF_8));
154 |
155 | for (Map.Entry entry : embeddedFiles.entrySet())
156 | {
157 | addToZip(zos, entry.getValue(), entry.getKey());
158 | }
159 |
160 | zos.close();
161 | }
162 |
163 | private static void addToZip(
164 | final ZipOutputStream zos,
165 | final String path,
166 | final byte[] data) throws IOException
167 | {
168 | final ZipEntry entry = new ZipEntry(path);
169 | zos.putNextEntry(entry);
170 | zos.write(data);
171 | zos.closeEntry();
172 | }
173 |
174 | private static void addToZip(
175 | final ZipOutputStream zos,
176 | final String path,
177 | final File file) throws IOException
178 | {
179 | final ZipEntry entry = new ZipEntry(path);
180 | zos.putNextEntry(entry);
181 |
182 | try (FileInputStream fileInputStream = new FileInputStream(file))
183 | {
184 | byte[] data = new byte[65536];
185 | int size = 0;
186 | while((size = fileInputStream.read(data)) != -1)
187 | zos.write(data, 0, size);
188 |
189 | zos.flush();
190 | }
191 |
192 | zos.closeEntry();
193 | }
194 |
195 | public static InputStreamReader stripBom(InputStream inputStream) throws IOException
196 | {
197 | BOMInputStream bomInputStream = new BOMInputStream(inputStream ,
198 | ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE);
199 | Charset charset;
200 | if(!bomInputStream.hasBOM()) charset = StandardCharsets.UTF_8;
201 | else if(bomInputStream.hasBOM(ByteOrderMark.UTF_8)) charset = StandardCharsets.UTF_8;
202 | else if(bomInputStream.hasBOM(ByteOrderMark.UTF_16LE)) charset = StandardCharsets.UTF_16LE;
203 | else if(bomInputStream.hasBOM(ByteOrderMark.UTF_16BE)) charset = StandardCharsets.UTF_16BE;
204 | else { throw new IOException("The charset is not supported.");}
205 |
206 | return new InputStreamReader(bomInputStream, charset);
207 | }
208 |
209 | public static Project loadProject(final File file) throws IOException
210 | {
211 | try(ZipFile zipFile = new ZipFile(file))
212 | {
213 | ZipEntry projectEntry = zipFile.getEntry(PROJECT_FILE);
214 | Project project = fromXML(stripBom(zipFile.getInputStream(projectEntry)), Project.class);
215 | return project;
216 | }
217 | }
218 |
219 | public static MetaData loadMetadata(final File file) throws IOException
220 | {
221 | try(ZipFile zipFile = new ZipFile(file))
222 | {
223 | ZipEntry entry = zipFile.getEntry(METADATA_FILE);
224 | MetaData metadata = fromXML(stripBom(zipFile.getInputStream(entry)), MetaData.class);
225 | return metadata;
226 | }
227 | }
228 |
229 | public static InputStream streamEmbedded(final File file, final String embeddedPath) throws IOException
230 | {
231 | final ZipFile zipFile = new ZipFile (file);
232 | final ZipEntry entry = zipFile.getEntry (embeddedPath);
233 | final InputStream zipInputStream = zipFile.getInputStream (entry);
234 |
235 | // Ensure that both the stream and the ZIP file gets closed
236 | return new InputStream ()
237 | {
238 | @Override
239 | public int read () throws IOException
240 | {
241 | return zipInputStream.read ();
242 | }
243 |
244 |
245 | @Override
246 | public void close () throws IOException
247 | {
248 | zipInputStream.close ();
249 | zipFile.close ();
250 | }
251 | };
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/java_classes/DoubleAdapter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import java.util.Locale;
4 |
5 | import jakarta.xml.bind.annotation.adapters.XmlAdapter;
6 |
7 | public class DoubleAdapter extends XmlAdapter
8 | {
9 | @Override
10 | public Double unmarshal(String v) throws Exception {
11 | if (v == null || v.isEmpty() || v.equals("null")) {
12 | return null;
13 | }
14 | return Double.parseDouble(v.replace("inf", "Infinity"));
15 | }
16 |
17 | @Override
18 | public String marshal(Double v) throws Exception {
19 | if (v == null) {
20 | return null;
21 | }
22 | return String.format(Locale.US, "%.6f", v).replace("Infinity", "inf");
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/java_classes/EnumParameter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 | /** Represents an enumerated parameter which can provide a value and be used as an automation target. */
6 | @XmlRootElement(name = "EnumParameter")
7 | public class EnumParameter extends Parameter
8 | {
9 | /** Index of the enum value. */
10 | @XmlAttribute
11 | public Integer value;
12 |
13 | /** Number of entries in enum value. value will be in the range [0 .. count-1]. */
14 | @XmlAttribute(required = true)
15 | public Integer count;
16 |
17 | /** Labels of the individual enum values. */
18 | @XmlAttribute(required = false)
19 | public String[] labels;
20 | }
21 |
--------------------------------------------------------------------------------
/java_classes/ExpressionType.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | public enum ExpressionType
4 | {
5 | gain,
6 | pan,
7 | transpose,
8 | timbre,
9 | formant,
10 | pressure,
11 |
12 | // MIDI
13 | channelController,
14 | channelPressure,
15 | polyPressure,
16 | pitchBend,
17 | programChange,
18 | }
19 |
--------------------------------------------------------------------------------
/java_classes/FileReference.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 |
5 | /** References a file either within a DAWPROJECT container or on disk. */
6 |
7 | public class FileReference
8 | {
9 | /** File path. either
10 | * path within the container
11 | * relative to .dawproject file (when external = "true")
12 | * absolute path (when external = "true" and path starts with a slash or windows drive letter)
13 | * */
14 | @XmlAttribute(required = true)
15 | public String path;
16 |
17 | /** When true, the path is relative to the .dawproject file. Default value is false. */
18 | @XmlAttribute(required = false)
19 | public Boolean external;
20 | }
21 |
--------------------------------------------------------------------------------
/java_classes/IntegerParameter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /** Represents an enumerated parameter which can provide a value and be used as an automation target. */
7 | @XmlRootElement(name = "IntegerParameter")
8 | public class IntegerParameter extends Parameter
9 | {
10 | /** Integer value for this parameter. */
11 | @XmlAttribute
12 | public Integer value;
13 |
14 | /** Minimum value this parameter can have (inclusive). */
15 | @XmlAttribute
16 | public Integer min;
17 |
18 | /** Maximum value this parameter can have (inclusive). */
19 | @XmlAttribute
20 | public Integer max;
21 | }
22 |
--------------------------------------------------------------------------------
/java_classes/Interpolation.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | public enum Interpolation
4 | {
5 | hold,
6 | linear
7 | }
8 |
--------------------------------------------------------------------------------
/java_classes/Lane.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | public abstract class Lane extends Referenceable
4 | {
5 | }
6 |
--------------------------------------------------------------------------------
/java_classes/MetaData.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlElement;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 |
6 | /** Metadata root element of the DAWPROJECT format. This is stored in the file metadata.xml file inside the container. */
7 |
8 | @XmlRootElement(name = "MetaData")
9 | public class MetaData
10 | {
11 | /** Title of the song/project. */
12 | @XmlElement(name = "Title")
13 | public String title;
14 |
15 | /** Recording Artist. */
16 | @XmlElement(name = "Artist")
17 | public String artist;
18 |
19 | /** Album. */
20 | @XmlElement(name = "Album")
21 | public String album;
22 |
23 | /** Original Artist. */
24 | @XmlElement(name = "OriginalArtist")
25 | public String originalArtist;
26 |
27 | /** Composer. */
28 | @XmlElement(name = "Composer")
29 | public String composer;
30 |
31 | /** Songwriter. */
32 | @XmlElement(name = "Songwriter")
33 | public String songwriter;
34 |
35 | /** Producer. */
36 | @XmlElement(name = "Producer")
37 | public String producer;
38 |
39 | /** Arranger. */
40 | @XmlElement(name = "Arranger")
41 | public String arranger;
42 |
43 | /** Year this project/song was recorded. */
44 | @XmlElement(name = "Year")
45 | public String year;
46 |
47 | /** Genre/style */
48 | @XmlElement(name = "Genre")
49 | public String genre;
50 |
51 | /** Copyright notice. */
52 | @XmlElement(name = "Copyright")
53 | public String copyright;
54 |
55 | /** URL to website related to this project. */
56 | @XmlElement(name = "Website")
57 | public String website;
58 |
59 | /** General comment or description. */
60 | @XmlElement(name = "Comment")
61 | public String comment;
62 | }
63 |
--------------------------------------------------------------------------------
/java_classes/MixerRole.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 | import jakarta.xml.bind.annotation.XmlEnumValue;
5 |
6 | @XmlEnum
7 | public enum MixerRole
8 | {
9 | @XmlEnumValue("regular") regular,
10 | @XmlEnumValue("master") master,
11 | @XmlEnumValue("effect") effectTrack,
12 | @XmlEnumValue("submix") subMix,
13 | @XmlEnumValue("vca") vca
14 | }
15 |
--------------------------------------------------------------------------------
/java_classes/Nameable.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 |
5 | public abstract class Nameable
6 | {
7 | /** Name/label of this object. */
8 | @XmlAttribute
9 | public String name;
10 |
11 | /** Color of this object in HTML-style format. (#rrggbb) */
12 | @XmlAttribute
13 | public String color;
14 |
15 | /** Comment/description of this object. */
16 | @XmlAttribute
17 | public String comment;
18 | }
19 |
--------------------------------------------------------------------------------
/java_classes/Parameter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 | import jakarta.xml.bind.annotation.XmlSeeAlso;
6 |
7 | /** Represents a parameter which can provide a value and be used as an automation target. */
8 | @XmlRootElement
9 | @XmlSeeAlso({RealParameter.class, BoolParameter.class, IntegerParameter.class, EnumParameter.class, TimeSignatureParameter.class})
10 | public abstract class Parameter extends Referenceable
11 | {
12 |
13 | /** Parameter ID as used by VST2 (index), VST3(ParamID) */
14 | @XmlAttribute(required = false)
15 | public Integer parameterID;
16 | }
17 |
--------------------------------------------------------------------------------
/java_classes/Project.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import com.bitwig.dawproject.device.Device;
7 | import com.bitwig.dawproject.timeline.Timeline;
8 | import jakarta.xml.bind.annotation.XmlAttribute;
9 | import jakarta.xml.bind.annotation.XmlElement;
10 | import jakarta.xml.bind.annotation.XmlElementRef;
11 | import jakarta.xml.bind.annotation.XmlElementWrapper;
12 | import jakarta.xml.bind.annotation.XmlRootElement;
13 | import jakarta.xml.bind.annotation.XmlSeeAlso;
14 |
15 | /** The main root element of the DAWPROJECT format. This is stored in the file project.xml file inside the container. */
16 |
17 | @XmlRootElement(name = "Project")
18 | @XmlSeeAlso({Device.class, Timeline.class})
19 | public class Project
20 | {
21 | public static String CURRENT_VERSION = "1.0";
22 |
23 | /** Version of DAWPROJECT format this file was saved as. */
24 | @XmlAttribute(required = true)
25 | public String version = CURRENT_VERSION;
26 |
27 | /** Metadata (name/version) about the application that saved this file. */
28 | @XmlElement(name = "Application", required = true)
29 | public Application application = new Application();
30 |
31 | /** Transport element containing playback parameters such as Tempo and Time-signature. */
32 | @XmlElement(name = "Transport")
33 | public Transport transport;
34 |
35 | /** Track/Channel structure of this file. */
36 | @XmlElementWrapper(name="Structure")
37 | @XmlElementRef
38 | public List structure = new ArrayList<>();
39 |
40 | /** The main Arrangement timeline of this file. */
41 | @XmlElement(name="Arrangement", type = Arrangement.class, required = false)
42 | public Arrangement arrangement;
43 |
44 | /** Clip Launcher scenes of this file. */
45 | @XmlElementWrapper(name="Scenes")
46 | @XmlElement(name="Scene")
47 | public List scenes = new ArrayList<>();
48 | }
49 |
--------------------------------------------------------------------------------
/java_classes/RealParameter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
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 | /** Represents a real valued (double) parameter which can provide a value and be used as an automation target. */
9 | @XmlRootElement(name = "RealParameter")
10 | @XmlSeeAlso({Unit.class})
11 | public class RealParameter extends Parameter
12 | {
13 | /** Real (double) value for this parameter.
14 | * When serializing value to text for XML, infinite values are allowed and should be represented as inf and -inf.
*/
15 | @XmlAttribute
16 | @XmlJavaTypeAdapter(DoubleAdapter.class)
17 | public Double value;
18 |
19 | /** Unit in which value, min and max are defined.
20 | * Using this rather than normalized value ranges allows transfer of parameter values and automation data.
*/
21 | @XmlAttribute(required = true)
22 | public Unit unit;
23 |
24 | /** Minimum value this parameter can have (inclusive). */
25 | @XmlAttribute
26 | @XmlJavaTypeAdapter(DoubleAdapter.class)
27 | public Double min;
28 |
29 | /** Maximum value this parameter can have (inclusive). */
30 | @XmlAttribute
31 | @XmlJavaTypeAdapter(DoubleAdapter.class)
32 | public Double max;
33 | }
34 |
--------------------------------------------------------------------------------
/java_classes/Referenceable.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAccessOrder;
4 | import jakarta.xml.bind.annotation.XmlAccessorOrder;
5 | import jakarta.xml.bind.annotation.XmlAttribute;
6 | import jakarta.xml.bind.annotation.XmlID;
7 |
8 | @XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL)
9 | public abstract class Referenceable extends Nameable
10 | {
11 | /** Unique string identifier of this element. This is used for referencing this instance from other elements. */
12 | @XmlAttribute
13 | @XmlID()
14 | public final String id;
15 |
16 | public Referenceable()
17 | {
18 | this.id = "id" + (ID++);
19 | }
20 |
21 | public static int ID = 0;
22 |
23 | /** call before export */
24 | public static void resetID()
25 | {
26 | ID = 0;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/java_classes/Scene.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import com.bitwig.dawproject.timeline.Timeline;
4 | import jakarta.xml.bind.annotation.XmlElementRef;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 |
7 | /** Represents a clip launcher Scene of a DAW. */
8 | @XmlRootElement(name = "Scene")
9 | public class Scene extends Referenceable
10 | {
11 | /** Content timeline of this scene, will typically be structured like this:
12 | * {@code
13 | *
14 | *
15 | *
16 | *
17 | * ...
18 | *
19 | *
20 | * ...
21 | *
22 | *
23 | * }
24 | * */
25 | @XmlElementRef(name = "Timeline")
26 | public Timeline content;
27 | }
28 |
--------------------------------------------------------------------------------
/java_classes/Send.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlElement;
5 | import jakarta.xml.bind.annotation.XmlIDREF;
6 |
7 | /** A single send of a mixer channel. */
8 | public class Send extends Referenceable
9 | {
10 | /** Send level. */
11 | @XmlElement(required = true, name = "Volume")
12 | public RealParameter volume;
13 |
14 | /** Send pan/balance. */
15 | @XmlElement(required = false, name = "Pan")
16 | public RealParameter pan;
17 |
18 | /** Send type. */
19 | @XmlAttribute
20 | public SendType type = SendType.post;
21 |
22 | /** Send destination. */
23 | @XmlAttribute
24 | @XmlIDREF
25 | public Channel destination;
26 | }
27 |
--------------------------------------------------------------------------------
/java_classes/SendType.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | public enum SendType
4 | {
5 | pre,
6 | post,
7 | }
8 |
--------------------------------------------------------------------------------
/java_classes/TimeSignatureParameter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlAttribute;
4 | import jakarta.xml.bind.annotation.XmlRootElement;
5 | /** Represents a (the) time-signature parameter which can provide a value and be used as an automation target. */
6 | @XmlRootElement(name = "TimeSignatureParameter")
7 | public class TimeSignatureParameter extends Parameter
8 | {
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 |
--------------------------------------------------------------------------------
/java_classes/Track.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
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.XmlIDREF;
10 | import jakarta.xml.bind.annotation.XmlList;
11 | import jakarta.xml.bind.annotation.XmlRootElement;
12 |
13 | /** Represents a sequencer track. */
14 | @XmlRootElement(name = "Track")
15 | public class Track extends Lane
16 | {
17 | /** Role of this track in timelines & arranger. Can be multiple (comma-separated). */
18 | @XmlAttribute(required = false)
19 | @XmlList()
20 | public ContentType[] contentType;
21 |
22 | /** If this track is loaded/active of not. */
23 | @XmlAttribute(required = false)
24 | public Boolean loaded;
25 |
26 | /** Mixer channel used for the output of this track. */
27 | @XmlElement(name = "Channel", required = false)
28 | public Channel channel;
29 |
30 | /** Child tracks, typically used to represent group/folder tracks with contentType="tracks". */
31 | @XmlElement(name = "Track")
32 | public List tracks = new ArrayList<>();
33 | }
34 |
--------------------------------------------------------------------------------
/java_classes/Transport.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlElement;
4 |
5 | /** Transport element containing playback parameters such as Tempo and Time-signature. */
6 | public class Transport
7 | {
8 | /** Tempo parameter for setting and/or automating the tempo. */
9 | @XmlElement(name = "Tempo")
10 | public RealParameter tempo;
11 |
12 | /** Time-signature parameter. */
13 | @XmlElement(name = "TimeSignature")
14 | public TimeSignatureParameter timeSignature;
15 | }
16 |
--------------------------------------------------------------------------------
/java_classes/Unit.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 |
5 | @XmlEnum
6 | public enum Unit
7 | {
8 | linear,
9 | normalized,
10 | percent,
11 | decibel,
12 | hertz,
13 | semitones,
14 | seconds,
15 | beats,
16 | bpm,
17 | }
18 |
--------------------------------------------------------------------------------
/java_classes/Utility.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject;
2 |
3 | import java.util.Collections;
4 | import java.util.EnumSet;
5 |
6 | import com.bitwig.dawproject.timeline.Audio;
7 | import com.bitwig.dawproject.timeline.Clip;
8 | import com.bitwig.dawproject.timeline.Clips;
9 | import com.bitwig.dawproject.timeline.TimeUnit;
10 | import com.bitwig.dawproject.timeline.Timeline;
11 | import com.bitwig.dawproject.timeline.Warp;
12 |
13 | public class Utility
14 | {
15 | public static Track createTrack(final String name, final EnumSet contentTypes, final MixerRole mixerRole, final double volume, final double pan)
16 | {
17 | final Track track = new Track();
18 | track.channel = new Channel();
19 | track.name = name;
20 | final var volumeParameter = new RealParameter();
21 | volumeParameter.value = volume;
22 | volumeParameter.unit = Unit.linear;
23 | track.channel.volume = volumeParameter;
24 |
25 | final var panParameter = new RealParameter();
26 | panParameter.value = pan;
27 | panParameter.unit = Unit.normalized;
28 | track.channel.pan = panParameter;
29 |
30 | track.contentType = contentTypes.toArray(new ContentType[]{});
31 | track.channel.role = mixerRole;
32 |
33 | return track;
34 | }
35 |
36 | public static Audio createAudio(final String relativePath, final int sampleRate, final int channels, final double duration)
37 | {
38 | final var audio = new Audio();
39 | audio.timeUnit = TimeUnit.seconds;
40 | audio.file = new FileReference();
41 | audio.file.path = relativePath;
42 | audio.file.external = false;
43 | audio.sampleRate = sampleRate;
44 | audio.channels = channels;
45 | audio.duration = duration;
46 | return audio;
47 | }
48 |
49 | public static Warp createWarp(final double time, final double contentTime)
50 | {
51 | final var warp = new Warp();
52 | warp.time = time;
53 | warp.contentTime = contentTime;
54 | return warp;
55 | }
56 |
57 | public static Clip createClip(final Timeline content, final double time, final double duration)
58 | {
59 | final var clip = new Clip();
60 | clip.content = content;
61 | clip.time = time;
62 | clip.duration = duration;
63 | return clip;
64 | }
65 |
66 | public static Clips createClips(final Clip... clips)
67 | {
68 | final var timeline = new Clips();
69 | Collections.addAll(timeline.clips, clips);
70 |
71 | return timeline;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/java_classes/device/AuPlugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | @XmlRootElement(name = "AuPlugin")
6 | public class AuPlugin extends Plugin
7 | {
8 | }
9 |
--------------------------------------------------------------------------------
/java_classes/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 | @XmlRootElement(name = "BuiltinDevice")
7 | @XmlSeeAlso({Equalizer.class, Compressor.class, NoiseGate.class, Limiter.class})
8 | public class BuiltinDevice extends Device
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/java_classes/device/ClapPlugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /** A CLAP Plug-in instance.
6 | * The CLAP plug-in state should be stored in .clap-preset format.
7 | * */
8 | @XmlRootElement(name = "ClapPlugin")
9 | public class ClapPlugin extends Plugin
10 | {
11 | }
12 |
--------------------------------------------------------------------------------
/java_classes/device/Compressor.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import com.bitwig.dawproject.BoolParameter;
4 | import com.bitwig.dawproject.Parameter;
5 | import com.bitwig.dawproject.RealParameter;
6 | import jakarta.xml.bind.annotation.XmlElement;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | @XmlRootElement(name = "Compressor")
10 | public class Compressor extends BuiltinDevice
11 | {
12 | @XmlElement(name = "Threshold")
13 | public RealParameter threshold;
14 |
15 | @XmlElement(name = "Ratio")
16 | public RealParameter ratio;
17 |
18 | @XmlElement(name = "Attack")
19 | public RealParameter attack;
20 |
21 | @XmlElement(name = "Release")
22 | public RealParameter release;
23 |
24 | /** Pre-compression gain stage. (input/gain/drive) */
25 | @XmlElement(name = "InputGain")
26 | public RealParameter inputGain;
27 |
28 | /** Post-compression gain stage. (output/makeup gain) */
29 | @XmlElement(name = "OutputGain")
30 | public RealParameter outputGain;
31 |
32 | @XmlElement(name = "AutoMakeup")
33 | public BoolParameter autoMakeup;
34 | }
35 |
--------------------------------------------------------------------------------
/java_classes/device/Device.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import com.bitwig.dawproject.BoolParameter;
4 | import com.bitwig.dawproject.FileReference;
5 | import com.bitwig.dawproject.Referenceable;
6 | import jakarta.xml.bind.annotation.XmlAttribute;
7 | import jakarta.xml.bind.annotation.XmlElement;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | import com.bitwig.dawproject.Parameter;
13 | import jakarta.xml.bind.annotation.XmlElementRef;
14 | import jakarta.xml.bind.annotation.XmlElementWrapper;
15 | import jakarta.xml.bind.annotation.XmlRootElement;
16 | import jakarta.xml.bind.annotation.XmlSeeAlso;
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, Parameter.class})
21 | public class Device extends Referenceable
22 | {
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 = true;
34 |
35 | /** Name of the device/plugin */
36 | @XmlAttribute(required = true)
37 | public String deviceName;
38 |
39 | /** Unique identifier of device/plug-in.
40 | * Standards which use UUID as an identifier use the canonical textual representation of the UUID (8-4-4-4-12 with no braces) (VST3)
41 | * Standards which use an integer as an identifier use the value in decimal form. (base-10 unsigned) (VST2)
42 | * Text-based identifiers are used as-is. (CLAP) */
43 | @XmlAttribute
44 | public String deviceID;
45 |
46 | /** Vendor name of the device/plugin */
47 | @XmlAttribute
48 | public String deviceVendor;
49 |
50 | /** Path to a file representing the device / plug-in state in its native format.
51 | * This file must be embedded inside the container ZIP and have the FileReference configured with (external=false).
*/
52 | @XmlElement(name = "State", required = false)
53 | public FileReference state;
54 |
55 | /** Parameters for this device, which is required for automated parameters in order to provide an ID.
56 | * Note: If the automated parameter is already present like the BuiltinDevice parameters, it should not be included here as well. */
57 | @XmlElementWrapper(name="Parameters", required = false)
58 | @XmlElementRef
59 | public List automatedParameters = new ArrayList<>();
60 | }
61 |
--------------------------------------------------------------------------------
/java_classes/device/DeviceRole.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | public enum DeviceRole
4 | {
5 | instrument,
6 | noteFX,
7 | audioFX,
8 | analyzer
9 | }
10 |
--------------------------------------------------------------------------------
/java_classes/device/EqBand.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import com.bitwig.dawproject.BoolParameter;
4 | import com.bitwig.dawproject.RealParameter;
5 | import jakarta.xml.bind.annotation.XmlAttribute;
6 | import jakarta.xml.bind.annotation.XmlElement;
7 |
8 | public class EqBand
9 | {
10 | @XmlElement(name = "Freq", required = true)
11 | public RealParameter freq;
12 |
13 | @XmlElement(name = "Gain")
14 | public RealParameter gain;
15 |
16 | @XmlElement(name = "Q")
17 | public RealParameter Q;
18 |
19 | @XmlElement(name = "Enabled")
20 | public BoolParameter enabled;
21 |
22 | @XmlAttribute(required = true)
23 | public EqBandType type;
24 |
25 | @XmlAttribute
26 | public Integer order;
27 | }
28 |
--------------------------------------------------------------------------------
/java_classes/device/EqBandType.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 |
5 | @XmlEnum
6 | public enum EqBandType
7 | {
8 | highPass,
9 | lowPass,
10 | bandPass,
11 | highShelf,
12 | lowShelf,
13 | bell,
14 | notch,
15 | }
16 |
--------------------------------------------------------------------------------
/java_classes/device/Equalizer.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import com.bitwig.dawproject.RealParameter;
7 | import jakarta.xml.bind.annotation.XmlElement;
8 | import jakarta.xml.bind.annotation.XmlElementWrapper;
9 | import jakarta.xml.bind.annotation.XmlRootElement;
10 |
11 | @XmlRootElement(name = "Equalizer")
12 | public class Equalizer extends BuiltinDevice
13 | {
14 | @XmlElement(name="Band")
15 | public List bands = new ArrayList<>();
16 |
17 | @XmlElement(name = "InputGain")
18 | public RealParameter inputGain;
19 |
20 | @XmlElement(name = "OutputGain")
21 | public RealParameter outputGain;
22 | }
23 |
--------------------------------------------------------------------------------
/java_classes/device/Limiter.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import com.bitwig.dawproject.RealParameter;
4 | import jakarta.xml.bind.annotation.XmlElement;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 |
7 | @XmlRootElement(name = "Limiter")
8 | public class Limiter extends BuiltinDevice
9 | {
10 | @XmlElement(name = "Threshold")
11 | public RealParameter threshold;
12 |
13 | @XmlElement(name = "InputGain")
14 | public RealParameter inputGain;
15 |
16 | @XmlElement(name = "OutputGain")
17 | public RealParameter outputGain;
18 |
19 | @XmlElement(name = "Attack")
20 | public RealParameter attack;
21 |
22 | @XmlElement(name = "Release")
23 | public RealParameter release;
24 | }
25 |
--------------------------------------------------------------------------------
/java_classes/device/NoiseGate.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import com.bitwig.dawproject.RealParameter;
4 | import jakarta.xml.bind.annotation.XmlElement;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 |
7 | @XmlRootElement(name = "NoiseGate")
8 | public class NoiseGate extends BuiltinDevice
9 | {
10 | @XmlElement(name = "Threshold")
11 | public RealParameter threshold;
12 |
13 | @XmlElement(name = "Ratio")
14 | public RealParameter ratio;
15 |
16 | @XmlElement(name = "Attack")
17 | public RealParameter attack;
18 |
19 | @XmlElement(name = "Release")
20 | public RealParameter release;
21 |
22 | /** Range or amount of maximum gain reduction. Possible range [-inf to 0] */
23 | @XmlElement(name = "Range")
24 | public RealParameter range;
25 | }
26 |
--------------------------------------------------------------------------------
/java_classes/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 | {
8 | /** Version of the plug-in */
9 | @XmlAttribute
10 | public String pluginVersion;
11 | }
12 |
--------------------------------------------------------------------------------
/java_classes/device/Vst2Plugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /** A VST2 Plug-in instance.
6 | * The VST2 plug-in state should be stored in FXB or FXP format.
7 | * */
8 |
9 | @XmlRootElement(name = "Vst2Plugin")
10 | public class Vst2Plugin extends Plugin
11 | {
12 | }
13 |
--------------------------------------------------------------------------------
/java_classes/device/Vst3Plugin.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.device;
2 |
3 | import jakarta.xml.bind.annotation.XmlRootElement;
4 |
5 | /** A VST3 Plug-in instance.
6 | * The VST3 plug-in state should be stored in .vstpreset format.
7 | * */
8 | @XmlRootElement(name = "Vst3Plugin")
9 | public class Vst3Plugin extends Plugin
10 | {
11 | }
12 |
--------------------------------------------------------------------------------
/java_classes/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 | /** Representation of an audio file as a timeline. Duration should be the entire length of the file, any clipping
7 | * should be done by placing the Audio element within a Clip element. The timeUnit attribute should always be set to
8 | * seconds. */
9 |
10 | @XmlRootElement(name = "Audio")
11 | public class Audio extends MediaFile
12 | {
13 | /** Sample-rate of audio-file. */
14 | @XmlAttribute(required = true)
15 | public int sampleRate;
16 |
17 | /** Number of channels of audio-file (1=mono, 2=stereo...). */
18 | @XmlAttribute(required = true)
19 | public int channels;
20 |
21 | /** Playback algorithm used to warp audio (vendor-specific). */
22 | @XmlAttribute(required = false)
23 | public String algorithm;
24 | }
25 |
--------------------------------------------------------------------------------
/java_classes/timeline/AutomationTarget.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.ExpressionType;
4 | import com.bitwig.dawproject.Parameter;
5 | import jakarta.xml.bind.annotation.XmlAttribute;
6 | import jakarta.xml.bind.annotation.XmlIDREF;
7 |
8 | /**
9 | * Defines the target of automation or expression, usually used within a Points element.
10 | *
11 | * Either it points directly ot a parameter or an expression, and in the expression case
12 | * it can either be monophonic (such as MIDI CCs) or per-note/polyphonic (such as poly pressure)
13 | */
14 |
15 | public class AutomationTarget
16 | {
17 | /** Parameter to automate. */
18 | @XmlIDREF
19 | @XmlAttribute(required = false)
20 | public Parameter parameter;
21 |
22 | /** Expression type to control. */
23 | @XmlAttribute(required = false)
24 | public ExpressionType expression;
25 |
26 | /** MIDI channel */
27 | @XmlAttribute(required = false)
28 | public Integer channel;
29 |
30 | /** MIDI key. Used when expression="polyPressure".
*/
31 | @XmlAttribute(required = false)
32 | public Integer key;
33 |
34 | /** MIDI Channel Controller Number (0 based index). Used when expression="channelController".
*/
35 | @XmlAttribute(required = false)
36 | public Integer controller;
37 | }
38 |
--------------------------------------------------------------------------------
/java_classes/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 | {
10 | /** Boolean value of this point (true/false). */
11 | @XmlAttribute(required = true)
12 | public Boolean value;
13 | }
14 |
--------------------------------------------------------------------------------
/java_classes/timeline/Clip.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.Nameable;
4 | import jakarta.xml.bind.annotation.XmlAttribute;
5 | import jakarta.xml.bind.annotation.XmlElementRef;
6 | import jakarta.xml.bind.annotation.XmlIDREF;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 |
9 | /** A Clip provides a clipped view on to a Timeline, and is used either on a Clips timeline (typically for arrangements) or inside a ClipSlot element (for clip launcher Scenes).
10 | * A Clip must either have a child-element inheriting from Timeline or provide a ID reference to a timeline somewhere else (for linked/alias clips).
11 | */
12 |
13 | @XmlRootElement(name = "Clip")
14 | public class Clip extends Nameable
15 | {
16 | /** Time on the parent timeline where this clips starts playing. */
17 | @XmlAttribute(required = true)
18 | public double time;
19 |
20 | /** Duration on the parent timeline of this clip.
21 | * If duration is omitted, it should be inferred from the playStop - playStart instead.
22 | * This is particularity useful when timeUnit and contentTimeUnit are different, like when placing an audio
23 | * clip with content length defined in seconds onto an arrangement defined in beats. */
24 | @XmlAttribute(required = false)
25 | public Double duration;
26 |
27 | /** The TimeUnit used by the scope inside this timeline. This affects the content/reference, playStart, playStop,
28 | * loopStart, loopEnd but not time and duration which are using the TimeUnit of the parent scope. */
29 | @XmlAttribute(required = false)
30 | public TimeUnit contentTimeUnit;
31 |
32 | /** Time inside the content timeline (or reference) where the clip starts playing. */
33 | @XmlAttribute(required = false)
34 | public Double playStart;
35 |
36 | /** Time inside the content timeline (or reference) where the clip stops playing. */
37 | @XmlAttribute(required = false)
38 | public Double playStop;
39 |
40 | /** Time inside the content timeline (or reference) where the clip loop starts. */
41 | @XmlAttribute(required = false)
42 | public Double loopStart;
43 |
44 | /** Time inside the content timeline (or reference) where the clip loop ends. */
45 | @XmlAttribute(required = false)
46 | public Double loopEnd;
47 |
48 | /** The TimeUnit used by the fadeInTime and fadeOutTime. */
49 | @XmlAttribute(required = false)
50 | public TimeUnit fadeTimeUnit;
51 |
52 | /** Duration of fade-in.
53 | * To create cross-fade, use a negative value which will make this Clip start at t = time - abs(fadeInTime)
*/
54 | @XmlAttribute(required = false)
55 | public Double fadeInTime;
56 |
57 | /** Duration of fade-out. */
58 | @XmlAttribute(required = false)
59 | public Double fadeOutTime;
60 |
61 | /** Content Timeline this clip is playing. */
62 | @XmlElementRef(required = false)
63 | public Timeline content;
64 |
65 | /**
66 | * Reference to a Content Timeline this clip is playing, in case of linked/alias clips. You can use either content
67 | * or reference for one clip, but not both.
68 | */
69 | @XmlAttribute(required = false)
70 | @XmlIDREF
71 | public Timeline reference;
72 | }
73 |
--------------------------------------------------------------------------------
/java_classes/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 | /** Represent a clip launcher slot within a Scene which can contain a Clip. It is generally set to a specific track. */
8 | @XmlRootElement(name = "ClipSlot")
9 | public class ClipSlot extends Timeline
10 | {
11 | /** Whether launching this slot should stop the track playback when this slot is empty. */
12 | @XmlAttribute(required = false)
13 | public Boolean hasStop;
14 |
15 | /** Contained clip. */
16 | @XmlElement(name = "Clip", required = false)
17 | public Clip clip;
18 | }
19 |
--------------------------------------------------------------------------------
/java_classes/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 | /** Represents a timeline of clips. Each contained Clip have its time and duration that defines its location on this
10 | * timeline (defined by timeUnit of the Clips element). */
11 |
12 | @XmlRootElement(name = "Clips")
13 | public class Clips extends Timeline
14 | {
15 |
16 | /** Clips of this timeline. */
17 | @XmlElement(name = "Clip")
18 | public List clips = new ArrayList<>();
19 | }
20 |
--------------------------------------------------------------------------------
/java_classes/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 | {
10 | /** Integer value of the Enum index for this point. */
11 | @XmlAttribute(required = true)
12 | public Integer value;
13 | }
14 |
--------------------------------------------------------------------------------
/java_classes/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 | {
10 | /** Integer value of this point. */
11 | @XmlAttribute(required = true)
12 | public Integer value;
13 | }
14 |
--------------------------------------------------------------------------------
/java_classes/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 | /** The Lanes element provides the ability to contain multiple parallel timelines inside it, and is the main layering
10 | * element of the format. It is also a natural fit for defining the scope of contained timelines to a specific track. */
11 | @XmlRootElement(name = "Lanes")
12 | public class Lanes extends Timeline
13 | {
14 | /** Lanes representing nested content. */
15 | @XmlElementRef
16 | public List lanes = new ArrayList<>();
17 | }
18 |
--------------------------------------------------------------------------------
/java_classes/timeline/Marker.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.Nameable;
4 | import jakarta.xml.bind.annotation.XmlAttribute;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 |
7 | /** A single cue-marker. */
8 | @XmlRootElement(name = "Marker")
9 | public class Marker extends Nameable
10 | {
11 | /** Time on the parent timeline of this marker. */
12 | @XmlAttribute(required = true)
13 | public double time;
14 | }
15 |
--------------------------------------------------------------------------------
/java_classes/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 | {
13 | /** Markers of this timeline. */
14 | @XmlElement(required = true, name = "Marker")
15 | public List markers = new ArrayList<>();
16 | }
17 |
--------------------------------------------------------------------------------
/java_classes/timeline/MediaFile.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.FileReference;
4 | import jakarta.xml.bind.annotation.XmlAttribute;
5 | import jakarta.xml.bind.annotation.XmlElement;
6 |
7 | /**
8 | * A media file. (audio or video).
9 | * The duration attribute is intended to be provide the file length (and not be interpreted as a playback parameter, use a Clip or Warps element for that).
10 | */
11 | public class MediaFile extends Timeline
12 | {
13 | /** The media file. */
14 | @XmlElement(name = "File", required = true)
15 | public FileReference file = new FileReference();
16 |
17 | /** Duration in seconds of the media file (as stored). */
18 | @XmlAttribute(required = true)
19 | public double duration;
20 | }
21 |
--------------------------------------------------------------------------------
/java_classes/timeline/Note.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.DoubleAdapter;
4 | import jakarta.xml.bind.annotation.XmlAttribute;
5 | import jakarta.xml.bind.annotation.XmlElementRef;
6 | import jakarta.xml.bind.annotation.XmlRootElement;
7 | import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
8 |
9 | /** A single Note (MIDI-style).
10 | * It can additionally contain child timelines to hold per-note expression.
11 | * */
12 | @XmlRootElement(name = "Note")
13 | public final class Note
14 | {
15 | /** Time on the parent timeline where this note starts playing. */
16 | @XmlAttribute(required = true)
17 | @XmlJavaTypeAdapter(DoubleAdapter.class)
18 | public Double time;
19 |
20 | /** Duration on the parent timeline of this note. */
21 | @XmlAttribute(required = true)
22 | @XmlJavaTypeAdapter(DoubleAdapter.class)
23 | public Double duration;
24 |
25 | /** MIDI channel of this note. */
26 | @XmlAttribute(required = false)
27 | public int channel;
28 |
29 | /** MIDI key of this note. */
30 | @XmlAttribute(required = true)
31 | public int key;
32 |
33 | /** Note On Velocity of this note. (normalized) */
34 | @XmlAttribute(name = "vel")
35 | @XmlJavaTypeAdapter(DoubleAdapter.class)
36 | public Double velocity;
37 |
38 | /** Note Off Velocity of this note. (normalized) */
39 | @XmlAttribute(name = "rel")
40 | @XmlJavaTypeAdapter(DoubleAdapter.class)
41 | public Double releaseVelocity;
42 |
43 | /** Per-note expressions can be stored within the note object as timelines. */
44 | @XmlElementRef(name = "Content", required = false)
45 | public Timeline content;
46 | }
47 |
--------------------------------------------------------------------------------
/java_classes/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.XmlElementRef;
8 | import jakarta.xml.bind.annotation.XmlRootElement;
9 |
10 | /** Timeline containing Notes (MIDI-style) */
11 |
12 | @XmlRootElement(name = "Notes")
13 | public class Notes extends Timeline
14 | {
15 | /** Contained notes. */
16 | @XmlElement(name = "Note")
17 | public List notes = new ArrayList<>();
18 | }
19 |
--------------------------------------------------------------------------------
/java_classes/timeline/Point.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.DoubleAdapter;
4 | import jakarta.xml.bind.annotation.XmlAttribute;
5 | import jakarta.xml.bind.annotation.XmlRootElement;
6 | import jakarta.xml.bind.annotation.XmlSeeAlso;
7 | import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
8 |
9 | /** A single automation point (abstract class). */
10 | @XmlRootElement(name = "Point")
11 | @XmlSeeAlso({RealPoint.class, EnumPoint.class, BoolPoint.class, IntegerPoint.class, TimeSignaturePoint.class})
12 | public abstract class Point
13 | {
14 | /** Time (within enclosing Points timeline) of this event */
15 | @XmlJavaTypeAdapter(DoubleAdapter.class)
16 | @XmlAttribute(required = true)
17 | public Double time;
18 | }
19 |
--------------------------------------------------------------------------------
/java_classes/timeline/Points.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import com.bitwig.dawproject.Interpolation;
7 | import com.bitwig.dawproject.Unit;
8 | import jakarta.xml.bind.annotation.XmlAttribute;
9 | import jakarta.xml.bind.annotation.XmlElement;
10 | import jakarta.xml.bind.annotation.XmlElementRef;
11 | import jakarta.xml.bind.annotation.XmlRootElement;
12 | import jakarta.xml.bind.annotation.XmlType;
13 |
14 | /** A timeline of points for automation or expression.
15 | * All the points should be of the same element-type and match the target.
16 | * */
17 | @XmlRootElement(name = "Points")
18 | @XmlType(propOrder={"target","points","unit"})
19 | public class Points extends Timeline
20 | {
21 | /** The parameter or expression this timeline should target. */
22 | @XmlElement(name = "Target", required = true)
23 | public AutomationTarget target = new AutomationTarget();
24 |
25 | /** The contained points. They should all be of the same type and match the target parameter. */
26 | @XmlElementRef(required = true)
27 | public List points = new ArrayList<>();
28 |
29 | /** A unit should be provided for when used with RealPoint elements. */
30 | @XmlAttribute(required = false)
31 | public Unit unit;
32 | }
33 |
--------------------------------------------------------------------------------
/java_classes/timeline/RealPoint.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.DoubleAdapter;
4 | import com.bitwig.dawproject.Interpolation;
5 | import jakarta.xml.bind.annotation.XmlAttribute;
6 | import jakarta.xml.bind.annotation.XmlRootElement;
7 | import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
8 |
9 | @XmlRootElement(name = "RealPoint")
10 | public class RealPoint extends Point
11 | {
12 | @XmlJavaTypeAdapter(DoubleAdapter.class)
13 | @XmlAttribute(required = true)
14 | public Double value;
15 |
16 | /** Interpolation mode used for the segment starting at this point. Default to 'hold' when unspecified. */
17 | @XmlAttribute(required = false)
18 | public Interpolation interpolation;
19 | }
20 |
--------------------------------------------------------------------------------
/java_classes/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 | {
10 | /** Numerator of the time-signature. (3/4 → 3, 4/4 → 4)*/
11 | @XmlAttribute(required = true)
12 | public Integer numerator;
13 |
14 | /** Denominator of the time-signature. (3/4 → 4, 7/8 → 8) */
15 | @XmlAttribute(required = true)
16 | public Integer denominator;
17 | }
18 |
--------------------------------------------------------------------------------
/java_classes/timeline/TimeUnit.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import jakarta.xml.bind.annotation.XmlEnum;
4 |
5 | @XmlEnum
6 | public enum TimeUnit
7 | {
8 | beats, // quarter-notes
9 | seconds;
10 | }
11 |
--------------------------------------------------------------------------------
/java_classes/timeline/Timeline.java:
--------------------------------------------------------------------------------
1 | package com.bitwig.dawproject.timeline;
2 |
3 | import com.bitwig.dawproject.Referenceable;
4 | import com.bitwig.dawproject.Track;
5 | import jakarta.xml.bind.annotation.XmlAttribute;
6 | import jakarta.xml.bind.annotation.XmlIDREF;
7 | import jakarta.xml.bind.annotation.XmlRootElement;
8 | import jakarta.xml.bind.annotation.XmlSeeAlso;
9 |
10 | /** Abstract base class for all timeline structures. */
11 | @XmlRootElement(name = "Timeline")
12 | @XmlSeeAlso({Note.class, Notes.class, Lanes.class, Clip.class, Clips.class, ClipSlot.class, Marker.class,
13 | Markers.class, Warps.class, Audio.class, Video.class, Point.class, Points.class})
14 | public abstract class Timeline extends Referenceable
15 | {
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 this or the parent scope then
23 | * 'beats' will be used.
24 | */
25 | @XmlAttribute(required = false)
26 | public TimeUnit timeUnit;
27 | }
28 |
--------------------------------------------------------------------------------
/java_classes/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 | /** Representation of a video file as a timeline. Duration should be the entire length of the file, any clipping
7 | * should be done by placing the Audio element within a Clip element. The timeUnit attribute should always be set to
8 | * seconds. */
9 | @XmlRootElement(name = "Video")
10 | public class Video extends MediaFile
11 | {
12 | /** sample-rate of audio (if present) */
13 | @XmlAttribute(required = false)
14 | public int sampleRate;
15 |
16 | /** number of channels of audio (1=mono..., if present) */
17 | @XmlAttribute(required = false)
18 | public int channels;
19 |
20 | /** Playback algorithm used to warp audio (vendor-specific, if present) */
21 | @XmlAttribute(required = false)
22 | public String algorithm;
23 | }
24 |
--------------------------------------------------------------------------------
/java_classes/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 | /** A single warp event, which defines the time both on the outer scope (time) and the inner scope (contentTime). The
7 | * time range between the Warp events are assumed to be linearly interpolated. */
8 | @XmlRootElement(name = "Warp")
9 | public class Warp
10 | {
11 | /**
12 | * The time this point represent to the 'outside' of the Warps element.
13 | * The TimeUnit is defined by the parent Warps element timeUnit attribute
14 | * or inherited from the parent element of the Warps container
15 | */
16 | @XmlAttribute(required = true)
17 | public double time;
18 |
19 | /**
20 | * The time this point represent to the 'inside' of the Warps element.
21 | * The TimeUnit is defined by the parent Warps element contentTimeUnit attribute
22 | */
23 | @XmlAttribute(required = true)
24 | public double contentTime;
25 | }
26 |
--------------------------------------------------------------------------------
/java_classes/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 | * A typical use case would be to warp an audio-file (contentTimeUnit = seconds) onto a timeline defined in beats
15 | * (timeUnit = beats) as defined by a set of Warp events.
16 | *
17 | * At least two Warp events need to present in order to define a usable beats/seconds conversion. For a plain
18 | * fixed-speed mapping, provide two event: One at (0,0) and a second event with the desired beat-time length along
19 | * with the length of the contained Audio file in seconds.
20 | *
21 | * {@code
22 | *
23 | *
24 | *
25 | *
26 | *
27 | *
28 | *
29 | *
30 | *
31 | *
32 | * }
33 | * */
34 |
35 | @XmlRootElement(name = "Warps")
36 | public class Warps extends Timeline
37 | {
38 | /** Warp events defining the transformation. (minimum 2) */
39 | @XmlElement(required = true, name = "Warp")
40 | public List events = new ArrayList<>();
41 |
42 | /**
43 | * Content timeline to be warped.
44 | */
45 | @XmlElementRef(name = "Content", required = true)
46 | public Timeline content;
47 |
48 | /**
49 | * The TimeUnit used by the content (nested) timeline and the contentTime attribute of the Warp events
50 | * */
51 | @XmlAttribute(required = true)
52 | public TimeUnit contentTimeUnit;
53 | }
54 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | lxml~=5.2.2
2 | chardet~=5.2.0
--------------------------------------------------------------------------------
/target/RoEx_Automix.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------