├── .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](https://img.shields.io/github/license/roex-audio/dawproject-py)](LICENSE) 5 | [![GitHub stars](https://img.shields.io/github/stars/roex-audio/dawproject-py.svg)](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 |  *       
    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 | --------------------------------------------------------------------------------