Skip to main content

oxihuman_export/
abc_notation_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! ABC music notation stub export.
6
7/// ABC notation tune fields.
8#[derive(Debug, Clone, Default)]
9pub struct AbcTuneHeader {
10    pub index: u32,
11    pub title: String,
12    pub composer: String,
13    pub meter: String,
14    pub default_note_length: String,
15    pub tempo: String,
16    pub key: String,
17}
18
19impl AbcTuneHeader {
20    pub fn new(
21        index: u32,
22        title: impl Into<String>,
23        composer: impl Into<String>,
24        meter: impl Into<String>,
25        key: impl Into<String>,
26    ) -> Self {
27        Self {
28            index,
29            title: title.into(),
30            composer: composer.into(),
31            meter: meter.into(),
32            default_note_length: "1/4".to_string(),
33            tempo: "120".to_string(),
34            key: key.into(),
35        }
36    }
37
38    pub fn to_abc_header(&self) -> String {
39        let mut s = String::new();
40        s.push_str(&format!("X:{}\n", self.index));
41        s.push_str(&format!("T:{}\n", self.title));
42        if !self.composer.is_empty() {
43            s.push_str(&format!("C:{}\n", self.composer));
44        }
45        s.push_str(&format!("M:{}\n", self.meter));
46        s.push_str(&format!("L:{}\n", self.default_note_length));
47        s.push_str(&format!("Q:{}\n", self.tempo));
48        s.push_str(&format!("K:{}\n", self.key));
49        s
50    }
51}
52
53/// A single ABC note token.
54#[derive(Debug, Clone)]
55pub struct AbcNote {
56    pub pitch: String,
57    pub duration_modifier: String,
58}
59
60impl AbcNote {
61    pub fn new(pitch: impl Into<String>) -> Self {
62        Self {
63            pitch: pitch.into(),
64            duration_modifier: String::new(),
65        }
66    }
67
68    pub fn with_duration(mut self, modifier: impl Into<String>) -> Self {
69        self.duration_modifier = modifier.into();
70        self
71    }
72
73    pub fn to_abc_token(&self) -> String {
74        format!("{}{}", self.pitch, self.duration_modifier)
75    }
76}
77
78/// An ABC tune body (sequence of bars).
79#[derive(Debug, Clone, Default)]
80pub struct AbcTuneBody {
81    pub bars: Vec<Vec<AbcNote>>,
82}
83
84impl AbcTuneBody {
85    pub fn new() -> Self {
86        Self { bars: Vec::new() }
87    }
88
89    pub fn add_bar(&mut self, notes: Vec<AbcNote>) {
90        self.bars.push(notes);
91    }
92
93    pub fn to_abc_body(&self) -> String {
94        self.bars
95            .iter()
96            .map(|bar| {
97                let tokens: String = bar
98                    .iter()
99                    .map(|n| n.to_abc_token())
100                    .collect::<Vec<_>>()
101                    .join(" ");
102                format!("{} |", tokens)
103            })
104            .collect::<Vec<_>>()
105            .join("\n")
106    }
107}
108
109/// A complete ABC tune (header + body).
110#[derive(Debug, Clone, Default)]
111pub struct AbcTune {
112    pub header: AbcTuneHeader,
113    pub body: AbcTuneBody,
114}
115
116impl AbcTune {
117    pub fn new(header: AbcTuneHeader) -> Self {
118        Self {
119            header,
120            body: AbcTuneBody::new(),
121        }
122    }
123}
124
125/// Generate ABC notation source from a tune.
126pub fn generate_abc_notation(tune: &AbcTune) -> String {
127    format!(
128        "{}\n{}\n",
129        tune.header.to_abc_header(),
130        tune.body.to_abc_body()
131    )
132}
133
134/// Validate that a string looks like ABC notation.
135pub fn is_valid_abc(src: &str) -> bool {
136    src.contains("X:") && src.contains("T:") && src.contains("K:")
137}
138
139/// Count total notes across all bars.
140pub fn count_abc_notes(body: &AbcTuneBody) -> usize {
141    body.bars.iter().map(|b| b.len()).sum()
142}
143
144/// Build a simple C major scale in ABC notation.
145pub fn c_major_scale_abc() -> AbcTune {
146    let header = AbcTuneHeader::new(1, "C Major Scale", "", "4/4", "C");
147    let mut tune = AbcTune::new(header);
148    let notes = ["C", "D", "E", "F", "G", "A", "B", "c"];
149    let bar1: Vec<AbcNote> = notes[0..4].iter().map(|&n| AbcNote::new(n)).collect();
150    let bar2: Vec<AbcNote> = notes[4..8].iter().map(|&n| AbcNote::new(n)).collect();
151    tune.body.add_bar(bar1);
152    tune.body.add_bar(bar2);
153    tune
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_header_x_field() {
162        let h = AbcTuneHeader::new(1, "Test", "", "4/4", "G");
163        let abc = h.to_abc_header();
164        assert!(abc.contains("X:1") /* tune index */);
165    }
166
167    #[test]
168    fn test_header_key_field() {
169        let h = AbcTuneHeader::new(1, "Test", "", "4/4", "D");
170        let abc = h.to_abc_header();
171        assert!(abc.contains("K:D") /* key of D */);
172    }
173
174    #[test]
175    fn test_abc_note_token() {
176        let note = AbcNote::new("C").with_duration("2");
177        assert_eq!(note.to_abc_token(), "C2" /* half note C */);
178    }
179
180    #[test]
181    fn test_abc_note_no_modifier() {
182        let note = AbcNote::new("G");
183        assert_eq!(note.to_abc_token(), "G" /* quarter note G */);
184    }
185
186    #[test]
187    fn test_generate_abc_valid() {
188        let tune = c_major_scale_abc();
189        let abc = generate_abc_notation(&tune);
190        assert!(is_valid_abc(&abc) /* valid ABC notation */);
191    }
192
193    #[test]
194    fn test_count_abc_notes() {
195        let tune = c_major_scale_abc();
196        assert_eq!(count_abc_notes(&tune.body), 8 /* 8 notes in C major */);
197    }
198
199    #[test]
200    fn test_is_valid_abc_false() {
201        assert!(!is_valid_abc("not abc notation") /* invalid */);
202    }
203
204    #[test]
205    fn test_body_contains_bar_separator() {
206        let tune = c_major_scale_abc();
207        let body = tune.body.to_abc_body();
208        assert!(body.contains('|') /* bar lines present */);
209    }
210
211    #[test]
212    fn test_composer_in_header() {
213        let h = AbcTuneHeader::new(1, "Test", "J.S. Bach", "3/4", "Am");
214        let abc = h.to_abc_header();
215        assert!(abc.contains("C:J.S. Bach") /* composer field */);
216    }
217}