guitarpro/writers/
gp.rs

1// gp_writer.rs - Guitar Pro modern format (.gp) writer
2
3use super::{FileWriter, Result};
4use crate::*;
5use byteorder::{LittleEndian, WriteBytesExt};
6use std::io::{Cursor, Write};
7
8/// Guitar Pro modern format writer
9pub struct GpWriter;
10
11impl FileWriter for GpWriter {
12    fn write_score(score: &Score) -> Result<Vec<u8>> {
13        let mut buffer = Vec::new();
14        let mut cursor = Cursor::new(&mut buffer);
15
16        // Write magic header
17        cursor.write_all(b"BCFZ")?;
18
19        // Write file chunks
20        write_file_chunks(&mut cursor, score)?;
21
22        Ok(buffer)
23    }
24}
25
26/// Write all file chunks
27fn write_file_chunks(cursor: &mut Cursor<&mut Vec<u8>>, score: &Score) -> Result<()> {
28    // Write score information chunk
29    write_chunk(cursor, 0x01, |chunk_cursor| {
30        write_score_info(chunk_cursor, score)
31    })?;
32
33    // Write tracks information chunk
34    write_chunk(cursor, 0x02, |chunk_cursor| {
35        write_tracks_info(chunk_cursor, score)
36    })?;
37
38    // Write measures data chunk
39    write_chunk(cursor, 0x03, |chunk_cursor| {
40        write_measures_data(chunk_cursor, score)
41    })?;
42
43    // Write automation data chunk (minimal)
44    write_chunk(cursor, 0x04, |chunk_cursor| {
45        write_automation_data(chunk_cursor, score)
46    })?;
47
48    Ok(())
49}
50
51/// Write a chunk with header
52fn write_chunk<F>(cursor: &mut Cursor<&mut Vec<u8>>, chunk_type: u32, write_fn: F) -> Result<()>
53where
54    F: FnOnce(&mut Cursor<&mut Vec<u8>>) -> Result<()>,
55{
56    // Create temporary buffer for chunk data
57    let mut chunk_data = Vec::new();
58    let mut chunk_cursor = Cursor::new(&mut chunk_data);
59
60    // Write chunk content
61    write_fn(&mut chunk_cursor)?;
62
63    // Write chunk header
64    cursor.write_u32::<LittleEndian>(chunk_type)?;
65    cursor.write_u32::<LittleEndian>(chunk_data.len() as u32)?;
66
67    // Write chunk data
68    cursor.write_all(&chunk_data)?;
69
70    Ok(())
71}
72
73/// Write score information
74fn write_score_info(cursor: &mut Cursor<&mut Vec<u8>>, score: &Score) -> Result<()> {
75    // Write metadata strings
76    write_gp_string(cursor, &score.metadata.title)?;
77    write_gp_string(cursor, &score.metadata.subtitle)?;
78    write_gp_string(cursor, &score.metadata.artist)?;
79    write_gp_string(cursor, &score.metadata.album)?;
80    write_gp_string(cursor, &score.metadata.author)?;
81    write_gp_string(cursor, &score.metadata.copyright)?;
82    write_gp_string(cursor, &score.metadata.tab_author)?;
83    write_gp_string(cursor, &score.metadata.instructions)?;
84
85    // Write tempo and master volume
86    cursor.write_u32::<LittleEndian>(score.tempo as u32)?;
87    cursor.write_u8(score.master_volume)?;
88
89    Ok(())
90}
91
92/// Write tracks information
93fn write_tracks_info(cursor: &mut Cursor<&mut Vec<u8>>, score: &Score) -> Result<()> {
94    cursor.write_u8(score.tracks.len() as u8)?;
95
96    for track in &score.tracks {
97        write_track_info(cursor, track)?;
98    }
99
100    Ok(())
101}
102
103/// Write individual track information
104fn write_track_info(cursor: &mut Cursor<&mut Vec<u8>>, track: &Track) -> Result<()> {
105    // Write track flags
106    let mut flags = 0u8;
107    if track.is_percussion {
108        flags |= 0x01;
109    }
110    if track.is_twelve_string {
111        flags |= 0x02;
112    }
113    cursor.write_u8(flags)?;
114
115    // Write track name
116    write_gp_string(cursor, &track.name)?;
117
118    // Write tuning
119    cursor.write_u8(track.tuning.len() as u8)?;
120    for &note in &track.tuning {
121        cursor.write_u8(note)?;
122    }
123
124    // Write channel and capo
125    cursor.write_u8(track.channel)?;
126    cursor.write_u8(track.capo)?;
127
128    // Write color
129    if let Some((r, g, b)) = track.color {
130        cursor.write_u8(r)?;
131        cursor.write_u8(g)?;
132        cursor.write_u8(b)?;
133    } else {
134        cursor.write_u8(255)?; // Default red
135        cursor.write_u8(0)?; // Default green
136        cursor.write_u8(0)?; // Default blue
137    }
138
139    Ok(())
140}
141
142/// Write measures data
143fn write_measures_data(cursor: &mut Cursor<&mut Vec<u8>>, score: &Score) -> Result<()> {
144    let measure_count = score.measure_count();
145    cursor.write_u16::<LittleEndian>(measure_count as u16)?;
146
147    for measure_idx in 0..measure_count {
148        write_measure_data(cursor, score, measure_idx)?;
149    }
150
151    Ok(())
152}
153
154/// Write individual measure data
155fn write_measure_data(
156    cursor: &mut Cursor<&mut Vec<u8>>,
157    score: &Score,
158    measure_idx: usize,
159) -> Result<()> {
160    for track in &score.tracks {
161        if let Some(measure) = track.measures.get(measure_idx) {
162            // Write beat count
163            cursor.write_u8(measure.beats.len() as u8)?;
164
165            // Write measure flags
166            let mut flags = 0u8;
167            if measure.time_signature.is_some() {
168                flags |= 0x01;
169            }
170            if measure.key_signature.is_some() {
171                flags |= 0x02;
172            }
173            if measure.tempo.is_some() {
174                flags |= 0x04;
175            }
176            cursor.write_u8(flags)?;
177
178            // Write time signature if present
179            if let Some(ref time_sig) = measure.time_signature {
180                cursor.write_u8(time_sig.numerator)?;
181                cursor.write_u8(time_sig.denominator)?;
182            }
183
184            // Write key signature if present
185            if let Some(ref key_sig) = measure.key_signature {
186                cursor.write_i8(key_sig.key)?;
187                cursor.write_u8(if key_sig.is_minor { 1 } else { 0 })?;
188            }
189
190            // Write tempo if present
191            if let Some(tempo) = measure.tempo {
192                cursor.write_u16::<LittleEndian>(tempo)?;
193            }
194
195            // Write beats
196            for beat in &measure.beats {
197                write_beat_data(cursor, beat)?;
198            }
199        } else {
200            // Write empty measure
201            cursor.write_u8(0)?; // No beats
202            cursor.write_u8(0)?; // No flags
203        }
204    }
205
206    Ok(())
207}
208
209/// Write beat data
210fn write_beat_data(cursor: &mut Cursor<&mut Vec<u8>>, beat: &Beat) -> Result<()> {
211    let mut beat_flags = 0u8;
212
213    if beat.is_rest {
214        beat_flags |= 0x40;
215    }
216    if beat.chord.is_some() {
217        beat_flags |= 0x02;
218    }
219
220    cursor.write_u8(beat_flags)?;
221
222    // Write duration
223    let duration_byte = match beat.duration {
224        NoteDuration::Whole => 0,
225        NoteDuration::Half => 1,
226        NoteDuration::Quarter => 2,
227        NoteDuration::Eighth => 3,
228        NoteDuration::Sixteenth => 4,
229        NoteDuration::ThirtySecond => 5,
230        NoteDuration::SixtyFourth => 6,
231    };
232    cursor.write_u8(duration_byte)?;
233
234    // Write chord information if present
235    if let Some(ref chord) = beat.chord {
236        write_chord_info(cursor, chord)?;
237    }
238
239    // Write notes
240    cursor.write_u8(beat.notes.len() as u8)?;
241    for note in &beat.notes {
242        write_note_data(cursor, note)?;
243    }
244
245    Ok(())
246}
247
248/// Write chord information
249fn write_chord_info(cursor: &mut Cursor<&mut Vec<u8>>, chord: &ChordInfo) -> Result<()> {
250    write_gp_string(cursor, &chord.name)?;
251
252    // Write fret positions
253    cursor.write_u8(chord.frets.len() as u8)?;
254    for &fret in &chord.frets {
255        cursor.write_i8(fret)?;
256    }
257
258    Ok(())
259}
260
261/// Write note data
262fn write_note_data(cursor: &mut Cursor<&mut Vec<u8>>, note: &Note) -> Result<()> {
263    let mut note_flags = 0u8;
264
265    // Check if note has effects
266    if has_note_effects(&note.effects) {
267        note_flags |= 0x01;
268    }
269
270    cursor.write_u8(note_flags)?;
271    cursor.write_u8(note.string)?;
272    cursor.write_u8(note.fret)?;
273    cursor.write_u8(note.velocity)?;
274
275    // Write effects if present
276    if has_note_effects(&note.effects) {
277        write_note_effects(cursor, &note.effects)?;
278    }
279
280    Ok(())
281}
282
283/// Check if note has any effects
284fn has_note_effects(effects: &NoteEffects) -> bool {
285    effects.vibrato
286        || effects.hammer_on
287        || effects.pull_off
288        || effects.palm_mute
289        || effects.let_ring
290        || effects.bend.is_some()
291        || effects.slide.is_some()
292}
293
294/// Write note effects
295fn write_note_effects(cursor: &mut Cursor<&mut Vec<u8>>, effects: &NoteEffects) -> Result<()> {
296    let mut effect_flags = 0u8;
297
298    if effects.vibrato {
299        effect_flags |= 0x01;
300    }
301    if effects.hammer_on {
302        effect_flags |= 0x02;
303    }
304    if effects.pull_off {
305        effect_flags |= 0x04;
306    }
307    if effects.palm_mute {
308        effect_flags |= 0x08;
309    }
310    if effects.let_ring {
311        effect_flags |= 0x10;
312    }
313    if effects.bend.is_some() {
314        effect_flags |= 0x20;
315    }
316    if effects.slide.is_some() {
317        effect_flags |= 0x40;
318    }
319
320    cursor.write_u8(effect_flags)?;
321
322    // Write bend effect if present
323    if let Some(ref bend) = effects.bend {
324        write_bend_effect(cursor, bend)?;
325    }
326
327    // Write slide effect if present
328    if let Some(ref slide) = effects.slide {
329        write_slide_effect(cursor, slide)?;
330    }
331
332    Ok(())
333}
334
335/// Write bend effect
336fn write_bend_effect(cursor: &mut Cursor<&mut Vec<u8>>, bend: &BendEffect) -> Result<()> {
337    cursor.write_u8(bend.points.len() as u8)?;
338
339    for point in &bend.points {
340        cursor.write_u8(point.position)?;
341        cursor.write_f32::<LittleEndian>(point.value)?;
342    }
343
344    Ok(())
345}
346
347/// Write slide effect
348fn write_slide_effect(cursor: &mut Cursor<&mut Vec<u8>>, slide: &SlideEffect) -> Result<()> {
349    let (slide_type, target_fret) = match slide {
350        SlideEffect::ShiftSlideTo(fret) => (0, *fret),
351        SlideEffect::LegatoSlideTo(fret) => (1, *fret),
352        SlideEffect::SlideOutDownward => (2, 0),
353        SlideEffect::SlideOutUpward => (3, 0),
354        SlideEffect::SlideInFromBelow => (4, 0),
355        SlideEffect::SlideInFromAbove => (5, 0),
356    };
357
358    cursor.write_u8(slide_type)?;
359    cursor.write_u8(target_fret)?;
360
361    Ok(())
362}
363
364/// Write automation data (minimal implementation)
365fn write_automation_data(cursor: &mut Cursor<&mut Vec<u8>>, _score: &Score) -> Result<()> {
366    // Write minimal automation data
367    cursor.write_u8(0)?; // No automation points
368    Ok(())
369}
370
371/// Write a Guitar Pro string (length-prefixed)
372fn write_gp_string(cursor: &mut Cursor<&mut Vec<u8>>, s: &str) -> Result<()> {
373    let bytes = s.as_bytes();
374    let length = bytes.len().min(255) as u8;
375
376    cursor.write_u8(length)?;
377    cursor.write_all(&bytes[..length as usize])?;
378
379    Ok(())
380}
381
382/// Helper function to create a basic score for testing
383impl Score {
384    /// Create a simple test score
385    pub fn create_test_score() -> Self {
386        let mut score = Score::new();
387        score.format = FileFormat::GuitarPro;
388
389        // Set metadata
390        score.metadata.title = "Test Song".to_string();
391        score.metadata.artist = "Test Artist".to_string();
392        score.tempo = 120;
393        score.master_volume = 200;
394
395        // Create a basic guitar track
396        let mut track = Track::new_guitar_track("Guitar".to_string());
397        track.channel = 0;
398
399        // Create a simple measure with a few notes
400        let mut measure = Measure::new();
401        measure.time_signature = Some(TimeSignature::new(4, 4));
402
403        // Add some beats with notes
404        let note1 = Note::new(0, 0); // Open E string
405        let beat1 = Beat::new_with_notes(vec![note1], NoteDuration::Quarter);
406        measure.add_beat(beat1);
407
408        let note2 = Note::new(2, 0); // 2nd fret E string
409        let beat2 = Beat::new_with_notes(vec![note2], NoteDuration::Quarter);
410        measure.add_beat(beat2);
411
412        let note3 = Note::new(3, 0); // 3rd fret E string
413        let beat3 = Beat::new_with_notes(vec![note3], NoteDuration::Quarter);
414        measure.add_beat(beat3);
415
416        // Add a rest
417        let rest_beat = Beat::new_rest(NoteDuration::Quarter);
418        measure.add_beat(rest_beat);
419
420        track.add_measure(measure);
421        score.add_track(track);
422
423        score
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use crate::writers::FileWriter;
430
431    use super::*;
432
433    #[test]
434    fn test_write_simple_score() -> Result<()> {
435        let score = Score::create_test_score();
436        let data = GpWriter::write_score(&score)?;
437
438        // Check that we have some data
439        assert!(!data.is_empty());
440
441        // Check magic header
442        assert_eq!(&data[0..4], b"BCFZ");
443
444        Ok(())
445    }
446
447    #[test]
448    fn test_round_trip() -> Result<()> {
449        let original_score = Score::create_test_score();
450
451        // Write to bytes
452        let data = GpWriter::write_score(&original_score)?;
453
454        // Read back (would need the reader implementation)
455        // let restored_score = GuitarProReader::read_score(&data)?;
456
457        // Basic checks on written data
458        assert!(!data.is_empty());
459        assert_eq!(&data[0..4], b"BCFZ");
460
461        Ok(())
462    }
463
464    #[test]
465    fn test_write_empty_score() -> Result<()> {
466        let score = Score::new();
467        let data = GpWriter::write_score(&score)?;
468
469        assert!(!data.is_empty());
470        assert_eq!(&data[0..4], b"BCFZ");
471
472        Ok(())
473    }
474}