1use std::collections::{BTreeMap, HashMap};
31use std::error::Error;
32use std::fmt;
33use std::sync::OnceLock;
34
35use serde::{Deserialize, Serialize};
36
37use crate::music::entry::{Entry, Group, Tag, Tier};
38
39pub mod check;
40pub mod chord;
41pub mod entry;
42pub mod key;
43pub mod pitch;
44pub mod rhythm;
45pub mod roman;
46pub mod scale;
47
48#[derive(Clone, Copy, Debug)]
50pub struct Topic {
51 pub name: &'static str,
53 pub title: &'static str,
55 pub groups: &'static [Group],
57 data: &'static str,
58}
59
60pub const TOPICS: [Topic; 10] = [
62 Topic {
63 name: "notation",
64 title: "Notes, keys and numerals",
65 groups: &[Group::Notation, Group::Key],
66 data: include_str!("../dictionary/music/notation.json"),
67 },
68 Topic {
69 name: "roles",
70 title: "Role pitch ranges",
71 groups: &[Group::Role],
72 data: include_str!("../dictionary/music/roles.json"),
73 },
74 Topic {
75 name: "scales",
76 title: "Scales, moods and outside notes",
77 groups: &[Group::Scale, Group::Mood, Group::Outside],
78 data: include_str!("../dictionary/music/scales.json"),
79 },
80 Topic {
81 name: "chords",
82 title: "Chords and symbols",
83 groups: &[Group::Chord],
84 data: include_str!("../dictionary/music/chords.json"),
85 },
86 Topic {
87 name: "voicing",
88 title: "Voicing and voice leading",
89 groups: &[Group::Voicing, Group::VoiceLeading],
90 data: include_str!("../dictionary/music/voicing.json"),
91 },
92 Topic {
93 name: "progressions",
94 title: "Progressions and harmony",
95 groups: &[Group::Progression, Group::Harmony],
96 data: include_str!("../dictionary/music/progressions.json"),
97 },
98 Topic {
99 name: "rhythm",
100 title: "Rhythm, swing and feel",
101 groups: &[
102 Group::Position,
103 Group::Swing,
104 Group::Feel,
105 Group::Rhythm,
106 Group::Euclid,
107 ],
108 data: include_str!("../dictionary/music/rhythm.json"),
109 },
110 Topic {
111 name: "technique",
112 title: "Articulation and technique",
113 groups: &[
114 Group::Articulation,
115 Group::Bass,
116 Group::Lead,
117 Group::Arp,
118 Group::Pad,
119 ],
120 data: include_str!("../dictionary/music/technique.json"),
121 },
122 Topic {
123 name: "phrase",
124 title: "Phrase structure",
125 groups: &[Group::Phrase],
126 data: include_str!("../dictionary/music/phrase.json"),
127 },
128 Topic {
129 name: "genres",
130 title: "Genre templates",
131 groups: &[Group::Genre],
132 data: include_str!("../dictionary/music/genres.json"),
133 },
134];
135
136const STATS: &str = include_str!("../dictionary/music/stats.json");
137const PLAN: &str = include_str!("../dictionary/music/plan.json");
138
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct Stat {
143 pub id: String,
145 pub set: Option<String>,
147 pub claim: String,
149 pub share: f64,
151 pub count: Option<u64>,
153 pub total: Option<u64>,
155 pub source: Vec<String>,
157 pub tag: Tag,
159}
160
161#[derive(Debug)]
163pub struct Dictionary {
164 pub entries: Vec<Entry>,
166 pub stats: Vec<Stat>,
168 pub plan: BTreeMap<Group, BTreeMap<Tier, u16>>,
170 topic_of: Vec<&'static str>,
171 by_id: HashMap<String, usize>,
172}
173
174impl Dictionary {
175 #[must_use]
177 pub fn entry(&self, id: &str) -> Option<&Entry> {
178 self.by_id.get(id).map(|&index| &self.entries[index])
179 }
180
181 pub fn group(&self, group: Group) -> impl Iterator<Item = &Entry> {
183 self.entries
184 .iter()
185 .filter(move |entry| entry.group == group)
186 }
187
188 pub fn topic(&self, name: &str) -> impl Iterator<Item = &Entry> {
190 self.entries
191 .iter()
192 .zip(&self.topic_of)
193 .filter(move |(_, topic)| **topic == name)
194 .map(|(entry, _)| entry)
195 }
196
197 #[must_use]
199 pub fn stat(&self, id: &str) -> Option<&Stat> {
200 self.stats.iter().find(|stat| stat.id == id)
201 }
202}
203
204pub fn dictionary() -> Result<&'static Dictionary, MusicError> {
211 static DICTIONARY: OnceLock<Result<Dictionary, String>> = OnceLock::new();
212 DICTIONARY
213 .get_or_init(build)
214 .as_ref()
215 .map_err(|cause| MusicError::BrokenData(cause.clone()))
216}
217
218fn build() -> Result<Dictionary, String> {
219 let mut entries = Vec::new();
220 let mut topic_of = Vec::new();
221 for topic in TOPICS {
222 let parsed: Vec<Entry> = serde_json::from_str(topic.data)
223 .map_err(|error| format!("dictionary/music/{}.json: {error}", topic.name))?;
224 topic_of.extend(std::iter::repeat_n(topic.name, parsed.len()));
225 entries.extend(parsed);
226 }
227 let stats: Vec<Stat> = serde_json::from_str(STATS)
228 .map_err(|error| format!("dictionary/music/stats.json: {error}"))?;
229 let plan = serde_json::from_str(PLAN)
230 .map_err(|error| format!("dictionary/music/plan.json: {error}"))?;
231 let by_id = entries
232 .iter()
233 .enumerate()
234 .map(|(index, entry)| (entry.id.clone(), index))
235 .collect();
236 let dictionary = Dictionary {
237 entries,
238 stats,
239 plan,
240 topic_of,
241 by_id,
242 };
243 let problems = check::problems(&dictionary);
244 if problems.is_empty() {
245 Ok(dictionary)
246 } else {
247 Err(problems.join("; "))
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum MusicError {
255 BrokenData(String),
257 UnknownKey(String),
259 UnknownNote(String),
262 UnknownNumeral(String),
264 UnsupportedChord(String),
266 UnknownScale(String),
268}
269
270impl fmt::Display for MusicError {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 match self {
273 Self::BrokenData(cause) => write!(f, "the music dictionary is broken: {cause}"),
274 Self::UnknownKey(text) => write!(
275 f,
276 "`{text}` is not a key name: write a letter with an optional sharp or flat and \
277 major or minor, such as `A minor` or `Bb`, a Camelot code such as `8A`, or an \
278 Open Key code such as `1m`; a mode such as dorian is a scale entry"
279 ),
280 Self::UnknownNote(text) => write!(
281 f,
282 "`{text}` is not a note name: write a letter, an optional sharp or flat and an \
283 octave number, such as `C4` or `F#2`, or a MIDI number from 0 to 127"
284 ),
285 Self::UnknownNumeral(text) => write!(
286 f,
287 "`{text}` is not a Roman numeral: write I to VII, upper case for major and \
288 lower case for minor, with an optional `b` or `#` before it"
289 ),
290 Self::UnsupportedChord(text) => write!(
291 f,
292 "chord symbol `{text}` is not in the music dictionary: write it with a \
293 spelling the chord reference lists, such as `Cmaj7` or `Am7b5`"
294 ),
295 Self::UnknownScale(text) => write!(
296 f,
297 "`{text}` is not a scale id: use an id from the scale reference, such as \
298 `scale.dorian`"
299 ),
300 }
301 }
302}
303
304impl Error for MusicError {}