music21/musescore/
program.rs1use crate::error::{Error, Result};
9use crate::musicxml::{ExportOptions, from_musicxml, to_musicxml};
10use crate::stream::Stream;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15pub const MUSESCORE_PATH: &str = "MUSESCORE_PATH";
18
19const NAMES: [&str; 7] = [
21 "MuseScore4",
22 "mscore4portable",
23 "musescore4",
24 "mscore",
25 "musescore",
26 "MuseScore3",
27 "mscore3",
28];
29
30#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct MuseScore {
43 executable: PathBuf,
44}
45
46fn failed(message: impl Into<String>) -> Error {
47 Error::muse_score(message.into()).of_kind(crate::error::FormatErrorKind::External)
48}
49
50struct Scratch(PathBuf);
56
57impl Scratch {
58 fn new() -> Result<Self> {
59 static COUNTER: AtomicU64 = AtomicU64::new(0);
60 #[cfg(not(unix))]
62 let builder = std::fs::DirBuilder::new();
63 #[cfg(unix)]
64 let builder = {
65 use std::os::unix::fs::DirBuilderExt;
66 let mut builder = std::fs::DirBuilder::new();
67 builder.mode(0o700);
68 builder
69 };
70 loop {
71 let serial = COUNTER.fetch_add(1, Ordering::Relaxed);
72 let nanos = std::time::SystemTime::now()
73 .duration_since(std::time::UNIX_EPOCH)
74 .map_or(0, |elapsed| elapsed.subsec_nanos());
75 let path = std::env::temp_dir().join(format!(
76 "music21-rs-{}-{serial}-{nanos}",
77 std::process::id()
78 ));
79 match builder.create(&path) {
82 Ok(()) => return Ok(Self(path)),
83 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
84 Err(error) => {
85 return Err(failed(format!(
86 "no scratch directory could be made in {}: {error}",
87 path.display()
88 )));
89 }
90 }
91 }
92 }
93
94 fn file(&self, name: &str) -> PathBuf {
96 self.0.join(name)
97 }
98}
99
100impl Drop for Scratch {
101 fn drop(&mut self) {
102 let _ = std::fs::remove_dir_all(&self.0);
103 }
104}
105
106impl MuseScore {
107 pub fn at(executable: impl Into<PathBuf>) -> Self {
109 Self {
110 executable: executable.into(),
111 }
112 }
113
114 pub fn find() -> Option<Self> {
117 if let Some(named) = std::env::var_os(MUSESCORE_PATH) {
118 let path = PathBuf::from(named);
119 if path.is_file() {
120 return Some(Self::at(path));
121 }
122 }
123 let suffix = std::env::consts::EXE_SUFFIX;
124 if let Some(paths) = std::env::var_os("PATH") {
125 for directory in std::env::split_paths(&paths) {
126 for name in NAMES {
127 let candidate = directory.join(format!("{name}{suffix}"));
128 if candidate.is_file() {
129 return Some(Self::at(candidate));
130 }
131 }
132 }
133 }
134 let installed: [&str; 6] = [
135 r"C:\Program Files\MuseScore 4\bin\MuseScore4.exe",
136 r"C:\Program Files\MuseScore 3\bin\MuseScore3.exe",
137 "/Applications/MuseScore 4.app/Contents/MacOS/mscore",
138 "/Applications/MuseScore 3.app/Contents/MacOS/mscore",
139 "/usr/bin/mscore",
140 "/usr/local/bin/mscore",
141 ];
142 installed
143 .iter()
144 .map(Path::new)
145 .find(|path| path.is_file())
146 .map(Self::at)
147 }
148
149 pub fn executable(&self) -> &Path {
151 &self.executable
152 }
153
154 pub fn convert(&self, input: &Path, output: &Path) -> Result<()> {
157 if !input.is_file() {
158 return Err(failed(format!("{} is not a file", input.display())));
159 }
160 let ran = Command::new(&self.executable)
161 .arg("-o")
162 .arg(output)
163 .arg(input)
164 .output()
165 .map_err(|error| {
166 failed(format!(
167 "{} could not be run: {error}",
168 self.executable.display()
169 ))
170 })?;
171 if !ran.status.success() || !output.is_file() {
172 let said = String::from_utf8_lossy(&ran.stderr);
173 return Err(failed(format!(
174 "MuseScore could not convert {} to {}{}{}",
175 input.display(),
176 output.display(),
177 if said.trim().is_empty() { "" } else { ": " },
178 said.trim()
179 )));
180 }
181 Ok(())
182 }
183
184 pub fn read(&self, path: &Path) -> Result<Stream> {
187 let scratch = Scratch::new()?;
188 let converted = scratch.file("score.musicxml");
189 self.convert(path, &converted)?;
190 let text = std::fs::read_to_string(&converted)
191 .map_err(|error| failed(format!("what MuseScore wrote could not be read: {error}")))?;
192 from_musicxml(&text)
193 }
194
195 pub fn write(&self, score: &Stream, path: &Path, options: &ExportOptions) -> Result<()> {
199 let scratch = Scratch::new()?;
200 let written = scratch.file("score.musicxml");
201 std::fs::write(&written, to_musicxml(score, options)?)
202 .map_err(|error| failed(format!("the score could not be written out: {error}")))?;
203 self.convert(&written, path)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::{Clef, Note, StreamKind, TimeSignature};
211
212 #[test]
213 fn a_missing_program_and_a_missing_file_are_errors() {
214 let nowhere = MuseScore::at("no-such-directory/mscore");
215 let missing = Path::new("no-such-file.mscz");
216 assert!(matches!(
217 nowhere.convert(missing, Path::new("out.musicxml")),
218 Err(Error::MuseScore(_))
219 ));
220 let scratch = Scratch::new().unwrap();
221 let here = scratch.file("score.musicxml");
222 std::fs::write(&here, "<score-partwise/>").unwrap();
223 let ran = nowhere.convert(&here, &scratch.file("score.mscz"));
224 assert!(
225 matches!(ran, Err(Error::MuseScore(read)) if read.message.contains("could not be run"))
226 );
227 }
228
229 #[test]
230 fn scratch_directories_are_new_private_and_removed() {
231 let first = Scratch::new().unwrap();
232 let second = Scratch::new().unwrap();
233 assert_ne!(first.0, second.0);
234 for scratch in [&first, &second] {
235 let found = std::fs::symlink_metadata(&scratch.0).unwrap();
236 assert!(found.is_dir());
237 assert_eq!(std::fs::read_dir(&scratch.0).unwrap().count(), 0);
238 #[cfg(unix)]
239 {
240 use std::os::unix::fs::PermissionsExt;
241 assert_eq!(found.permissions().mode() & 0o077, 0);
242 }
243 }
244 let gone = first.0.clone();
245 std::fs::write(first.file("score.musicxml"), "<score-partwise/>").unwrap();
246 drop(first);
247 assert!(!gone.exists());
248 }
249
250 #[test]
252 fn a_score_survives_a_trip_through_musescore() {
253 let Some(musescore) = MuseScore::find() else {
254 return;
256 };
257 let mut measure = Stream::with_kind(StreamKind::Measure);
258 measure.set_number(1);
259 measure.insert(0.0, Clef::treble());
260 measure.insert(0.0, TimeSignature::new(3, 4).unwrap());
261 for name in ["C4", "E4", "G4"] {
262 measure.push(Note::from_name(name).unwrap());
263 }
264 let mut part = Stream::with_kind(StreamKind::Part);
265 part.push(measure);
266 let mut score = Stream::with_kind(StreamKind::Score);
267 score.push(part);
268
269 let scratch = Scratch::new().unwrap();
270 let file = scratch.file("score.mscz");
271 musescore
272 .write(&score, &file, &ExportOptions::default())
273 .unwrap();
274 let read = musescore.read(&file).unwrap();
275 let names: Vec<String> = read
276 .pitches()
277 .iter()
278 .map(|pitch| pitch.name_with_octave())
279 .collect();
280 assert_eq!(names, ["C4", "E4", "G4"]);
281 }
282}