ustx/
project.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::fmt;
4use std::str::FromStr;
5
6use crate::error::Error;
7use crate::expression::ExpressionDescriptor;
8use crate::part::{VoicePart, WavePart};
9use crate::time::{Tempo, TimeSignature};
10use crate::track::Track;
11use crate::version::{CURRENT_VERSION, Version};
12
13/// Represents an `OpenUtau` project file (`.ustx`).
14///
15/// This is the root object of a `.ustx` file. It contains all the project settings,
16/// tracks, parts, and other data.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub struct Project {
20    /// The name of the project.
21    #[serde(default = "default_project_name")]
22    pub name: String,
23    /// A comment for the project.
24    #[serde(default)]
25    pub comment: String,
26    /// The output directory for rendered audio.
27    #[serde(default = "default_output_dir")]
28    pub output_dir: String,
29    /// The cache directory for temporary files.
30    #[serde(default = "default_cache_dir")]
31    pub cache_dir: String,
32    /// The version of the `.ustx` file format.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub ustx_version: Option<Version>,
35    /// The resolution of the project, in ticks per quarter note.
36    #[serde(default = "default_resolution")]
37    pub resolution: i32,
38    /// The initial tempo of the project, in beats per minute.
39    #[serde(default = "default_bpm")]
40    pub bpm: f64,
41    /// The number of beats per bar.
42    #[serde(default = "default_beat_per_bar")]
43    pub beat_per_bar: i32,
44    /// The beat unit, which defines the note value that represents one beat.
45    #[serde(default = "default_beat_unit")]
46    pub beat_unit: i32,
47    /// A map of expression names to their descriptors.
48    #[serde(default)]
49    pub expressions: BTreeMap<String, ExpressionDescriptor>,
50    /// A list of expression selectors.
51    #[serde(default = "default_exp_selectors")]
52    pub exp_selectors: Vec<String>,
53    /// The index of the primary expression.
54    #[serde(default)]
55    pub exp_primary: i32,
56    /// The index of the secondary expression.
57    #[serde(default = "default_exp_secondary")]
58    pub exp_secondary: i32,
59    /// The key of the project.
60    #[serde(default)]
61    pub key: i32,
62    /// A list of time signatures in the project.
63    #[serde(default = "default_time_signatures")]
64    pub time_signatures: Vec<TimeSignature>,
65    /// A list of tempos in the project.
66    #[serde(default = "default_tempos")]
67    pub tempos: Vec<Tempo>,
68    /// A list of tracks in the project.
69    #[serde(default = "default_tracks")]
70    pub tracks: Vec<Track>,
71    /// A list of voice parts in the project.
72    #[serde(default)]
73    pub voice_parts: Vec<VoicePart>,
74    /// A list of wave parts in the project.
75    #[serde(default)]
76    pub wave_parts: Vec<WavePart>,
77}
78
79impl Project {
80    /// Deserializes a `Project` from a YAML string.
81    #[inline]
82    pub fn from_yaml_str(input: &str) -> Result<Self, Error> {
83        let mut documents = serde_yaml::Deserializer::from_str(input);
84        let document = documents.next().ok_or_else(|| Error::MissingDocument)?;
85        let project = Project::deserialize(document)?;
86        Ok(project)
87    }
88
89    /// Serializes a `Project` to a YAML string.
90    #[inline]
91    pub fn to_yaml_string(&self) -> Result<String, Error> {
92        serde_yaml::to_string(self).map_err(Error::from)
93    }
94
95    /// Deserializes a `Project` from a YAML string with compatibility upgrades.
96    ///
97    /// This function will attempt to upgrade the project from older formats to the
98    /// current format.
99    #[inline]
100    pub fn from_yaml_str_with_compat(input: &str) -> Result<Self, Error> {
101        let mut project = Self::from_yaml_str(input)?;
102        project.convert_to(CURRENT_VERSION)?;
103        Ok(project)
104    }
105
106    /// Serializes a `Project` to a YAML string with compatibility upgrades.
107    #[inline]
108    pub fn to_yaml_string_with_compat(&self) -> Result<String, Error> {
109        let mut project = self.clone();
110        project.convert_to(CURRENT_VERSION)?;
111        serde_yaml::to_string(&project).map_err(Error::from)
112    }
113
114    /// Converts the project to the specified `target` version.
115    pub fn convert_to(&mut self, target: Version) -> Result<(), Error> {
116        let detected = self.ustx_version.unwrap_or_else(Version::zero);
117
118        if target > CURRENT_VERSION {
119            return Err(Error::unsupported_version(target.to_string()));
120        }
121        if detected > CURRENT_VERSION {
122            return Err(Error::unsupported_version(detected.to_string()));
123        }
124        if detected >= target {
125            self.ustx_version = Some(target);
126            return Ok(());
127        }
128
129        if detected < VERSION_0_4 && target >= VERSION_0_4 {
130            self.convert_pre_0_4();
131        }
132        if detected < VERSION_0_5 && target >= VERSION_0_5 {
133            self.convert_pre_0_5();
134        }
135        if detected < VERSION_0_6 && target >= VERSION_0_6 {
136            self.convert_pre_0_6();
137        }
138        if detected < VERSION_0_7 && target >= VERSION_0_7 {
139            self.convert_pre_0_7();
140        }
141
142        self.ustx_version = Some(target);
143        Ok(())
144    }
145}
146
147impl fmt::Display for Project {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        self.to_yaml_string_with_compat()
150            .map_or(Err(fmt::Error), |text| f.write_str(&text))
151    }
152}
153
154impl FromStr for Project {
155    type Err = Error;
156
157    #[inline]
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        Self::from_yaml_str_with_compat(s)
160    }
161}
162
163#[inline]
164fn default_project_name() -> String {
165    String::from("New Project")
166}
167
168#[inline]
169fn default_output_dir() -> String {
170    String::from("Vocal")
171}
172
173#[inline]
174fn default_cache_dir() -> String {
175    String::from("UCache")
176}
177
178#[inline]
179const fn default_resolution() -> i32 {
180    480
181}
182
183#[inline]
184const fn default_bpm() -> f64 {
185    120.0
186}
187
188#[inline]
189const fn default_beat_per_bar() -> i32 {
190    4
191}
192
193#[inline]
194const fn default_beat_unit() -> i32 {
195    4
196}
197
198#[inline]
199fn default_exp_selectors() -> Vec<String> {
200    vec![
201        "dyn".into(),
202        "pitd".into(),
203        "clr".into(),
204        "eng".into(),
205        "vel".into(),
206        "vol".into(),
207        "atk".into(),
208        "dec".into(),
209        "gen".into(),
210        "bre".into(),
211    ]
212}
213
214#[inline]
215const fn default_exp_secondary() -> i32 {
216    1
217}
218
219#[inline]
220fn default_time_signatures() -> Vec<TimeSignature> {
221    vec![TimeSignature::default()]
222}
223
224#[inline]
225fn default_tempos() -> Vec<Tempo> {
226    vec![Tempo::default()]
227}
228
229#[inline]
230fn default_tracks() -> Vec<Track> {
231    vec![Track::default()]
232}
233
234const VERSION_0_4: Version = Version::new(0, 4, 0);
235const VERSION_0_5: Version = Version::new(0, 5, 0);
236const VERSION_0_6: Version = Version::new(0, 6, 0);
237const VERSION_0_7: Version = Version::new(0, 7, 0);
238
239const OLD_ACCENT_ABBR: &str = "acc";
240const NEW_ACCENT_ABBR: &str = "atk";
241const NEW_ACCENT_NAME: &str = "attack";
242const DEFAULT_SELECTORS: [&str; 10] = [
243    "dyn", "pitd", "clr", "eng", "vel", "vol", "atk", "dec", "gen", "bre",
244];
245
246impl Project {
247    fn convert_pre_0_4(&mut self) {
248        if self
249            .expressions
250            .get(OLD_ACCENT_ABBR)
251            .is_none_or(|descriptor| descriptor.name != "accent")
252        {
253            return;
254        }
255        let Some(mut descriptor) = self.expressions.remove(OLD_ACCENT_ABBR) else {
256            return;
257        };
258        descriptor.abbr = String::from(NEW_ACCENT_ABBR);
259        descriptor.name = String::from(NEW_ACCENT_NAME);
260        self.expressions
261            .insert(String::from(NEW_ACCENT_ABBR), descriptor);
262
263        for part in &mut self.voice_parts {
264            for note in &mut part.notes {
265                for expression in &mut note.phoneme_expressions {
266                    if expression.abbr == OLD_ACCENT_ABBR {
267                        expression.abbr = String::from(NEW_ACCENT_ABBR);
268                    }
269                }
270            }
271        }
272    }
273
274    fn convert_pre_0_5(&mut self) {
275        for part in &mut self.voice_parts {
276            for note in &mut part.notes {
277                if note.lyric.starts_with("...") {
278                    note.lyric = note.lyric.replace("...", "+");
279                }
280            }
281        }
282    }
283
284    fn convert_pre_0_6(&mut self) {
285        let beat_per_bar = if self.beat_per_bar > 0 {
286            self.beat_per_bar
287        } else {
288            4
289        };
290        let beat_unit = if self.beat_unit > 0 {
291            self.beat_unit
292        } else {
293            4
294        };
295        let bpm = if self.bpm > 0.0 { self.bpm } else { 120.0 };
296
297        self.time_signatures = vec![TimeSignature {
298            bar_position: 0,
299            beat_per_bar,
300            beat_unit,
301        }];
302        self.tempos = vec![Tempo { position: 0, bpm }];
303    }
304
305    fn convert_pre_0_7(&mut self) {
306        if self.exp_selectors.len() >= DEFAULT_SELECTORS.len() {
307            return;
308        }
309        let mut selectors = DEFAULT_SELECTORS
310            .iter()
311            .map(|&value| String::from(value))
312            .collect::<Vec<_>>();
313        for (index, existing) in self.exp_selectors.iter().enumerate() {
314            if let Some(target) = selectors.get_mut(index) {
315                target.clone_from(existing);
316            } else {
317                break;
318            }
319        }
320        self.exp_selectors = selectors;
321    }
322}