diff --git a/.gitignore b/.gitignore index ada8be9..e506f20 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ Cargo.lock **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information -*.pdb \ No newline at end of file +*.pdb + +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..990db47 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```bash +cargo build # Build entire workspace +cargo build -p lib # Build core library only +cargo build -p cli # Build CLI tool +cargo test # Run all tests +cargo test -- --nocapture # Run tests with stdout visible +cargo test test_gp5 # Run tests matching pattern +cargo clippy # Lint +cargo run -p cli -- --input file.gp5 --tab # Run CLI with tablature output +``` + +Tests live in `lib/src/tests.rs` and parse files from the `test/` directory. + +## Architecture + +**Workspace crates:** +- `lib` (library name: `scorelib`) — Core parsing library for Guitar Pro files +- `cli` (binary: `score_tool`) — CLI for file inspection and ASCII tablature +- `web_server` (binary: `score_server`) — Experimental web server + +**Data model hierarchy:** `Song → Track → Measure → Voice → Beat → Note` + +- `Song` holds tracks, measure headers (shared metadata across tracks), MIDI channels, lyrics, and page setup +- Each `Track` has its own `Vec`, while `MeasureHeader` metadata is shared at the song level +- `Voice` (1-2 per measure) contains `Beat`s; each `Beat` contains `Note`s +- Effects are split: `NoteEffect` (bend, slide, harmonic, grace) on notes, `BeatEffects` (stroke, chord, mix table) on beats + +**Module layout (`lib/src/`):** +- `model/` — All data structures (Song, Track, Measure, Beat, Note, effects, enums, chord, etc.) +- `io/` — Binary I/O primitives (`primitive.rs`), GPIF XML structures (`gpif.rs`), ZIP handling (`gpx.rs`) +- `audio/` — MIDI channel definitions and GM instrument names + +**Trait-based parsing API:** Functionality is organized into ~12 traits (e.g., `SongTrackOps`, `SongMeasureOps`, `SongNoteOps`, `SongEffectOps`, `SongChordOps`) implemented on `Song`. Each trait provides `read_*` and `write_*` methods for its domain. All traits are re-exported from `lib.rs`. + +**Binary parsing pattern:** All GP3/4/5 parsing uses a `(data: &[u8], seek: &mut usize)` cursor pattern. Low-level reads go through `io::primitive` functions (`read_byte`, `read_int`, `read_short`, `read_int_byte_size_string`, etc.). + +**Version branching:** `Song.version.number` is a `(u8, u8, u8)` tuple. Format-specific logic branches on this throughout the parsing code (e.g., GP5 has separate author field, different padding bytes). + +**GP7+ (.gp) parsing** uses a different path: ZIP extraction → XML deserialization via `serde`/`quick-xml` into `Gpif` structs → conversion to `Song` model. + +## Format Support + +- GP3, GP4, GP5: Full read support, partial write support +- GP7 (.gp): Initial read support via GPIF XML +- GP6 (.gpx): Not yet implemented +- Error handling currently uses `panic!()` — no `Result` types yet + +## File Structure Documentation + +Binary format specs are in `lib/FILE-STRUCTURE*.md` files — useful when modifying parsing logic. diff --git a/Cargo.toml b/Cargo.toml index 5b2113e..67bbcf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ authors = ["slundi"] [workspace] +resolver = "2" members = ["lib", "cli", "web_server"] diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..fd248df --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,201 @@ +# Guitar Pro Parser Documentation + +## 1. Introduction + +`guitarproparser` is a Rust library (`scorelib`) and CLI tool (`score_tool`) for reading and writing Guitar Pro files (`.gp3`, `.gp4`, `.gp5`). It provides a comprehensive data structure to represent musical partitutres (tablature and notation). + +This documentation is designed to be exhaustive for both human developers and AI coding assistants needing to understand the codebase. + +## 2. Quick Start + +### CLI Tool (`score_tool`) + +A CLI is provided to inspect files and generate ASCII tablatures. + +**Usage:** +```bash +cargo run -p cli -- --input path/to/file.gp5 --tab +``` + +**Options:** +- `--input `: Path to the Guitar Pro file. +- `--tab` (or `-t`): Visualize the first track as ASCII tablature. + +### Library (`scorelib`) + +**Dependency:** +Add the library to your `Cargo.toml`: +```toml +[dependencies] +lib = { path = "path/to/guitarproparser/lib" } # Rename 'lib' to 'scorelib' recommended +``` + +**Basic Usage:** +```rust +use scorelib::gp::Song; +use std::fs; +use fraction::ToPrimitive; // crate 'fraction' is used for durations + +fn main() { + let mut song = Song::default(); + let data = fs::read("clementi.gp5").expect("File not found"); + + // Auto-detect format via extension usually, but here calling specific reader: + song.read_gp5(&data); + + println!("Song: {}", song.name); + + // Iterate tracks + for track in &song.tracks { + println!("Track: {} ({} strings)", track.name, track.strings.len()); + // Iterate measures + for measure in &track.measures { + // ... + } + } +} +``` + +### Traits and Extensions + +The library uses traits to extend `Song` with parsing and writing capabilities. This allows the core `Song` struct to remain clean while providing a large API for different formats and features. + +```rust +use scorelib::model::song::Song; +use scorelib::model::track::SongTrackOps; +// ... other trait imports ... + +// Now song has .read_tracks(), .write_tracks(), etc. +``` + +## 3. Architecture & Data Structures + +The data model follows a hierarchical structure typical of musical scores. + +### Hierarchy +`Song` -> `Track` -> `Measure` -> `Voice` -> `Beat` -> `Note` + +### key Structures + +#### `Song` (`lib/src/model/song.rs`) +The root object representing the entire file. +- **Metadata**: `name`, `artist`, `album`, `author`, `copyright`, `writer`, `transcriber`, `comments`. +- **Global Properties**: `tempo`, `key`, `version` (GP version tuple). +- **Content**: `tracks` (`Vec`), `measure_headers` (`Vec`). +- **Channels**: `channels` (`Vec`) - MIDI instrument configuration. + +#### `Track` (`lib/src/model/track.rs`) +Represents a single instrument (e.g., "Electric Guitar"). +- **Identity**: `name`, `color`, `channel_index`. +- **Instrument**: `strings` (`Vec<(i8, i8)>` - string number & midi tuning), `fret_count`, `capo`. +- **Content**: `measures` (`Vec`). +- **Settings**: `TrackSettings` (tablature/notation visibility, etc.). + +#### `Measure` (`lib/src/model/measure.rs`) +Represents a bar of music for a specific track. +*Note: Global measure info (time signature, key signature, repeat bars) is stored in `Song.measure_headers`.* +- **Structure**: `voices` (`Vec`) - usually contains 1 or 2 voices. +- **Properties**: `clef`, `line_break`. +- **Position**: `start` (accumulated time). + +#### `Voice` (`lib/src/model/beat.rs`) +A rhythmic container within a measure. GP5 supports up to 2 voices (e.g., Lead + Bass in one staff). +- **Content**: `beats` (`Vec`). + +#### `Beat` (`lib/src/model/beat.rs`) +A rhythmic unit containing notes. +- **Rhythm**: `duration` (`Duration` struct), `tuplets`. +- **Content**: `notes` (`Vec`), `text` (lyrics/text above), `effect` (`BeatEffects` - e.g., mix table changes, strokes). +- **Properties**: `status` (Normal, Rest, Empty). + +#### `Note` (`lib/src/model/note.rs`) +A single sound event. +- **Pitch**: `value` (fret number 0-99), `string` (string index 1-N). +- **Dynamics**: `velocity` (MIDI velocity). +- **Effects**: `NoteEffect` (bend, slide, hammer, harmonic, vibrato, grace note, etc.). +- **Type**: `kind` (Normal, Tie, Dead, Rest). + +## 4. Effects System + +Effects are categorized by where they apply: + +- **Note Effects** (`lib/src/model/note.rs` -> `NoteEffect`): + - `bend`: `BendEffect` (points, type). + - `grace`: `GraceEffect` (fret, duration, transition). + - `slides`: `Vec`. + - `harmonic`: `HarmonicEffect` (Natural, Artificial, Tapped, Pinch, Semi). + - `hammer`/`pull_off`, `palm_mute`, `staccato`, `let_ring`, `vibrato`, `trill`, `tremolo_picking`. + +- **Beat Effects** (`lib/src/model/beat.rs` -> `BeatEffects`): + - `stroke`: Up/Down strums. + - `mix_table_change`: Tempo, Volume, Pan, Instrument automation changes. + - `pick_stroke`. + +- **Track Effects**: RSE (Realistic Sound Engine) settings, EQ, Humanize. + +## 5. Low-Level I/O (`lib/src/io.rs`) + +The `io` module provides primitives for reading binary data types used in GP formats: +- `read_byte`, `read_signed_byte`, `read_bool`. +- `read_int` (4 bytes), `read_short` (2 bytes). +- `read_chunk_string`: Pascal-style strings. +- `read_string_byte`: String prefixed by byte length. +- `read_string_int`: String prefixed by int length. + +The parsing is sequential. Functions take a `data: &[u8]` slice and a mutable `seek: &mut usize` cursor. + +## 6. Supported Formats + +| Feature | GP3 (`.gp3`) | GP4 (`.gp4`) | GP5 (`.gp5`) | GP6/GP7 (`.gpx`/`.gp`) | +|---------|--------------|--------------|--------------|-----------------------| +| **Read** | ✅ Full | ✅ Full | ✅ High | ✅ Initial (experimental) | +| **Write** | ⚠️ Partial | ⚠️ Partial | ⚠️ Partial | ❌ Not Implemented | + +**Known Limitations in GP5:** +- Complex "Direction" symbols (Segno, Coda) on advanced files may parsing issues. +- RSE (Realistic Sound Engine) data is largely skipped or partially read. + +## 7. Development Guide + +### Running Tests +The repository contains a suite of integration tests in `lib/src/lib.rs` (module `test`). +```bash +cd lib +cargo test +``` +*Note: `test_gp5_demo_complex` and `test_gp3_writing` are currently ignored due to known limitations.* + +### Error Handling (Current State) +The parser currently uses `panic!` for errors (e.g., file too short, invalid format). +*Future Plan:* Migrate to `Result` for robust error handling. + +### Adding Support for New Version +To add parsing for a new version: +1. Define reader methods in `Song` (e.g., `read_gp6`). +2. Implement version-specific logic in `read_track`, `read_measure`, etc., usually switched by `self.version.number`. + +## 8. For AI Agents (Context) + +When navigating this codebase: +1. **Entry Point**: `lib/src/lib.rs` exports modules. `lib/src/song.rs` contains the `read_gpX` entry methods. +2. **Logic Flow**: `read_gpX` -> `read_version` -> `read_info` -> `read_tracks` -> `read_measures`. +3. **Data Flow**: Measures are stored per track. `song.tracks[t].measures[m]` corresponds to `song.measure_headers[m]`. The header contains timing info (time sig, key sig) shared across all tracks for that measure index. +4. **One-indexing**: GP formats often use 1-based indexing for user-facing values (strings, tracks). Internally, Vectors are 0-indexed. Be careful with loop bounds. + - Note strings: `note.string` is often 1-based in GP data. + - Fret values: 0 is open string, >0 is fret. + +### Common Tasks Patterns +- **Iterating Notes**: + ```rust + for track in &song.tracks { + for measure in &track.measures { + for voice in &measure.voices { + for beat in &voice.beats { + for note in &beat.notes { + // Process note + } + } + } + } + } + ``` diff --git a/README.md b/README.md index 07c4946..e5efcbd 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,47 @@ -# Guitar tools +# Guitar Pro Tools -## Introduction +A comprehensive suite of tools for parsing, manipulating, and visualizing Guitar Pro files in Rust. -In the beginning, this was just a library to read gGuitar Pro files, but I had lot of ideas so I split this sproject: +## Project Structure -- the [library](lib/README.md) to read **Guitar Pro** files (GP3, GP4, GP5, GPX, GP) and **MuseScore** files (MSCZ) -- the [CLI tool](cli/README.md) to perform batch operations -- a [web server](web_server/README.md) to search music scores and tabs using a database through a future documented API +This workspace is divided into several crates: -## About me +- **[lib](lib/README.md)** (`scorelib`): The core library to read and write **Guitar Pro** files (GP3, GP4, GP5, GPX, GP7) and **MuseScore** files (MSCZ). It provides a unified data model for musical scores. +- **[cli](cli/README.md)** (`score_tool`): A command-line interface to inspect files, view metadata, and generate ASCII tablatures. +- **web_server**: (Experimental) A web server to search and browse music scores through an API. -I started to play guitar in 2019 and I have a pack of Guitar Pro files and sometimes I get a new files from [Songsterr](https://www.songsterr.com/). I want to write a better documentation but I don't know all the stuffs that I can see on a score ;) +## Features -In order to make another software (with advanced search, chord detection, ...), I need a library that is able to parse and write Guitar Pro files. +- **Multi-format support**: Read GP3, GP4, GP5, and early support for GP6/7 (.gp, .gpx). +- **Rich Data Model**: Exhaustive representation of tracks, measures, beats, notes, and musical effects. +- **ASCII Visualization**: Generate text-based tablatures directly from the CLI. +- **Extensible Architecture**: Module-based design with traits for easy extension. + +## Usage + +To get started with the CLI, run: + +```bash +cargo run -p cli -- --input path/to/song.gp5 --tab +``` + +## Roadmap + +### Library +- [x] Refactor core into `model`, `io`, and `audio` modules. +- [x] Comprehensive trait-based API for `Song` operations. +- [x] High-fidelity GP5 parsing. +- [x] Initial support for GP6/7 (.gp/.gpx) formats. +- [ ] Improved MuseScore (.mscz) support. +- [ ] Full RSE (Realistic Sound Engine) data parsing. +- [ ] Export to MIDI/Audio. + +### CLI +- [x] Basic metadata inspection. +- [x] ASCII Tablature generation. +- [ ] Batch conversion tool. +- [ ] Advanced search and filtering. + +## License + +This project is licensed under the MIT License. diff --git a/cli/README.md b/cli/README.md index f464283..c778549 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,29 +1,41 @@ -# CLI - -Ideas: - -* [ ] `-l` Load bellow parameters from a file (YAML?, JSON?, other?) -* [ ] `-f` Find in files - * [ ] song information (artist, title, album, ...) with wildcard and regexes - * [ ] instrument - * [ ] `-ft ` [tuning](https://en.wikipedia.org/wiki/List_of_guitar_tunings) (value example: `E-A-D-g-b-e`, `EADgbe`, ` E2–A2–D3–G3–B3–E4`, ) - * [ ] range (string count, piano keys count, drum elements, ...) - * [ ] `-f? ` tempo: is constant, range if variable, min, max, ... (example: `80`, `60-90`, `60>`, `<120` or `0-120`, `60,80,90` or `80,70-85` for a list) - * [ ] `-fb `beats - * [ ] `-fn ` notes (example: `DADDC` anywhere in the track, `|DADDC|` in a mesure, `|A|E|CC|` measures with those notes) - * [ ] `-fr` Repetitions: - * [ ] `-frm <1|2|4>` [same mesures](https://musescore.org/en/handbook/4/measure-and-multi-measure-repeats) - * [ ] `-frs` [repeat signs](https://musescore.org/en/handbook/4/repeat-signs) - * [ ] `-frv` [voltas](https://musescore.org/en/handbook/4/voltas) - * [ ] `-fv 0.8` detect verse with a similarity percentage -* [ ] `-x` Extract: - * [ ] `-xi ` above information in various format (CSV, JSON) - * [ ] `-xt ` tracks - * [ ] `-xl ` lyrics -* [ ] `-c format` Conversion between formats with alerts when information are lost (like GP5 -> GP3) -* [ ] `-r` Replace repetitions - * [ ] `m` [same mesures](https://musescore.org/en/handbook/4/measure-and-multi-measure-repeats) - * [ ] `s` repeat signs and `v` voltas when mesures are the same for all tracks -* [ ] `-t -0.5` Change tuning if possible -* [ ] `-ts x2` Divide/multiply time signatures (I had once an guitar tab that needed to be rewritten by changing the time signature and the beams) -* [ ] `-p` Apply page format parametters (margin, spacing, ...) +# score_tool (CLI) + +`score_tool` is the command-line interface for `scorelib`. It allows you to quickly inspect Guitar Pro files, view their metadata, and visualize them as ASCII tablature. + +## Installation + +From the root project directory: + +```bash +cargo build -p cli +``` + +## Usage + +```bash +# Basic inspection (metadata only) +cargo run -p cli -- --input path/to/file.gp5 + +# Generate ASCII tablature for the first track +cargo run -p cli -- --input path/to/file.gp5 --tab +``` + +## Options + +- `--input ` (or `-i`): **(Required)** Path to the Guitar Pro file (.gp3, .gp4, .gp5, .gp). +- `--tab` (or `-t`): Display the first track as ASCII tablature in the terminal. + +## Current Infrastructure + +The CLI currently supports: +- **Metadata extraction**: Title, Artist, Album, Author, Version, etc. +- **ASCII Rendering**: Responsive text-based tablature alignment. +- **Format Auto-detection**: Based on file extension. + +## Planned Features + +- [ ] Batch processing of directories. +- [ ] Export to JSON/CSV for data analysis. +- [ ] Search for specific patterns (chords, sequences). +- [ ] Transposition and tuning adjustment. +- [ ] Conversion between Guitar Pro versions. diff --git a/cli/src/main.rs b/cli/src/main.rs index c9c202a..f706114 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,57 +1,177 @@ use clap::Parser; -use fraction::ToPrimitive; -use scorelib::gp; -use std::path::Path; -use std::ffi::OsStr; +use scorelib::Song; +use scorelib::Track; use std::fs; use std::io::Read; +use std::path::Path; -const GUITAR_FILE_MAX_SIZE:usize = 16777216; //16 MB, it should be enough +const GUITAR_FILE_MAX_SIZE: usize = 16777216; // 16 MB #[derive(Parser, Debug)] -#[clap(author="slundi", version, about="Perform operation on music files", long_about = None)] +#[clap(author="slundi", version, about="Guitar Pro File Parser CLI", long_about = None)] struct Args { - /// Action - #[clap()] action: String, - - /// Input file - #[clap(short='i', long, help="Input file path")] input: String, + /// Input file path (.gp3, .gp4, .gp5) + #[clap(short, long)] + input: String, + /// Show full tablature for the first track + #[clap(short, long)] + tab: bool, } fn main() { - let args: Args = Args::parse(); - let f = Path::new(&args.input); - //check if path OK, file exists and is file - if !f.exists() || !f.is_file() {panic!("Unable to access file: {}", &args.input);} - //check file format - let ext = f.extension().and_then(OsStr::to_str).unwrap_or_else(||{panic!("Cannont get input file extension");}).to_uppercase(); - let size: usize = fs::metadata(&args.input).unwrap_or_else(|_e|{panic!("Unable to get file size")}).len().to_usize().unwrap(); - if size > GUITAR_FILE_MAX_SIZE {panic!("File is too big (bigger than 16 MB)");} - let f = fs::OpenOptions::new().read(true).open(&args.input).unwrap_or_else(|_error| { - /*if error.kind() == fs::ErrorKind::NotFound {panic!("File {} was not found", &file);} - else if error.kind() == fs::ErrorKind::PermissionDenieds {panic!("File {} is unreadable, check permissions", &file);} - else {panic!("Unknown error while opening {}", &file);}*/ - panic!("Unknown error while opening {}", args.input); - }); - let mut data: Vec = Vec::with_capacity(size); - f.take(u64::from_ne_bytes(size.to_ne_bytes())).read_to_end(&mut data).unwrap_or_else(|_error|{panic!("Unable to read file contents");}); - let mut song: gp::Song = gp::Song::default(); - match ext.as_str() { + let args = Args::parse(); + let path = Path::new(&args.input); + + if !path.exists() { + eprintln!("Error: File '{}' not found.", args.input); + std::process::exit(1); + } + + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_uppercase()) + .unwrap_or_else(|| "UNKNOWN".to_string()); + + let size = fs::metadata(&args.input) + .map(|m| m.len() as usize) + .unwrap_or(0); + + if size > GUITAR_FILE_MAX_SIZE { + eprintln!("Error: File is too large (> 16MB)"); + std::process::exit(1); + } + + let mut file = fs::File::open(&args.input).expect("Cannot open file"); + let mut data = Vec::with_capacity(size); + file.read_to_end(&mut data).expect("Cannot read file"); + + let mut song = Song::default(); + let result = match ext.as_str() { "GP3" => song.read_gp3(&data), "GP4" => song.read_gp4(&data), "GP5" => song.read_gp5(&data), - "GPX" => println!("Guitar pro file (new version) is not supported yet"), //new Guitar Pro files - _ => panic!("Unable to process a {} file (GP1 and GP2 files are not supported)", ext), - } - println!("Artist: \"{}\"", song.artist); - println!("Title: \"{}\"", song.name); - println!("Album: \"{}\"", song.album); - println!("Author: \"{}\"", song.author); - println!("Date: \"{}\"", song.date); - println!("Copyright: \"{}\"", song.copyright); - println!("Writer: \"{}\"", song.writer); - println!("Transcriber: \"{}\"", song.transcriber); - println!("Comments: \"{}\"", song.comments); - let _out = song.write((3,0,0), Some(false)); + "GP" => song.read_gp(&data), + "GPX" => song.read_gpx(&data), + _ => { + eprintln!( + "Error: Unsupported format '{}'. Supported: GP3, GP4, GP5, GP.", + ext + ); + std::process::exit(1); + } + }; + if let Err(e) = result { + eprintln!("Error reading file: {}", e); + std::process::exit(1); + } + + print_metadata(&song); + + if args.tab { + if let Some(track) = song.tracks.first() { + println!("\nGenerating Tablature for Track 1: {}", track.name); + print_ascii_tab(track); + } else { + println!("\nNo tracks found in the song."); + } + } else { + println!("\nTip: Use --tab or -t to see the ASCII tablature."); + } +} + +fn print_metadata(song: &Song) { + println!("=== Metadata ==="); + println!("Title: {}", song.name); + println!("Artist: {}", song.artist); + println!("Album: {}", song.album); + println!("Author: {}", song.author); + println!("Date: {}", song.date); + println!("Copyright: {}", song.copyright); + println!("Transcriber: {}", song.transcriber); + println!("Comments: {}", song.comments); + println!( + "Version: {}.{}.{}", + song.version.number.0, song.version.number.1, song.version.number.2 + ); + println!("Tracks: {}", song.tracks.len()); + println!("Tempos: MixTable items (approx)"); +} + +fn print_ascii_tab(track: &Track) { + let num_strings = track.strings.len(); + if num_strings == 0 { + return; + } + + // Buffer for each string (reversed because string 1 is highest pitch = top line) + // Actually track.strings[0] is usually String 1 (High E). + // Tab lines: 0=High E, 1=B, 2=G ... + let mut lines: Vec = vec![String::new(); num_strings]; + + // Tuning info + let tuning_names = ["E", "B", "G", "D", "A", "E", "B", "F#"]; // Simple approximation + for i in 0..num_strings { + let sc = if i < tuning_names.len() { + tuning_names[i] + } else { + "?" + }; + lines[i].push_str(&format!("{} |", sc)); + } + + // Iterate measures + for measure in track.measures.iter() { + // Start of measure bar + for line in &mut lines { + line.push('|'); + } + + if measure.voices.is_empty() { + // Empty measure pad + for line in &mut lines { + line.push_str("----"); + } + continue; + } + + // We only verify Voice 0 + let voice = &measure.voices[0]; + + for beat in &voice.beats { + // Determine columns needed for this beat (e.g., 3 chars: "12-" or "-") + // Check notes in this beat + let mut col_vals: Vec = vec!["-".to_string(); num_strings]; + + for note in &beat.notes { + // Note string index (1-based usually) + // If note.string is 1, it corresponds to track.strings[0] (High E) -> lines[0] + let s_idx = (note.string - 1) as usize; + if s_idx < num_strings { + col_vals[s_idx] = note.value.to_string(); + } + } + + // Find max width for this column (beat) to align vertical start + let max_width = col_vals.iter().map(|s| s.len()).max().unwrap_or(1); + let cell_width = max_width + 1; // +1 for spacing + + for i in 0..num_strings { + let s = &col_vals[i]; + lines[i].push_str(s); + // Padding + for _ in 0..(cell_width - s.len()) { + lines[i].push('-'); + } + } + } + } + + // Print lines + println!(); + for line in lines { + println!("{}", line); + } + println!(); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index bfd3fef..d8c1408 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,6 +18,9 @@ path = "src/lib.rs" clap = { version = "4", features = ["derive"], optional = true } fraction = "0.13" encoding_rs = "0.8" +zip = "0.6" +quick-xml = { version = "0.31", features = ["serialize"] } +serde = { version = "1.0", features = ["derive"] } [features] build-binary = ["clap"] diff --git a/lib/README.md b/lib/README.md index 4518f19..4f2134d 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1,47 +1,50 @@ -# Guitarpro +# scorelib -A Rust safe library to parse and write guitar pro files. +A safe, modular Rust library to parse and write Guitar Pro files. -[![Tests](https://github.com/slundi/guitarpro/actions/workflows/rust.yml/badge.svg)](https://github.com/slundi/guitarpro/actions/workflows/rust.yml) [![rust-clippy](https://github.com/slundi/guitarpro/actions/workflows/rust-clippy.yml/badge.svg)](https://github.com/slundi/guitarpro/actions/workflows/rust-clippy.yml) +## Usage -It is based on [Perlence/PyGuitarPro](https://github.com/Perlence/PyGuitarPro), [TuxGuitar](http://tuxguitar.com.ar/) and [MuseScore](https://musescore.org) sources. +Add this to your `Cargo.toml`: -## usage +```toml +[dependencies] +scorelib = { path = "../lib" } +``` + +Basic usage: ```rust -use guitarpro; +use scorelib::model::song::Song; +use scorelib::model::track::SongTrackOps; fn main() { - let f = fs::OpenOptions::new().read(true).open("my_awesome_song.gp5").unwrap_or_else(|_error| { - panic!("Unknown error while opening my_awesome_song.gp5"); - }); - let mut data: Vec = Vec::with_capacity(size); - f.take(u64::from_ne_bytes(size.to_ne_bytes())).read_to_end(&mut data).unwrap_or_else(|_error|{panic!("Unable to read file contents");}); - let mut song: guitarpro::Song = gp::Song::default(); - match ext.as_str() { - "GP3" => song.read_gp3(&data), - "GP4" => song.read_gp4(&data), - "GP5" => song.read_gp5(&data), - "GPX" => println!("Guitar pro file (new version) is not supported yet"), //new Guitar Pro files - _ => panic!("Unable to process a {} file (GP1 and GP2 files are not supported)", ext), + let data = std::fs::read("my_awesome_song.gp5").expect("Unable to read file"); + + let mut song = Song::default(); + // Use the trait-based reader (e.g., SongTrackOps is needed for internal track reading) + song.read_gp5(&data); + + println!("Song Title: {}", song.name); + for track in &song.tracks { + println!("Track: {}", track.name); } } ``` +## Features + +- **GP3, GP4, GP5**: High-fidelity reading and writing support. +- **GP6/GP7 (.gp, .gpx)**: Initial experimental reading support. +- **MuseScore (.mscz)**: Basic XML/ZIP parsing. +- **Modular Design**: Separated into `model`, `io` (low-level primitives), and `audio` (MIDI). +- **Extensible**: Uses Rust traits to add format-specific functionality to the core `Song` model. + ## Roadmap -### Library - -* [ ] Documentation -* [x] Read GP3 files -* [x] Read GP4 files -* [ ] Read GP5 files: almost working, Coda and similar directions are not working, use `test/Demo v5.gp5` to test/fix/pull request/... -* [ ] Read GPX files (version 6) -* [ ] Read GPX files (version 7) -* [ ] Read MuseScore files (ZIP + XML) -* [ ] Write GP3 files -* [ ] Write GP4 files -* [ ] Write GP5 files -* [ ] Write GPX files (version 6) -* [ ] Write GPX files (version 7) -* [ ] Write MuseScore files +- [x] Refactor into `model`, `io`, and `audio` modules. +- [x] Convert `impl Song` blocks into specialized traits. +- [x] Improve GP5 parsing (better handling of complex directions). +- [ ] Stabilize GP6/7 support. +- [ ] Support for chords and rhythm details in GP6/7. +- [ ] Write support for newer formats. +- [ ] Comprehensive documentation of the data model. diff --git a/lib/audit_report.txt b/lib/audit_report.txt new file mode 100644 index 0000000..96549d7 --- /dev/null +++ b/lib/audit_report.txt @@ -0,0 +1,249 @@ +001_Funky_Guy.gp5: OK +2 whole bars.tmp: SKIP +Chords.gp3: OK +Chords.gp4: OK +Chords.gp5: OK +Demo v5.gp5: OK +Directions.gp5: OK +Duration.gp3: OK +Effects.gp3: OK +Effects.gp4: OK +Effects.gp5: OK +Harmonics.gp3: OK +Harmonics.gp4: OK +Harmonics.gp5: OK +Key.gp4: OK +Key.gp5: OK +No Wah.gp5: OK +RSE.gp5: OK +Repeat.gp4: OK +Repeat.gp5: OK +Slides.gp4: OK +Slides.gp5: OK +Strokes.gp4: OK +Strokes.gp5: OK +Unknown Chord Extension.gp5: OK +Unknown-m.gp5: OK +Unknown.gp5: OK +Vibrato.gp4: OK +Voices.gp5: OK +Wah-m.gp5: OK +Wah.gp5: OK +accent.gp: OK +accent.gpx: OK +all-percussion.gp: OK +all-percussion.gp5: OK +all-percussion.gpx: OK +arpeggio.gp: OK +arpeggio.gpx: OK +artificial-harmonic.gp: OK +artificial-harmonic.gpx: OK +barre.gp: OK +barre.gpx: OK +basic-bend.gp: OK +basic-bend.gp5: OK +basic-bend.gpx: OK +beams-stems-ledger-lines.gp: OK +beams-stems-ledger-lines.gp5: OK +beams-stems-ledger-lines.gpx: OK +bend.gp: OK +bend.gp3: OK +bend.gp4: OK +bend.gp5: OK +bend.gpx: OK +brush.gp: OK +brush.gp4: OK +brush.gp5: OK +brush.gpx: OK +capo-fret.gp3: OK +capo-fret.gp4: OK +capo-fret.gp5: OK +chord_without_notes.gp5: OK +chordnames_keyboard.gp: OK +chordnames_keyboard.gpx: OK +clefs.gp: OK +clefs.gpx: OK +copyright.gp: OK +copyright.gp3: OK +copyright.gp4: OK +copyright.gp5: OK +copyright.gpx: OK +crescendo-diminuendo.gp: OK +crescendo-diminuendo.gpx: OK +dead-note.gp: OK +dead-note.gpx: OK +directions.gp: OK +directions.gpx: OK +dotted-gliss.gp: OK +dotted-gliss.gp3: OK +dotted-gliss.gpx: OK +dotted-tuplets.gp: OK +dotted-tuplets.gp5: OK +dotted-tuplets.gpx: OK +double-bar.gp: OK +double-bar.gpx: OK +dynamic.gp: OK +dynamic.gp5: OK +dynamic.gpx: OK +fade-in.gp: OK +fade-in.gp4: OK +fade-in.gp5: OK +fade-in.gpx: OK +fermata.gp: OK +fermata.gpx: OK +fingering.gp: OK +fingering.gp4: OK +fingering.gp5: OK +fingering.gpx: OK +free-time.gp: OK +free-time.gpx: OK +fret-diagram.gp: OK +fret-diagram.gp4: OK +fret-diagram.gp5: OK +fret-diagram.gpx: OK +fret-diagram_2instruments.gp: OK +fret-diagram_2instruments.gpx: OK +gamma_ray-heading_for_tomorrow.gp3: OK +ghost-note.gp: OK +ghost-note.gpx: OK +ghost_note.gp3: OK +grace-before-beat.gp: OK +grace-before-beat.gpx: OK +grace-on-beat.gp: OK +grace-on-beat.gpx: OK +grace.gp: OK +grace.gp5: OK +grace.gpx: OK +heavy-accent.gp: OK +heavy-accent.gp5: OK +heavy-accent.gpx: OK +high-pitch.gp: OK +high-pitch.gp3: OK +high-pitch.gpx: OK +iron-maiden-doctor_doctor.gp5: OK +keysig.gp: OK +keysig.gp4: OK +keysig.gp5: OK +keysig.gpx: OK +led-zeppelin-babe_i_m_gonna_leave_you.gp4: OK +legato-slide.gp: OK +legato-slide.gp4: OK +legato-slide.gp5: OK +legato-slide.gpx: OK +let-ring.gp: OK +let-ring.gp4: OK +let-ring.gp5: OK +let-ring.gpx: OK +mordents.gp: OK +mordents.gpx: OK +multivoices.gp: OK +multivoices.gpx: OK +ottava1.gp: OK +ottava1.gpx: OK +ottava2.gp: OK +ottava2.gpx: OK +ottava3.gp: OK +ottava3.gpx: OK +ottava4.gp: OK +ottava4.gpx: OK +ottava5.gp: OK +ottava5.gpx: OK +palm-mute.gp: OK +palm-mute.gp4: OK +palm-mute.gp5: OK +palm-mute.gpx: OK +pick-up-down.gp: OK +pick-up-down.gp4: OK +pick-up-down.gp5: OK +pick-up-down.gpx: OK +rage-against-the-machine_bombtrack-official-2210247.gpx: OK +rasg.gp: OK +rasg.gpx: OK +repeated-bars.gp: OK +repeated-bars.gpx: OK +repeats.gp: OK +repeats.gpx: OK +rest-centered.gp: OK +rest-centered.gp4: OK +rest-centered.gp5: OK +rest-centered.gpx: OK +sforzato.gp: OK +sforzato.gp4: OK +sforzato.gpx: OK +shift-slide.gp: OK +shift-slide.gp4: OK +shift-slide.gp5: OK +shift-slide.gpx: OK +slide-in-above.gp: OK +slide-in-above.gp4: OK +slide-in-above.gp5: OK +slide-in-above.gpx: OK +slide-in-below.gp: OK +slide-in-below.gp4: OK +slide-in-below.gp5: OK +slide-in-below.gpx: OK +slide-out-down.gp: OK +slide-out-down.gp4: OK +slide-out-down.gp5: OK +slide-out-down.gpx: OK +slide-out-up.gp: OK +slide-out-up.gp4: OK +slide-out-up.gp5: OK +slide-out-up.gpx: OK +slur-notes-effect-mask.gp: OK +slur-notes-effect-mask.gp5: OK +slur-notes-effect-mask.gpx: OK +slur.gp: OK +slur.gp4: OK +slur.gpx: OK +slur_hammer_slur.gp: OK +slur_hammer_slur.gpx: OK +slur_over_3_measures.gp: OK +slur_over_3_measures.gpx: OK +slur_slur_hammer.gp: OK +slur_slur_hammer.gpx: OK +slur_voices.gp: OK +slur_voices.gpx: OK +tap-slap-pop.gp: OK +tap-slap-pop.gp5: OK +tempo.gp: OK +tempo.gp3: OK +tempo.gp4: OK +tempo.gp5: OK +tempo.gpx: OK +test.gp: OK +test.gp5: OK +testIrrTuplet.gp: OK +testIrrTuplet.gp4: OK +testIrrTuplet.gpx: OK +text.gp: OK +text.gpx: OK +the-beatles-let_it_be.gp3: OK +timer.gp: OK +timer.gpx: OK +tremolo-bar.gp: OK +tremolos.gp: OK +tremolos.gp5: OK +tremolos.gpx: OK +trill.gp: OK +trill.gp4: OK +trill.gpx: OK +tuplet-with-slur.gp: OK +tuplet-with-slur.gp4: OK +tuplet-with-slur.gpx: OK +tuplets.gpx: OK +tuplets2.gpx: OK +turn.gp: OK +turn.gpx: OK +vibrato.gp: OK +vibrato.gp5: OK +vibrato.gpx: OK +volta.gp: OK +volta.gp3: OK +volta.gp4: OK +volta.gp5: OK +volta.gpx: OK +volume-swell.gp: OK +volume-swell.gpx: OK +wah.gp: OK +wah.gpx: OK \ No newline at end of file diff --git a/lib/src/audio/midi.rs b/lib/src/audio/midi.rs new file mode 100644 index 0000000..8b10836 --- /dev/null +++ b/lib/src/audio/midi.rs @@ -0,0 +1,274 @@ +use fraction::ToPrimitive; + +use crate::{io::primitive::*, model::song::*, error::GpResult}; + +//MIDI channels + +pub const CHANNEL_DEFAULT_NAMES: [&str; 128] = [ + "Piano", + "Bright Piano", + "Electric Grand", + "Honky Tonk Piano", + "Electric Piano 1", + "Electric Piano 2", + "Harpsichord", + "Clavinet", + "Celesta", + "Glockenspiel", + "Music Box", + "Vibraphone", + "Marimba", + "Xylophone", + "Tubular Bell", + "Dulcimer", + "Hammond Organ", + "Perc Organ", + "Rock Organ", + "Church Organ", + "Reed Organ", + "Accordion", + "Harmonica", + "Tango Accordion", + "Nylon Str Guitar", + "Steel String Guitar", + "Jazz Electric Gtr", + "Clean Guitar", + "Muted Guitar", + "Overdrive Guitar", + "Distortion Guitar", + "Guitar Harmonics", + "Acoustic Bass", + "Fingered Bass", + "Picked Bass", + "Fretless Bass", + "Slap Bass 1", + "Slap Bass 2", + "Syn Bass 1", + "Syn Bass 2", + "Violin", + "Viola", + "Cello", + "Contrabass", + "Tremolo Strings", + "Pizzicato Strings", + "Orchestral Harp", + "Timpani", + "Ensemble Strings", + "Slow Strings", + "Synth Strings 1", + "Synth Strings 2", + "Choir Aahs", + "Voice Oohs", + "Syn Choir", + "Orchestra Hit", + "Trumpet", + "Trombone", + "Tuba", + "Muted Trumpet", + "French Horn", + "Brass Ensemble", + "Syn Brass 1", + "Syn Brass 2", + "Soprano Sax", + "Alto Sax", + "Tenor Sax", + "Baritone Sax", + "Oboe", + "English Horn", + "Bassoon", + "Clarinet", + "Piccolo", + "Flute", + "Recorder", + "Pan Flute", + "Bottle Blow", + "Shakuhachi", + "Whistle", + "Ocarina", + "Syn Square Wave", + "Syn Square Wave", + "Syn Calliope", + "Syn Chiff", + "Syn Charang", + "Syn Voice", + "Syn Fifths Saw", + "Syn Brass and Lead", + "Fantasia", + "Warm Pad", + "Polysynth", + "Space Vox", + "Bowed Glass", + "Metal Pad", + "Halo Pad", + "Sweep Pad", + "Ice Rain", + "Soundtrack", + "Crystal", + "Atmosphere", + "Brightness", + "Goblins", + "Echo Drops", + "Sci Fu", + "Sitar", + "Banjo", + "Shamisen", + "Koto", + "Kalimba", + "Bag Pipe", + "Fiddle", + "Shanai", + "Tinkle Bell", + "Agogo", + "Steel Drums", + "Woodblock", + "Taiko Drum", + "Melodic Tom", + "Syn Drum", + "Reverse Cymbal", + "Guitar Fret Noise", + "Breath Noise", + "Seashore", + "Bird", + "Telephone", + "Helicopter", + "Applause", + "Gunshot", +]; + +pub const DEFAULT_PERCUSSION_CHANNEL: u8 = 9; +/// A MIDI channel describes playing data for a track. +#[derive(Debug, Copy, Clone)] +pub struct MidiChannel { + pub channel: u8, + pub effect_channel: u8, + pub instrument: i32, + pub volume: i8, + pub balance: i8, + pub chorus: i8, + pub reverb: i8, + pub phaser: i8, + pub tremolo: i8, + pub bank: u8, +} +impl Default for MidiChannel { + fn default() -> Self { + MidiChannel { + channel: 0, + effect_channel: 1, + instrument: 25, + volume: 104, + balance: 64, + chorus: 0, + reverb: 0, + phaser: 0, + tremolo: 0, + bank: 0, + } + } +} +impl MidiChannel { + pub(crate) fn is_percussion_channel(&self) -> bool { + (self.channel % 16) == DEFAULT_PERCUSSION_CHANNEL + } + pub(crate) fn set_instrument(&mut self, instrument: i32) { + if instrument == -1 && self.is_percussion_channel() { + self.instrument = 0; + } else { + self.instrument = instrument; + } + } + + pub(crate) fn _get_instrument(self) -> i32 { + self.instrument + } +} + +pub trait SongMidiOps { + fn read_midi_channels(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()>; + fn read_midi_channel(&self, data: &[u8], seek: &mut usize, channel: u8) -> GpResult; + fn read_channel(&mut self, data: &[u8], seek: &mut usize) -> GpResult; + fn write_midi_channels(&self, data: &mut Vec); +} + +impl SongMidiOps for Song { + /// Read all the MIDI channels + fn read_midi_channels(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()> { + for i in 0u8..64u8 { + self.channels.push(self.read_midi_channel(data, seek, i)?); + } + Ok(()) + } + /// Read MIDI channels. Guitar Pro format provides 64 channels (4 MIDI ports by 16 hannels), the channels are stored in this order: + ///`port1/channel1`, `port1/channel2`, ..., `port1/channel16`, `port2/channel1`, ..., `port4/channel16`. + /// + /// Each channel has the following form: + /// + /// * **Instrument**: `int` + /// * **Volume**: `byte` + /// * **Balance**: `byte` + /// * **Chorus**: `byte` + /// * **Reverb**: `byte` + /// * **Phaser**: `byte` + /// * **Tremolo**: `byte` + /// * **blank1**: `byte` => Backward compatibility with version 3.0 + /// * **blank2**: `byte` => Backward compatibility with version 3.0 + fn read_midi_channel(&self, data: &[u8], seek: &mut usize, channel: u8) -> GpResult { + let instrument = read_int(data, seek)?; + let mut c = MidiChannel { + channel, + effect_channel: channel, + ..Default::default() + }; + c.volume = read_signed_byte(data, seek)?; + c.balance = read_signed_byte(data, seek)?; + c.chorus = read_signed_byte(data, seek)?; + c.reverb = read_signed_byte(data, seek)?; + c.phaser = read_signed_byte(data, seek)?; + c.tremolo = read_signed_byte(data, seek)?; + c.set_instrument(instrument); + //println!("Channel: {}\t Volume: {}\tBalance: {}\tInstrument={}, {}, {}", c.channel, c.volume, c.balance, instrument, c.get_instrument(), c.get_instrument_name()); + *seek += 2; //Backward compatibility with version 3.0 + Ok(c) + } + + /// Read MIDI channel. MIDI channel in Guitar Pro is represented by two integers. First is zero-based number of channel, second is zero-based number of channel used for effects. + fn read_channel(&mut self, data: &[u8], seek: &mut usize) -> GpResult { + //TODO: fixme for writing + let index = read_int(data, seek)? - 1; + let effect_channel = read_int(data, seek)? - 1; + if 0 <= index && index < self.channels.len().to_i32().unwrap() { + if self.channels[index.to_usize().unwrap()].instrument < 0 { + self.channels[index.to_usize().unwrap()].instrument = 0; + } + if !self.channels[index.to_usize().unwrap()].is_percussion_channel() { + self.channels[index.to_usize().unwrap()].effect_channel = + effect_channel.to_u8().unwrap(); + } + } + Ok(index.to_usize().unwrap()) + } + + fn write_midi_channels(&self, data: &mut Vec) { + for i in 0..self.channels.len() { + println!("writing channel: {:?}", self.channels[i]); + if self.channels[i].is_percussion_channel() && self.channels[i].instrument == 0 { + write_i32(data, -1); + } else { + write_i32(data, self.channels[i].instrument); + } + write_signed_byte(data, Self::from_channel_short(self.channels[i].volume)); + write_signed_byte(data, Self::from_channel_short(self.channels[i].balance)); + write_signed_byte(data, Self::from_channel_short(self.channels[i].chorus)); + write_signed_byte(data, Self::from_channel_short(self.channels[i].reverb)); + write_signed_byte(data, Self::from_channel_short(self.channels[i].phaser)); + write_signed_byte(data, Self::from_channel_short(self.channels[i].tremolo)); + write_placeholder_default(data, 2); //Backward compatibility with version 3.0 + } + } +} + +impl Song { + fn from_channel_short(data: i8) -> i8 { + ((data >> 3) - 1).clamp(-128, 127) + 1 + } +} diff --git a/lib/src/audio/mod.rs b/lib/src/audio/mod.rs new file mode 100644 index 0000000..89df7e4 --- /dev/null +++ b/lib/src/audio/mod.rs @@ -0,0 +1,2 @@ +pub mod midi; + diff --git a/lib/src/error.rs b/lib/src/error.rs new file mode 100644 index 0000000..be62445 --- /dev/null +++ b/lib/src/error.rs @@ -0,0 +1,62 @@ +use std::fmt; + +/// Error type for Guitar Pro file parsing +#[derive(Debug)] +pub enum GpError { + /// Reached end of binary data unexpectedly + UnexpectedEof { offset: usize, needed: usize }, + /// Invalid enum/flag value encountered during parsing + InvalidValue { context: &'static str, value: i64 }, + /// String decoding failure + StringDecode { offset: usize }, + /// ZIP, XML, or format-level errors from GP6/GP7 parsing + FormatError(String), + /// IO errors + Io(std::io::Error), +} + +/// Convenience type alias +pub type GpResult = Result; + +impl fmt::Display for GpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GpError::UnexpectedEof { offset, needed } => { + write!(f, "Unexpected end of file at offset {}, needed {} more bytes", offset, needed) + } + GpError::InvalidValue { context, value } => { + write!(f, "Invalid value {} for {}", value, context) + } + GpError::StringDecode { offset } => { + write!(f, "Unable to decode string at offset {}", offset) + } + GpError::FormatError(msg) => { + write!(f, "Format error: {}", msg) + } + GpError::Io(err) => { + write!(f, "IO error: {}", err) + } + } + } +} + +impl std::error::Error for GpError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + GpError::Io(err) => Some(err), + _ => None, + } + } +} + +impl From for GpError { + fn from(err: std::io::Error) -> Self { + GpError::Io(err) + } +} + +impl From for GpError { + fn from(msg: String) -> Self { + GpError::FormatError(msg) + } +} diff --git a/lib/src/headers.rs b/lib/src/headers.rs deleted file mode 100644 index 9e0d415..0000000 --- a/lib/src/headers.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::collections::HashMap; - -use fraction::ToPrimitive; - -use crate::{io::*, gp::*, key_signature::*, enums::*}; - -#[derive(Debug,Clone,PartialEq,Eq)] -pub struct Version { - pub data: String, - pub number: (u8, u8, u8), - pub clipboard: bool -} - -#[derive(Debug,Clone,PartialEq,Eq)] -pub struct Clipboard { - pub start_measure: i32, - pub stop_measure: i32, - pub start_track: i32, - pub stop_track: i32, - pub start_beat: i32, - pub stop_beat: i32, - pub sub_bar_copy: bool -} -impl Default for Clipboard { - fn default() -> Self { Clipboard {start_measure: 1, stop_measure: 1, start_track: 1, stop_track: 1, start_beat: 1, stop_beat: 1, sub_bar_copy: false} } -} - -#[derive(Debug,Clone)] -pub struct MeasureHeader { - pub number: u16, - pub start: i64, - pub time_signature: TimeSignature, - pub tempo: i32, - pub marker: Option, - pub repeat_open: bool, - pub repeat_alternative: u8, - pub repeat_close: i8, - pub triplet_feel: TripletFeel, - pub direction: Option, - /// Tonality of the measure - pub key_signature: KeySignature, - pub double_bar: bool, -} -impl Default for MeasureHeader { - fn default() -> Self { MeasureHeader { - number: 1, - start: DURATION_QUARTER_TIME, - tempo: 0, - repeat_open: false, - repeat_alternative: 0, - repeat_close: -1, - triplet_feel: TripletFeel::None, - direction: None, - key_signature: KeySignature::default(), - double_bar: false, - marker: None, - time_signature: TimeSignature {numerator: 4, denominator: Duration::default(), beams: vec![2, 2, 2, 2]}, - }} -} -impl MeasureHeader { - pub(crate) fn length(&self) -> i64 {self.time_signature.numerator.to_i64().unwrap() * self.time_signature.denominator.time().to_i64().unwrap()} - pub(crate) fn _end(&self) -> i64 {self.start + self.length()} -} - -/// A marker annotation for beats. -#[derive(Debug,Clone)] -pub struct Marker { - pub title: String, - pub color: i32, -} -impl Default for Marker {fn default() -> Self { Marker {title: "Section".to_owned(), color: 0xff0000}}} -/// Read a marker. The markers are written in two steps: -/// - first is written an integer equal to the marker's name length + 1 -/// - then a string containing the marker's name. Finally the marker's color is written. -fn read_marker(data: &[u8], seek: &mut usize) -> Marker { - let mut marker = Marker{title: read_int_size_string(data, seek), ..Default::default()}; - marker.color = read_color(data, seek); - marker -} - -/// This class can store the information about a group of measures which are repeated. -#[derive(Debug,Clone,Default)] -pub struct RepeatGroup { - /// List of measure header indexes. - pub measure_headers: Vec, - pub closings: Vec, - pub openings: Vec, - pub is_closed: bool, -} -//impl Default for RepeatGroup {fn default() -> Self { RepeatGroup {measure_headers: Vec::new(), closings: Vec::new(), openings: Vec::new(), is_closed: false, }}} -/*impl RepeatGroup { - pub(crate) fn add_measure_header(&mut self, measure_header: &MeasureHeader) { - let index = measure_header.number.to_usize().unwrap(); - if self.openings.is_empty() {self.openings.push(index);} //if not len(self.openings): self.openings.append(h) - self.measure_headers.push(index); - if measure_header.repeat_close > 0 { - self.closings.push(index); - self.is_closed = true; - } else { //A new item after the header was closed? -> repeat alternative, reopens the group - self.is_closed = false; - self.openings.push(index); - } - } -}*/ - -impl Song { - fn _add_measure_header(&mut self, header: MeasureHeader) { - // if the group is closed only the next upcoming header can reopen the group in case of a repeat alternative, so we remove the current group - //TODO: if header.repeat_open or self.current_repeat_group.is_closed && header.repeat_alternative <= 0 {self.current_repeat_group = RepeatGroup::default();} - self.measure_headers.push(header); - } - - pub(crate) fn read_clipboard(&mut self, data: &[u8], seek: &mut usize) -> Option { - if !self.version.clipboard {return None;} - let mut c = Clipboard{start_measure: read_int(data, seek), ..Default::default()}; - c.stop_measure = read_int(data, seek); - c.start_track = read_int(data, seek); - c.stop_track = read_int(data, seek); - if self.version.number.0 == 5 { - c.start_beat = read_int(data, seek); - c.stop_beat = read_int(data, seek); - c.sub_bar_copy = read_int(data, seek) != 0; - } - println!("read_clipboard(): {:?}", c); - Some(c) - } - - /// Read measure headers. The *measures* are written one after another, their number have been specified previously. - /// * `measure_count`: number of measures to expect. - pub(crate) fn read_measure_headers(&mut self, data: &[u8], seek: &mut usize, measure_count: usize) { - //println!("read_measure_headers()"); - let mut previous: Option = None; - for i in 1..measure_count + 1 { - let r: (MeasureHeader, u8) = self.read_measure_header(data, seek, i, previous); - previous = Some(r.0.clone()); - self.measure_headers.push(r.0); //TODO: use add_measure_header - } - } - - pub(crate) fn read_measure_headers_v5(&mut self, data: &[u8], seek: &mut usize, measure_count: usize, directions: &(HashMap, HashMap)) { - //println!("read_measure_headers_v5()"); - let mut previous: Option = None; - for i in 1..measure_count + 1 { - let r: (MeasureHeader, u8) = self.read_measure_header_v5(data, seek, i, previous); - previous = Some(r.0.clone()); - self.measure_headers.push(r.0); //TODO: use add_measure_header - } - for s in &directions.0 { if s.1 > &-1 {self.measure_headers[s.1.to_usize().unwrap() - 1].direction = Some(s.0.clone());} } - for s in &directions.1 { if s.1 > &-1 {self.measure_headers[s.1.to_usize().unwrap() - 1].direction = Some(s.0.clone());} } - } - - /// Read measure header. The first byte is the measure's flags. It lists the data given in the current measure. - /// - /// | **Bit 7** | **Bit 6** | **Bit 5** | **Bit 4** | **Bit 3** | **Bit 2** | **Bit 1** | **Bit 0** | - /// |-----------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| - /// | Presence of a double bar | Tonality of the measure | Presence of a marker | Number of alternate ending | End of repeat | Beginning of repeat | Denominator of the (key) signature | Numerator of the (key) signature | - /// - /// Each of these elements is present only if the corresponding bit is a 1. The different elements are written (if they are present) from lowest to highest bit. - /// Exceptions are made for the double bar and the beginning of repeat whose sole presence is enough, complementary data is not necessary. - - /// * **Numerator of the (key) signature**: `byte`. Numerator of the (key) signature of the piece - /// * **Denominator of the (key) signature**: `byte`. Denominator of the (key) signature of the piece - /// * **End of repeat**: `byte`. Number of repeats until the previous Beginning of repeat. Nombre de renvoi jusqu'au début de renvoi précédent. - /// * **Number of alternate ending**: `byte`. The number of alternate ending. - /// * **Marker**: The markers are written in two steps: - /// 1) First is written an `integer` equal to the marker's name length + 1 - /// 2) a string containing the marker's name. Finally the marker's color is written. - /// * **Tonality of the measure**: `byte`. This value encodes a key (signature) change on the current piece. It is encoded as: `0: C`, `1: G (#)`, `2: D (##)`, `-1: F (b)`, ... - pub(crate) fn read_measure_header(&mut self, data: &[u8], seek: &mut usize, number: usize, previous: Option) -> (MeasureHeader, u8) { - let flag = read_byte(data, seek); - //println!("read_measure_header(), flags: {} \t N: {} \t Measure header count: {}", flag, number, self.measure_headers.len()); - let mut mh = MeasureHeader{number: number.to_u16().unwrap(), ..Default::default()}; - mh.start = 0; - mh.triplet_feel = self.triplet_feel.clone(); //TODO: use ref & lifetime - //we need a previous header for the next 2 flags - //Numerator of the (key) signature - if (flag & 0x01 )== 0x01 {mh.time_signature.numerator = read_signed_byte(data, seek);} - else if number > 1 {mh.time_signature.numerator = previous.clone().unwrap().time_signature.numerator;} - //Denominator of the (key) signature - if (flag & 0x02) == 0x02 {mh.time_signature.denominator.value = read_signed_byte(data, seek).to_u16().unwrap();} - else if number > 1 {mh.time_signature.denominator = previous.clone().unwrap().time_signature.denominator;} - - mh.repeat_open = (flag & 0x04) == 0x04; //Beginning of repeat - if (flag & 0x08) == 0x08 {mh.repeat_close = read_signed_byte(data, seek);} //End of repeat - if (flag & 0x10) == 0x10 {mh.repeat_alternative = if self.version.number.0 == 5 {self.read_repeat_alternative_v5(data, seek)} else {self.read_repeat_alternative(data, seek)};} //Number of alternate ending - if (flag & 0x20) == 0x20 {mh.marker = Some(read_marker(data, seek));} //Presence of a marker - if (flag & 0x40) == 0x40 { //Tonality of the measure - mh.key_signature.key = read_signed_byte(data, seek); - mh.key_signature.is_minor = read_signed_byte(data, seek) != 0; - } else if mh.number > 1 {mh.key_signature = previous.unwrap().key_signature;} - mh.double_bar = (flag & 0x80) == 0x80; //presence of a double bar - (mh, flag) - } - /// Read measure header. Measure header format in Guitar Pro 5 differs from one if Guitar Pro 3. - /// - /// First, there is a blank byte if measure is not first. Then measure header is read as in GP3's `read_measure_header_v3()`. Then measure header is read as follows: - /// - Time signature beams: 4 `Bytes `. Appears If time signature was set, i.e. flags *0x01* and *0x02* are both set. - /// - Blank `byte` if flag at *0x10* is set. - /// - Triplet feel: `byte`. See `TripletFeel`. - pub(crate) fn read_measure_header_v5(&mut self, data: &[u8], seek: &mut usize, number: usize, previous: Option) -> (MeasureHeader,u8) { - if previous.is_some() { *seek += 1; } //always - let r = self.read_measure_header(data, seek, number, previous.clone()); - let mut mh = r.0; - let flags = r.1; - //println!("read_measure_header_v5(), flags: {}", flags); - if mh.repeat_close > -1 {mh.repeat_close -= 1;} - if (flags & 0x03) == 0x03 { - for i in 0..4 {mh.time_signature.beams[i] = read_byte(data, seek);} - } else {mh.time_signature.beams = previous.unwrap().time_signature.beams;}; - if (flags & 0x10) == 0 { *seek += 1; } //always 0 - mh.triplet_feel = get_triplet_feel(read_byte(data, seek).to_i8().unwrap()); - //println!("################################### {:?}", mh.triplet_feel); - (mh, flags) - } - - fn read_repeat_alternative(&mut self, data: &[u8], seek: &mut usize) -> u8 { - //println!("read_repeat_alternative()"); - let value = read_byte(data, seek).to_u16().unwrap(); - let mut existing_alternative = 0u16; - for i in (0..self.measure_headers.len()).rev() { - if self.measure_headers[i].repeat_open {break;} - existing_alternative |= self.measure_headers[i].repeat_alternative.to_u16().unwrap(); - } - //println!("read_repeat_alternative(), value: {}, existing_alternative: {}", value, existing_alternative); - //println!("read_repeat_alternative(), return: {}", ((1 << value) - 1) ^ existing_alternative); - (((1 << value) - 1) ^ existing_alternative).to_u8().unwrap() - } - fn read_repeat_alternative_v5(&mut self, data: &[u8], seek: &mut usize) -> u8 {read_byte(data, seek)} - - /// Read directions. Directions is a list of 19 `ShortInts ` each pointing at the number of measure. - /// - /// Directions are read in the following order: - /// - Coda - /// - Double Coda - /// - Segno - /// - Segno Segno - /// - Fine - /// - Da Capo - /// - Da Capo al Coda - /// - Da Capo al Double Coda - /// - Da Capo al Fine - /// - Da Segno - /// - Da Segno al Coda - /// - Da Segno al Double Coda - /// - Da Segno al Fine - /// - Da Segno Segno - /// - Da Segno Segno al Coda - /// - Da Segno Segno al Double Coda - /// - Da Segno Segno al Fine - /// - Da Coda - /// - Da Double Coda - pub(crate) fn read_directions(&self, data: &[u8], seek: &mut usize) -> (HashMap, HashMap) { - let mut signs: HashMap = HashMap::with_capacity(4); - let mut from_signs: HashMap = HashMap::with_capacity(15); - //signs - signs.insert(DirectionSign::Coda, read_short(data, seek)); - signs.insert(DirectionSign::DoubleCoda, read_short(data, seek)); - signs.insert(DirectionSign::Segno, read_short(data, seek)); - signs.insert(DirectionSign::SegnoSegno, read_short(data, seek)); - signs.insert(DirectionSign::Fine, read_short(data, seek)); - //from signs - from_signs.insert(DirectionSign::DaCapo, read_short(data, seek)); - from_signs.insert(DirectionSign::DaCapoAlCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaCapoAlDoubleCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaCapoAlFine, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegno, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoAlCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoAlDoubleCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoAlFine, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoSegno, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoSegnoAlCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoSegnoAlDoubleCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaSegnoSegnoAlFine, read_short(data, seek)); - from_signs.insert(DirectionSign::DaCoda, read_short(data, seek)); - from_signs.insert(DirectionSign::DaDoubleCoda, read_short(data, seek)); - (signs, from_signs) - } - - pub(crate) fn write_measure_headers(&self, data: &mut Vec, version: &(u8,u8,u8)) { - let mut previous: Option = None; - for i in 0..self.measure_headers.len() { - //self.current_measure_number = Some(self.tracks[0].measures[i].number); - self.write_measure_header(data, i, previous, version); - previous = Some(i); - } - } - - fn write_measure_header(&self, data: &mut Vec, header: usize, previous: Option, version: &(u8,u8,u8)) { - //pack measure header flags - let mut flags: u8 = 0x00; - if let Some(p) = previous { - if self.measure_headers[header].time_signature.numerator != self.measure_headers[p].time_signature.numerator {flags |= 0x01;} - if self.measure_headers[header].time_signature.denominator.value != self.measure_headers[p].time_signature.denominator.value {flags |= 0x02;} - } else { - flags |= 0x01; - flags |= 0x02; - if self.measure_headers[header].repeat_open {flags |= 0x04;} - if self.measure_headers[header].repeat_close > -1 {flags |= 0x08;} - if self.measure_headers[header].repeat_alternative > 0 {flags |= 0x10;} - if self.measure_headers[header].marker.is_some() {flags |= 0x20;} - } - if version.0 >= 4 { - if previous.is_none() {flags |= 0x40;} - else if let Some(p) = previous {if self.measure_headers[header].key_signature == self.measure_headers[p].key_signature {flags |= 0x40;}} - if self.measure_headers[header].double_bar {flags |= 0x80;} - } - if version.0 >= 5 { - if let Some(p) = previous { - if self.measure_headers[header].time_signature != self.measure_headers[p].time_signature {flags |= 0x03;} - write_placeholder_default(data, 1); - } - } - //end pack - //write measure header values - write_byte(data, flags); - if (flags & 0x01) == 0x01 {write_signed_byte(data, self.measure_headers[header].time_signature.numerator);} - if (flags & 0x02) == 0x02 {write_signed_byte(data, self.measure_headers[header].time_signature.denominator.value.to_i8().unwrap());} - if (flags & 0x08) == 0x08 {write_signed_byte(data, if version.0 < 5 {self.measure_headers[header].repeat_close} else {self.measure_headers[header].repeat_close + 1});} - if (flags & 0x10) == 0x10 { //write repeat alternative - if version.0 ==5 {write_byte(data, self.measure_headers[header].repeat_alternative);} - else { - let mut first_one = false; - let mut ra:u8 = 0; - for i in 0u8..9-self.measure_headers[header].repeat_alternative.leading_zeros().to_u8().unwrap() { - ra = i; - if (self.measure_headers[header].repeat_alternative & 1 << i) > 0 {first_one = true;} - else if first_one {break;} - } - write_byte(data, ra); - } - } - if (flags & 0x20) == 0x20 { //write marker - if let Some(marker) = &self.measure_headers[header].marker { - write_int_byte_size_string(data, &marker.title); - write_color(data, marker.color); - } - } - if version.0 >= 4 { - write_signed_byte(data, self.measure_headers[header].key_signature.key); - write_signed_byte(data, i8::from(self.measure_headers[header].key_signature.is_minor)); - } - if version.0 >= 5 { - if (flags & 0x03) == 0x03 { - for i in 0..self.measure_headers[header].time_signature.beams.len() {write_byte(data, self.measure_headers[header].time_signature.beams[i]);} - } - if (flags & 0x10) == 0x10 {write_placeholder_default(data, 1);} - write_byte(data, from_triplet_feel(&self.measure_headers[header].triplet_feel)); - } - } - - pub(crate) fn write_clipboard(&self, data: &mut Vec, version: &(u8,u8,u8)) { - if let Some(c) = &self.clipboard { - write_i32(data, c.start_measure.to_i32().unwrap()); - write_i32(data, c.stop_measure.to_i32().unwrap()); - write_i32(data, c.start_track.to_i32().unwrap()); - write_i32(data, c.stop_track.to_i32().unwrap()); - if version.0 == 5 { - write_i32(data, c.start_beat.to_i32().unwrap()); - write_i32(data, c.stop_beat.to_i32().unwrap()); - write_i32(data, i32::from(c.sub_bar_copy)); - } - } - } - pub(crate) fn write_directions(&self, data: &mut Vec) { - let mut map: HashMap= HashMap::with_capacity(19); - for i in 1..self.measure_headers.len() { - if let Some(d) = &self.measure_headers[i].direction { map.insert(d.clone(), i.to_i16().unwrap()); } - } - let order: Vec = vec![DirectionSign::Coda, DirectionSign::DoubleCoda, DirectionSign::Segno, DirectionSign::SegnoSegno, DirectionSign::Fine, - DirectionSign::DaCapo, - DirectionSign::DaCapoAlCoda, - DirectionSign::DaCapoAlDoubleCoda, - DirectionSign::DaCapoAlFine, - DirectionSign::DaSegno, - DirectionSign::DaSegnoAlCoda, - DirectionSign::DaSegnoAlDoubleCoda, - DirectionSign::DaSegnoAlFine, - DirectionSign::DaSegnoSegno, - DirectionSign::DaSegnoSegnoAlCoda, - DirectionSign::DaSegnoSegnoAlDoubleCoda, - DirectionSign::DaSegnoSegnoAlFine, - DirectionSign::DaCoda, - DirectionSign::DaDoubleCoda]; - for d in order { - let x = map.get(&d); - if let Some(dir) = x {write_i16(data, *dir);} - else {write_i16(data, -1);} - } - } -} diff --git a/lib/src/io/gpif.rs b/lib/src/io/gpif.rs new file mode 100644 index 0000000..21b7dbf --- /dev/null +++ b/lib/src/io/gpif.rs @@ -0,0 +1,477 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Gpif { + /// GP7 uses "GPVersion", GP6 uses "GPRevision" + #[serde(rename = "GPVersion", default)] + pub version: Option, + #[serde(rename = "GPRevision", default)] + pub revision: Option, + #[serde(rename = "Score")] + pub score: Score, + #[serde(rename = "MasterTrack")] + pub master_track: MasterTrack, + #[serde(rename = "Tracks")] + pub tracks: TracksWrapper, + #[serde(rename = "MasterBars")] + pub master_bars: MasterBarsWrapper, + #[serde(rename = "Bars")] + pub bars: BarsWrapper, + #[serde(rename = "Voices")] + pub voices: VoicesWrapper, + #[serde(rename = "Beats")] + pub beats: BeatsWrapper, + #[serde(rename = "Notes")] + pub notes: NotesWrapper, + #[serde(rename = "Rhythms")] + pub rhythms: RhythmsWrapper, +} + +// --------------------------------------------------------------------------- +// Score metadata +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Score { + #[serde(default)] + pub title: String, + #[serde(default)] + pub sub_title: String, + #[serde(default)] + pub artist: String, + #[serde(default)] + pub album: String, + #[serde(rename = "Words", default)] + pub words: String, + #[serde(rename = "Music", default)] + pub music: String, + #[serde(default)] + pub copyright: String, + #[serde(default)] + pub tabber: String, + #[serde(default)] + pub instructions: String, + #[serde(default)] + pub notices: String, +} + +// --------------------------------------------------------------------------- +// MasterTrack (tempo automations) +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct MasterTrack { + #[serde(rename = "Tracks", default)] + pub tracks_count: String, + #[serde(rename = "Automations", default)] + pub automations: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AutomationsWrapper { + #[serde(rename = "Automation", default)] + pub automations: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Automation { + #[serde(rename = "Type", default)] + pub automation_type: String, + #[serde(rename = "Value", default)] + pub value: String, + #[serde(rename = "Bar", default)] + pub bar: i32, + #[serde(rename = "Position", default)] + pub position: i32, +} + +// --------------------------------------------------------------------------- +// Tracks +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct TracksWrapper { + #[serde(rename = "Track", default)] + pub tracks: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Track { + #[serde(rename = "@id", default)] + pub id: i32, + #[serde(rename = "Name", default)] + pub name: String, + #[serde(rename = "ShortName", default)] + pub short_name: String, + #[serde(rename = "Color", default)] + pub color: Option, + /// GP6: track-level properties (Tuning, DiagramCollection, etc.) + #[serde(rename = "Properties", default)] + pub properties: Option, + /// GP7: staves with per-staff properties + #[serde(rename = "Staves", default)] + pub staves: Option, + #[serde(rename = "GeneralMidi", default)] + pub general_midi: Option, + #[serde(rename = "Transpose", default)] + pub transpose: Option, +} + +#[derive(Debug, Deserialize)] +pub struct TrackPropertiesWrapper { + #[serde(rename = "Property", default)] + pub properties: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct StavesWrapper { + #[serde(rename = "Staff", default)] + pub staves: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Staff { + #[serde(rename = "Properties", default)] + pub properties: Option, +} + +#[derive(Debug, Deserialize)] +pub struct StaffPropertiesWrapper { + #[serde(rename = "Property", default)] + pub properties: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct GeneralMidi { + #[serde(rename = "Program", default)] + pub program: Option, + #[serde(rename = "Port", default)] + pub port: Option, + #[serde(rename = "PrimaryChannel", default)] + pub primary_channel: Option, + #[serde(rename = "SecondaryChannel", default)] + pub secondary_channel: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Transpose { + #[serde(rename = "Chromatic", default)] + pub chromatic: Option, + #[serde(rename = "Octave", default)] + pub octave: Option, +} + +// --------------------------------------------------------------------------- +// MasterBars (measure headers) +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct MasterBarsWrapper { + #[serde(rename = "MasterBar", default)] + pub master_bars: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct MasterBar { + #[serde(rename = "Key", default)] + pub key: Option, + #[serde(rename = "Time", default)] + pub time: String, + #[serde(rename = "Bars", default)] + pub bars: String, + #[serde(rename = "Repeat", default)] + pub repeat: Option, + #[serde(rename = "AlternateEndings", default)] + pub alternate_endings: Option, + #[serde(rename = "DoubleBar", default)] + pub double_bar: Option, + #[serde(rename = "Section", default)] + pub section: Option
, + #[serde(rename = "Fermatas", default)] + pub fermatas: Option, + #[serde(rename = "FreeTime", default)] + pub free_time: Option, + #[serde(rename = "Directions", default)] + pub directions: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Key { + #[serde(rename = "AccidentalCount", default)] + pub accidental_count: i32, + #[serde(rename = "Mode", default)] + pub mode: String, +} + +impl Default for Key { + fn default() -> Self { + Key { accidental_count: 0, mode: "Major".to_string() } + } +} + +#[derive(Debug, Deserialize)] +pub struct Repeat { + #[serde(rename = "@start", default)] + pub start: String, + #[serde(rename = "@end", default)] + pub end: String, + #[serde(rename = "@count", default)] + pub count: i32, +} + +#[derive(Debug, Deserialize)] +pub struct Section { + #[serde(rename = "Letter", default)] + pub letter: Option, + #[serde(rename = "Text", default)] + pub text: Option, +} + +#[derive(Debug, Deserialize)] +pub struct DirectionsWrapper { + #[serde(rename = "Target", default)] + pub target: Option, + #[serde(rename = "Jump", default)] + pub jump: Option, +} + +#[derive(Debug, Deserialize)] +pub struct FermatasWrapper { + #[serde(rename = "Fermata", default)] + pub fermatas: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Fermata { + #[serde(rename = "Type", default)] + pub fermata_type: Option, + #[serde(rename = "Offset", default)] + pub offset: Option, +} + +// --------------------------------------------------------------------------- +// Bars (per-track measures) +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct BarsWrapper { + #[serde(rename = "Bar", default)] + pub bars: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Bar { + #[serde(rename = "@id", default)] + pub id: i32, + #[serde(rename = "Voices", default)] + pub voices: String, + #[serde(rename = "Clef", default)] + pub clef: Option, + #[serde(rename = "SimileMark", default)] + pub simile_mark: Option, +} + +// --------------------------------------------------------------------------- +// Voices +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct VoicesWrapper { + #[serde(rename = "Voice", default)] + pub voices: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Voice { + #[serde(rename = "@id", default)] + pub id: i32, + #[serde(rename = "Beats", default)] + pub beats: String, +} + +// --------------------------------------------------------------------------- +// Beats +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct BeatsWrapper { + #[serde(rename = "Beat", default)] + pub beats: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Beat { + #[serde(rename = "@id", default)] + pub id: i32, + #[serde(rename = "Notes", default)] + pub notes: Option, + #[serde(rename = "Rhythm", default)] + pub rhythm: Option, + #[serde(rename = "Dynamic", default)] + pub dynamic: Option, + #[serde(rename = "GraceNotes", default)] + pub grace_notes: Option, + /// Note: "Fadding" is a typo in the upstream GP6 XML format (should be "Fading"). + #[serde(rename = "Fadding", default)] + pub fadding: Option, + #[serde(rename = "Tremolo", default)] + pub tremolo: Option, + #[serde(rename = "Wah", default)] + pub wah: Option, + #[serde(rename = "FreeText", default)] + pub free_text: Option, + #[serde(rename = "Properties", default)] + pub properties: Option, +} + +#[derive(Debug, Deserialize)] +pub struct BeatPropertiesWrapper { + #[serde(rename = "Property", default)] + pub properties: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct BeatProperty { + #[serde(rename = "@name", default)] + pub name: String, + #[serde(rename = "Direction", default)] + pub direction: Option, + #[serde(rename = "Enable", default)] + pub enable: Option, + #[serde(rename = "Float", default)] + pub float: Option, + #[serde(rename = "Flags", default)] + pub flags: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RhythmRef { + #[serde(rename = "@ref", default)] + pub r#ref: i32, +} + +// --------------------------------------------------------------------------- +// Notes +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct NotesWrapper { + #[serde(rename = "Note", default)] + pub notes: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Note { + #[serde(rename = "@id", default)] + pub id: i32, + #[serde(rename = "Properties")] + pub properties: NoteProperties, + #[serde(rename = "Tie", default)] + pub tie: Option, + #[serde(rename = "Vibrato", default)] + pub vibrato: Option, + #[serde(rename = "LetRing", default)] + pub let_ring: Option, + #[serde(rename = "AntiAccent", default)] + pub anti_accent: Option, + #[serde(rename = "Accent", default)] + pub accent: Option, + #[serde(rename = "Trill", default)] + pub trill: Option, + #[serde(rename = "Ornament", default)] + pub ornament: Option, +} + +#[derive(Debug, Deserialize)] +pub struct TieInfo { + #[serde(rename = "@origin", default)] + pub origin: String, + #[serde(rename = "@destination", default)] + pub destination: String, +} + +/// An empty self-closing tag used as a presence flag (e.g., ``, ``). +#[derive(Debug, Deserialize)] +pub struct EnableTag; + +#[derive(Debug, Deserialize)] +pub struct NoteProperties { + #[serde(rename = "Property", default)] + pub properties: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Property { + #[serde(rename = "@name", default)] + pub name: String, + // Value sub-elements — each property uses at most one of these + #[serde(rename = "Fret", default)] + pub fret: Option, + #[serde(rename = "String", default)] + pub string: Option, + #[serde(rename = "Pitch", default)] + pub pitch: Option, + #[serde(rename = "Number", default)] + pub number: Option, + #[serde(rename = "Enable", default)] + pub enable: Option, + #[serde(rename = "Float", default)] + pub float: Option, + #[serde(rename = "Flags", default)] + pub flags: Option, + #[serde(rename = "HFret", default)] + pub hfret: Option, + #[serde(rename = "HType", default)] + pub htype: Option, + #[serde(rename = "Pitches", default)] + pub pitches: Option, + #[serde(rename = "Direction", default)] + pub direction: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Pitch { + #[serde(rename = "Step", default)] + pub step: String, + #[serde(rename = "Octave", default)] + pub octave: i32, + #[serde(rename = "Accidental", default)] + pub accidental: Option, +} + +// --------------------------------------------------------------------------- +// Rhythms +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct RhythmsWrapper { + #[serde(rename = "Rhythm", default)] + pub rhythms: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Rhythm { + #[serde(rename = "@id", default)] + pub id: i32, + #[serde(rename = "NoteValue", default)] + pub note_value: String, + #[serde(rename = "AugmentationDot", default)] + pub augmentation_dot: Option, + #[serde(rename = "PrimaryTuplet", default)] + pub primary_tuplet: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AugmentationDot { + #[serde(rename = "@count", default)] + pub count: i32, +} + +#[derive(Debug, Deserialize)] +pub struct PrimaryTuplet { + #[serde(rename = "@num", default)] + pub num: i32, + #[serde(rename = "@den", default)] + pub den: i32, +} diff --git a/lib/src/io/gpif_import.rs b/lib/src/io/gpif_import.rs new file mode 100644 index 0000000..414fad7 --- /dev/null +++ b/lib/src/io/gpif_import.rs @@ -0,0 +1,744 @@ +use std::collections::HashMap; + +use crate::io::gpif::*; +use crate::model::{ + beat::{Beat as SongBeat, Voice as SongVoice}, + effects::*, + enums::*, + headers::{Marker, MeasureHeader}, + key_signature::*, + measure::Measure, + note::Note as SongNote, + song::*, + track::Track as SongTrack, +}; + +pub trait SongGpifOps { + fn read_gpif(&mut self, gpif: &Gpif); +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/// Convert GPIF note value string to Duration.value. +/// Falls back to Quarter (4) for unknown values. +fn note_value_to_duration(s: &str) -> u16 { + match s { + "Whole" => 1, + "Half" => 2, + "Quarter" => 4, + "Eighth" => 8, + "16th" => 16, + "32nd" => 32, + "64th" => 64, + "128th" => 128, + _ => { + eprintln!( + "Warning: unknown GPIF note value '{}', defaulting to Quarter", + s + ); + 4 + } + } +} + +/// Convert GPIF dynamic string to MIDI velocity +fn dynamic_to_velocity(s: &str) -> i16 { + match s { + "PPP" => MIN_VELOCITY, + "PP" => MIN_VELOCITY + VELOCITY_INCREMENT, + "P" => MIN_VELOCITY + VELOCITY_INCREMENT * 2, + "MP" => MIN_VELOCITY + VELOCITY_INCREMENT * 3, + "MF" => MIN_VELOCITY + VELOCITY_INCREMENT * 4, + "F" => FORTE, + "FF" => MIN_VELOCITY + VELOCITY_INCREMENT * 6, + "FFF" => MIN_VELOCITY + VELOCITY_INCREMENT * 7, + _ => FORTE, + } +} + +/// Parse space-separated integer IDs from a string. +fn parse_ids(s: &str) -> Vec { + s.split_whitespace() + .filter_map(|tok| tok.parse::().ok()) + .collect() +} + +/// Parse slide flags bitmask into a list of `SlideType` values. +/// +/// Uses the same encoding as GP5 binary format: +/// - bit 0 (0x01): Shift slide to next note +/// - bit 1 (0x02): Legato slide to next note +/// - bit 2 (0x04): Slide out downwards +/// - bit 3 (0x08): Slide out upwards +/// - bit 4 (0x10): Slide in from below +/// - bit 5 (0x20): Slide in from above +fn parse_slide_flags(flags: i32) -> Vec { + let mut v = Vec::with_capacity(6); + if (flags & 0x01) != 0 { + v.push(SlideType::ShiftSlideTo); + } + if (flags & 0x02) != 0 { + v.push(SlideType::LegatoSlideTo); + } + if (flags & 0x04) != 0 { + v.push(SlideType::OutDownwards); + } + if (flags & 0x08) != 0 { + v.push(SlideType::OutUpWards); + } + if (flags & 0x10) != 0 { + v.push(SlideType::IntoFromBelow); + } + if (flags & 0x20) != 0 { + v.push(SlideType::IntoFromAbove); + } + v +} + +/// Parse a GPIF harmonic type string (e.g. "Natural", "Artificial", "Pinch") +/// into a `HarmonicEffect`. Falls back to `Natural` for unrecognised values. +/// "Feedback" is mapped to `Pinch` as Guitar Pro treats them equivalently. +fn parse_harmonic_type(htype: &str) -> HarmonicEffect { + let kind = match htype { + "Natural" => HarmonicType::Natural, + "Artificial" => HarmonicType::Artificial, + "Pinch" => HarmonicType::Pinch, + "Tap" | "Tapped" => HarmonicType::Tapped, + "Semi" => HarmonicType::Semi, + "Feedback" => HarmonicType::Pinch, + _ => HarmonicType::Natural, + }; + HarmonicEffect { + kind, + ..Default::default() + } +} + +/// Parse direction string to DirectionSign enum. +fn parse_direction_sign(s: &str) -> Option { + match s { + "Coda" => Some(DirectionSign::Coda), + "DoubleCoda" => Some(DirectionSign::DoubleCoda), + "Segno" => Some(DirectionSign::Segno), + "SegnoSegno" => Some(DirectionSign::SegnoSegno), + "Fine" => Some(DirectionSign::Fine), + "DaCapo" => Some(DirectionSign::DaCapo), + "DaCapoAlCoda" => Some(DirectionSign::DaCapoAlCoda), + "DaCapoAlDoubleCoda" => Some(DirectionSign::DaCapoAlDoubleCoda), + "DaCapoAlFine" => Some(DirectionSign::DaCapoAlFine), + "DaSegno" => Some(DirectionSign::DaSegno), + "DaSegnoAlCoda" => Some(DirectionSign::DaSegnoAlCoda), + "DaSegnoAlDoubleCoda" => Some(DirectionSign::DaSegnoAlDoubleCoda), + "DaSegnoAlFine" => Some(DirectionSign::DaSegnoAlFine), + "DaSegnoSegno" => Some(DirectionSign::DaSegnoSegno), + "DaSegnoSegnoAlCoda" => Some(DirectionSign::DaSegnoSegnoAlCoda), + "DaSegnoSegnoAlDoubleCoda" => Some(DirectionSign::DaSegnoSegnoAlDoubleCoda), + "DaSegnoSegnoAlFine" => Some(DirectionSign::DaSegnoSegnoAlFine), + "DaCoda" => Some(DirectionSign::DaCoda), + "DaDoubleCoda" => Some(DirectionSign::DaDoubleCoda), + _ => None, + } +} + +/// Build bend effect from GPIF origin/destination values (float, in 1/100 semitone). +fn build_bend_effect(origin: f64, destination: f64) -> BendEffect { + let mut bend = BendEffect::default(); + let origin_val = (origin / GP_BEND_SEMITONE as f64).round() as i8; + let dest_val = (destination / GP_BEND_SEMITONE as f64).round() as i8; + + if origin == 0.0 && destination > 0.0 { + bend.kind = BendType::Bend; + } else if origin > 0.0 && destination == 0.0 { + bend.kind = BendType::ReleaseUp; + } else if origin > 0.0 && destination > 0.0 { + if destination > origin { + bend.kind = BendType::Bend; + } else if destination < origin { + bend.kind = BendType::ReleaseUp; + } else { + bend.kind = BendType::Bend; + } + } + + bend.value = (destination.max(origin) / GP_BEND_SEMITONE as f64 * 2.0).round() as i16; + bend.points.push(BendPoint { + position: 0, + value: origin_val, + vibrato: false, + }); + bend.points.push(BendPoint { + position: 6, + value: ((origin_val as i16 + dest_val as i16) / 2) as i8, + vibrato: false, + }); + bend.points.push(BendPoint { + position: 12, + value: dest_val, + vibrato: false, + }); + bend +} + +/// Extract tuning pitches from a property list. +fn extract_tuning(properties: &[Property]) -> Vec<(i8, i8)> { + for prop in properties { + if prop.name == "Tuning" { + if let Some(pitches_str) = &prop.pitches { + let pitches: Vec = pitches_str + .split_whitespace() + .filter_map(|s| s.parse::().ok()) + .collect(); + return pitches + .iter() + .enumerate() + .map(|(i, &pitch)| ((i + 1) as i8, pitch)) + .collect(); + } + } + } + Vec::new() +} + +// --------------------------------------------------------------------------- +// Main conversion +// --------------------------------------------------------------------------- + +impl SongGpifOps for Song { + fn read_gpif(&mut self, gpif: &Gpif) { + // 1. Metadata + self.name = gpif.score.title.clone(); + self.subtitle = gpif.score.sub_title.clone(); + self.artist = gpif.score.artist.clone(); + self.album = gpif.score.album.clone(); + self.words = gpif.score.words.clone(); + self.author = gpif.score.music.clone(); + self.writer = gpif.score.music.clone(); + self.transcriber = gpif.score.tabber.clone(); + self.copyright = gpif.score.copyright.clone(); + self.comments = gpif.score.instructions.clone(); + // Notices + if !gpif.score.notices.is_empty() { + self.notice = gpif.score.notices.lines().map(|l| l.to_string()).collect(); + } + + // 2. Tempo from MasterTrack automations + if let Some(automations) = &gpif.master_track.automations { + for auto in &automations.automations { + if auto.automation_type == "Tempo" && auto.bar == 0 { + if let Some(tempo_str) = auto.value.split_whitespace().next() { + self.tempo = match tempo_str.parse::() { + Ok(v) => v as i16, + Err(_) => { + eprintln!( + "Warning: failed to parse tempo '{}', defaulting to 120", + tempo_str + ); + 120 + } + }; + } + } + } + } + + // 3. Build lookup maps + let bars_map: HashMap = gpif.bars.bars.iter().map(|b| (b.id, b)).collect(); + let voices_map: HashMap = + gpif.voices.voices.iter().map(|v| (v.id, v)).collect(); + let beats_map: HashMap = gpif.beats.beats.iter().map(|b| (b.id, b)).collect(); + let notes_map: HashMap = gpif.notes.notes.iter().map(|n| (n.id, n)).collect(); + let rhythms_map: HashMap = + gpif.rhythms.rhythms.iter().map(|r| (r.id, r)).collect(); + + // 4. Measure Headers (MasterBars) — also collects per-track bar IDs + self.measure_headers.clear(); + let num_tracks = gpif.tracks.tracks.len(); + let mut track_bar_ids: Vec> = vec![Vec::new(); num_tracks]; + + for (mh_idx, mb) in gpif.master_bars.master_bars.iter().enumerate() { + let mut mh = MeasureHeader { + number: (mh_idx + 1) as u16, + ..Default::default() + }; + + // Time signature + let time_parts: Vec<&str> = mb.time.split('/').collect(); + if time_parts.len() == 2 { + mh.time_signature.numerator = time_parts[0].parse().unwrap_or(4) as i8; + mh.time_signature.denominator.value = time_parts[1].parse().unwrap_or(4) as u16; + } + + // Key signature + if let Some(key) = &mb.key { + mh.key_signature.key = key.accidental_count as i8; + mh.key_signature.is_minor = key.mode == "Minor"; + } + + // Tempo at this bar + if let Some(automations) = &gpif.master_track.automations { + for auto in &automations.automations { + if auto.automation_type == "Tempo" && auto.bar == mh_idx as i32 { + if let Some(tempo_str) = auto.value.split_whitespace().next() { + mh.tempo = tempo_str.parse::().unwrap_or(0.0) as i32; + } + } + } + } + + // Repeat + if let Some(repeat) = &mb.repeat { + mh.repeat_open = repeat.start == "true"; + if repeat.end == "true" { + mh.repeat_close = repeat.count.max(1) as i8; + } + } + + // Alternate endings (volta) + if let Some(alt_str) = &mb.alternate_endings { + let mut bitmask: u8 = 0; + for tok in alt_str.split_whitespace() { + if let Ok(n) = tok.parse::() { + if n > 0 && n <= 8 { + bitmask |= 1 << (n - 1); + } + } + } + mh.repeat_alternative = bitmask; + } + + // Double bar + mh.double_bar = mb.double_bar.is_some(); + + // Marker (Section) + if let Some(section) = &mb.section { + let title = section + .text + .as_deref() + .unwrap_or(section.letter.as_deref().unwrap_or("Section")); + // GP6/7 GPIF XML does not include marker color; use the default (red). + mh.marker = Some(Marker { + title: title.to_string(), + color: 0xff0000, + }); + } + + // Fermatas + if let Some(fermatas_w) = &mb.fermatas { + for f in &fermatas_w.fermatas { + let ftype = f.fermata_type.as_deref().unwrap_or("Medium").to_string(); + let offset = f.offset.as_deref().unwrap_or("").to_string(); + mh.fermatas.push((ftype, offset)); + } + } + + // Free time + mh.free_time = mb.free_time.is_some(); + + // Directions + if let Some(dirs) = &mb.directions { + if let Some(target) = &dirs.target { + mh.direction = parse_direction_sign(target); + } else if let Some(jump) = &dirs.jump { + mh.direction = parse_direction_sign(jump); + } + } + + // Per-track bar IDs + let bar_ids = parse_ids(&mb.bars); + for (t_idx, &bar_id) in bar_ids.iter().enumerate() { + if t_idx < num_tracks { + track_bar_ids[t_idx].push(bar_id); + } + } + + self.measure_headers.push(mh); + } + + let num_measures = self.measure_headers.len(); + + // 5. Tracks + self.tracks.clear(); + + for (t_idx, g_track) in gpif.tracks.tracks.iter().enumerate() { + let mut track = SongTrack { + name: g_track.name.clone(), + short_name: g_track.short_name.clone(), + number: (t_idx + 1) as i32, + ..Default::default() + }; + + // Color + if let Some(color_str) = &g_track.color { + let rgb: Vec = color_str + .split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect(); + if rgb.len() == 3 { + track.color = rgb[0] * 65536 + rgb[1] * 256 + rgb[2]; + } + } + + // Tuning: GP6 track-level properties, GP7 staves + if let Some(props) = &g_track.properties { + track.strings = extract_tuning(&props.properties); + } + if track.strings.is_empty() { + if let Some(staves) = &g_track.staves { + for staff in &staves.staves { + if let Some(props) = &staff.properties { + track.strings = extract_tuning(&props.properties); + if !track.strings.is_empty() { + break; + } + } + } + } + } + if track.strings.is_empty() { + track.strings = vec![(1, 64), (2, 59), (3, 55), (4, 50), (5, 45), (6, 40)]; + } + + track.fret_count = 24; + + // MIDI + if let Some(gm) = &g_track.general_midi { + if let Some(ch) = gm.primary_channel { + track.channel_index = ch as usize; + track.percussion_track = ch == 9; + } + track.midi_program_gpif = gm.program; + if let Some(port) = gm.port { + track.port = port as u8; + } + } + + // Transpose + if let Some(tr) = &g_track.transpose { + track.transpose_chromatic = tr.chromatic.unwrap_or(0); + track.transpose_octave = tr.octave.unwrap_or(0); + } + + // Current dynamic (persists across beats) + let mut current_velocity: i16 = FORTE; + + // Measures + for m_idx in 0..num_measures { + let mut measure = Measure { + number: m_idx + 1, + track_index: t_idx, + ..Default::default() + }; + + if m_idx < self.measure_headers.len() { + measure.time_signature = self.measure_headers[m_idx].time_signature.clone(); + measure.key_signature = self.measure_headers[m_idx].key_signature.clone(); + } + + let bar_id = if m_idx < track_bar_ids[t_idx].len() { + track_bar_ids[t_idx][m_idx] + } else { + -1 + }; + + if let Some(bar) = bars_map.get(&bar_id) { + measure.simile_mark = bar.simile_mark.clone(); + let voice_ids = parse_ids(&bar.voices); + measure.voices.clear(); + + for &vid in &voice_ids { + if vid < 0 { + continue; + } + let mut s_voice = SongVoice::default(); + + if let Some(g_voice) = voices_map.get(&vid) { + let beat_ids = parse_ids(&g_voice.beats); + + for &bid in &beat_ids { + if let Some(g_beat) = beats_map.get(&bid) { + let s_beat = convert_beat( + g_beat, + &rhythms_map, + ¬es_map, + &mut current_velocity, + ); + s_voice.beats.push(s_beat); + } + } + } + measure.voices.push(s_voice); + } + } + track.measures.push(measure); + } + self.tracks.push(track); + } + } +} + +fn convert_beat( + g_beat: &Beat, + rhythms_map: &HashMap, + notes_map: &HashMap, + current_velocity: &mut i16, +) -> SongBeat { + let mut s_beat = SongBeat::default(); + + // Duration from Rhythm + if let Some(rhythm_ref) = &g_beat.rhythm { + if let Some(rhythm) = rhythms_map.get(&rhythm_ref.r#ref) { + s_beat.duration.value = note_value_to_duration(&rhythm.note_value); + if let Some(dot) = &rhythm.augmentation_dot { + match dot.count { + 1 => s_beat.duration.dotted = true, + 2 => s_beat.duration.double_dotted = true, + _ => {} + } + } + if let Some(tuplet) = &rhythm.primary_tuplet { + s_beat.duration.tuplet_enters = tuplet.num as u8; + s_beat.duration.tuplet_times = tuplet.den as u8; + } + } + } + + // Dynamic + if let Some(dyn_str) = &g_beat.dynamic { + *current_velocity = dynamic_to_velocity(dyn_str); + } + + // Grace notes + let is_grace_beat = g_beat.grace_notes.is_some(); + let grace_on_beat = g_beat.grace_notes.as_deref() == Some("OnBeat"); + + // Text + if let Some(text) = &g_beat.free_text { + s_beat.text = text.clone(); + } + + // Fade in + if let Some(fadding) = &g_beat.fadding { + if fadding == "FadeIn" { + s_beat.effect.fade_in = true; + } + } + + // Wah effect + if let Some(wah_str) = &g_beat.wah { + if wah_str == "Open" { + s_beat.effect.slap_effect = SlapEffect::None; // placeholder, wah is stored at mix table level in GP5 + } + } + + // Tremolo bar + if let Some(tremolo_str) = &g_beat.tremolo { + if let Ok(val) = tremolo_str.parse::() { + if val != 0.0 { + s_beat.effect.tremolo_bar = Some(build_bend_effect(0.0, val)); + } + } + } + + // Beat properties + if let Some(beat_props) = &g_beat.properties { + for bp in &beat_props.properties { + match bp.name.as_str() { + "Brush" => { + if let Some(dir) = &bp.direction { + s_beat.effect.stroke.direction = match dir.as_str() { + "Down" => BeatStrokeDirection::Down, + "Up" => BeatStrokeDirection::Up, + _ => BeatStrokeDirection::None, + }; + s_beat.effect.stroke.value = DURATION_EIGHTH as u16; + } + } + "Rasgueado" => { + s_beat.effect.has_rasgueado = true; + } + "PickStroke" => { + if let Some(dir) = &bp.direction { + s_beat.effect.pick_stroke = match dir.as_str() { + "Down" => BeatStrokeDirection::Down, + "Up" => BeatStrokeDirection::Up, + _ => BeatStrokeDirection::None, + }; + } + } + _ => {} + } + } + } + + // Notes + match &g_beat.notes { + Some(notes_str) => { + let note_ids = parse_ids(notes_str); + s_beat.status = if note_ids.is_empty() { + BeatStatus::Rest + } else { + BeatStatus::Normal + }; + + for &nid in ¬e_ids { + if let Some(g_note) = notes_map.get(&nid) { + let s_note = + convert_note(g_note, *current_velocity, is_grace_beat, grace_on_beat); + s_beat.notes.push(s_note); + } + } + } + None => { + s_beat.status = BeatStatus::Rest; + } + } + + s_beat +} + +fn convert_note( + g_note: &Note, + velocity: i16, + is_grace_beat: bool, + grace_on_beat: bool, +) -> SongNote { + let mut s_note = SongNote { + velocity, + kind: NoteType::Normal, + ..Default::default() + }; + + let mut bend_origin: Option = None; + let mut bend_dest: Option = None; + + for prop in &g_note.properties.properties { + match prop.name.as_str() { + "Fret" => { + if let Some(f) = prop.fret { + s_note.value = f as i16; + } + } + "String" => { + if let Some(s) = prop.string { + s_note.string = s as i8; + } + } + "PalmMuted" => { + if prop.enable.is_some() { + s_note.effect.palm_mute = true; + } + } + "BendOriginValue" => { + bend_origin = prop.float; + } + "BendDestinationValue" => { + bend_dest = prop.float; + } + "Slide" => { + if let Some(flags) = prop.flags { + s_note.effect.slides = parse_slide_flags(flags); + } + } + "HarmonicType" => { + if let Some(htype) = &prop.htype { + s_note.effect.harmonic = Some(parse_harmonic_type(htype)); + } + } + "HarmonicFret" => { + if let Some(hfret) = prop.hfret { + if let Some(ref mut h) = s_note.effect.harmonic { + h.fret = Some(hfret as i8); + } + } + } + "HopoOrigin" | "HopoDestination" => { + if prop.enable.is_some() { + s_note.effect.hammer = true; + } + } + "Dead" | "Muted" => { + if prop.enable.is_some() { + s_note.kind = NoteType::Dead; + } + } + _ => {} + } + } + + // Bend + if let (Some(orig), Some(dest)) = (bend_origin, bend_dest) { + if orig != 0.0 || dest != 0.0 { + s_note.effect.bend = Some(build_bend_effect(orig, dest)); + } + } + + // Tie + if let Some(tie) = &g_note.tie { + if tie.destination == "true" { + s_note.kind = NoteType::Tie; + } + } + + // Vibrato + if g_note.vibrato.is_some() { + s_note.effect.vibrato = true; + } + + // Let Ring + if g_note.let_ring.is_some() { + s_note.effect.let_ring = true; + } + + // Ghost note + if g_note.anti_accent.is_some() { + s_note.effect.ghost_note = true; + } + + // Accent bitmask + if let Some(accent) = g_note.accent { + if (accent & 0x01) != 0 { + s_note.effect.staccato = true; + } + if (accent & 0x02) != 0 || (accent & 0x08) != 0 { + s_note.effect.accentuated_note = true; + } + if (accent & 0x04) != 0 { + s_note.effect.heavy_accentuated_note = true; + } + } + + // Ornament + if let Some(orn) = &g_note.ornament { + s_note.effect.ornament = Some(orn.clone()); + } + + // Trill + if let Some(trill_fret) = g_note.trill { + s_note.effect.trill = Some(TrillEffect { + fret: trill_fret as i8, + duration: Duration::default(), + }); + } + + // Grace note + if is_grace_beat { + s_note.effect.grace = Some(GraceEffect { + fret: s_note.value as i8, + velocity: s_note.velocity, + duration: 1, + is_dead: s_note.kind == NoteType::Dead, + is_on_beat: grace_on_beat, + transition: if s_note.effect.hammer { + GraceEffectTransition::Hammer + } else if !s_note.effect.slides.is_empty() { + GraceEffectTransition::Slide + } else { + GraceEffectTransition::None + }, + }); + } + + s_note +} diff --git a/lib/src/io/gpx.rs b/lib/src/io/gpx.rs new file mode 100644 index 0000000..ddd3f83 --- /dev/null +++ b/lib/src/io/gpx.rs @@ -0,0 +1,233 @@ +use std::io::{Read, Cursor}; +use zip::ZipArchive; +use crate::io::gpif::Gpif; +use crate::error::GpResult; +use quick_xml::de::from_str; + +/// Reads a .gp (GP7+) file which is a ZIP archive containing 'Content/score.gpif'. +pub fn read_gp(data: &[u8]) -> GpResult { + let cursor = Cursor::new(data); + let mut zip = ZipArchive::new(cursor).map_err(|e| format!("Zip error: {}", e))?; + + // Standard path for GP7 files + let mut file = zip.by_name("Content/score.gpif").map_err(|e| format!("Could not find score.gpif: {}", e))?; + + let mut contents = String::new(); + file.read_to_string(&mut contents).map_err(|e| format!("Read error: {}", e))?; + + let gpif: Gpif = from_str(&contents).map_err(|e| format!("XML Parse error: {}", e))?; + Ok(gpif) +} + +// --------------------------------------------------------------------------- +// GP6 (.gpx) BCFZ/BCFS container support +// --------------------------------------------------------------------------- + +const BCFZ_MAGIC: &[u8; 4] = b"BCFZ"; +const BCFS_MAGIC: &[u8; 4] = b"BCFS"; +const SECTOR_SIZE: usize = 0x1000; + +/// Bit-level reader for BCFZ decompression. +/// Reads bits MSB-first within each byte. +struct BitStream<'a> { + data: &'a [u8], + bit_position: usize, +} + +impl<'a> BitStream<'a> { + fn new(data: &'a [u8]) -> Self { + BitStream { data, bit_position: 0 } + } + + fn byte_offset(&self) -> usize { + self.bit_position / 8 + } + + fn is_eof(&self) -> bool { + self.byte_offset() >= self.data.len() + } + + /// Read a single bit (MSB-first within the current byte). + fn read_bit(&mut self) -> u8 { + let byte_index = self.bit_position / 8; + let bit_index = 7 - (self.bit_position % 8); + if byte_index >= self.data.len() { + return 0; + } + self.bit_position += 1; + (self.data[byte_index] >> bit_index) & 1 + } + + /// Read `count` bits, accumulated big-endian (MSB first). + fn read_bits(&mut self, count: usize) -> u32 { + let mut result: u32 = 0; + for _ in 0..count { + result = (result << 1) | self.read_bit() as u32; + } + result + } + + /// Read `count` bits, accumulated little-endian (LSB first / "reversed"). + fn read_bits_reversed(&mut self, count: usize) -> u32 { + let mut result: u32 = 0; + for i in 0..count { + result |= (self.read_bit() as u32) << i; + } + result + } +} + +/// Decompress a BCFZ-compressed buffer into raw BCFS data. +fn decompress_bcfz(data: &[u8]) -> Result, String> { + if data.len() < 8 { + return Err("BCFZ data too short".to_string()); + } + if &data[0..4] != BCFZ_MAGIC { + return Err(format!("Expected BCFZ magic, got {:?}", &data[0..4])); + } + + let raw_len = i32::from_le_bytes([data[4], data[5], data[6], data[7]]); + if raw_len < 0 { + return Err(format!("BCFZ: negative expected length {}", raw_len)); + } + let expected_len = raw_len as usize; + let mut output = Vec::with_capacity(expected_len); + let mut bits = BitStream::new(&data[8..]); + + while output.len() < expected_len && !bits.is_eof() { + let flag = bits.read_bit(); + if flag == 1 { + // Back-reference (LZ77-style) + let word_size = bits.read_bits(4) as usize; + let offset = bits.read_bits_reversed(word_size) as usize; + let size = bits.read_bits_reversed(word_size) as usize; + if offset == 0 || offset > output.len() { + return Err(format!( + "BCFZ: invalid back-reference offset {} (output len {})", + offset, output.len() + )); + } + let source_start = output.len() - offset; + // LZ77 overlapping copy: when size > offset the source overlaps + // the destination, so we must copy byte-by-byte with modular indexing. + for i in 0..size { + let byte = output[source_start + (i % offset)]; + output.push(byte); + } + } else { + // Literal bytes + let size = bits.read_bits_reversed(2) as usize; + for _ in 0..size { + let byte = bits.read_bits(8) as u8; + output.push(byte); + } + } + } + + output.truncate(expected_len); + Ok(output) +} + +/// A file extracted from a BCFS virtual filesystem. +struct BcfsFile { + name: String, + data: Vec, +} + +/// Read the integer at the given offset (little-endian i32). +fn read_le_i32(data: &[u8], offset: usize) -> i32 { + i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} + +/// Parse the BCFS virtual filesystem and extract all files. +/// +/// The BCFS format starts with a 4-byte magic ("BCFS"), followed by sector-based data. +/// The Java reference implementation (TuxGuitar) strips the 4-byte magic and then treats +/// the remaining data as a virtual disk with 0x1000-byte sectors. +fn parse_bcfs(data: &[u8]) -> Result, String> { + if data.len() < 4 { + return Err("BCFS data too short".to_string()); + } + if &data[0..4] != BCFS_MAGIC { + return Err(format!("Expected BCFS magic, got {:?}", &data[0..4])); + } + + // Strip the 4-byte magic — all sector offsets are relative to this base. + let disk = &data[4..]; + let mut files = Vec::new(); + let mut sector_offset = SECTOR_SIZE; // Skip sector 0 (header area) + + while sector_offset + 3 < disk.len() { + let entry_type = read_le_i32(disk, sector_offset); + + if entry_type == 2 { + // File directory entry — requires at least 0x98 bytes from sector_offset + if sector_offset + 0x98 > disk.len() { + sector_offset += SECTOR_SIZE; + continue; + } + + let name_start = sector_offset + 4; + let name_end = (name_start + 127).min(disk.len()); + let name_bytes = &disk[name_start..name_end]; + let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(name_bytes.len()); + let name = String::from_utf8_lossy(&name_bytes[..name_len]).to_string(); + + let file_size = read_le_i32(disk, sector_offset + 0x8C) as usize; + + // Block index table at +0x94, array of i32, terminated by 0 + let mut file_data = Vec::with_capacity(file_size); + let mut idx_offset = sector_offset + 0x94; + loop { + if idx_offset + 4 > sector_offset + SECTOR_SIZE { + break; + } + let block_idx = read_le_i32(disk, idx_offset); + if block_idx == 0 { + break; + } + let block_start = block_idx as usize * SECTOR_SIZE; + let block_end = (block_start + SECTOR_SIZE).min(disk.len()); + if block_start < disk.len() { + file_data.extend_from_slice(&disk[block_start..block_end]); + } + idx_offset += 4; + } + + file_data.truncate(file_size); + if !name.is_empty() { + files.push(BcfsFile { name, data: file_data }); + } + } + + sector_offset += SECTOR_SIZE; + } + + Ok(files) +} + +/// Reads a .gpx (GP6) file which is a BCFZ/BCFS container holding 'score.gpif'. +pub fn read_gpx(data: &[u8]) -> GpResult { + let decompressed = decompress_bcfz(data)?; + let files = parse_bcfs(&decompressed)?; + + let score_file = files.iter() + .find(|f| f.name == "score.gpif") + .ok_or_else(|| { + let names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect(); + format!("score.gpif not found in GPX archive. Files found: {:?}", names) + })?; + + let xml_str = std::str::from_utf8(&score_file.data) + .map_err(|e| format!("UTF-8 error in score.gpif: {}", e))?; + + let gpif: Gpif = from_str(xml_str) + .map_err(|e| format!("XML parse error in score.gpif: {}", e))?; + + Ok(gpif) +} diff --git a/lib/src/io/mod.rs b/lib/src/io/mod.rs new file mode 100644 index 0000000..4b0cad3 --- /dev/null +++ b/lib/src/io/mod.rs @@ -0,0 +1,4 @@ +pub mod primitive; +pub mod gpif; +pub mod gpif_import; +pub mod gpx; diff --git a/lib/src/io.rs b/lib/src/io/primitive.rs similarity index 76% rename from lib/src/io.rs rename to lib/src/io/primitive.rs index acf843e..7fc37df 100644 --- a/lib/src/io.rs +++ b/lib/src/io/primitive.rs @@ -1,5 +1,6 @@ use fraction::ToPrimitive; use encoding_rs::*; +use crate::error::{GpError, GpResult}; //reading functions @@ -7,113 +8,117 @@ use encoding_rs::*; /// * `data` - array of bytes /// * `seek` - start position to read /// * returns the read byte as u8 -pub(crate) fn read_byte(data: &[u8], seek: &mut usize ) -> u8 { - if data.len() < *seek {panic!("End of filee reached");} +pub(crate) fn read_byte(data: &[u8], seek: &mut usize ) -> GpResult { + if *seek >= data.len() {return Err(GpError::UnexpectedEof { offset: *seek, needed: 1 });} let b = data[*seek]; *seek += 1; - b + Ok(b) } /// Read a signed byte and increase the cursor position by 1 /// * `data` - array of bytes /// * `seek` - start position to read -/// * returns the read byte as u8 -pub(crate) fn read_signed_byte(data: &[u8], seek: &mut usize ) -> i8 { - if data.len() < *seek {panic!("End of file reached");} +/// * returns the read byte as i8 +pub(crate) fn read_signed_byte(data: &[u8], seek: &mut usize ) -> GpResult { + if *seek >= data.len() {return Err(GpError::UnexpectedEof { offset: *seek, needed: 1 });} let b = data[*seek] as i8; *seek += 1; - b + Ok(b) } /// Read a boolean and increase the cursor position by 1 /// * `data` - array of bytes /// * `seek` - start position to read /// * returns boolean value -pub(crate) fn read_bool(data: &[u8], seek: &mut usize ) -> bool { - if data.len() < *seek {panic!("End of file reached");} +pub(crate) fn read_bool(data: &[u8], seek: &mut usize ) -> GpResult { + if *seek >= data.len() {return Err(GpError::UnexpectedEof { offset: *seek, needed: 1 });} let b = data[*seek]; *seek += 1; - b != 0 + Ok(b != 0) } /// Read a short and increase the cursor position by 2 (2 little-endian bytes) /// * `data` - array of bytes /// * `seek` - start position to read /// * returns the short value -pub(crate) fn read_short(data: &[u8], seek: &mut usize ) -> i16 { - if data.len() < *seek + 2 {panic!("End of file reached");} +pub(crate) fn read_short(data: &[u8], seek: &mut usize ) -> GpResult { + if *seek + 2 > data.len() {return Err(GpError::UnexpectedEof { offset: *seek, needed: 2 });} let n = i16::from_le_bytes([data[*seek], data[*seek+1]]); *seek += 2; - n + Ok(n) } /// Read an integer and increase the cursor position by 4 (4 little-endian bytes) /// * `data` - array of bytes /// * `seek` - start position to read /// * returns the integer value -pub(crate) fn read_int(data: &[u8], seek: &mut usize ) -> i32 { - if data.len() < *seek + 4 {panic!("End of file reached");} +pub(crate) fn read_int(data: &[u8], seek: &mut usize ) -> GpResult { + if *seek + 4 > data.len() {return Err(GpError::UnexpectedEof { offset: *seek, needed: 4 });} let n = i32::from_le_bytes([data[*seek], data[*seek+1], data[*seek+2], data[*seek+3]]); *seek += 4; - n + Ok(n) } /*/// Read a float and increase the cursor position by 4 (4 little-endian bytes) /// * `data` - array of bytes /// * `seek` - start position to read /// * returns the float value -pub(crate) fn read_float(data: &[u8], seek: &mut usize ) -> f32 { +pub(crate) fn read_float(data: &[u8], seek: &mut usize ) -> GpResult { let n = f32::from_le_bytes([data[*seek], data[*seek+1], data[*seek+2], data[*seek+3]]); *seek += 4; - n + Ok(n) }*/ /// Read a double and increase the cursor position by 8 (8 little-endian bytes) /// * `data` - array of bytes /// * `seek` - start position to read /// * returns the float value -pub(crate) fn read_double(data: &[u8], seek: &mut usize ) -> f64 { +pub(crate) fn read_double(data: &[u8], seek: &mut usize ) -> GpResult { + if *seek + 8 > data.len() {return Err(GpError::UnexpectedEof { offset: *seek, needed: 8 });} let n = f64::from_le_bytes([data[*seek], data[*seek+1], data[*seek+2], data[*seek+3], data[*seek+4], data[*seek+5], data[*seek+6], data[*seek+7]]); *seek += 8; - n + Ok(n) } /// Read length of the string stored in 1 integer and followed by character bytes. -pub(crate) fn read_int_size_string(data: &[u8], seek: &mut usize) -> String { - let size = read_int(data, seek).to_usize().unwrap(); +pub(crate) fn read_int_size_string(data: &[u8], seek: &mut usize) -> GpResult { + let size = read_int(data, seek)?.to_usize().unwrap(); read_string(data, seek, size, None) } /// Read length of the string increased by 1 and stored in 1 integer followed by length of the string in 1 byte and finally followed by character bytes. -pub(crate) fn read_int_byte_size_string(data: &[u8], seek: &mut usize) -> String { - let s = (read_int(data, seek) - 1).to_usize().unwrap(); +pub(crate) fn read_int_byte_size_string(data: &[u8], seek: &mut usize) -> GpResult { + let val = read_int(data, seek)?; + if val <= 0 { return Ok(String::new()); } + let s = (val - 1).to_usize().unwrap_or(0); + if *seek + 1 + s > data.len() { return Ok(String::new()); } // Safety check read_byte_size_string(data, seek, s) } /// Read length of the string stored in 1 byte and followed by character bytes. /// * `size`: string length that we should attempt to read. -pub(crate) fn read_byte_size_string(data: &[u8], seek: &mut usize, size: usize) -> String { - //println!("read_int_byte_size_string(), size={}", size); - let length = read_byte(data, seek).to_usize().unwrap(); +pub(crate) fn read_byte_size_string(data: &[u8], seek: &mut usize, size: usize) -> GpResult { + let length = read_byte(data, seek)?.to_usize().unwrap(); read_string(data, seek, size, Some(length)) } /// Read a string /// * `size`: real string length /// * `length`: optionnal provided length (in case of blank chars after the string) -fn read_string(data: &[u8], seek: &mut usize, size: usize, length: Option) -> String { - //println!("read_string(), size={} \t length={:?}", size, length); +fn read_string(data: &[u8], seek: &mut usize, size: usize, length: Option) -> GpResult { let length = length.unwrap_or(size); - //let count = if size > 0 {size} else {length}; + if *seek + length > data.len() { + return Err(GpError::UnexpectedEof { offset: *seek, needed: length }); + } let (cow, _encoding_used, had_errors) = WINDOWS_1252.decode(&data[*seek..*seek+length]); if had_errors { let parse = std::str::from_utf8(&data[*seek..*seek+length]); - if parse.is_err() {panic!("Unable to read string");} + if parse.is_err() {return Err(GpError::StringDecode { offset: *seek });} *seek += size; - return parse.unwrap().to_string(); + return Ok(parse.unwrap().to_string()); } *seek += size; - cow.to_string() + Ok(cow.to_string()) } pub const VERSIONS: [((u8,u8,u8), bool, &str); 10] = [((3, 0, 0), false, "FICHIER GUITAR PRO v3.00"), @@ -133,9 +138,8 @@ pub const VERSIONS: [((u8,u8,u8), bool, &str); 10] = [((3, 0, 0), false, "FICHIE /// * `data` - array of bytes /// * `seek` - cursor that will be incremented /// * returns version -pub(crate) fn read_version_string(data: &[u8], seek: &mut usize) -> crate::headers::Version { - let mut v = crate::headers::Version {data: read_byte_size_string(data, seek, 30), number: (5,2,0), clipboard: false}; - //println!("Version {} {}", n, s); +pub(crate) fn read_version_string(data: &[u8], seek: &mut usize) -> GpResult { + let mut v = crate::model::headers::Version {data: read_byte_size_string(data, seek, 30)?, number: (5,2,0), clipboard: false}; //get the version for x in VERSIONS { if v.data == x.2 { @@ -144,17 +148,16 @@ pub(crate) fn read_version_string(data: &[u8], seek: &mut usize) -> crate::heade break; } } - //println!("########################## Version: {:?}", v); - v + Ok(v) } /// Read a color. Colors are used by `Marker` and `Track`. They consist of 3 consecutive bytes and one blank byte. -pub(crate) fn read_color(data: &[u8], seek: &mut usize) -> i32 { - let r = read_byte(data, seek).to_i32().unwrap(); - let g = read_byte(data, seek).to_i32().unwrap(); - let b = read_byte(data, seek).to_i32().unwrap(); +pub(crate) fn read_color(data: &[u8], seek: &mut usize) -> GpResult { + let r = read_byte(data, seek)?.to_i32().unwrap(); + let g = read_byte(data, seek)?.to_i32().unwrap(); + let b = read_byte(data, seek)?.to_i32().unwrap(); *seek += 1; - r * 65536 + g * 256 + b + Ok(r * 65536 + g * 256 + b) } //writing functions @@ -208,7 +211,7 @@ pub(crate) fn write_version(data: &mut Vec, version: (u8,u8,u8)) { #[cfg(test)] mod test { - use crate::io::*; + use super::*; #[test] fn test_read_byte_size_string() { @@ -217,21 +220,21 @@ mod test { 0x50,0x52,0x4f,0x20,0x76,0x33,0x2e,0x30, 0x30]; let mut seek = 0usize; - assert_eq!(read_byte_size_string(&data, &mut seek, 30), "FICHIER GUITAR PRO v3.00"); + assert_eq!(read_byte_size_string(&data, &mut seek, 30).unwrap(), "FICHIER GUITAR PRO v3.00"); } #[test] fn test_read_int_size_string() { let data: Vec = vec![0x08,0x00,0x00,0x00, 0x25,0x41,0x52,0x54,0x49,0x53,0x54,0x25]; let mut seek = 0usize; - assert_eq!(read_int_size_string(&data, &mut seek), "%ARTIST%"); + assert_eq!(read_int_size_string(&data, &mut seek).unwrap(), "%ARTIST%"); } #[test] fn test_read_int_byte_size_string() { let data: Vec = vec![0x09,0x00,0x00,0x00, 0x08, 0x25,0x41,0x52,0x54,0x49,0x53,0x54,0x25]; let mut seek = 0usize; - assert_eq!(read_int_byte_size_string(&data, &mut seek), "%ARTIST%"); + assert_eq!(read_int_byte_size_string(&data, &mut seek).unwrap(), "%ARTIST%"); } #[test] @@ -248,7 +251,8 @@ mod test { fn test_write_int_size_string() { let mut out: Vec = Vec::with_capacity(16); write_int_size_string(&mut out, "%ARTIST%"); - let expected_result: Vec = vec![0x09,0x00,0x00,0x00, 0x08,0x25,0x41,0x52,0x54,0x49,0x53,0x54,0x25]; + // int_size_string = int(length+1), then string bytes (no byte length) + let expected_result: Vec = vec![0x09,0x00,0x00,0x00, 0x25,0x41,0x52,0x54,0x49,0x53,0x54,0x25]; assert_eq!(out, expected_result); } #[test] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 2e61d08..c8990d0 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,504 +1,40 @@ -#[path = "song.rs"] pub mod gp; -mod io; -pub mod enums; -pub mod headers; -pub mod track; -pub mod measure; -pub mod effects; -pub mod key_signature; -pub mod midi; -pub mod mix_table; -pub mod chord; -pub mod page; -pub mod rse; -pub mod note; -pub mod lyric; -pub mod beat; +pub mod audio; +pub mod error; +pub mod io; +pub mod model; + +// Re-export error types +pub use crate::error::{GpError, GpResult}; + +// Re-export core types +pub use crate::model::beat::{Beat, Voice}; +pub use crate::model::chord::Chord; +pub use crate::model::enums::*; +pub use crate::model::headers::MeasureHeader; +pub use crate::model::key_signature::{KeySignature, TimeSignature}; +pub use crate::model::measure::Measure; +pub use crate::model::note::Note; +pub use crate::model::page::PageSetup; +pub use crate::model::song::Song; +pub use crate::model::track::Track; + +// Re-export traits for easy use +pub use crate::audio::midi::SongMidiOps; +pub use crate::io::gpif_import::SongGpifOps; +pub use crate::model::beat::SongBeatOps; +pub use crate::model::chord::SongChordOps; +pub use crate::model::effects::SongEffectOps; +pub use crate::model::headers::SongHeaderOps; +pub use crate::model::lyric::SongLyricOps; +pub use crate::model::measure::SongMeasureOps; +pub use crate::model::mix_table::SongMixTableOps; +pub use crate::model::note::SongNoteOps; +pub use crate::model::page::SongPageOps; +pub use crate::model::rse::SongRseOps; +pub use crate::model::track::SongTrackOps; #[cfg(test)] -mod test { - use std::{io::Read, fs}; - use fraction::ToPrimitive; - use crate::gp::Song; +mod tests; - fn read_file(path: String) -> Vec { - let f = fs::OpenOptions::new().read(true).open(&path).expect("Cannot open file"); - let size: usize = fs::metadata(&path).unwrap_or_else(|_e|{panic!("Unable to get file size")}).len().to_usize().unwrap(); - let mut data: Vec = Vec::with_capacity(size); - f.take(u64::from_ne_bytes(size.to_ne_bytes())).read_to_end(&mut data).unwrap_or_else(|_error|{panic!("Unable to read file contents");}); - data - } - - //chords - #[test] - fn test_gp3_chord() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/Chords.gp3"))); - } - #[test] - fn test_gp4_chord() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Chords.gp4"))); - } - #[test] - fn test_gp5_chord() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Chords.gp5"))); - } - #[test] - fn test_gp5_unknown_chord_extension() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Unknown Chord Extension.gp5"))); - } - #[test] - fn test_gp5_chord_without_notes() { //Read chord even if there's no fingering - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/chord_without_notes.gp5"))); - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/001_Funky_Guy.gp5"))); - } - - //duration - #[test] - fn test_gp3_duration() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/Duration.gp3"))); - } - - //effects - #[test] - fn test_gp3_effects() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/Effects.gp3"))); - } - #[test] - fn test_gp4_effects() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Effects.gp4"))); - } - #[test] - fn test_gp5_effects() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Effects.gp5"))); - } - - //harmonics - #[test] - fn test_gp3_harmonics() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/Harmonics.gp3"))); - } - #[test] - fn test_gp4_harmonics() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Harmonics.gp4"))); - } - #[test] - fn test_gp5_harmonics() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Harmonics.gp5"))); - } - - //key - #[test] - fn test_gp4_key() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Key.gp4"))); - } - #[test] - fn test_gp5_key() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Key.gp5"))); - } - - //demo - - //repeat - #[test] - fn test_gp4_repeat() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Repeat.gp4"))); - } - #[test] - fn test_gp5_repeat() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Repeat.gp5"))); - } - - //RSE - #[test] - fn test_gp5_rse() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/RSE.gp5"))); - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Demo v5.gp5"))); - } - - //slides - #[test] - fn test_gp4_slides() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Slides.gp4"))); - } - #[test] - fn test_gp5_slides() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Slides.gp5"))); - } - - //strokes - #[test] - fn test_gp4_strokes() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Strokes.gp4"))); - } - #[test] - fn test_gp5_strokes() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Strokes.gp5"))); - } - - //vibrato - #[test] - fn test_gp4_vibrato() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/Vibrato.gp4"))); - } - - //voices - #[test] - fn test_gp5_voices() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Voices.gp5"))); - } - - //wah - #[test] - fn test_gp5_no_wah() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/No Wah.gp5"))); - } - #[test] - fn test_gp5_wah() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Wah.gp5"))); - } - #[test] - fn test_gp5_wah_m() { //Handle gradual wah-wah changes - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/Wah-m.gp5"))); - } - - //MuseScore tests - #[test] - fn test_gp5_all_percussion() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/all-percussion.gp5"))); - } - #[test] - fn test_gp5_basic_bend() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/basic-bend.gp5"))); - } - #[test] - fn test_gp5_beams_sterms_ledger_lines() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/beams-stems-ledger-lines.gp5"))); - } - #[test] - fn test_gp5_brush() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/brush.gp5"))); - } - #[test] - fn test_gp3_capo_fret() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/capo-fret.gp3"))); - } - #[test] - fn test_gp4_capo_fret() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/capo-fret.gp4"))); - } - #[test] - fn test_gp5_capo_fret() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/capo-fret.gp5"))); - } - #[test] - fn test_gp3_copyright() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/copyright.gp3"))); - } - #[test] - fn test_gp4_copyright() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/copyright.gp4"))); - } - #[test] - fn test_gp5_copyright() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/copyright.gp5"))); - } - #[test] - fn test_gp3_dotted_gliss() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/dotted-gliss.gp3"))); - } - #[test] - fn test_gp5_dotted_tuplets() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/dotted-tuplets.gp5"))); - } - #[test] - fn test_gp5_dynamic() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/dynamic.gp5"))); - } - #[test] - fn test_gp4_fade_in() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/fade-in.gp4"))); - } - #[test] - fn test_gp5_fade_in() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/fade-in.gp5"))); - } - #[test] - fn test_gp4_fingering() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/fingering.gp4"))); - } - #[test] - fn test_gp5_fingering() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/fingering.gp5"))); - } - #[test] - fn test_gp4_fret_diagram() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/fret-diagram.gp4"))); - } - #[test] - fn test_gp5_fret_diagram() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/fret-diagram.gp5"))); - } - #[test] - fn test_gp3_ghost_note() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/ghost_note.gp3"))); - } - #[test] - fn test_gp5_grace() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/grace.gp5"))); - } - #[test] - fn test_gp5_heavy_accent() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/heavy-accent.gp5"))); - } - #[test] - fn test_gp3_high_pitch() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/high-pitch.gp3"))); - } - #[test] - fn test_gp4_keysig() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/keysig.gp4"))); - } - #[test] - fn test_gp5_keysig() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/keysig.gp5"))); - } - #[test] - fn test_gp4_legato_slide() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/legato-slide.gp4"))); - } - #[test] - fn test_gp5_legato_slide() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/legato-slide.gp5"))); - } - #[test] - fn test_gp4_let_ring() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/let-ring.gp4"))); - } - #[test] - fn test_gp5_let_ring() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/let-ring.gp5"))); - } - #[test] - fn test_gp4_palm_mute() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/palm-mute.gp4"))); - } - #[test] - fn test_gp5_palm_mute() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/palm-mute.gp5"))); - } - #[test] - fn test_gp4_pick_up_down() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/pick-up-down.gp4"))); - } - #[test] - fn test_gp5_pick_up_down() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/pick-up-down.gp5"))); - } - #[test] - fn test_gp4_rest_centered() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/rest-centered.gp4"))); - } - #[test] - fn test_gp5_rest_centered() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/rest-centered.gp5"))); - } - #[test] - fn test_gp4_sforzato() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/sforzato.gp4"))); - } - #[test] - fn test_gp4_shift_slide() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/shift-slide.gp4"))); - } - #[test] - fn test_gp5_shift_slide() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/shift-slide.gp5"))); - } - #[test] - fn test_gp4_slide_in_above() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/slide-in-above.gp4"))); - } - #[test] - fn test_gp5_slide_in_above() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/slide-in-above.gp5"))); - } - #[test] - fn test_gp4_slide_in_below() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/slide-in-below.gp4"))); - } - #[test] - fn test_gp5_slide_in_below() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/slide-in-below.gp5"))); - } - #[test] - fn test_gp4_slide_out_down() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/slide-out-down.gp4"))); - } - #[test] - fn test_gp5_slide_out_down() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/slide-out-down.gp5"))); - } - #[test] - fn test_gp4_slide_out_up() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/slide-out-up.gp4"))); - } - #[test] - fn test_gp5_slide_out_up() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/slide-out-up.gp5"))); - } - #[test] - fn test_gp4_slur() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/slur.gp4"))); - } - #[test] - fn test_gp5_slur_notes_effect_mask() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/slur-notes-effect-mask.gp5"))); - } - #[test] - fn test_gp5_tap_slap_pop() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/tap-slap-pop.gp5"))); - } - #[test] - fn test_gp3_tempo() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/tempo.gp3"))); - } - #[test] - fn test_gp4_tempo() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/tempo.gp4"))); - } - #[test] - fn test_gp5_tempo() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/tempo.gp5"))); - } - #[test] - fn test_gp4_test_irr_tuplet() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/testIrrTuplet.gp4"))); - } - #[test] - fn test_gp5_tremolos() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/tremolos.gp5"))); - } - #[test] - fn test_gp4_trill() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/trill.gp4"))); - } - #[test] - fn test_gp4_tuplet_with_slur() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/tuplet-with-slur.gp4"))); - } - #[test] - fn test_gp5_vibrato() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/vibrato.gp5"))); - } - #[test] - fn test_gp3_volta() { - let mut song: Song = Song::default(); - song.read_gp3(&read_file(String::from("test/volta.gp3"))); - } - #[test] - fn test_gp4_volta() { - let mut song: Song = Song::default(); - song.read_gp4(&read_file(String::from("test/volta.gp4"))); - } - #[test] - fn test_gp5_volta() { - let mut song: Song = Song::default(); - song.read_gp5(&read_file(String::from("test/volta.gp5"))); - } - - //writing - #[test] - fn test_gp3_writing() { - let mut song = Song::default(); - let data = read_file(String::from("test/Chords.gp3")); - song.read_gp3(&data); - let out = song.write((3,0,0), None); - assert_eq!(out, data[0..out.len()]); - song.read_gp3(&out); - } -} +#[cfg(test)] +mod test_audit; diff --git a/lib/src/measure.rs b/lib/src/measure.rs deleted file mode 100644 index 06d6716..0000000 --- a/lib/src/measure.rs +++ /dev/null @@ -1,156 +0,0 @@ -use fraction::ToPrimitive; - -use crate::{beat::*, gp::*, key_signature::*, io::*, enums::*}; - -const MAX_VOICES: usize = 2; - -/// A measure header contains metadata for measures over multiple tracks. -#[derive(Debug,Clone)] -pub struct Measure { - pub number: usize, - pub start: i64, - pub has_double_bar: bool, - pub key_signature: KeySignature, - pub time_signature: TimeSignature, - pub track_index: usize, - pub header_index: usize, - pub clef: MeasureClef, - /// Max voice count is 2 - pub voices: Vec, - pub line_break: LineBreak, - - /*marker: Optional['Marker'] = None - isRepeatOpen: bool = False - repeatAlternative: int = 0 - repeatClose: int = -1 - tripletFeel: TripletFeel = TripletFeel.none - direction: Optional[DirectionSign] = None - fromDirection: Optional[DirectionSign] = None*/ -} -impl Default for Measure {fn default() -> Self { Measure { - number: 1, - start: DURATION_QUARTER_TIME, - has_double_bar: false, - key_signature: KeySignature::default(), //Cmajor - time_signature: TimeSignature::default(), - track_index: 0, - header_index: 0, - clef: MeasureClef::Treble, - voices: Vec::with_capacity(2), - line_break: LineBreak::None -}}} - -impl Song { - /// Read measures. Measures are written in the following order: - /// - measure 1/track 1 - /// - measure 1/track 2 - /// - ... - /// - measure 1/track m - /// - measure 2/track 1 - /// - measure 2/track 2 - /// - ... - /// - measure 2/track m - /// - ... - /// - measure n/track 1 - /// - measure n/track 2 - /// - ... - /// - measure n/track m - pub(crate) fn read_measures(&mut self, data: &[u8], seek: &mut usize) { - let mut start = DURATION_QUARTER_TIME; - for h in 0..self.measure_headers.len() { - self.measure_headers[h].start = start; - for t in 0..self.tracks.len() { - self.current_track = Some(t); - let mut m = Measure{track_index:t, header_index:h, ..Default::default()}; - self.current_measure_number = Some(m.number); - if self.version.number < (5,0,0) {self.read_measure(data, seek, &mut m, t);}else {self.read_measure_v5(data, seek, &mut m, t);} - self.tracks[t].measures.push(m); - } - //println!("read_measures(), start: {} \t numerator: {} \t denominator: {} \t length: {}", start, self.measure_headers[h].time_signature.numerator, self.measure_headers[h].time_signature.denominator.value, self.measure_headers[h].length()); - start += self.measure_headers[h].length(); - } - self.current_track = None; - self.current_measure_number = None; - } - - /// Read measure. The measure is written as number of beats followed by sequence of beats. - fn read_measure(&mut self, data: &[u8], seek: &mut usize, measure: &mut Measure, track_index: usize) { - //println!("read_measure()"); - let mut voice = Voice::default(); - self.current_voice_number = Some(1); - self.read_voice(data, seek, &mut voice, &mut measure.start, track_index); - self.current_voice_number = None; - measure.voices.push(voice); - /* - //read a voice - let beats = read_int(data, seek).to_usize().unwrap(); - - //println!("read_measure() read_voice(), beat count: {}", beats); - for i in 0..beats { - self.current_beat_number = Some(i + 1); - //println!("read_measure() read_voice(), start: {}", measure.start); - measure.start += self.read_beat(data, seek, &mut measure.voices[0], measure.start, track_index); - //println!("read_measure() read_voice(), start: {}", measure.start); - } - self.current_beat_number = None; - //end read a voice - self.current_voice_number = None;*/ - } - /// Read measure. Guitar Pro 5 stores twice more measures compared to Guitar Pro 3. One measure consists of two sub-measures for each of two voices. - /// - /// Sub-measures are followed by a `LineBreak` stored in `byte`. - fn read_measure_v5(&mut self, data: &[u8], seek: &mut usize, measure: &mut Measure, track_index: usize) { - //println!("read_measure_v5()"); - let mut start = measure.start; - for number in 0..MAX_VOICES { - self.current_voice_number = Some(number + 1); - //println!("read_measure_v5() {:?}",self.current_voice_number); - let mut voice = Voice::default(); - self.read_voice(data, seek, &mut voice, &mut start, track_index); - measure.voices.push(voice); - } - self.current_voice_number = None; - if *seek < data.len() {measure.line_break = get_line_break(read_byte(data, seek));} else {measure.line_break = get_line_break(0);} - } - - fn read_voice(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: &mut i64, track_index: usize) { - let beats = read_int(data, seek).to_usize().unwrap(); - for i in 0..beats { - self.current_beat_number = Some(i + 1); - //println!("read_measure() read_voice(), start: {}", measure.start); - *start += if self.version.number < (5,0,0) {self.read_beat(data, seek, voice, *start, track_index)} else {self.read_beat_v5(data, seek, voice, &mut *start, track_index)}; - //println!("read_measure() read_voice(), start: {}", measure.start); - } - self.current_beat_number = None; - } - - pub(crate) fn write_measures(&self, data: &mut Vec, version: &(u8,u8,u8)) { - for i in 0..self.tracks.len() { - //self.current_track = Some(i); - for m in 0..self.tracks[i].measures.len() { - //self.current_measure_number = Some(self.tracks[i].measure.number); - self.write_measure(data, i, m, version); - } - } - //self.current_track = None; - //self.current_measure_number = None; - } - fn write_measure(&self, data: &mut Vec, track: usize, measure: usize, version: &(u8,u8,u8)) { - //self.current_voice_number = Some(1); - if version.0 < 5 {self.write_voice(data, track, measure,0, version);} - else { - for v in 0..self.tracks[track].measures[measure].voices.len() {self.write_voice(data, track, measure,v, version);} //self.current_voice_number = Some(v+1); - if version.0 == 5 {write_byte(data, from_line_break(&self.tracks[track].measures[measure].line_break));} - } - //self.current_voice_number = None; - } - fn write_voice(&self, data: &mut Vec, track: usize, measure: usize, voice: usize, version: &(u8,u8,u8)) { - write_i32(data, self.tracks[track].measures[measure].voices[voice].beats.len().to_i32().unwrap()); - for b in 0..self.tracks[track].measures[measure].voices[voice].beats.len() { - //self.current_beat_number = Some(b+1); - if version.0 ==3 {self.write_beat_v3(data, &self.tracks[track].measures[measure].voices[voice].beats[b]);} - else {self.write_beat(data, &self.tracks[track].measures[measure].voices[voice].beats[b], &self.tracks[track].strings, version);} - //self.current_beat_number = None; - } - } -} diff --git a/lib/src/midi.rs b/lib/src/midi.rs deleted file mode 100644 index 7391fa9..0000000 --- a/lib/src/midi.rs +++ /dev/null @@ -1,128 +0,0 @@ -use fraction::ToPrimitive; - -use crate::{io::*, gp::*}; - -//MIDI channels - -pub const CHANNEL_DEFAULT_NAMES: [&str; 128] = ["Piano", "Bright Piano", "Electric Grand", "Honky Tonk Piano", "Electric Piano 1", "Electric Piano 2", - "Harpsichord", "Clavinet", "Celesta", - "Glockenspiel", - "Music Box", - "Vibraphone", "Marimba", "Xylophone", "Tubular Bell", - "Dulcimer", - "Hammond Organ", "Perc Organ", "Rock Organ", "Church Organ", "Reed Organ", - "Accordion", - "Harmonica", - "Tango Accordion", - "Nylon Str Guitar", "Steel String Guitar", "Jazz Electric Gtr", "Clean Guitar", "Muted Guitar", "Overdrive Guitar", "Distortion Guitar", "Guitar Harmonics", - "Acoustic Bass", "Fingered Bass", "Picked Bass", "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Syn Bass 1", "Syn Bass 2", - "Violin", "Viola", "Cello", "Contrabass", - "Tremolo Strings", "Pizzicato Strings", - "Orchestral Harp", - "Timpani", - "Ensemble Strings", "Slow Strings", "Synth Strings 1", "Synth Strings 2", - "Choir Aahs", "Voice Oohs", "Syn Choir", - "Orchestra Hit", - "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", "Brass Ensemble", "Syn Brass 1", "Syn Brass 2", - "Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", - "Oboe", "English Horn", "Bassoon", "Clarinet", "Piccolo", "Flute", "Recorder", "Pan Flute", "Bottle Blow", "Shakuhachi", "Whistle", "Ocarina", - "Syn Square Wave", "Syn Saw Wave", "Syn Calliope", "Syn Chiff", "Syn Charang", "Syn Voice", "Syn Fifths Saw", "Syn Brass and Lead", - "Fantasia", "Warm Pad", "Polysynth", "Space Vox", "Bowed Glass", "Metal Pad", "Halo Pad", "Sweep Pad", "Ice Rain", "Soundtrack", "Crystal", "Atmosphere", - "Brightness", "Goblins", "Echo Drops", "Sci Fi", - "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", - "Bag Pipe", - "Fiddle", - "Shanai", - "Tinkle Bell", - "Agogo", - "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Syn Drum", "Reverse Cymbal", - "Guitar Fret Noise", "Breath Noise", - "Seashore", "Bird", "Telephone", "Helicopter", "Applause", "Gunshot"]; - -pub const DEFAULT_PERCUSSION_CHANNEL: u8 = 9; -/// A MIDI channel describes playing data for a track. -#[derive(Debug,Copy,Clone)] -pub struct MidiChannel { - pub channel: u8, - pub effect_channel: u8, - instrument: i32, - pub volume: i8, - pub balance: i8, - pub chorus: i8, - pub reverb: i8, - pub phaser: i8, - pub tremolo: i8, - pub bank: u8, -} -impl Default for MidiChannel { - fn default() -> Self { MidiChannel { channel: 0, effect_channel: 1, instrument: 25, volume: 104, balance: 64, chorus: 0, reverb: 0, phaser: 0, tremolo: 0, bank: 0, }} -} -impl MidiChannel { - pub(crate) fn is_percussion_channel(self) -> bool { - (self.channel % 16) == DEFAULT_PERCUSSION_CHANNEL - } - pub(crate) fn set_instrument(mut self, instrument: i32) { - if instrument == -1 && self.is_percussion_channel() { self.instrument = 0; } - else {self.instrument = instrument;} - } - - pub(crate) fn _get_instrument(self) -> i32 {self.instrument} - pub(crate) fn get_instrument_name(&self) -> String {String::from(CHANNEL_DEFAULT_NAMES[self.instrument.to_usize().unwrap()])} //TODO: FIXME: does not seems OK -} - -impl Song{ - /// Read all the MIDI channels - pub(crate) fn read_midi_channels(&mut self, data: &[u8], seek: &mut usize) { for i in 0u8..64u8 { self.channels.push(self.read_midi_channel(data, seek, i)); } } - /// Read MIDI channels. Guitar Pro format provides 64 channels (4 MIDI ports by 16 hannels), the channels are stored in this order: - ///`port1/channel1`, `port1/channel2`, ..., `port1/channel16`, `port2/channel1`, ..., `port4/channel16`. - /// - /// Each channel has the following form: - /// - /// * **Instrument**: `int` - /// * **Volume**: `byte` - /// * **Balance**: `byte` - /// * **Chorus**: `byte` - /// * **Reverb**: `byte` - /// * **Phaser**: `byte` - /// * **Tremolo**: `byte` - /// * **blank1**: `byte` => Backward compatibility with version 3.0 - /// * **blank2**: `byte` => Backward compatibility with version 3.0 - pub(crate) fn read_midi_channel(&self, data: &[u8], seek: &mut usize, channel: u8) -> MidiChannel { - let instrument = read_int(data, seek); - let mut c = MidiChannel{channel, effect_channel: channel, ..Default::default()}; - c.volume = read_signed_byte(data, seek); c.balance = read_signed_byte(data, seek); - c.chorus = read_signed_byte(data, seek); c.reverb = read_signed_byte(data, seek); c.phaser = read_signed_byte(data, seek); c.tremolo = read_signed_byte(data, seek); - c.set_instrument(instrument); - //println!("Channel: {}\t Volume: {}\tBalance: {}\tInstrument={}, {}, {}", c.channel, c.volume, c.balance, instrument, c.get_instrument(), c.get_instrument_name()); - *seek += 2; //Backward compatibility with version 3.0 - c - } - - /// Read MIDI channel. MIDI channel in Guitar Pro is represented by two integers. First is zero-based number of channel, second is zero-based number of channel used for effects. - pub(crate) fn read_channel(&mut self, data: &[u8], seek: &mut usize) -> usize { //TODO: fixme for writing - let index = read_int(data, seek) - 1; - let effect_channel = read_int(data, seek) - 1; - if 0 <= index && index < self.channels.len().to_i32().unwrap() { - if self.channels[index.to_usize().unwrap()].instrument < 0 {self.channels[index.to_usize().unwrap()].instrument = 0;} - if !self.channels[index.to_usize().unwrap()].is_percussion_channel() {self.channels[index.to_usize().unwrap()].effect_channel = effect_channel.to_u8().unwrap();} - } - index.to_usize().unwrap() - } - - pub(crate) fn write_midi_channels(&self, data: &mut Vec) { - for i in 0..self.channels.len() { - println!("writing channel: {:?}", self.channels[i]); - if self.channels[i].is_percussion_channel() && self.channels[i].instrument == 0 {write_i32(data, -1);} - else {write_i32(data, self.channels[i].instrument);} - write_signed_byte(data, Self::from_channel_short(self.channels[i].volume)); - write_signed_byte(data, Self::from_channel_short(self.channels[i].balance)); - write_signed_byte(data, Self::from_channel_short(self.channels[i].chorus)); - write_signed_byte(data, Self::from_channel_short(self.channels[i].reverb)); - write_signed_byte(data, Self::from_channel_short(self.channels[i].phaser)); - write_signed_byte(data, Self::from_channel_short(self.channels[i].tremolo)); - write_placeholder_default(data, 2); //Backward compatibility with version 3.0 - } - } - - fn from_channel_short(data: i8) -> i8 { ((data >> 3) - 1).clamp(-128, 127) + 1 } -} \ No newline at end of file diff --git a/lib/src/beat.rs b/lib/src/model/beat.rs similarity index 84% rename from lib/src/beat.rs rename to lib/src/model/beat.rs index e9c7bdb..8a10b90 100644 --- a/lib/src/beat.rs +++ b/lib/src/model/beat.rs @@ -1,6 +1,7 @@ use fraction::ToPrimitive; -use crate::{mix_table::*, effects::*, chord::*, key_signature::*, note::*, io::*, gp::*, enums::*}; +use crate::{model::{mix_table::*, effects::*, chord::*, key_signature::*, note::*, enums::*, song::*}, io::primitive::*}; +use crate::error::GpResult; /// Parameters of beat display #[derive(Debug,Clone,PartialEq,Eq)] @@ -21,8 +22,9 @@ impl Default for BeatDisplay { fn default() -> Self { BeatDisplay { break_beam:f pub struct BeatStroke { pub direction: BeatStrokeDirection, pub value: u16, + pub swap: bool, } -impl Default for BeatStroke { fn default() -> Self { BeatStroke { direction: BeatStrokeDirection::None, value: 0 }}} +impl Default for BeatStroke { fn default() -> Self { BeatStroke { direction: BeatStrokeDirection::None, value: 0, swap: false }}} impl BeatStroke { pub(crate) fn swap_direction(&mut self) { if self.direction == BeatStrokeDirection::Up {self.direction = BeatStrokeDirection::Down} @@ -117,7 +119,24 @@ impl Beat { } } -impl Song { +pub trait SongBeatOps { + fn read_beat(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: i64, track_index: usize) -> GpResult; + fn read_beat_v5(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: &mut i64, track_index: usize) -> GpResult; + fn read_beat_effects_v3(&self, data: &[u8], seek: &mut usize, note_effect: &mut NoteEffect) -> GpResult; + fn read_beat_effects_v4(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_beat_stroke(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn stroke_value(&self, value: i8) -> u8; + fn read_tremolo_bar(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn write_beat_v3(&self, data: &mut Vec, beat: &Beat); + fn write_beat(&self, data: &mut Vec, beat: &Beat, strings: &[(i8,i8)], version: &(u8,u8,u8)); + fn write_beat_effect_v3(&self, data: &mut Vec, beat: &Beat); + fn write_beat_effect_v4(&self, data: &mut Vec, beat: &Beat, version: &(u8,u8,u8)); + fn write_tremolo_bar(&self, data: &mut Vec, bar: &Option); + fn write_beat_stroke(&self, data: &mut Vec, stroke: &BeatStroke, version: &(u8,u8,u8)); + fn from_stroke_value(value: u8) -> i8; +} + +impl SongBeatOps for Song { /// Read beat. The first byte is the beat flags. It lists the data present in the current beat: /// - *0x01*: dotted notes- *0x02*: presence of a chord diagram /// - *0x04*: presence of a text @@ -134,8 +153,8 @@ impl Song { /// - Text: `int-byte-size-string`. /// - Beat effects. See `BeatEffects::read()`. /// - Mix table change effect. See `MixTableChange::read()`. - pub(crate) fn read_beat(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: i64, track_index: usize) -> i64 { - let flags = read_byte(data, seek); + fn read_beat(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: i64, track_index: usize) -> GpResult { + let flags = read_byte(data, seek)?; //println!("read_beat(), flags: {} \t seek: {}", flags, *seek); //get a beat let mut b = 0; @@ -149,24 +168,24 @@ impl Song { voice.beats.push(Beat{start: Some(start), ..Default::default() }); b = voice.beats.len() - 1; } - - if (flags & 0x40) == 0x40 { voice.beats[b].status = get_beat_status(read_byte(data, seek));} //else { voice.beats[b].status = BeatStatus::Normal;} - let duration = read_duration(data, seek, flags); + + if (flags & 0x40) == 0x40 { voice.beats[b].status = get_beat_status(read_byte(data, seek)?);} //else { voice.beats[b].status = BeatStatus::Normal;} + let duration = read_duration(data, seek, flags)?; let mut note_effect = NoteEffect::default(); - if (flags & 0x02) == 0x02 {voice.beats[b].effect.chord = Some(self.read_chord(data, seek, self.tracks[track_index].strings.len().to_u8().unwrap()));} - if (flags & 0x04) == 0x04 {voice.beats[b].text = read_int_byte_size_string(data, seek);} + if (flags & 0x02) == 0x02 {voice.beats[b].effect.chord = Some(self.read_chord(data, seek, self.tracks[track_index].strings.len().to_u8().unwrap())?);} + if (flags & 0x04) == 0x04 {voice.beats[b].text = read_int_byte_size_string(data, seek)?;} if (flags & 0x08) == 0x08 { let chord = voice.beats[b].effect.chord.clone(); - if self.version.number.0 == 3 {voice.beats[b].effect = self.read_beat_effects_v3(data, seek, &mut note_effect); } - else {voice.beats[b].effect = self.read_beat_effects_v4(data, seek);} + if self.version.number.0 == 3 {voice.beats[b].effect = self.read_beat_effects_v3(data, seek, &mut note_effect)?; } + else {voice.beats[b].effect = self.read_beat_effects_v4(data, seek)?;} voice.beats[b].effect.chord = chord; } if (flags & 0x10) == 0x10 { - let mtc = self.read_mix_table_change(data, seek); + let mtc = self.read_mix_table_change(data, seek)?; voice.beats[b].effect.mix_table_change = Some(mtc); } - self.read_notes(data, seek, track_index, &mut voice.beats[b], &duration, note_effect); - if voice.beats[b].status == BeatStatus::Empty {0} else {duration.time().to_i64().unwrap()} + self.read_notes(data, seek, track_index, &mut voice.beats[b], &duration, note_effect)?; + Ok(if voice.beats[b].status == BeatStatus::Empty {0} else {duration.time().to_i64().unwrap()}) } /// Read beat. First, beat is read is in Guitar Pro 3 `guitarpro.gp3.readBeat`. Then it is followed by set of flags stored in `short`. /// - *0x0001*: break beams @@ -183,12 +202,12 @@ impl Song { /// - *0x1000*: break secondary tuplet /// - *0x2000*: force tuplet bracket /// - Break secondary beams: `byte`. Appears if flag at *0x0800* is set. Signifies how much beams should be broken. - pub(crate) fn read_beat_v5(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: &mut i64, track_index: usize) -> i64 { - let duration = self.read_beat(data, seek, voice, *start, track_index); + fn read_beat_v5(&mut self, data: &[u8], seek: &mut usize, voice: &mut Voice, start: &mut i64, track_index: usize) -> GpResult { + let duration = self.read_beat(data, seek, voice, *start, track_index)?; //get the beat used in read_beat() let b = voice.beats.len() - 1; - let flags2 = read_short(data, seek); + let flags2 = read_short(data, seek)?; //println!("read_beat_v5(), flags2: {} \t seek: {}", flags2, *seek); if (flags2 & 0x0010) == 0x0010 {voice.beats[b].octave = Octave::Ottava;} if (flags2 & 0x0020) == 0x0020 {voice.beats[b].octave = Octave::OttavaBassa;} @@ -203,9 +222,9 @@ impl Song { if (flags2 & 0x0008) == 0x0008 {voice.beats[b].display.beam_direction = VoiceDirection::Up;} if (flags2 & 0x0200) == 0x0200 {voice.beats[b].display.tuplet_bracket = TupletBracket::Start;} if (flags2 & 0x0400) == 0x0400 {voice.beats[b].display.tuplet_bracket = TupletBracket::End;} - if (flags2 & 0x0800) == 0x0800 {voice.beats[b].display.break_secondary = read_byte(data, seek);} + if (flags2 & 0x0800) == 0x0800 {voice.beats[b].display.break_secondary = read_byte(data, seek)?;} - duration + Ok(duration) } /// Read beat effects. The first byte is effects flags: @@ -221,22 +240,22 @@ impl Song { /// - *2*: slap /// - *3*: pop /// - Beat stroke direction. See `BeatStroke::read()` - fn read_beat_effects_v3(&self, data: &[u8], seek: &mut usize, note_effect: &mut NoteEffect) -> BeatEffects { + fn read_beat_effects_v3(&self, data: &[u8], seek: &mut usize, note_effect: &mut NoteEffect) -> GpResult { //println!("read_beat_effects()"); let mut be = BeatEffects::default(); - let flags = read_byte(data, seek); + let flags = read_byte(data, seek)?; note_effect.vibrato = (flags & 0x01) == 0x01 || note_effect.vibrato; be.vibrato = (flags & 0x02) == 0x02 || be.vibrato; be.fade_in = (flags & 0x10) == 0x10; if (flags & 0x20) == 0x20 { - be.slap_effect = get_slap_effect(read_byte(data, seek)); - if be.slap_effect == SlapEffect::None {be.tremolo_bar = Some(self.read_tremolo_bar(data, seek));} else {read_int(data, seek);} + be.slap_effect = get_slap_effect(read_byte(data, seek)?)?; + if be.slap_effect == SlapEffect::None {be.tremolo_bar = Some(self.read_tremolo_bar(data, seek)?);} else {read_int(data, seek)?;} } - if (flags & 0x40) == 0x40 {be.stroke = self.read_beat_stroke(data, seek);} + if (flags & 0x40) == 0x40 {be.stroke = self.read_beat_stroke(data, seek)?;} //In GP3 harmonics apply to the whole beat, not the individual notes. Here we set the noteEffect for all the notes in the beat. if (flags & 0x04) == 0x04 {note_effect.harmonic = Some(HarmonicEffect::default());} if (flags & 0x08) == 0x08 {note_effect.harmonic = Some(HarmonicEffect {kind: HarmonicType::Artificial, ..Default::default()});} - be + Ok(be) } ///Read beat effects. Beat effects are read using two byte flags. The first byte of flags is: /// - *0x01*: *blank* @@ -263,27 +282,27 @@ impl Song { /// - Tremolo bar. See `readTremoloBar`. /// - Beat stroke. See `readBeatStroke`. /// - Pick stroke: `signed-byte`. For value mapping see `BeatStrokeDirection`. - fn read_beat_effects_v4(&self, data: &[u8], seek: &mut usize) -> BeatEffects { + fn read_beat_effects_v4(&self, data: &[u8], seek: &mut usize) -> GpResult { let mut be = BeatEffects::default(); - let flags1 = read_signed_byte(data, seek); - let flags2 = read_signed_byte(data, seek); + let flags1 = read_signed_byte(data, seek)?; + let flags2 = read_signed_byte(data, seek)?; be.vibrato = (flags1 & 0x02) == 0x02 || be.vibrato; be.fade_in = (flags1 & 0x10) == 0x10; - if (flags1 & 0x20) == 0x20 {be.slap_effect = get_slap_effect(read_signed_byte(data, seek).to_u8().unwrap());} - if (flags2 & 0x04) == 0x04 {be.tremolo_bar = self.read_bend_effect(data, seek);} - if (flags1 & 0x40) == 0x40 {be.stroke = self.read_beat_stroke(data, seek);} + if (flags1 & 0x20) == 0x20 {be.slap_effect = get_slap_effect(read_signed_byte(data, seek)?.to_u8().unwrap())?;} + if (flags2 & 0x04) == 0x04 {be.tremolo_bar = self.read_bend_effect(data, seek)?;} + if (flags1 & 0x40) == 0x40 {be.stroke = self.read_beat_stroke(data, seek)?;} be.has_rasgueado = (flags2 &0x01) == 0x01; - if (flags2 & 0x02) == 0x02 {be.pick_stroke = get_beat_stroke_direction(read_signed_byte(data, seek));} + if (flags2 & 0x02) == 0x02 {be.pick_stroke = get_beat_stroke_direction(read_signed_byte(data, seek)?)?;} //println!("Beat effect: {:?}", be); - be + Ok(be) } /// Read beat stroke. Beat stroke consists of two `Bytes ` which correspond to stroke up /// and stroke down speed. See `BeatStrokeDirection` for value mapping. - fn read_beat_stroke(&self, data: &[u8], seek: &mut usize) -> BeatStroke { + fn read_beat_stroke(&self, data: &[u8], seek: &mut usize) -> GpResult { //println!("read_beat_stroke()"); let mut bs = BeatStroke::default(); - let down = read_signed_byte(data, seek); - let up = read_signed_byte(data, seek); + let down = read_signed_byte(data, seek)?; + let up = read_signed_byte(data, seek)?; if up > 0 { bs.direction = BeatStrokeDirection::Up; bs.value = self.stroke_value(up).to_u16().unwrap(); @@ -293,7 +312,7 @@ impl Song { bs.value = self.stroke_value(down).to_u16().unwrap(); } if self.version.number >= (5,0,0) {bs.swap_direction();} - bs + Ok(bs) } fn stroke_value(&self, value: i8) -> u8 { @@ -309,19 +328,19 @@ impl Song { } /// Read tremolo bar beat effect. The only type of tremolo bar effect Guitar Pro 3 supports is `dip `. The value of the /// effect is encoded in `Int` and shows how deep tremolo bar is pressed. - fn read_tremolo_bar(&self, data: &[u8], seek: &mut usize) -> BendEffect { + fn read_tremolo_bar(&self, data: &[u8], seek: &mut usize) -> GpResult { //println!("read_tremolo_bar()"); let mut be = BendEffect{kind: BendType::Dip, ..Default::default()}; - be.value = read_int(data, seek).to_i16().unwrap(); + be.value = read_int(data, seek)?.to_i16().unwrap(); be.points.push(BendPoint{ position: 0, value: 0, ..Default::default() }); be.points.push(BendPoint{ position: BEND_EFFECT_MAX_POSITION / 2, value: (-f32::from(be.value) / GP_BEND_SEMITONE).round().to_i8().unwrap(), ..Default::default() }); be.points.push(BendPoint{ position: BEND_EFFECT_MAX_POSITION, value: 0, ..Default::default() }); - be + Ok(be) } - pub(crate) fn write_beat_v3(&self, data: &mut Vec, beat: &Beat) { + fn write_beat_v3(&self, data: &mut Vec, beat: &Beat) { let mut flags = 0u8; if beat.duration.dotted {flags |= 0x01;} if beat.effect.is_chord() {flags |= 0x02;} @@ -342,7 +361,7 @@ impl Song { self.write_notes(data, beat, &Vec::new(), &(3,0,0)); } - pub(crate) fn write_beat(&self, data: &mut Vec, beat: &Beat, strings: &[(i8,i8)], version: &(u8,u8,u8)) { + fn write_beat(&self, data: &mut Vec, beat: &Beat, strings: &[(i8,i8)], version: &(u8,u8,u8)) { let mut flags = 0u8; if beat.duration.dotted {flags |= 0x01;} if beat.effect.is_chord() {flags |= 0x02;} @@ -433,8 +452,8 @@ impl Song { if version.0 == 5 {stroke.swap_direction();} let mut stroke_down = 0i8; let mut stroke_up = 0i8; - if stroke.direction == BeatStrokeDirection::Up { stroke_up = Song::from_stroke_value(stroke.value.to_u8().unwrap()); } - else if stroke.direction == BeatStrokeDirection::Down { stroke_down = Song::from_stroke_value(stroke.value.to_u8().unwrap()); } + if stroke.direction == BeatStrokeDirection::Up { stroke_up = Self::from_stroke_value(stroke.value.to_u8().unwrap()); } + else if stroke.direction == BeatStrokeDirection::Down { stroke_down = Self::from_stroke_value(stroke.value.to_u8().unwrap()); } write_signed_byte(data, stroke_down); write_signed_byte(data, stroke_up); } diff --git a/lib/src/chord.rs b/lib/src/model/chord.rs similarity index 82% rename from lib/src/chord.rs rename to lib/src/model/chord.rs index 1e58096..218c3df 100644 --- a/lib/src/chord.rs +++ b/lib/src/model/chord.rs @@ -1,6 +1,7 @@ use fraction::ToPrimitive; -use crate::{io::*, gp::*, enums::*}; +use crate::{io::primitive::*, model::{song::*, enums::*}}; +use crate::error::GpResult; /// A chord annotation for beats #[derive(Debug,Clone,PartialEq,Eq,Default)] @@ -64,7 +65,7 @@ impl PitchClass { let value = p.just % 12; //println!("PitchClass(), value: {}", value); p.note = if value >= 0 {String::from(SHARP_NOTES[value as usize])} else {String::from(SHARP_NOTES[(12 + value).to_usize().unwrap()])}; //try: note = SHARP_NOTES[p.value]; except KeyError: note = FLAT_NOTES[p.value]; - //if FLAT_NOTES[p.value] == ¬e {note=String::from(FLAT_NOTES[p.value]); p.sharp = false;} + //if FLAT_NOTES[p.value] == ¬e {note=String::from(FLAT_NOTES[p.value]); p.sharp = false;} if p.note.ends_with('b') {accidental2 = -1; p.sharp = false;} else if p.note.ends_with('#') {accidental2 = 1;} else {accidental2 = 0;} @@ -77,6 +78,7 @@ impl PitchClass { if sharp.is_none() { p.sharp = p.accidental >= 0; } p } + #[allow(dead_code)] pub(crate) fn from_note(note: String) -> PitchClass { let mut p = PitchClass {note, just:0, accidental:0, value:-1, sharp: true,}; if p.note.ends_with('b') {p.accidental = -1; p.sharp = false;} @@ -84,7 +86,7 @@ impl PitchClass { for i in 0i8..12i8 { if SHARP_NOTES[i as usize] == p.note || FLAT_NOTES[i as usize] == p.note {p.value = i; break;} } - let pitch = p.value - p.accidental; + let pitch = p.value - p.accidental; p.just = pitch % 12; p.value = p.just + p.accidental; p @@ -98,34 +100,49 @@ impl std::fmt::Display for PitchClass { } } -impl Song { - /// Read chord diagram. First byte is chord header. If it's set to 0, then following chord is written in +pub trait SongChordOps { + fn read_chord(&self, data: &[u8], seek: &mut usize, string_count: u8) -> GpResult; + fn read_old_format_chord(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) -> GpResult<()>; + fn read_new_format_chord_v3(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) -> GpResult<()>; + fn read_new_format_chord_v4(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) -> GpResult<()>; + fn write_chord(&self, data: &mut Vec, beat: &crate::model::beat::Beat); + fn write_new_format_chord(&self, data: &mut Vec, chord: &Chord); + fn write_old_format_chord(&self, data: &mut Vec, chord: &Chord); + fn write_chord_v4(&self, data: &mut Vec, beat: &crate::model::beat::Beat); +} + +impl SongChordOps for Song { + /// Read chord diagram. First byte is chord header. If it's set to 0, then following chord is written in /// default (GP3) format. If chord header is set to 1, then chord diagram in encoded in more advanced (GP4) format. - pub(crate) fn read_chord(&self, data: &[u8], seek: &mut usize, string_count: u8) -> Chord { + fn read_chord(&self, data: &[u8], seek: &mut usize, string_count: u8) -> GpResult { let mut c = Chord {length: string_count, strings: vec![-1; string_count.into()], ..Default::default()}; for _ in 0..string_count {c.strings.push(-1);} - c.new_format = Some(read_bool(data, seek)); + c.new_format = Some(read_bool(data, seek)?); if c.new_format == Some(true) { - if self.version.number.0 == 3 { self.read_new_format_chord_v3(data, seek, &mut c); } - else { self.read_new_format_chord_v4(data, seek, &mut c);} + if self.version.number.0 == 3 { self.read_new_format_chord_v3(data, seek, &mut c)?; } + else { self.read_new_format_chord_v4(data, seek, &mut c)?;} + } + else { + if self.version.number.0 == 3 { read_byte(data, seek)?; } + self.read_old_format_chord(data, seek, &mut c)?; } - else {self.read_old_format_chord(data, seek, &mut c);} - c + Ok(c) } /// Read chord diagram encoded in GP3 format. Chord diagram is read as follows: /// - Name: `int-byte-size-string`. Name of the chord, e.g. *Em*. /// - First fret: `int`. The fret from which the chord is displayed in chord editor. /// - List of frets: 6 `ints`. Frets are listed in order: fret on the string 1, fret on the string 2, ..., fret on the - /// string 6. If string is untouched then the values of fret is *-1*. - fn read_old_format_chord(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) { - chord.name = read_int_size_string(data, seek); - chord.first_fret = Some(read_int(data, seek).to_u8().unwrap()); + /// string 6. If string is untouched then the values of fret is *-1*. + fn read_old_format_chord(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) -> GpResult<()> { + chord.name = read_int_byte_size_string(data, seek)?; + chord.first_fret = Some(read_int(data, seek)? as u8); if chord.first_fret.is_some() { for i in 0u8..6u8 { - let fret = read_int(data, seek).to_i8().unwrap(); + let fret = read_int(data, seek)? as i8; if i < chord.strings.len().to_u8().unwrap() {chord.strings.push(fret);} //chord.strings[i] = fret; } } + Ok(()) } /// Read new-style (GP4) chord diagram. New-style chord diagram is read as follows: /// - Sharp: `bool`. If true, display all semitones as sharps, otherwise display as flats. @@ -151,36 +168,37 @@ impl Song { /// - Barre end string: 2 `Ints `. /// - Omissions: 7 `Bools `. If the value is true then note is played in chord. /// - Blank space, 1 `byte`. - fn read_new_format_chord_v3(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) { - chord.sharp = Some(read_bool(data, seek)); + fn read_new_format_chord_v3(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) -> GpResult<()> { + chord.sharp = Some(read_bool(data, seek)?); *seek += 3; - chord.root = Some(PitchClass::from(read_int(data, seek).to_i8().unwrap(), None, chord.sharp)); - chord.kind = Some(get_chord_type(read_int(data, seek).to_u8().unwrap())); - chord.extension = Some(get_chord_extension(read_int(data, seek).to_u8().unwrap())); - chord.bass = Some(PitchClass::from(read_int(data, seek).to_i8().unwrap(), None, chord.sharp)); - chord.tonality = Some(get_chord_alteration(read_int(data, seek).to_u8().unwrap())); - chord.add = Some(read_bool(data, seek)); - chord.name = read_byte_size_string(data, seek, 22); - chord.fifth = Some(get_chord_alteration(read_int(data, seek).to_u8().unwrap())); - chord.ninth = Some(get_chord_alteration(read_int(data, seek).to_u8().unwrap())); - chord.eleventh = Some(get_chord_alteration(read_int(data, seek).to_u8().unwrap())); - chord.first_fret = Some(read_int(data, seek).to_u8().unwrap()); + chord.root = Some(PitchClass::from(read_int(data, seek)?.to_i8().unwrap(), None, chord.sharp)); + chord.kind = Some(get_chord_type(read_int(data, seek)?.to_u8().unwrap())); + chord.extension = Some(get_chord_extension(read_int(data, seek)?.to_u8().unwrap())); + chord.bass = Some(PitchClass::from(read_int(data, seek)?.to_i8().unwrap(), None, chord.sharp)); + chord.tonality = Some(get_chord_alteration(read_int(data, seek)?.to_u8().unwrap())?); + chord.add = Some(read_bool(data, seek)?); + chord.name = read_byte_size_string(data, seek, 22)?; + chord.fifth = Some(get_chord_alteration(read_int(data, seek)?.to_u8().unwrap())?); + chord.ninth = Some(get_chord_alteration(read_int(data, seek)?.to_u8().unwrap())?); + chord.eleventh = Some(get_chord_alteration(read_int(data, seek)?.to_u8().unwrap())?); + chord.first_fret = Some(read_int(data, seek)?.to_u8().unwrap()); for i in 0u8..6u8 { - let fret = read_int(data, seek).to_i8().unwrap(); + let fret = read_int(data, seek)?.to_i8().unwrap(); if i < chord.strings.len().to_u8().unwrap() {chord.strings.push(fret);} //chord.strings[i] = fret; } //barre - let barre_count = read_int(data, seek).to_usize().unwrap(); + let barre_count = read_int(data, seek)?.to_usize().unwrap(); let mut barre_frets: Vec = Vec::with_capacity(2); let mut barre_starts: Vec = Vec::with_capacity(2); let mut barre_ends: Vec = Vec::with_capacity(2); - for _ in 0u8..2u8 {barre_frets.push(read_int(data, seek));} - for _ in 0u8..2u8 {barre_starts.push(read_int(data, seek));} - for _ in 0u8..2u8 {barre_ends.push(read_int(data, seek));} + for _ in 0u8..2u8 {barre_frets.push(read_int(data, seek)?);} + for _ in 0u8..2u8 {barre_starts.push(read_int(data, seek)?);} + for _ in 0u8..2u8 {barre_ends.push(read_int(data, seek)?);} for i in 0..barre_count {chord.barres.push(Barre{fret:barre_frets[i].to_i8().unwrap(), start:barre_starts[i].to_i8().unwrap(), end:barre_ends[i].to_i8().unwrap()});} - for _ in 0u8..7u8 {chord.omissions.push(read_bool(data, seek));} + for _ in 0u8..7u8 {chord.omissions.push(read_bool(data, seek)?);} *seek += 1; + Ok(()) } /// Read new-style (GP4) chord diagram. New-style chord diagram is read as follows: @@ -208,42 +226,43 @@ impl Song { /// - Omissions: 7 `Bools `. If the value is true then note is played in chord. /// - Blank space, 1 `byte`. /// - Fingering: 7 `SignedBytes `. For value mapping, see `Fingering`. - fn read_new_format_chord_v4(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) { - chord.sharp = Some(read_bool(data, seek)); + fn read_new_format_chord_v4(&self, data: &[u8], seek: &mut usize, chord: &mut Chord) -> GpResult<()> { + chord.sharp = Some(read_bool(data, seek)?); *seek += 3; - chord.root = Some(PitchClass::from(read_byte(data, seek).to_i8().unwrap(), None, chord.sharp)); - chord.kind = Some(get_chord_type(read_byte(data, seek))); - chord.extension = Some(get_chord_extension(read_byte(data, seek))); - let i = read_int(data, seek); + chord.root = Some(PitchClass::from(read_byte(data, seek)?.to_i8().unwrap(), None, chord.sharp)); + chord.kind = Some(get_chord_type(read_byte(data, seek)?)); + chord.extension = Some(get_chord_extension(read_byte(data, seek)?)); + let i = read_int(data, seek)?; //println!("{:?}", i); chord.bass = Some(PitchClass::from(i.to_i8().unwrap(), None, chord.sharp)); - chord.tonality = Some(get_chord_alteration(read_int(data, seek).to_u8().unwrap())); - chord.add = Some(read_bool(data, seek)); - chord.name = read_byte_size_string(data, seek, 22); - chord.fifth = Some(get_chord_alteration(read_byte(data, seek))); - chord.ninth = Some(get_chord_alteration(read_byte(data, seek))); - chord.eleventh = Some(get_chord_alteration(read_byte(data, seek))); - chord.first_fret = Some(read_int(data, seek).to_u8().unwrap()); + chord.tonality = Some(get_chord_alteration(read_int(data, seek)?.to_u8().unwrap())?); + chord.add = Some(read_bool(data, seek)?); + chord.name = read_byte_size_string(data, seek, 22)?; + chord.fifth = Some(get_chord_alteration(read_byte(data, seek)?)?); + chord.ninth = Some(get_chord_alteration(read_byte(data, seek)?)?); + chord.eleventh = Some(get_chord_alteration(read_byte(data, seek)?)?); + chord.first_fret = Some(read_int(data, seek)?.to_u8().unwrap()); for i in 0u8..7u8 { - let fret = read_int(data, seek).to_i8().unwrap(); + let fret = read_int(data, seek)?.to_i8().unwrap(); if i < chord.strings.len().to_u8().unwrap() {chord.strings.push(fret);} //chord.strings[i] = fret; } //barre - let barre_count = read_byte(data, seek).to_usize().unwrap(); + let barre_count = read_byte(data, seek)?.to_usize().unwrap(); let mut barre_frets: Vec = Vec::with_capacity(5); let mut barre_starts: Vec = Vec::with_capacity(5); let mut barre_ends: Vec = Vec::with_capacity(5); - for _ in 0u8..5u8 {barre_frets.push(read_byte(data, seek));} - for _ in 0u8..5u8 {barre_starts.push(read_byte(data, seek));} - for _ in 0u8..5u8 {barre_ends.push(read_byte(data, seek));} + for _ in 0u8..5u8 {barre_frets.push(read_byte(data, seek)?);} + for _ in 0u8..5u8 {barre_starts.push(read_byte(data, seek)?);} + for _ in 0u8..5u8 {barre_ends.push(read_byte(data, seek)?);} for i in 0..barre_count {chord.barres.push(Barre{fret:barre_frets[i].to_i8().unwrap(), start:barre_starts[i].to_i8().unwrap(), end:barre_ends[i].to_i8().unwrap()});} - for _ in 0u8..7u8 {chord.omissions.push(read_bool(data, seek));} + for _ in 0u8..7u8 {chord.omissions.push(read_bool(data, seek)?);} *seek += 1; - for _ in 0u8..7u8 {chord.fingerings.push(get_fingering(read_signed_byte(data, seek)));} - chord.show = Some(read_bool(data, seek)); + for _ in 0u8..7u8 {chord.fingerings.push(get_fingering(read_signed_byte(data, seek)?));} + chord.show = Some(read_bool(data, seek)?); + Ok(()) } - pub(crate) fn write_chord(&self, data: &mut Vec, beat: &crate::beat::Beat) { + fn write_chord(&self, data: &mut Vec, beat: &crate::model::beat::Beat) { if let Some(c) = &beat.effect.chord { write_bool(data, c.new_format == Some(true)); if c.new_format == Some(true) {self.write_new_format_chord(data, c);} @@ -258,7 +277,7 @@ impl Song { if let Some(r) = &chord.root {write_i32(data, r.value.to_i32().unwrap());} else {write_i32(data, 0);} //chord type - if let Some(t) = &chord.kind {write_i32(data, from_chord_type(t).to_i32().unwrap());} + if let Some(t) = &chord.kind {write_i32(data, from_chord_type(t).to_i32().unwrap());} else {write_i32(data, 0);} //chord extension if let Some(e) = &chord.extension {write_i32(data, from_chord_extension(e).to_i32().unwrap());} @@ -316,7 +335,7 @@ impl Song { } } - pub(crate) fn write_chord_v4(&self, data: &mut Vec, beat: &crate::beat::Beat) { + fn write_chord_v4(&self, data: &mut Vec, beat: &crate::model::beat::Beat) { if let Some(c) = &beat.effect.chord { write_signed_byte(data, 1); //signify GP4 chord format write_bool(data, c.sharp == Some(true)); @@ -325,7 +344,7 @@ impl Song { if let Some(r) = &c.root {write_i32(data, r.value.to_i32().unwrap());} else {write_i32(data, 0);} //chord type - if let Some(t) = &c.kind {write_i32(data, from_chord_type(t).to_i32().unwrap());} + if let Some(t) = &c.kind {write_i32(data, from_chord_type(t).to_i32().unwrap());} else {write_i32(data, 0);} //chord extension if let Some(e) = &c.extension {write_i32(data, from_chord_extension(e).to_i32().unwrap());} @@ -383,7 +402,7 @@ impl Song { #[cfg(test)] mod test { - use crate::chord::PitchClass; + use crate::model::chord::PitchClass; #[test] fn test_pitch_1() { diff --git a/lib/src/effects.rs b/lib/src/model/effects.rs similarity index 71% rename from lib/src/effects.rs rename to lib/src/model/effects.rs index dd61994..150b687 100644 --- a/lib/src/effects.rs +++ b/lib/src/model/effects.rs @@ -1,6 +1,6 @@ use fraction::ToPrimitive; -use crate::{io::*, gp::*, chord::*, key_signature::*, enums::*}; +use crate::{error::GpResult, io::primitive::*, model::{song::*, chord::*, key_signature::*, enums::*}}; /// A single point within the BendEffect #[derive(Debug,Clone,PartialEq, Eq, Default)] @@ -70,7 +70,7 @@ pub struct GraceEffect { impl Default for GraceEffect { fn default() -> Self { GraceEffect {duration: 1, fret: 0, is_dead: false, is_on_beat: false, transition: GraceEffectTransition::None, velocity: DEFAULT_VELOCITY }}} impl GraceEffect { pub(crate) fn _duration_time(self) -> i16 { - (f32::from(crate::key_signature::DURATION_QUARTER_TIME.to_i16().unwrap()) / 16f32 * f32::from(self.duration)).to_i16().expect("Cannot get bend point time").to_i16().unwrap() + (f32::from(crate::model::key_signature::DURATION_QUARTER_TIME.to_i16().unwrap()) / 16f32 * f32::from(self.duration)).to_i16().expect("Cannot get bend point time").to_i16().unwrap() } } @@ -94,12 +94,12 @@ pub struct TremoloPickingEffect {pub duration: Duration,} /// - *1*: eighth /// - *2*: sixteenth /// - *3*: thirtySecond -fn from_tremolo_value(value: i8) -> u8 { +fn from_tremolo_value(value: i8) -> GpResult { match value { - 1 => DURATION_EIGHTH, - 3 => DURATION_SIXTEENTH, - 2 => DURATION_THIRTY_SECOND, - _ => panic!("Cannot get tremolo value") + 1 => Ok(DURATION_EIGHTH), + 3 => Ok(DURATION_SIXTEENTH), + 2 => Ok(DURATION_THIRTY_SECOND), + _ => Err(crate::error::GpError::InvalidValue { context: "tremolo picking value", value: value as i64 }) } } @@ -111,7 +111,34 @@ pub struct TrillEffect { } //impl Default for TrillEffect { fn default() -> Self {TrillEffect { fret:0, duration: Duration::default() }}} -impl Song { +pub trait SongEffectOps { + fn read_bend_effect(&self, data: &[u8], seek: &mut usize) -> GpResult>; + fn read_grace_effect(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_grace_effect_v5(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_tremolo_picking(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_slides_v5(&self, data: &[u8], seek: &mut usize) -> GpResult>; + fn read_harmonic(&self, data: &[u8], seek: &mut usize, note: &crate::model::note::Note) -> GpResult; + fn read_harmonic_v5(&mut self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_trill(&self, data: &[u8], seek: &mut usize) -> GpResult; + // write methods stay the same + fn write_bend(&self, data: &mut Vec, bend: &Option); + fn write_grace(&self, data: &mut Vec, grace: &Option); + fn write_grace_v5(&self, data: &mut Vec, grace: &Option); + fn write_harmonic(&self, data: &mut Vec, note: &crate::model::note::Note, strings: &[(i8,i8)]); + fn write_harmonic_v5(&self, data: &mut Vec, note: &crate::model::note::Note, strings: &[(i8,i8)]); + fn write_slides_v5(&self, data: &mut Vec, slides: &[SlideType]); +} + +fn from_trill_period(period: i8) -> GpResult { + match period { + 1 => Ok(DURATION_SIXTEENTH), + 2 => Ok(DURATION_THIRTY_SECOND), + 3 => Ok(DURATION_SIXTY_FOURTH), + _ => Err(crate::error::GpError::InvalidValue { context: "trill period", value: period as i64 }), + }.map(|v| v.to_u16().unwrap()) +} + +impl SongEffectOps for Song { /// Read a bend. It is encoded as: /// - Bend type: `signed-byte`. See BendType. /// - Bend value: `int`. @@ -120,18 +147,18 @@ impl Song { /// * Position: `int`. Shows where point is set along *x*-axis. /// * Value: `int`. Shows where point is set along *y*-axis. /// * Vibrato: `bool`. - pub(crate) fn read_bend_effect(&self, data: &[u8], seek: &mut usize) -> Option { - let mut be = BendEffect{kind: get_bend_type(read_signed_byte(data, seek)), ..Default::default()}; - be.value = read_int(data, seek).to_i16().unwrap(); - let count: u8 = read_int(data, seek).to_u8().unwrap(); + fn read_bend_effect(&self, data: &[u8], seek: &mut usize) -> GpResult> { + let mut be = BendEffect{kind: get_bend_type(read_signed_byte(data, seek)?)?, ..Default::default()}; + be.value = read_int(data, seek)?.to_i16().unwrap_or(0); + let count: u8 = read_int(data, seek)?.to_u8().unwrap_or(0); for _ in 0..count { - let mut bp = BendPoint{position: (f32::from(read_int(data, seek).to_i16().unwrap()) * f32::from(BEND_EFFECT_MAX_POSITION) / GP_BEND_POSITION).round().to_u8().unwrap(), ..Default::default()}; - bp.value = (f32::from(read_int(data, seek).to_i16().unwrap()) * f32::from(be.semitone_length) / GP_BEND_SEMITONE).round().to_i8().unwrap(); - bp.vibrato = read_bool(data, seek); + let mut bp = BendPoint{position: (f32::from(read_int(data, seek)?.to_i16().unwrap_or(0)) * f32::from(BEND_EFFECT_MAX_POSITION) / GP_BEND_POSITION).round().to_u8().unwrap_or(0), ..Default::default()}; + bp.value = (f32::from(read_int(data, seek)?.to_i16().unwrap_or(0)) * f32::from(be.semitone_length) / GP_BEND_SEMITONE).round().to_i8().unwrap_or(0); + bp.vibrato = read_bool(data, seek)?; be.points.push(bp); } //println!("read_bend_effect(): {:?}", be); - if count > 0 {Some(be)} else {None} + if count > 0 {Ok(Some(be))} else {Ok(None)} } /// Read grace note effect. /// @@ -147,15 +174,15 @@ impl Song { /// * 8: fff /// - Transition: `byte`. This variable determines the transition type used to make the grace note: `0: None`, `1: Slide`, `2: Bend`, `3: Hammer` (defined in `GraceEffectTransition`). /// - Duration: `byte`. Determines the grace note duration, coded this way: `3: Sixteenth note`, `2: Twenty-fourth note`, `1: Thirty-second note`. - pub(crate) fn read_grace_effect(&self, data: &[u8], seek: &mut usize) -> GraceEffect { + fn read_grace_effect(&self, data: &[u8], seek: &mut usize) -> GpResult { //println!("read_grace_effect()"); - let mut g = GraceEffect{fret: read_signed_byte(data, seek), ..Default::default()}; - g.velocity = unpack_velocity(read_byte(data, seek).to_i16().unwrap()); - g.duration = 1 << (7 - read_byte(data, seek)); + let mut g = GraceEffect{fret: read_signed_byte(data, seek)?, ..Default::default()}; + g.velocity = unpack_velocity(read_byte(data, seek)?.to_i16().unwrap()); + g.duration = 1 << (7 - read_byte(data, seek)?); //g.duration = 1 << (7 - read_byte(data, seek)); g.is_dead = g.fret == -1; - g.transition = get_grace_effect_transition(read_signed_byte(data, seek)); - g + g.transition = get_grace_effect_transition(read_signed_byte(data, seek)?)?; + Ok(g) } /// Read grace note effect. @@ -169,22 +196,22 @@ impl Song { /// - Flags: `byte`. /// - *0x01*: grace note is muted (dead) /// - *0x02*: grace note is on beat - pub(crate) fn read_grace_effect_v5(&self, data: &[u8], seek: &mut usize) -> GraceEffect { - let mut g = GraceEffect{fret: read_byte(data, seek).to_i8().unwrap(), ..Default::default()}; - g.velocity = unpack_velocity(read_byte(data, seek).to_i16().unwrap()); - g.transition = get_grace_effect_transition(read_byte(data, seek).to_i8().unwrap()); - g.duration = 1 << (7 - read_byte(data, seek)); - let flags = read_byte(data, seek); + fn read_grace_effect_v5(&self, data: &[u8], seek: &mut usize) -> GpResult { + let mut g = GraceEffect{fret: read_byte(data, seek)?.to_i8().unwrap(), ..Default::default()}; + g.velocity = unpack_velocity(read_byte(data, seek)?.to_i16().unwrap()); + g.transition = get_grace_effect_transition(read_byte(data, seek)?.to_i8().unwrap())?; + g.duration = 1 << (7 - read_byte(data, seek)?); + let flags = read_byte(data, seek)?; g.is_dead = (flags &0x01) == 0x01; g.is_on_beat = (flags &0x02) == 0x02; - g + Ok(g) } /// Read tremolo picking. Tremolo constists of picking speed encoded in `signed-byte`. For value mapping refer to `from_tremolo_value()`. - pub(crate) fn read_tremolo_picking(&self, data: &[u8], seek: &mut usize) -> TremoloPickingEffect { + fn read_tremolo_picking(&self, data: &[u8], seek: &mut usize) -> GpResult { let mut tp = TremoloPickingEffect::default(); - tp.duration.value = from_tremolo_value(read_signed_byte(data, seek)).to_u16().unwrap(); - tp + tp.duration.value = from_tremolo_value(read_signed_byte(data, seek)?)?.to_u16().unwrap(); + Ok(tp) } ///// Read slides. Slide is encoded in `signed-byte`. See `SlideType` for value mapping. //pub(crate) fn read_slides(&self, data: &[u8], seek: &mut usize) -> SlideType { get_slide_type(read_signed_byte(data, seek)) } @@ -196,8 +223,8 @@ impl Song { /// - *0x08*: slide out upwards /// - *0x10*: slide into from below /// - *0x20*: slide into from above - pub(crate) fn read_slides_v5(&self, data: &[u8], seek: &mut usize) -> Vec { - let t = read_byte(data, seek); + fn read_slides_v5(&self, data: &[u8], seek: &mut usize) -> GpResult> { + let t = read_byte(data, seek)?; let mut v: Vec = Vec::with_capacity(6); if (t & 0x01) == 0x01 {v.push(SlideType::ShiftSlideTo);} if (t & 0x02) == 0x02 {v.push(SlideType::LegatoSlideTo);} @@ -205,7 +232,7 @@ impl Song { if (t & 0x08) == 0x08 {v.push(SlideType::OutUpWards);} if (t & 0x10) == 0x10 {v.push(SlideType::IntoFromBelow);} if (t & 0x20) == 0x20 {v.push(SlideType::IntoFromAbove);} - v + Ok(v) } /// Read harmonic. Harmonic is encoded in `signed-byte`. Values correspond to: /// - *1*: natural harmonic @@ -215,9 +242,9 @@ impl Song { /// - *15*: artificial harmonic on (*n + 5*)th fret /// - *17*: artificial harmonic on (*n + 7*)th fret /// - *22*: artificial harmonic on (*n + 12*)th fret - pub(crate) fn read_harmonic(&self, data: &[u8], seek: &mut usize, note: &crate::note::Note) -> HarmonicEffect { + fn read_harmonic(&self, data: &[u8], seek: &mut usize, note: &crate::model::note::Note) -> GpResult { let mut he = HarmonicEffect::default(); - match read_signed_byte(data, seek) { + match read_signed_byte(data, seek)? { 1 => he.kind = HarmonicType::Natural, 3 => he.kind = HarmonicType::Tapped, 4 => he.kind = HarmonicType::Pinch, @@ -237,9 +264,9 @@ impl Song { he.octave = Some(Octave::Ottava); he.kind = HarmonicType::Artificial; }, - _ => panic!("Cannot read harmonic type"), + v => return Err(crate::error::GpError::InvalidValue { context: "harmonic type", value: v as i64 }), }; - he + Ok(he) } /// Read harmonic. First `byte` is harmonic type: @@ -256,48 +283,40 @@ impl Song { /// /// If harmonic type is tapped: /// - Fret: `byte`. - pub(crate) fn read_harmonic_v5(&mut self, data: &[u8], seek: &mut usize) -> HarmonicEffect { + fn read_harmonic_v5(&mut self, data: &[u8], seek: &mut usize) -> GpResult { let mut he = HarmonicEffect::default(); - match read_signed_byte(data, seek) { + match read_signed_byte(data, seek)? { 1 => he.kind = HarmonicType::Natural, 2 => { // C = 0, D = 2, E = 4, F = 5... // b = -1, # = 1 // loco = 0, 8va = 1, 15ma = 2 he.kind = HarmonicType::Artificial; - let semitone = read_byte(data, seek).to_i8().unwrap(); - let accidental = read_signed_byte(data, seek); + let semitone = read_byte(data, seek)?.to_i8().unwrap(); + let accidental = read_signed_byte(data, seek)?; he.pitch = Some(PitchClass::from(semitone, Some(accidental), None)); - he.octave = Some(get_octave(read_byte(data, seek))); + he.octave = Some(get_octave(read_byte(data, seek)?)?); }, 3 => { he.kind = HarmonicType::Tapped; - he.fret = Some(read_byte(data, seek).to_i8().unwrap()); + he.fret = Some(read_byte(data, seek)?.to_i8().unwrap()); }, 4 => he.kind = HarmonicType::Pinch, 5 => he.kind = HarmonicType::Semi, - _ => panic!("Cannot read harmonic type"), + v => return Err(crate::error::GpError::InvalidValue { context: "harmonic type", value: v as i64 }), }; - he + Ok(he) } /// Read trill. /// - Fret: `signed-byte`. /// - Period: `signed-byte`. See `from_trill_period`. - pub(crate) fn read_trill(&self, data: &[u8], seek: &mut usize) -> TrillEffect { - let mut t = TrillEffect{fret: read_signed_byte(data, seek), ..Default::default()}; - t.duration.value = Self::from_trill_period(read_signed_byte(data, seek)); - t - } - fn from_trill_period(period: i8) -> u16 { - match period { - 1 => DURATION_SIXTEENTH, - 2 => DURATION_THIRTY_SECOND, - 3 => DURATION_SIXTY_FOURTH, - _ => panic!("Cannot get trill period"), - }.to_u16().unwrap() + fn read_trill(&self, data: &[u8], seek: &mut usize) -> GpResult { + let mut t = TrillEffect{fret: read_signed_byte(data, seek)?, ..Default::default()}; + t.duration.value = from_trill_period(read_signed_byte(data, seek)?)?; + Ok(t) } - pub(crate) fn write_bend(&self, data: &mut Vec, bend: &Option) { + fn write_bend(&self, data: &mut Vec, bend: &Option) { if let Some(b) = bend { write_signed_byte(data, from_bend_type(&b.kind)); write_i32(data, b.value.to_i32().unwrap()); @@ -309,14 +328,14 @@ impl Song { } } } - pub(crate) fn write_grace(&self, data: &mut Vec, grace: &Option) { + fn write_grace(&self, data: &mut Vec, grace: &Option) { let g = grace.clone().unwrap(); write_signed_byte(data, g.fret); write_byte(data, pack_velocity(g.velocity).to_u8().unwrap()); write_byte(data, g.duration.leading_zeros().to_u8().unwrap()); //8 - grace.duration.bit_length() write_signed_byte(data, from_grace_effect_transition(&g.transition)); } - pub(crate) fn write_grace_v5(&self, data: &mut Vec, grace: &Option) { + fn write_grace_v5(&self, data: &mut Vec, grace: &Option) { let g = grace.clone().unwrap(); write_byte(data, g.fret.to_u8().unwrap()); write_byte(data, pack_velocity(g.velocity).to_u8().unwrap()); @@ -327,7 +346,7 @@ impl Song { if g.is_on_beat {flags |= 0x02;} write_byte(data, flags); } - pub(crate) fn write_harmonic(&self, data: &mut Vec, note: &crate::note::Note, strings: &[(i8,i8)]) { + fn write_harmonic(&self, data: &mut Vec, note: &crate::model::note::Note, strings: &[(i8,i8)]) { if let Some(h) = ¬e.effect.harmonic { let mut byte = from_harmonic_type(&h.kind); if h.kind != HarmonicType::Artificial { @@ -342,7 +361,7 @@ impl Song { write_signed_byte(data, byte); } } - pub(crate) fn write_harmonic_v5(&self, data: &mut Vec, note: &crate::note::Note, strings: &[(i8,i8)]) { + fn write_harmonic_v5(&self, data: &mut Vec, note: &crate::model::note::Note, strings: &[(i8,i8)]) { if let Some(h) = ¬e.effect.harmonic { write_signed_byte(data, from_harmonic_type(&h.kind)); if h.kind == HarmonicType::Artificial && (h.pitch.is_none() || h.octave.is_none()) { @@ -355,7 +374,7 @@ impl Song { else if h.kind == HarmonicType::Tapped {write_byte(data, h.fret.unwrap().to_u8().unwrap());} } } - pub(crate) fn write_slides_v5(&self, data: &mut Vec, slides: &[SlideType]) { + fn write_slides_v5(&self, data: &mut Vec, slides: &[SlideType]) { let mut st = 0u8; //slide type for s in slides { if s == &SlideType::ShiftSlideTo {st |= 0x01;} diff --git a/lib/src/enums.rs b/lib/src/model/enums.rs similarity index 79% rename from lib/src/enums.rs rename to lib/src/model/enums.rs index 8080741..e0f13bb 100644 --- a/lib/src/enums.rs +++ b/lib/src/model/enums.rs @@ -1,14 +1,16 @@ +use crate::error::{GpError, GpResult}; + /// An enumeration of different triplet feels. #[repr(u8)] #[derive(Debug,Clone,PartialEq,Eq)] pub enum TripletFeel { None, Eighth, Sixteenth } -pub(crate) fn get_triplet_feel(value: i8) -> TripletFeel { +pub(crate) fn get_triplet_feel(value: i8) -> GpResult { match value { - 0 => TripletFeel::None, - 1 => TripletFeel::Eighth, - 2 => TripletFeel::Sixteenth, - _ => panic!("Invalid triplet feel"), + 0 => Ok(TripletFeel::None), + 1 => Ok(TripletFeel::Eighth), + 2 => Ok(TripletFeel::Sixteenth), + _ => Err(GpError::InvalidValue { context: "triplet feel", value: value as i64 }), } } pub(crate) fn from_triplet_feel(value: &TripletFeel) -> u8 { @@ -55,16 +57,16 @@ pub enum SlideType { OutDownwards, OutUpWards } -pub(crate) fn get_slide_type(value: i8) -> SlideType { +pub(crate) fn get_slide_type(value: i8) -> GpResult { match value { - -2 => SlideType::IntoFromAbove, - -1 => SlideType::IntoFromBelow, - 0 => SlideType::None, - 1 => SlideType::ShiftSlideTo, - 2 => SlideType::LegatoSlideTo, - 3 => SlideType::OutDownwards, - 4 => SlideType::OutUpWards, - _ => panic!("Invalid slide type"), + -2 => Ok(SlideType::IntoFromAbove), + -1 => Ok(SlideType::IntoFromBelow), + 0 => Ok(SlideType::None), + 1 => Ok(SlideType::ShiftSlideTo), + 2 => Ok(SlideType::LegatoSlideTo), + 3 => Ok(SlideType::OutDownwards), + 4 => Ok(SlideType::OutUpWards), + _ => Err(GpError::InvalidValue { context: "slide type", value: value as i64 }), } } pub(crate) fn from_slide_type(value: &SlideType) -> i8 { @@ -133,14 +135,14 @@ pub enum TupletBracket {None, Start, End} #[repr(u8)] #[derive(Debug,Clone,PartialEq,Eq)] pub enum Octave { None, Ottava, Quindicesima, OttavaBassa, QuindicesimaBassa } -pub(crate) fn get_octave(value: u8) -> Octave { +pub(crate) fn get_octave(value: u8) -> GpResult { match value { - 0 => Octave::None, - 1 => Octave::Ottava, - 2 => Octave::Quindicesima, - 3 => Octave::OttavaBassa, - 4 => Octave::QuindicesimaBassa, - _ => panic!("Cannot get octave value"), + 0 => Ok(Octave::None), + 1 => Ok(Octave::Ottava), + 2 => Ok(Octave::Quindicesima), + 3 => Ok(Octave::OttavaBassa), + 4 => Ok(Octave::QuindicesimaBassa), + _ => Err(GpError::InvalidValue { context: "octave", value: value as i64 }), } } pub(crate) fn from_octave(value: &Octave) -> u8 { @@ -157,12 +159,12 @@ pub(crate) fn from_octave(value: &Octave) -> u8 { #[repr(u8)] #[derive(Debug,Clone,PartialEq,Eq)] pub enum BeatStrokeDirection { None, Up, Down } -pub(crate) fn get_beat_stroke_direction(value: i8) -> BeatStrokeDirection { +pub(crate) fn get_beat_stroke_direction(value: i8) -> GpResult { match value { - 0 => BeatStrokeDirection::None, - 1 => BeatStrokeDirection::Up, - 2 => BeatStrokeDirection::Down, - _ => panic!("Cannot read beat stroke direction"), + 0 => Ok(BeatStrokeDirection::None), + 1 => Ok(BeatStrokeDirection::Up), + 2 => Ok(BeatStrokeDirection::Down), + _ => Err(GpError::InvalidValue { context: "beat stroke direction", value: value as i64 }), } } pub(crate) fn from_beat_stroke_direction(value: &BeatStrokeDirection) -> i8 { @@ -176,13 +178,13 @@ pub(crate) fn from_beat_stroke_direction(value: &BeatStrokeDirection) -> i8 { #[repr(u8)] #[derive(Debug,Clone,PartialEq,Eq)] pub enum SlapEffect { None, Tapping, Slapping, Popping } -pub(crate) fn get_slap_effect(value: u8) -> SlapEffect { +pub(crate) fn get_slap_effect(value: u8) -> GpResult { match value { - 0 => SlapEffect::None, - 1 => SlapEffect::Tapping, - 2 => SlapEffect::Slapping, - 3 => SlapEffect::Popping, - _ => panic!("Cannot read slap effect for the beat effects"), + 0 => Ok(SlapEffect::None), + 1 => Ok(SlapEffect::Tapping), + 2 => Ok(SlapEffect::Slapping), + 3 => Ok(SlapEffect::Popping), + _ => Err(GpError::InvalidValue { context: "slap effect", value: value as i64 }), } } pub(crate) fn from_slap_effect(value: &SlapEffect) -> u8 { @@ -289,12 +291,12 @@ pub enum ChordAlteration { /// Augmented. Augmented, } -pub(crate) fn get_chord_alteration(value: u8) -> ChordAlteration { +pub(crate) fn get_chord_alteration(value: u8) -> GpResult { match value { - 0 => ChordAlteration::Perfect, - 1 => ChordAlteration::Diminished, - 2 => ChordAlteration::Augmented, - _ => panic!("Cannot read chord fifth (new format)"), + 0 => Ok(ChordAlteration::Perfect), + 1 => Ok(ChordAlteration::Diminished), + 2 => Ok(ChordAlteration::Augmented), + _ => Err(GpError::InvalidValue { context: "chord alteration", value: value as i64 }), } } pub(crate) fn from_chord_alteration(value: &ChordAlteration) -> u8 { @@ -413,21 +415,21 @@ pub enum BendType { /// Release the bar down. ReleaseDown } -pub(crate) fn get_bend_type(value: i8) -> BendType { +pub(crate) fn get_bend_type(value: i8) -> GpResult { match value { - 0 => BendType::None, - 1 => BendType::Bend, - 2 => BendType::BendRelease, - 3 => BendType::BendReleaseBend, - 4 => BendType::Prebend, - 5 => BendType::PrebendRelease, - 6 => BendType::Dip, - 7 => BendType::Dive, - 8 => BendType::ReleaseUp, - 9 => BendType::InvertedDip, - 10 => BendType::Return, - 11 => BendType::ReleaseDown, - _ => panic!("Cannot read bend type"), + 0 => Ok(BendType::None), + 1 => Ok(BendType::Bend), + 2 => Ok(BendType::BendRelease), + 3 => Ok(BendType::BendReleaseBend), + 4 => Ok(BendType::Prebend), + 5 => Ok(BendType::PrebendRelease), + 6 => Ok(BendType::Dip), + 7 => Ok(BendType::Dive), + 8 => Ok(BendType::ReleaseUp), + 9 => Ok(BendType::InvertedDip), + 10 => Ok(BendType::Return), + 11 => Ok(BendType::ReleaseDown), + _ => Err(GpError::InvalidValue { context: "bend type", value: value as i64 }), } } pub(crate) fn from_bend_type(value: &BendType) -> i8 { @@ -460,13 +462,13 @@ pub enum GraceEffectTransition { ///Perform a hammer on. Hammer } -pub(crate) fn get_grace_effect_transition(value: i8) -> GraceEffectTransition { +pub(crate) fn get_grace_effect_transition(value: i8) -> GpResult { match value { - 0 => GraceEffectTransition::None, - 1 => GraceEffectTransition::Slide, - 2 => GraceEffectTransition::Bend, - 3 => GraceEffectTransition::Hammer, - _ => panic!("Cannot get transition for the grace effect"), + 0 => Ok(GraceEffectTransition::None), + 1 => Ok(GraceEffectTransition::Slide), + 2 => Ok(GraceEffectTransition::Bend), + 3 => Ok(GraceEffectTransition::Hammer), + _ => Err(GpError::InvalidValue { context: "grace effect transition", value: value as i64 }), } } pub(crate) fn from_grace_effect_transition(value: &GraceEffectTransition) -> i8 { @@ -501,15 +503,15 @@ pub(crate) fn from_harmonic_type(value: &HarmonicType) -> i8 { #[repr(u8)] #[derive(Debug,Clone)] pub enum Accentuation { None, VerySoft, Soft, Medium, Strong, VeryStrong } -pub(crate) fn get_accentuation(value: u8) -> Accentuation { +pub(crate) fn get_accentuation(value: u8) -> GpResult { match value { - 0 => Accentuation::None, - 1 => Accentuation::VerySoft, - 2 => Accentuation::Soft, - 3 => Accentuation::Medium, - 4 => Accentuation::Strong, - 5 => Accentuation::VeryStrong, - _ => panic!("Cannot get accentuation"), + 0 => Ok(Accentuation::None), + 1 => Ok(Accentuation::VerySoft), + 2 => Ok(Accentuation::Soft), + 3 => Ok(Accentuation::Medium), + 4 => Ok(Accentuation::Strong), + 5 => Ok(Accentuation::VeryStrong), + _ => Err(GpError::InvalidValue { context: "accentuation", value: value as i64 }), } } pub(crate) fn from_accentuation(value: &Accentuation) -> u8 { diff --git a/lib/src/model/headers.rs b/lib/src/model/headers.rs new file mode 100644 index 0000000..f325542 --- /dev/null +++ b/lib/src/model/headers.rs @@ -0,0 +1,640 @@ +use std::collections::HashMap; + +use fraction::ToPrimitive; + +use crate::error::GpResult; +use crate::{ + io::primitive::*, + model::{enums::*, key_signature::*, song::*}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Version { + pub data: String, + pub number: (u8, u8, u8), + pub clipboard: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Clipboard { + pub start_measure: i32, + pub stop_measure: i32, + pub start_track: i32, + pub stop_track: i32, + pub start_beat: i32, + pub stop_beat: i32, + pub sub_bar_copy: bool, +} +impl Default for Clipboard { + fn default() -> Self { + Clipboard { + start_measure: 1, + stop_measure: 1, + start_track: 1, + stop_track: 1, + start_beat: 1, + stop_beat: 1, + sub_bar_copy: false, + } + } +} + +#[derive(Debug, Clone)] +pub struct MeasureHeader { + pub number: u16, + pub start: i64, + pub time_signature: TimeSignature, + pub tempo: i32, + pub marker: Option, + pub repeat_open: bool, + pub repeat_alternative: u8, + pub repeat_close: i8, + pub triplet_feel: TripletFeel, + pub direction: Option, + /// Tonality of the measure + pub key_signature: KeySignature, + pub double_bar: bool, + /// Fermatas from GPIF (GP6/GP7) + pub fermatas: Vec<(String, String)>, + /// Free time (no metronome) from GPIF (GP6/GP7) + pub free_time: bool, +} +impl Default for MeasureHeader { + fn default() -> Self { + MeasureHeader { + number: 1, + start: DURATION_QUARTER_TIME, + tempo: 0, + repeat_open: false, + repeat_alternative: 0, + repeat_close: -1, + triplet_feel: TripletFeel::None, + direction: None, + key_signature: KeySignature::default(), + double_bar: false, + marker: None, + time_signature: TimeSignature { + numerator: 4, + denominator: Duration::default(), + beams: vec![2, 2, 2, 2], + }, + fermatas: Vec::new(), + free_time: false, + } + } +} +impl MeasureHeader { + #[allow(dead_code)] + pub(crate) fn length(&self) -> i64 { + self.time_signature.numerator.to_i64().unwrap() + * crate::model::key_signature::DURATION_QUARTER_TIME + * 4 + / self.time_signature.denominator.value.to_i64().unwrap() + } + pub(crate) fn _end(&self) -> i64 { + self.start + self.length() + } +} + +/// A marker annotation for beats. +#[derive(Debug, Clone)] +pub struct Marker { + pub title: String, + pub color: i32, +} +impl Default for Marker { + fn default() -> Self { + Marker { + title: "Section".to_owned(), + color: 0xff0000, + } + } +} +/// Read a marker. The markers are written in two steps: +/// - first is written an integer equal to the marker's name length + 1 +/// - then a string containing the marker's name. Finally the marker's color is written. +fn read_marker(data: &[u8], seek: &mut usize) -> GpResult { + let mut marker = Marker { + title: read_int_size_string(data, seek)?, + ..Default::default() + }; + marker.color = read_color(data, seek)?; + Ok(marker) +} + +/// This class can store the information about a group of measures which are repeated. +#[derive(Debug, Clone, Default)] +pub struct RepeatGroup { + /// List of measure header indexes. + pub measure_headers: Vec, + pub closings: Vec, + pub openings: Vec, + pub is_closed: bool, +} + +pub trait SongHeaderOps { + fn _add_measure_header(&mut self, header: MeasureHeader); + fn read_clipboard(&mut self, data: &[u8], seek: &mut usize) -> GpResult>; + fn read_measure_headers( + &mut self, + data: &[u8], + seek: &mut usize, + measure_count: usize, + ) -> GpResult<()>; + fn read_measure_headers_v5( + &mut self, + data: &[u8], + seek: &mut usize, + measure_count: usize, + directions: &(HashMap, HashMap), + ) -> GpResult<()>; + fn read_measure_header( + &mut self, + data: &[u8], + seek: &mut usize, + number: usize, + previous: Option, + ) -> GpResult<(MeasureHeader, u8)>; + fn read_measure_header_v5( + &mut self, + data: &[u8], + seek: &mut usize, + number: usize, + previous: Option, + ) -> GpResult<(MeasureHeader, u8)>; + fn read_repeat_alternative(&mut self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_repeat_alternative_v5(&mut self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_directions( + &self, + data: &[u8], + seek: &mut usize, + ) -> GpResult<(HashMap, HashMap)>; + fn write_measure_headers(&self, data: &mut Vec, version: &(u8, u8, u8)); + fn write_measure_header( + &self, + data: &mut Vec, + header: usize, + previous: Option, + version: &(u8, u8, u8), + ); + fn write_clipboard(&self, data: &mut Vec, version: &(u8, u8, u8)); + fn write_directions(&self, data: &mut Vec); +} + +impl SongHeaderOps for Song { + fn _add_measure_header(&mut self, header: MeasureHeader) { + // if the group is closed only the next upcoming header can reopen the group in case of a repeat alternative, so we remove the current group + //TODO: if header.repeat_open or self.current_repeat_group.is_closed && header.repeat_alternative <= 0 {self.current_repeat_group = RepeatGroup::default();} + self.measure_headers.push(header); + } + + fn read_clipboard(&mut self, data: &[u8], seek: &mut usize) -> GpResult> { + if !self.version.clipboard { + return Ok(None); + } + let mut c = Clipboard { + start_measure: read_int(data, seek)?, + ..Default::default() + }; + c.stop_measure = read_int(data, seek)?; + c.start_track = read_int(data, seek)?; + c.stop_track = read_int(data, seek)?; + if self.version.number.0 == 5 { + c.start_beat = read_int(data, seek)?; + c.stop_beat = read_int(data, seek)?; + c.sub_bar_copy = read_int(data, seek)? != 0; + } + println!("read_clipboard(): {:?}", c); + Ok(Some(c)) + } + + /// Read measure headers. The *measures* are written one after another, their number have been specified previously. + /// * `measure_count`: number of measures to expect. + fn read_measure_headers( + &mut self, + data: &[u8], + seek: &mut usize, + measure_count: usize, + ) -> GpResult<()> { + //println!("read_measure_headers()"); + let mut previous: Option = None; + for i in 1..measure_count + 1 { + let r: (MeasureHeader, u8) = self.read_measure_header(data, seek, i, previous)?; + previous = Some(r.0.clone()); + self.measure_headers.push(r.0); //TODO: use add_measure_header + } + Ok(()) + } + + fn read_measure_headers_v5( + &mut self, + data: &[u8], + seek: &mut usize, + measure_count: usize, + directions: &(HashMap, HashMap), + ) -> GpResult<()> { + //println!("read_measure_headers_v5()"); + let mut previous: Option = None; + for i in 1..measure_count + 1 { + let r: (MeasureHeader, u8) = self.read_measure_header_v5(data, seek, i, previous)?; + previous = Some(r.0.clone()); + self.measure_headers.push(r.0); //TODO: use add_measure_header + } + for s in &directions.0 { + if s.1 > &-1 { + self.measure_headers[s.1.to_usize().unwrap() - 1].direction = Some(s.0.clone()); + } + } + for s in &directions.1 { + if s.1 > &-1 { + self.measure_headers[s.1.to_usize().unwrap() - 1].direction = Some(s.0.clone()); + } + } + Ok(()) + } + + /// Read measure header. The first byte is the measure's flags. It lists the data given in the current measure. + /// + /// | **Bit 7** | **Bit 6** | **Bit 5** | **Bit 4** | **Bit 3** | **Bit 2** | **Bit 1** | **Bit 0** | + /// |-----------|-----------|-----------|-----------|-----------|-----------|-----------|-----------| + /// | Presence of a double bar | Tonality of the measure | Presence of a marker | Number of alternate ending | End of repeat | Beginning of repeat | Denominator of the (key) signature | Numerator of the (key) signature | + /// + /// Each of these elements is present only if the corresponding bit is a 1. The different elements are written (if they are present) from lowest to highest bit. + /// Exceptions are made for the double bar and the beginning of repeat whose sole presence is enough, complementary data is not necessary. + /// + /// * **Numerator of the (key) signature**: `byte`. Numerator of the (key) signature of the piece + /// * **Denominator of the (key) signature**: `byte`. Denominator of the (key) signature of the piece + /// * **End of repeat**: `byte`. Number of repeats until the previous Beginning of repeat. Nombre de renvoi jusqu'au début de renvoi précédent. + /// * **Number of alternate ending**: `byte`. The number of alternate ending. + /// * **Marker**: The markers are written in two steps: + /// 1) First is written an `integer` equal to the marker's name length + 1 + /// 2) a string containing the marker's name. Finally the marker's color is written. + /// * **Tonality of the measure**: `byte`. This value encodes a key (signature) change on the current piece. It is encoded as: `0: C`, `1: G (#)`, `2: D (##)`, `-1: F (b)`, ... + fn read_measure_header( + &mut self, + data: &[u8], + seek: &mut usize, + number: usize, + previous: Option, + ) -> GpResult<(MeasureHeader, u8)> { + let flag = read_byte(data, seek)?; + //println!("read_measure_header(), flags: {} \t N: {} \t Measure header count: {}", flag, number, self.measure_headers.len()); + let mut mh = MeasureHeader { + number: number.to_u16().unwrap(), + ..Default::default() + }; + mh.start = 0; + mh.triplet_feel = self.triplet_feel.clone(); //TODO: use ref & lifetime + //we need a previous header for the next 2 flags + //Numerator of the (key) signature + if (flag & 0x01) == 0x01 { + mh.time_signature.numerator = read_signed_byte(data, seek)?; + } else if number > 1 { + mh.time_signature.numerator = previous.clone().unwrap().time_signature.numerator; + } + //Denominator of the (key) signature + if (flag & 0x02) == 0x02 { + mh.time_signature.denominator.value = read_signed_byte(data, seek)?.to_u16().unwrap(); + } else if number > 1 { + mh.time_signature.denominator = previous.clone().unwrap().time_signature.denominator; + } + + mh.repeat_open = (flag & 0x04) == 0x04; //Beginning of repeat + if (flag & 0x08) == 0x08 { + mh.repeat_close = read_signed_byte(data, seek)?; + } //End of repeat + if (flag & 0x10) == 0x10 { + mh.repeat_alternative = if self.version.number.0 == 5 { + self.read_repeat_alternative_v5(data, seek)? + } else { + self.read_repeat_alternative(data, seek)? + }; + } //Number of alternate ending + if (flag & 0x20) == 0x20 { + mh.marker = Some(read_marker(data, seek)?); + } //Presence of a marker + if (flag & 0x40) == 0x40 { + //Tonality of the measure + mh.key_signature.key = read_signed_byte(data, seek)?; + mh.key_signature.is_minor = read_signed_byte(data, seek)? != 0; + } else if mh.number > 1 { + mh.key_signature = previous.unwrap().key_signature; + } + mh.double_bar = (flag & 0x80) == 0x80; //presence of a double bar + Ok((mh, flag)) + } + /// Read measure header. Measure header format in Guitar Pro 5 differs from one if Guitar Pro 3. + /// + /// First, there is a blank byte if measure is not first. Then measure header is read as in GP3's `read_measure_header_v3()`. Then measure header is read as follows: + /// - Time signature beams: 4 `Bytes `. Appears If time signature was set, i.e. flags *0x01* and *0x02* are both set. + /// - Blank `byte` if flag at *0x10* is set. + /// - Triplet feel: `byte`. See `TripletFeel`. + fn read_measure_header_v5( + &mut self, + data: &[u8], + seek: &mut usize, + number: usize, + previous: Option, + ) -> GpResult<(MeasureHeader, u8)> { + if previous.is_some() { + *seek += 1; + } //always + let r = self.read_measure_header(data, seek, number, previous.clone())?; + let mut mh = r.0; + let flags = r.1; + //println!("read_measure_header_v5(), flags: {}", flags); + if mh.repeat_close > -1 { + mh.repeat_close -= 1; + } + if (flags & 0x03) == 0x03 { + for i in 0..4 { + mh.time_signature.beams[i] = read_byte(data, seek)?; + } + } else { + mh.time_signature.beams = previous.unwrap().time_signature.beams; + }; + if (flags & 0x10) == 0 { + *seek += 1; + } //always 0 + mh.triplet_feel = get_triplet_feel(read_byte(data, seek)?.to_i8().unwrap())?; + //println!("################################### {:?}", mh.triplet_feel); + Ok((mh, flags)) + } + + fn read_repeat_alternative(&mut self, data: &[u8], seek: &mut usize) -> GpResult { + //println!("read_repeat_alternative()"); + let value = read_byte(data, seek)?.to_u16().unwrap(); + let mut existing_alternative = 0u16; + for i in (0..self.measure_headers.len()).rev() { + if self.measure_headers[i].repeat_open { + break; + } + existing_alternative |= self.measure_headers[i].repeat_alternative.to_u16().unwrap(); + } + //println!("read_repeat_alternative(), value: {}, existing_alternative: {}", value, existing_alternative); + //println!("read_repeat_alternative(), return: {}", ((1 << value) - 1) ^ existing_alternative); + Ok((((1 << value) - 1) ^ existing_alternative).to_u8().unwrap()) + } + fn read_repeat_alternative_v5(&mut self, data: &[u8], seek: &mut usize) -> GpResult { + read_byte(data, seek) + } + + /// Read directions. Directions is a list of 19 `ShortInts ` each pointing at the number of measure. + /// + /// Directions are read in the following order: + /// - Coda + /// - Double Coda + /// - Segno + /// - Segno Segno + /// - Fine + /// - Da Capo + /// - Da Capo al Coda + /// - Da Capo al Double Coda + /// - Da Capo al Fine + /// - Da Segno + /// - Da Segno al Coda + /// - Da Segno al Double Coda + /// - Da Segno al Fine + /// - Da Segno Segno + /// - Da Segno Segno al Coda + /// - Da Segno Segno al Double Coda + /// - Da Segno Segno al Fine + /// - Da Coda + /// - Da Double Coda + fn read_directions( + &self, + data: &[u8], + seek: &mut usize, + ) -> GpResult<(HashMap, HashMap)> { + let mut signs: HashMap = HashMap::with_capacity(4); + let mut from_signs: HashMap = HashMap::with_capacity(15); + //signs + signs.insert(DirectionSign::Coda, read_short(data, seek)?); + signs.insert(DirectionSign::DoubleCoda, read_short(data, seek)?); + signs.insert(DirectionSign::Segno, read_short(data, seek)?); + signs.insert(DirectionSign::SegnoSegno, read_short(data, seek)?); + signs.insert(DirectionSign::Fine, read_short(data, seek)?); + //from signs + from_signs.insert(DirectionSign::DaCapo, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaCapoAlCoda, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaCapoAlDoubleCoda, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaCapoAlFine, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaSegno, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaSegnoAlCoda, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaSegnoAlDoubleCoda, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaSegnoAlFine, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaSegnoSegno, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaSegnoSegnoAlCoda, read_short(data, seek)?); + from_signs.insert( + DirectionSign::DaSegnoSegnoAlDoubleCoda, + read_short(data, seek)?, + ); + from_signs.insert(DirectionSign::DaSegnoSegnoAlFine, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaCoda, read_short(data, seek)?); + from_signs.insert(DirectionSign::DaDoubleCoda, read_short(data, seek)?); + Ok((signs, from_signs)) + } + + fn write_measure_headers(&self, data: &mut Vec, version: &(u8, u8, u8)) { + let mut previous: Option = None; + for i in 0..self.measure_headers.len() { + //self.current_measure_number = Some(self.tracks[0].measures[i].number); + self.write_measure_header(data, i, previous, version); + previous = Some(i); + } + } + + fn write_measure_header( + &self, + data: &mut Vec, + header: usize, + previous: Option, + version: &(u8, u8, u8), + ) { + //pack measure header flags + let mut flags: u8 = 0x00; + if let Some(p) = previous { + if self.measure_headers[header].time_signature.numerator + != self.measure_headers[p].time_signature.numerator + { + flags |= 0x01; + } + if self.measure_headers[header] + .time_signature + .denominator + .value + != self.measure_headers[p].time_signature.denominator.value + { + flags |= 0x02; + } + } else { + flags |= 0x01; + flags |= 0x02; + if self.measure_headers[header].repeat_open { + flags |= 0x04; + } + if self.measure_headers[header].repeat_close > -1 { + flags |= 0x08; + } + if self.measure_headers[header].repeat_alternative > 0 { + flags |= 0x10; + } + if self.measure_headers[header].marker.is_some() { + flags |= 0x20; + } + } + if version.0 >= 4 { + if previous.is_none() { + flags |= 0x40; + } else if let Some(p) = previous { + if self.measure_headers[header].key_signature + == self.measure_headers[p].key_signature + { + flags |= 0x40; + } + } + if self.measure_headers[header].double_bar { + flags |= 0x80; + } + } + if version.0 >= 5 { + if let Some(p) = previous { + if self.measure_headers[header].time_signature + != self.measure_headers[p].time_signature + { + flags |= 0x03; + } + write_placeholder_default(data, 1); + } + } + //end pack + //write measure header values + write_byte(data, flags); + if (flags & 0x01) == 0x01 { + write_signed_byte(data, self.measure_headers[header].time_signature.numerator); + } + if (flags & 0x02) == 0x02 { + write_signed_byte( + data, + self.measure_headers[header] + .time_signature + .denominator + .value + .to_i8() + .unwrap(), + ); + } + if (flags & 0x08) == 0x08 { + write_signed_byte( + data, + if version.0 < 5 { + self.measure_headers[header].repeat_close + } else { + self.measure_headers[header].repeat_close + 1 + }, + ); + } + if (flags & 0x10) == 0x10 { + //write repeat alternative + if version.0 == 5 { + write_byte(data, self.measure_headers[header].repeat_alternative); + } else { + let mut first_one = false; + let mut ra: u8 = 0; + for i in 0u8..9 - self.measure_headers[header] + .repeat_alternative + .leading_zeros() + .to_u8() + .unwrap() + { + ra = i; + if (self.measure_headers[header].repeat_alternative & 1 << i) > 0 { + first_one = true; + } else if first_one { + break; + } + } + write_byte(data, ra); + } + } + if (flags & 0x20) == 0x20 { + //write marker + if let Some(marker) = &self.measure_headers[header].marker { + write_int_byte_size_string(data, &marker.title); + write_color(data, marker.color); + } + } + if version.0 >= 4 { + write_signed_byte(data, self.measure_headers[header].key_signature.key); + write_signed_byte( + data, + i8::from(self.measure_headers[header].key_signature.is_minor), + ); + } + if version.0 >= 5 { + if (flags & 0x03) == 0x03 { + for i in 0..self.measure_headers[header].time_signature.beams.len() { + write_byte(data, self.measure_headers[header].time_signature.beams[i]); + } + } + if (flags & 0x10) == 0x10 { + write_placeholder_default(data, 1); + } + write_byte( + data, + from_triplet_feel(&self.measure_headers[header].triplet_feel), + ); + } + } + + fn write_clipboard(&self, data: &mut Vec, version: &(u8, u8, u8)) { + if let Some(c) = &self.clipboard { + write_i32(data, c.start_measure.to_i32().unwrap()); + write_i32(data, c.stop_measure.to_i32().unwrap()); + write_i32(data, c.start_track.to_i32().unwrap()); + write_i32(data, c.stop_track.to_i32().unwrap()); + if version.0 == 5 { + write_i32(data, c.start_beat.to_i32().unwrap()); + write_i32(data, c.stop_beat.to_i32().unwrap()); + write_i32(data, i32::from(c.sub_bar_copy)); + } + } + } + fn write_directions(&self, data: &mut Vec) { + let mut map: HashMap = HashMap::with_capacity(19); + for i in 1..self.measure_headers.len() { + if let Some(d) = &self.measure_headers[i].direction { + map.insert(d.clone(), i.to_i16().unwrap()); + } + } + let order: Vec = vec![ + DirectionSign::Coda, + DirectionSign::DoubleCoda, + DirectionSign::Segno, + DirectionSign::SegnoSegno, + DirectionSign::Fine, + DirectionSign::DaCapo, + DirectionSign::DaCapoAlCoda, + DirectionSign::DaCapoAlDoubleCoda, + DirectionSign::DaCapoAlFine, + DirectionSign::DaSegno, + DirectionSign::DaSegnoAlCoda, + DirectionSign::DaSegnoAlDoubleCoda, + DirectionSign::DaSegnoAlFine, + DirectionSign::DaSegnoSegno, + DirectionSign::DaSegnoSegnoAlCoda, + DirectionSign::DaSegnoSegnoAlDoubleCoda, + DirectionSign::DaSegnoSegnoAlFine, + DirectionSign::DaCoda, + DirectionSign::DaDoubleCoda, + ]; + for d in order { + let x = map.get(&d); + if let Some(dir) = x { + write_i16(data, *dir); + } else { + write_i16(data, -1); + } + } + } +} diff --git a/lib/src/key_signature.rs b/lib/src/model/key_signature.rs similarity index 95% rename from lib/src/key_signature.rs rename to lib/src/model/key_signature.rs index 2a4e926..2a046a7 100644 --- a/lib/src/key_signature.rs +++ b/lib/src/model/key_signature.rs @@ -1,5 +1,6 @@ use fraction::ToPrimitive; -use crate::io::*; +use crate::io::primitive::*; +use crate::error::GpResult; pub const DURATION_QUARTER_TIME: i64 = 960; //pub const DURATION_WHOLE: u8 = 1; @@ -114,13 +115,16 @@ impl Duration { /// * *3*: thirty-second note /// /// If flag at *0x20* is true, the tuplet is read -pub(crate) fn read_duration(data: &[u8], seek: &mut usize, flags: u8) -> Duration { +pub(crate) fn read_duration(data: &[u8], seek: &mut usize, flags: u8) -> GpResult { //println!("read_duration()"); - let mut d = Duration{value: 1 << (read_signed_byte(data, seek) + 2), ..Default::default()}; + let b = read_signed_byte(data, seek)?; + let shift = b + 2; + let val = if (0..16).contains(&shift) { 1u16 << shift } else { 1u16 }; // Fallback to 1 (whole note?) or whatever safe + let mut d = Duration{value: val, ..Default::default()}; //let b = read_signed_byte(data, seek); println!("B: {}", b); d.value = 1 << (b + 2); d.dotted = (flags & 0x01) == 0x01; if (flags & 0x20) == 0x20 { - let i_tuplet = read_int(data, seek); + let i_tuplet = read_int(data, seek)?; if i_tuplet == 3 {d.tuplet_enters = 3; d.tuplet_times = 2;} else if i_tuplet == 5 {d.tuplet_enters = 5; d.tuplet_times = 4;} else if i_tuplet == 6 {d.tuplet_enters = 6; d.tuplet_times = 4;} @@ -131,7 +135,7 @@ pub(crate) fn read_duration(data: &[u8], seek: &mut usize, flags: u8) -> Duratio else if i_tuplet == 12 {d.tuplet_enters = 12; d.tuplet_times = 8;} else if i_tuplet == 13 {d.tuplet_enters = 13; d.tuplet_times = 8;} } - d + Ok(d) } /*/// A *n:m* tuplet. diff --git a/lib/src/lyric.rs b/lib/src/model/lyric.rs similarity index 75% rename from lib/src/lyric.rs rename to lib/src/model/lyric.rs index e6d2e29..f571328 100644 --- a/lib/src/lyric.rs +++ b/lib/src/model/lyric.rs @@ -1,6 +1,7 @@ use fraction::ToPrimitive; -use crate::io::*; +use crate::{io::primitive::*, model::song::*}; +use crate::error::GpResult; pub const _MAX_LYRICS_LINE_COUNT: u8 = 5; @@ -26,20 +27,25 @@ impl std::fmt::Display for Lyrics { } } -impl crate::gp::Song { +pub trait SongLyricOps { + fn read_lyrics(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn write_lyrics(&self, data: &mut Vec); +} + +impl SongLyricOps for Song { /// Read lyrics. /// /// First, read an `i32` that points to the track lyrics are bound to. Then it is followed by 5 lyric lines. Each one consists of /// number of starting measure encoded in`i32` and`int-size-string` holding text of the lyric line. - pub(crate) fn read_lyrics(&self, data: &[u8], seek: &mut usize) -> Lyrics { - let mut lyrics = Lyrics{track_choice: read_int(data, seek).to_u8().unwrap(), ..Default::default()}; + fn read_lyrics(&self, data: &[u8], seek: &mut usize) -> GpResult { + let mut lyrics = Lyrics{track_choice: read_int(data, seek)?.to_u8().unwrap(), ..Default::default()}; for i in 0..5u8 { - let starting_measure = read_int(data, seek).to_u16().unwrap(); - lyrics.lines.push((i, starting_measure, read_int_size_string(data, seek))); + let starting_measure = read_int(data, seek)?.to_u16().unwrap(); + lyrics.lines.push((i, starting_measure, read_int_size_string(data, seek)?)); } - lyrics + Ok(lyrics) } - pub(crate) fn write_lyrics(&self, data: &mut Vec) { + fn write_lyrics(&self, data: &mut Vec) { write_i32(data, self.lyrics.track_choice.to_i32().unwrap()); for i in 0..5 { write_i32(data, self.lyrics.lines[i].1.to_i32().unwrap()); diff --git a/lib/src/model/measure.rs b/lib/src/model/measure.rs new file mode 100644 index 0000000..242cf1f --- /dev/null +++ b/lib/src/model/measure.rs @@ -0,0 +1,297 @@ +use fraction::ToPrimitive; + +use crate::{ + io::primitive::*, + model::{beat::*, enums::*, key_signature::*, song::*}, +}; +use crate::error::GpResult; + +const MAX_VOICES: usize = 2; + +/// A measure header contains metadata for measures over multiple tracks. +#[derive(Debug, Clone)] +pub struct Measure { + pub number: usize, + pub start: i64, + pub has_double_bar: bool, + pub key_signature: KeySignature, + pub time_signature: TimeSignature, + pub track_index: usize, + pub header_index: usize, + pub clef: MeasureClef, + /// Max voice count is 2 + pub voices: Vec, + pub line_break: LineBreak, + /// Simile mark from GPIF (GP6/GP7) + pub simile_mark: Option, + /*marker: Optional['Marker'] = None + isRepeatOpen: bool = False + repeatAlternative: int = 0 + repeatClose: int = -1 + tripletFeel: TripletFeel = TripletFeel.none + direction: Optional[DirectionSign] = None + fromDirection: Optional[DirectionSign] = None*/ +} +impl Default for Measure { + fn default() -> Self { + Measure { + number: 1, + start: DURATION_QUARTER_TIME, + has_double_bar: false, + key_signature: KeySignature::default(), //Cmajor + time_signature: TimeSignature::default(), + track_index: 0, + header_index: 0, + clef: MeasureClef::Treble, + voices: Vec::with_capacity(2), + line_break: LineBreak::None, + simile_mark: None, + } + } +} + +pub trait SongMeasureOps { + fn read_measures(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()>; + fn read_measure( + &mut self, + data: &[u8], + seek: &mut usize, + measure: &mut Measure, + track_index: usize, + ) -> GpResult<()>; + fn read_measure_v5( + &mut self, + data: &[u8], + seek: &mut usize, + measure: &mut Measure, + track_index: usize, + ) -> GpResult<()>; + fn read_voice( + &mut self, + data: &[u8], + seek: &mut usize, + voice: &mut Voice, + start: &mut i64, + track_index: usize, + ) -> GpResult<()>; + fn write_measures(&self, data: &mut Vec, version: &(u8, u8, u8)); + fn write_measure( + &self, + data: &mut Vec, + track: usize, + measure: usize, + version: &(u8, u8, u8), + ); + fn write_voice( + &self, + data: &mut Vec, + track: usize, + measure: usize, + voice: usize, + version: &(u8, u8, u8), + ); +} + +impl SongMeasureOps for Song { + /// Read measures. Measures are written in the following order: + /// - measure 1/track 1 + /// - measure 1/track 2 + /// - ... + /// - measure 1/track m + /// - measure 2/track 1 + /// - measure 2/track 2 + /// - ... + /// - measure 2/track m + /// - ... + /// - measure n/track 1 + /// - measure n/track 2 + /// - ... + /// - measure n/track m + fn read_measures(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()> { + for h in 0..self.measure_headers.len() { + for t in 0..self.tracks.len() { + //println!("Reading measure H:{} T:{} Seek:{}", h, t, seek); + self.current_track = Some(t); + let mut m = Measure { + track_index: t, + header_index: h, + ..Default::default() + }; + self.current_measure_number = Some(m.number); + if self.version.number < (5, 0, 0) { + self.read_measure(data, seek, &mut m, t)?; + } else { + self.read_measure_v5(data, seek, &mut m, t)?; + } + self.tracks[t].measures.push(m); + } + //println!("read_measures(), start: {} \t numerator: {} \t denominator: {} \t length: {}", start, self.measure_headers[h].time_signature.numerator, self.measure_headers[h].time_signature.denominator.value, self.measure_headers[h].length()); + } + self.current_track = None; + self.current_measure_number = None; + Ok(()) + } + + /// Read measure. The measure is written as number of beats followed by sequence of beats. + fn read_measure( + &mut self, + data: &[u8], + seek: &mut usize, + measure: &mut Measure, + track_index: usize, + ) -> GpResult<()> { + //println!("read_measure()"); + let mut voice = Voice::default(); + self.current_voice_number = Some(1); + self.read_voice(data, seek, &mut voice, &mut measure.start, track_index)?; + self.current_voice_number = None; + measure.voices.push(voice); + /* + //read a voice + let beats = read_int(data, seek).to_usize().unwrap(); + + //println!("read_measure() read_voice(), beat count: {}", beats); + for i in 0..beats { + self.current_beat_number = Some(i + 1); + //println!("read_measure() read_voice(), start: {}", measure.start); + measure.start += self.read_beat(data, seek, &mut measure.voices[0], measure.start, track_index); + //println!("read_measure() read_voice(), start: {}", measure.start); + } + self.current_beat_number = None; + //end read a voice + self.current_voice_number = None;*/ + Ok(()) + } + /// Read measure. Guitar Pro 5 stores twice more measures compared to Guitar Pro 3. One measure consists of two sub-measures for each of two voices. + /// + /// Sub-measures are followed by a `LineBreak` stored in `byte`. + fn read_measure_v5( + &mut self, + data: &[u8], + seek: &mut usize, + measure: &mut Measure, + track_index: usize, + ) -> GpResult<()> { + //println!("read_measure_v5()"); + let mut start = measure.start; + for number in 0..MAX_VOICES { + self.current_voice_number = Some(number + 1); + //println!("read_measure_v5() {:?}",self.current_voice_number); + let mut voice = Voice::default(); + self.read_voice(data, seek, &mut voice, &mut start, track_index)?; + measure.voices.push(voice); + } + self.current_voice_number = None; + if *seek < data.len() { + measure.line_break = get_line_break(read_byte(data, seek)?); + } else { + measure.line_break = get_line_break(0); + } + Ok(()) + } + + fn read_voice( + &mut self, + data: &[u8], + seek: &mut usize, + voice: &mut Voice, + start: &mut i64, + track_index: usize, + ) -> GpResult<()> { + if *seek + 4 > data.len() { + return Ok(()); + } + let beats = read_int(data, seek)?.to_usize().unwrap_or(0); + //Sanity check + if beats > 256 { + return Ok(()); + } + for i in 0..beats { + if *seek + 5 > data.len() { + break; + } + self.current_beat_number = Some(i + 1); + //println!("read_measure() read_voice(), start: {}", measure.start); + *start += if self.version.number < (5, 0, 0) { + self.read_beat(data, seek, voice, *start, track_index)? + } else { + self.read_beat_v5(data, seek, voice, &mut *start, track_index)? + }; + //println!("read_measure() read_voice(), start: {}", measure.start); + } + self.current_beat_number = None; + Ok(()) + } + + fn write_measures(&self, data: &mut Vec, version: &(u8, u8, u8)) { + for i in 0..self.tracks.len() { + //self.current_track = Some(i); + for m in 0..self.tracks[i].measures.len() { + //self.current_measure_number = Some(self.tracks[i].measure.number); + self.write_measure(data, i, m, version); + } + } + //self.current_track = None; + //self.current_measure_number = None; + } + fn write_measure( + &self, + data: &mut Vec, + track: usize, + measure: usize, + version: &(u8, u8, u8), + ) { + //self.current_voice_number = Some(1); + if version.0 < 5 { + self.write_voice(data, track, measure, 0, version); + } else { + for v in 0..self.tracks[track].measures[measure].voices.len() { + self.write_voice(data, track, measure, v, version); + } //self.current_voice_number = Some(v+1); + if version.0 == 5 { + write_byte( + data, + from_line_break(&self.tracks[track].measures[measure].line_break), + ); + } + } + //self.current_voice_number = None; + } + fn write_voice( + &self, + data: &mut Vec, + track: usize, + measure: usize, + voice: usize, + version: &(u8, u8, u8), + ) { + write_i32( + data, + self.tracks[track].measures[measure].voices[voice] + .beats + .len() + .to_i32() + .unwrap(), + ); + for b in 0..self.tracks[track].measures[measure].voices[voice] + .beats + .len() + { + //self.current_beat_number = Some(b+1); + if version.0 == 3 { + self.write_beat_v3( + data, + &self.tracks[track].measures[measure].voices[voice].beats[b], + ); + } else { + self.write_beat( + data, + &self.tracks[track].measures[measure].voices[voice].beats[b], + &self.tracks[track].strings, + version, + ); + } + //self.current_beat_number = None; + } + } +} diff --git a/lib/src/mix_table.rs b/lib/src/model/mix_table.rs similarity index 76% rename from lib/src/mix_table.rs rename to lib/src/model/mix_table.rs index 49c155e..20e1cf9 100644 --- a/lib/src/mix_table.rs +++ b/lib/src/model/mix_table.rs @@ -1,8 +1,9 @@ use fraction::ToPrimitive; -use crate::rse::*; -use crate::io::*; -use crate::gp::*; +use crate::model::{rse::*, song::*}; +use crate::io::primitive::*; +use crate::error::GpResult; +// use crate::gp::*; /// A mix table item describes a mix parameter, e.g. volume or reverb #[derive(Debug,Clone,PartialEq,Eq,Default)] @@ -13,19 +14,20 @@ pub struct MixTableItem { } //impl Default for MixTableItem { fn default() -> Self { MixTableItem { value: 0, duration: 0, all_tracks: false }}} +#[allow(dead_code)] const WAH_EFFECT_OFF: i8 = -2; const WAH_EFFECT_NONE: i8 = -1; #[derive(Debug,Clone,PartialEq,Eq)] pub struct WahEffect { - value: i8, - display: bool, + pub value: i8, + pub display: bool, } impl Default for WahEffect { fn default() -> Self { WahEffect { value: WAH_EFFECT_NONE, display: false }}} impl WahEffect { pub(crate) fn _check_value(value: i8) { if !(WAH_EFFECT_OFF..=100).contains(&value) {panic!("Value for a wah effect must be in range from -2 to 100")} } - pub(crate) fn _is_on(&self) -> bool {self.value <= 0 && self.value <= 100} + pub(crate) fn _is_on(&self) -> bool {self.value >= 0 && self.value <= 100} pub(crate) fn _is_off(&self) -> bool {self.value == WAH_EFFECT_OFF} pub(crate) fn _is_none(&self) -> bool {self.value == WAH_EFFECT_NONE} } @@ -48,7 +50,7 @@ pub struct MixTableChange { pub use_rse: bool, } impl Default for MixTableChange { fn default() -> Self { MixTableChange { instrument:None, rse:RseInstrument::default(), volume:None, balance:None, chorus:None, reverb:None, phaser:None, tremolo:None, - tempo_name:String::new(), tempo:None, hide_tempo:true, wah:None, use_rse:false, + tempo_name:String::new(), tempo:None, hide_tempo:true, wah:None, use_rse:false, }}} impl MixTableChange { pub(crate) fn is_just_wah(&self) -> bool { @@ -56,35 +58,48 @@ impl MixTableChange { } } -impl Song { +pub trait SongMixTableOps { + fn read_mix_table_change(&mut self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_mix_table_change_values(&mut self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> GpResult<()>; + fn read_mix_table_change_durations(&self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> GpResult<()>; + fn read_mix_table_change_flags(&self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> GpResult; + fn read_wah_effect(&self, data: &[u8], seek: &mut usize, flags: i8) -> GpResult; + fn write_mix_table_change(&self, data: &mut Vec, mix_table_change: &Option, version: &(u8,u8,u8)); + fn write_mix_table_change_values(&self, data: &mut Vec, mix_table_change: &MixTableChange, version: &(u8,u8,u8)); + fn write_mix_table_change_durations(&self, data: &mut Vec, mix_table_change: &MixTableChange, version: &(u8,u8,u8)); + fn write_mix_table_change_flags_v4(&self, data: &mut Vec, mix_table_change: &MixTableChange); + fn write_mix_table_change_flags_v5(&self, data: &mut Vec, mix_table_change: &MixTableChange); +} + +impl SongMixTableOps for Song { /// Read mix table change. List of values is read first. See `read_values()`. - /// + /// /// List of values is followed by the list of durations for parameters that have changed. See `read_durations()`. - /// + /// /// Mix table change in Guitar Pro 4 format extends Guitar Pro 3 format. It constists of `values `, /// `durations `, and, new to GP3, `flags `. - /// + /// /// Mix table change was modified to support RSE instruments. It is read as in Guitar Pro 3 and is followed by: /// - Wah effect. See :meth:`read_wah_effect()`. /// - RSE instrument effect. See :meth:`read_rse_instrument_effect()`. - pub(crate) fn read_mix_table_change(&mut self, data: &[u8], seek: &mut usize) -> MixTableChange { + fn read_mix_table_change(&mut self, data: &[u8], seek: &mut usize) -> GpResult { let mut tc = MixTableChange::default(); - self.read_mix_table_change_values(data, seek, &mut tc); - self.read_mix_table_change_durations(data, seek, &mut tc); + self.read_mix_table_change_values(data, seek, &mut tc)?; + self.read_mix_table_change_durations(data, seek, &mut tc)?; //println!("read_mix_table_change()"); if self.version.number >= (4,0,0) { - let flags = self.read_mix_table_change_flags(data, seek, &mut tc); + let flags = self.read_mix_table_change_flags(data, seek, &mut tc)?; if self.version.number >= (5,0,0) { - tc.wah = Some(self.read_wah_effect(data, seek, flags)); - self.read_rse_instrument_effect(data, seek, &mut tc.rse); + tc.wah = Some(self.read_wah_effect(data, seek, flags)?); + self.read_rse_instrument_effect(data, seek, &mut tc.rse)?; } } - tc + Ok(tc) } /// Read mix table change values. Mix table change values consist of 7 `signed-byte` and an `int`, which correspond to: /// - instrument /// - RSE instrument. See `read_rse_instrument()` (GP5). - /// - volume + /// - volume /// - balance /// - chorus /// - reverb @@ -92,55 +107,55 @@ impl Song { /// - tremolo /// - Tempo name: `int-byte-size-string` (GP5). /// - tempo - /// + /// /// If signed byte is *-1* then corresponding parameter hasn't changed. - fn read_mix_table_change_values(&mut self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) { + fn read_mix_table_change_values(&mut self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> GpResult<()> { //instrument - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.instrument = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //RSE instrument GP5 - if self.version.number.0 == 5 {mtc.rse = self.read_rse_instrument(data, seek);} + if self.version.number.0 == 5 {mtc.rse = self.read_rse_instrument(data, seek)?;} if self.version.number == (5,0,0) { *seek += 1; } //volume - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.volume = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //balance - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.balance = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //chorus - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.chorus = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //reverb - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.reverb = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //phaser - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.phaser = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //tremolo - let b = read_signed_byte(data, seek); + let b = read_signed_byte(data, seek)?; if b >= 0 {mtc.tremolo = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} //tempo - if self.version.number >= (5,0,0) {mtc.tempo_name = read_int_byte_size_string(data, seek);} - let b = read_int(data, seek); - if b >= 0 {mtc.tempo = Some(MixTableItem{value: b.to_u8().unwrap(), ..Default::default()});} + if self.version.number >= (5,0,0) {mtc.tempo_name = read_int_byte_size_string(data, seek)?;} + let b = read_int(data, seek)?; + if b >= 0 {mtc.tempo = Some(MixTableItem{value: b.clamp(0, 255) as u8, ..Default::default()});} + Ok(()) } /// Read mix table change durations. Durations are read for each non-null `MixTableItem`. Durations are encoded in `signed-byte`. - /// + /// /// If tempo did change, then one :ref:`bool` is read. If it's true, then tempo change won't be displayed on the score. - fn read_mix_table_change_durations(&self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) { - if mtc.volume.is_some() {mtc.volume.take().unwrap().duration = read_signed_byte(data, seek).to_u8().unwrap();} - if mtc.balance.is_some() {mtc.balance.take().unwrap().duration = read_signed_byte(data, seek).to_u8().unwrap();} - if mtc.chorus.is_some() {mtc.chorus.take().unwrap().duration = read_signed_byte(data, seek).to_u8().unwrap();} - if mtc.reverb.is_some() {mtc.reverb.take().unwrap().duration = read_signed_byte(data, seek).to_u8().unwrap();} - if mtc.phaser.is_some() {mtc.phaser.take().unwrap().duration = read_signed_byte(data, seek).to_u8().unwrap();} - if mtc.tremolo.is_some() {mtc.tremolo.take().unwrap().duration = read_signed_byte(data, seek).to_u8().unwrap();} - if mtc.tempo.is_some() { - let mut t = mtc.tempo.take().unwrap(); - t.duration = read_signed_byte(data, seek).to_u8().unwrap(); - mtc.tempo = Some(t); + fn read_mix_table_change_durations(&self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> GpResult<()> { + if let Some(ref mut item) = mtc.volume { item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); } + if let Some(ref mut item) = mtc.balance { item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); } + if let Some(ref mut item) = mtc.chorus { item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); } + if let Some(ref mut item) = mtc.reverb { item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); } + if let Some(ref mut item) = mtc.phaser { item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); } + if let Some(ref mut item) = mtc.tremolo { item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); } + if let Some(ref mut item) = mtc.tempo { + item.duration = read_signed_byte(data, seek)?.to_u8().unwrap_or(0); mtc.hide_tempo = false; - if self.version.number >= (5,0,0) {mtc.hide_tempo = read_bool(data, seek);} + if self.version.number >= (5,0,0) { mtc.hide_tempo = read_bool(data, seek)?; } } + Ok(()) } /// Read mix table change flags (Guitar Pro 4). The meaning of flags: @@ -150,12 +165,12 @@ impl Song { /// - *0x08*: change reverb for all tracks /// - *0x10*: change phaser for all tracks /// - *0x20*: change tremolo for all tracks - /// + /// /// In GP5, there is one additional flag: /// - *0x40*: use RSE /// - *0x80*: show wah-wah - fn read_mix_table_change_flags(&self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> i8 { - let flags = read_signed_byte(data, seek); + fn read_mix_table_change_flags(&self, data: &[u8], seek: &mut usize, mtc: &mut MixTableChange) -> GpResult { + let flags = read_signed_byte(data, seek)?; //println!("read_mix_table_change_flags(), flags: {}", flags); if mtc.volume.is_some() { let mut e = mtc.volume.take().unwrap(); @@ -188,14 +203,14 @@ impl Song { mtc.tremolo = Some(e); } if self.version.number >= (5,0,0) {mtc.use_rse = (flags & 0x40) == 0x40;} - flags + Ok(flags) } /// Read wah-wah. /// - Wah value: :ref:`signed-byte`. See `WahEffect` for value mapping. - fn read_wah_effect(&self, data: &[u8], seek: &mut usize, flags: i8) -> WahEffect {WahEffect{value: read_signed_byte(data, seek), display: (flags & -0x80) == -0x80 /*(flags & 0x80) == 0x80*/}} + fn read_wah_effect(&self, data: &[u8], seek: &mut usize, flags: i8) -> GpResult {Ok(WahEffect{value: read_signed_byte(data, seek)?, display: (flags & -0x80) == -0x80 /*(flags & 0x80) == 0x80*/})} - pub(crate) fn write_mix_table_change(&self, data: &mut Vec, mix_table_change: &Option, version: &(u8,u8,u8)) { + fn write_mix_table_change(&self, data: &mut Vec, mix_table_change: &Option, version: &(u8,u8,u8)) { if let Some(mtc) = mix_table_change { self.write_mix_table_change_values(data, mtc, version); self.write_mix_table_change_durations(data, mtc, version); @@ -287,4 +302,4 @@ impl Song { if let Some(w) = &mix_table_change.wah {if w.display {flags |= 0x80;}} write_byte(data, flags); } -} \ No newline at end of file +} diff --git a/lib/src/model/mod.rs b/lib/src/model/mod.rs new file mode 100644 index 0000000..9a1b7b0 --- /dev/null +++ b/lib/src/model/mod.rs @@ -0,0 +1,14 @@ +pub mod song; +pub mod track; +pub mod measure; +pub mod beat; +pub mod note; +pub mod chord; +pub mod lyric; +pub mod headers; +pub mod page; +pub mod key_signature; +pub mod effects; +pub mod enums; +pub mod mix_table; +pub mod rse; diff --git a/lib/src/note.rs b/lib/src/model/note.rs similarity index 82% rename from lib/src/note.rs rename to lib/src/model/note.rs index 6ddd9e3..19e3560 100644 --- a/lib/src/note.rs +++ b/lib/src/model/note.rs @@ -1,6 +1,7 @@ use fraction::ToPrimitive; -use crate::{effects::*, enums::*, io::*, gp::*, beat::*, key_signature::*}; +use crate::{model::{effects::*, enums::*, song::*, beat::*, key_signature::*}, io::primitive::*}; +use crate::error::GpResult; #[derive(Debug,Clone, PartialEq)] pub struct Note { @@ -11,8 +12,8 @@ pub struct Note { pub duration_percent: f32, pub swap_accidentals: bool, pub kind: NoteType, - duration: Option, - tuplet: Option + pub duration: Option, + pub tuplet: Option } impl Default for Note {fn default() -> Self {Note { value: 0, @@ -50,6 +51,8 @@ pub struct NoteEffect { pub tremolo_picking: Option, pub trill: Option, pub vibrato: bool, + /// Ornament type from GPIF (GP6/GP7) + pub ornament: Option, } impl Default for NoteEffect { fn default() -> Self {NoteEffect { @@ -69,6 +72,7 @@ impl Default for NoteEffect { tremolo_picking: None, trill: None, vibrato: false, + ornament: None, }} } impl NoteEffect { @@ -96,7 +100,23 @@ impl NoteEffect { pub(crate) fn is_fingering(&self) -> bool {self.left_hand_finger != Fingering::Open || self.right_hand_finger != Fingering::Open} } -impl Song { +pub trait SongNoteOps { + fn read_notes(&mut self, data: &[u8], seek: &mut usize, track_index: usize, beat: &mut Beat, duration: &Duration, note_effect: NoteEffect) -> GpResult<()>; + fn read_note(&mut self, data: &[u8], seek: &mut usize, note: &mut Note, guitar_string: (i8,i8), track_index: usize) -> GpResult<()>; + fn read_note_v5(&mut self, data: &[u8], seek: &mut usize, note: &mut Note, guitar_string: (i8,i8), track_index: usize) -> GpResult<()>; + fn read_note_effects_v3(&self, data: &[u8], seek: &mut usize, note: &mut Note) -> GpResult<()>; + fn read_note_effects_v4(&mut self, data: &[u8], seek: &mut usize, note: &mut Note) -> GpResult<()>; + fn get_tied_note_value(&self, string_index: i8, track_index: usize) -> i16; + fn write_notes(&self, data: &mut Vec, beat: &Beat, strings: &[(i8,i8)], version: &(u8,u8,u8)); + fn write_note_v3(&self, data: &mut Vec, note: &Note); + fn write_note_v4(&self, data: &mut Vec, note: &Note, strings: &[(i8,i8)], version: &(u8,u8,u8)); + fn write_note_v5(&self, data: &mut Vec, note: &Note, strings: &[(i8,i8)], version: &(u8,u8,u8)); + fn pack_note_flags(&self, note: &Note, version: &(u8,u8,u8)) -> u8; + fn write_note_effects_v3(&self, data: &mut Vec, note: &Note); + fn write_note_effects(&self, data: &mut Vec, note: &Note, strings: &[(i8,i8)], version: &(u8,u8,u8)); +} + +impl SongNoteOps for Song { /// Read notes. First byte lists played strings: /// - *0x01*: 7th string /// - *0x02*: 6th string @@ -106,18 +126,19 @@ impl Song { /// - *0x20*: 2th string /// - *0x40*: 1th string /// - *0x80*: *blank* - pub(crate) fn read_notes(&mut self, data: &[u8], seek: &mut usize, track_index: usize, beat: &mut Beat, duration: &Duration, note_effect: NoteEffect) { - let flags = read_byte(data, seek); + fn read_notes(&mut self, data: &[u8], seek: &mut usize, track_index: usize, beat: &mut Beat, duration: &Duration, note_effect: NoteEffect) -> GpResult<()> { + let flags = read_byte(data, seek)?; //println!("read_notes(), flags: {}", flags); for i in 0..self.tracks[track_index].strings.len() { if (flags & 1 << (7 - self.tracks[track_index].strings[i].0)) > 0 { let mut note = Note{effect: note_effect.clone(), ..Default::default()}; - if self.version.number < (5,0,0) {self.read_note(data, seek, &mut note, self.tracks[track_index].strings[i], track_index);} - else {self.read_note_v5(data, seek, &mut note, self.tracks[track_index].strings[i], track_index);} + if self.version.number < (5,0,0) {self.read_note(data, seek, &mut note, self.tracks[track_index].strings[i], track_index)?;} + else {self.read_note_v5(data, seek, &mut note, self.tracks[track_index].strings[i], track_index)?;} beat.notes.push(note); } beat.duration = duration.clone(); } + Ok(()) } /// Read note. The first byte is note flags: @@ -137,42 +158,43 @@ impl Song { /// - Fret number: `signed-byte`. If flag at *0x20* is set then read fret number. /// - Fingering: 2 `SignedBytes `. See `Fingering`. /// - Note effects. See `read_note_effects()`. - fn read_note(&mut self, data: &[u8], seek: &mut usize, note: &mut Note, guitar_string: (i8,i8), track_index: usize) { - let flags = read_byte(data, seek); + fn read_note(&mut self, data: &[u8], seek: &mut usize, note: &mut Note, guitar_string: (i8,i8), track_index: usize) -> GpResult<()> { + let flags = read_byte(data, seek)?; note.string = guitar_string.0; note.effect.ghost_note = (flags & 0x04) == 0x04; //println!("read_note(), flags: {} \t string: {} \t ghost note: {}", flags, guitar_string.0, note.effect.ghost_note); - if (flags & 0x20) == 0x20 {note.kind = get_note_type(read_byte(data, seek)); } + if (flags & 0x20) == 0x20 {note.kind = get_note_type(read_byte(data, seek)?); } if (flags & 0x01) == 0x01 { //println!("read_note(), duration: {} \t tuplet: {}",duration, tuplet); - note.duration = Some(read_signed_byte(data, seek)); - note.tuplet = Some(read_signed_byte(data, seek)); + note.duration = Some(read_signed_byte(data, seek)?); + note.tuplet = Some(read_signed_byte(data, seek)?); } if (flags & 0x10) == 0x10 { - let v = read_signed_byte(data, seek); + let v = read_signed_byte(data, seek)?; //println!("read_note(), v: {}", v); - note.velocity = crate::effects::unpack_velocity(v.to_i16().unwrap()); + note.velocity = crate::model::effects::unpack_velocity(v.to_i16().unwrap()); //println!("read_note(), velocity: {}", note.velocity); } if (flags & 0x20) == 0x20 { - let fret = read_signed_byte(data, seek); + let fret = read_signed_byte(data, seek)?; let value = if note.kind == NoteType::Tie { self.get_tied_note_value(guitar_string.0, track_index)} else {fret.to_i16().unwrap()}; note.value = value.clamp(0, 99); //println!("read_note(), value: {}", note.value); } if (flags & 0x80) == 0x80 { - note.effect.left_hand_finger = get_fingering(read_signed_byte(data, seek)); - note.effect.right_hand_finger= get_fingering(read_signed_byte(data, seek)); + note.effect.left_hand_finger = get_fingering(read_signed_byte(data, seek)?); + note.effect.right_hand_finger= get_fingering(read_signed_byte(data, seek)?); } if (flags & 0x08) == 0x08 { - if self.version.number == (3,0,0) {self.read_note_effects_v3(data, seek, note);} - else if self.version.number.0 == 4 {self.read_note_effects_v4(data, seek, note);} + if self.version.number == (3,0,0) {self.read_note_effects_v3(data, seek, note)?;} + else if self.version.number.0 == 4 {self.read_note_effects_v4(data, seek, note)?;} if note.effect.is_harmonic() && note.effect.harmonic.is_some() { let mut h = note.effect.harmonic.take().unwrap(); if h.kind == HarmonicType::Tapped {h.fret = Some(note.value.to_i8().unwrap() + 12);} note.effect.harmonic = Some(h); } } + Ok(()) } /// Read note. The first byte is note flags: /// - *0x01*: duration percent @@ -193,33 +215,34 @@ impl Song { /// - Second set of flags: `byte`. /// - *0x02*: swap accidentals. /// - Note effects. See `read_note_effects()`. - fn read_note_v5(&mut self, data: &[u8], seek: &mut usize, note: &mut Note, guitar_string: (i8,i8), track_index: usize) { - let flags = read_byte(data, seek); + fn read_note_v5(&mut self, data: &[u8], seek: &mut usize, note: &mut Note, guitar_string: (i8,i8), track_index: usize) -> GpResult<()> { + let flags = read_byte(data, seek)?; //println!("read_note_v5(), flags: {}", flags); note.string = guitar_string.0; note.effect.heavy_accentuated_note = (flags &0x02) == 0x02; note.effect.ghost_note = (flags &0x04) == 0x04; note.effect.accentuated_note = (flags &0x40) == 0x40; - if (flags &0x20) == 0x20 {note.kind = get_note_type(read_byte(data, seek));} + if (flags &0x20) == 0x20 {note.kind = get_note_type(read_byte(data, seek)?);} if (flags &0x10) == 0x10 { - let v = read_signed_byte(data, seek); + let v = read_signed_byte(data, seek)?; //println!("read_note(), v: {}", v); - note.velocity = crate::effects::unpack_velocity(v.to_i16().unwrap()); + note.velocity = crate::model::effects::unpack_velocity(v.to_i16().unwrap()); //println!("read_note(), velocity: {}", note.velocity); } if (flags &0x20) == 0x20 { - let fret = read_signed_byte(data, seek); + let fret = read_signed_byte(data, seek)?; let value = if note.kind == NoteType::Tie { self.get_tied_note_value(guitar_string.0, track_index)} else {fret.to_i16().unwrap()}; note.value = value.clamp(0, 99); //println!("read_note(), value: {}", note.value); } if (flags &0x80) == 0x80 { - note.effect.left_hand_finger = get_fingering(read_signed_byte(data, seek)); - note.effect.right_hand_finger= get_fingering(read_signed_byte(data, seek)); + note.effect.left_hand_finger = get_fingering(read_signed_byte(data, seek)?); + note.effect.right_hand_finger= get_fingering(read_signed_byte(data, seek)?); } - if (flags & 0x01) == 0x01 {note.duration_percent = read_double(data, seek).to_f32().unwrap();} - note.swap_accidentals = (read_byte(data, seek) & 0x02) == 0x02; - if (flags & 0x08) == 0x08 {self.read_note_effects_v4(data, seek, note);} + if (flags & 0x01) == 0x01 {note.duration_percent = read_double(data, seek)?.to_f32().unwrap();} + note.swap_accidentals = (read_byte(data, seek)? & 0x02) == 0x02; + if (flags & 0x08) == 0x08 {self.read_note_effects_v4(data, seek, note)?;} + Ok(()) } /// Read note effects. First byte is note effects flags: @@ -232,15 +255,16 @@ impl Song { /// Flags are followed by: /// - Bend. See `readBend`. /// - Grace note. See `readGrace`. - fn read_note_effects_v3(&self, data: &[u8], seek: &mut usize, note: &mut Note) { - let flags = read_byte(data, seek); + fn read_note_effects_v3(&self, data: &[u8], seek: &mut usize, note: &mut Note) -> GpResult<()> { + let flags = read_byte(data, seek)?; //println!("read_effect(), flags: {}", flags); note.effect.hammer = (flags & 0x02) == 0x02; note.effect.let_ring = (flags & 0x08) == 0x08; - if (flags & 0x01) == 0x01 {note.effect.bend = self.read_bend_effect(data, seek);} - if (flags & 0x10) == 0x10 {note.effect.grace = Some(self.read_grace_effect(data, seek));} + if (flags & 0x01) == 0x01 {note.effect.bend = self.read_bend_effect(data, seek)?;} + if (flags & 0x10) == 0x10 {note.effect.grace = Some(self.read_grace_effect(data, seek)?);} if (flags & 0x04) == 0x04 {note.effect.slides.push(SlideType::ShiftSlideTo);} //println!("read_note_effects(): {:?}", note); + Ok(()) } /// Read note effects. The effects presence for the current note is set by the 2 bytes of flags. First set of flags: /// - *0x01*: bend @@ -269,29 +293,30 @@ impl Song { /// - Slide. See `read_slides()`. /// - Harmonic. See `read_harmonic()`. /// - Trill. See `read_trill()`. - fn read_note_effects_v4(&mut self, data: &[u8], seek: &mut usize, note: &mut Note) { - let flags1 = read_signed_byte(data, seek); - let flags2 = read_signed_byte(data, seek); + fn read_note_effects_v4(&mut self, data: &[u8], seek: &mut usize, note: &mut Note) -> GpResult<()> { + let flags1 = read_signed_byte(data, seek)?; + let flags2 = read_signed_byte(data, seek)?; note.effect.hammer = (flags1 & 0x02) == 0x02; note.effect.let_ring = (flags1 & 0x08) == 0x08; note.effect.staccato = (flags2 & 0x01) == 0x01; note.effect.palm_mute = (flags2 & 0x02) == 0x02; note.effect.vibrato = (flags2 & 0x40) == 0x40 || note.effect.vibrato; - if (flags1 & 0x01) == 0x01 {note.effect.bend = self.read_bend_effect(data, seek);} + if (flags1 & 0x01) == 0x01 {note.effect.bend = self.read_bend_effect(data, seek)?;} if (flags1 & 0x10) == 0x10 { - if self.version.number >= (5,0,0) {note.effect.grace = Some(self.read_grace_effect_v5(data,seek));} - else {note.effect.grace = Some(self.read_grace_effect(data, seek));} + if self.version.number >= (5,0,0) {note.effect.grace = Some(self.read_grace_effect_v5(data,seek)?);} + else {note.effect.grace = Some(self.read_grace_effect(data, seek)?);} } - if (flags2 & 0x04) == 0x04 {note.effect.tremolo_picking = Some(self.read_tremolo_picking(data, seek));} + if (flags2 & 0x04) == 0x04 {note.effect.tremolo_picking = Some(self.read_tremolo_picking(data, seek)?);} if (flags2 & 0x08) == 0x08 { - if self.version.number >= (5,0,0) {note.effect.slides.extend(self.read_slides_v5(data, seek));} - else {note.effect.slides.push(get_slide_type(read_signed_byte(data, seek)));} + if self.version.number >= (5,0,0) {note.effect.slides.extend(self.read_slides_v5(data, seek)?);} + else {note.effect.slides.push(get_slide_type(read_signed_byte(data, seek)?)?);} } if (flags2 & 0x10) == 0x10 { - if self.version.number >= (5,0,0) {note.effect.harmonic = Some(self.read_harmonic_v5(data, seek));} - else {note.effect.harmonic = Some(self.read_harmonic(data, seek, note));} + if self.version.number >= (5,0,0) {note.effect.harmonic = Some(self.read_harmonic_v5(data, seek)?);} + else {note.effect.harmonic = Some(self.read_harmonic(data, seek, note)?);} } - if (flags2 & 0x20) == 0x20 {note.effect.trill = Some(self.read_trill(data, seek));} + if (flags2 & 0x20) == 0x20 {note.effect.trill = Some(self.read_trill(data, seek)?);} + Ok(()) } /// Get note value of tied note @@ -311,7 +336,7 @@ impl Song { -1 } - pub(crate) fn write_notes(&self, data: &mut Vec, beat: &Beat, strings: &[(i8,i8)], version: &(u8,u8,u8)) { + fn write_notes(&self, data: &mut Vec, beat: &Beat, strings: &[(i8,i8)], version: &(u8,u8,u8)) { let mut string_flags: u8 = 0; for i in 0..beat.notes.len() {string_flags |= 1 << (7 - beat.notes[i].string);} write_byte(data, string_flags); @@ -331,7 +356,7 @@ impl Song { write_signed_byte(data, note.duration.unwrap()); write_signed_byte(data, note.tuplet.unwrap()); } - if (flags & 0x10) == 0x10 {write_signed_byte(data, crate::effects::pack_velocity(note.velocity));} + if (flags & 0x10) == 0x10 {write_signed_byte(data, crate::model::effects::pack_velocity(note.velocity));} if (flags & 0x20) == 0x20 { if note.kind != NoteType::Rest {write_signed_byte(data, note.value.to_i8().unwrap());} else {write_signed_byte(data, 0);} @@ -346,7 +371,7 @@ impl Song { write_signed_byte(data, note.duration.unwrap()); write_signed_byte(data, note.tuplet.unwrap()); } - if (flags & 0x10) == 0x10 {write_signed_byte(data, crate::effects::pack_velocity(note.velocity));} + if (flags & 0x10) == 0x10 {write_signed_byte(data, crate::model::effects::pack_velocity(note.velocity));} if (flags & 0x20) == 0x20 { if note.kind != NoteType::Rest {write_signed_byte(data, note.value.to_i8().unwrap());} else {write_signed_byte(data, 0);} @@ -364,7 +389,7 @@ impl Song { let flags: u8 = self.pack_note_flags(note, version); write_byte(data, flags); if (flags & 0x20) == 0x20 {write_byte(data, from_note_type(¬e.kind));} - if (flags & 0x10) == 0x10 {write_signed_byte(data, crate::effects::pack_velocity(note.velocity));} + if (flags & 0x10) == 0x10 {write_signed_byte(data, crate::model::effects::pack_velocity(note.velocity));} if (flags & 0x20) == 0x20 { if note.kind != NoteType::Tie {write_signed_byte(data, note.value.to_i8().unwrap());} else {write_signed_byte(data, 0);} diff --git a/lib/src/page.rs b/lib/src/model/page.rs similarity index 85% rename from lib/src/page.rs rename to lib/src/model/page.rs index 571858b..5cfff1d 100644 --- a/lib/src/page.rs +++ b/lib/src/model/page.rs @@ -1,6 +1,7 @@ use fraction::ToPrimitive; -use crate::{gp::*, io::*}; +use crate::{io::primitive::*, model::song::*}; +use crate::error::GpResult; ///A padding construct #[derive(Debug,Clone)] @@ -67,7 +68,12 @@ impl Default for PageSetup {fn default() -> Self { PageSetup { page_size:Point{x page_number:String::from("Page %N%/%P%"), }}} -impl Song { +pub trait SongPageOps { + fn read_page_setup(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()>; + fn write_page_setup(&self, data: &mut Vec); +} + +impl SongPageOps for Song { /// Read page setup. Page setup is read as follows: /// - Page size: 2 `Ints `. Width and height of the page. /// - Page padding: 4 `Ints `. Left, right, top, bottom padding of the page. @@ -84,30 +90,31 @@ impl Song { /// * copyright1, e.g. *"Copyright %copyright%"* /// * copyright2, e.g. *"All Rights Reserved - International Copyright Secured"* /// * pageNumber - pub(crate) fn read_page_setup(&mut self, data: &[u8], seek: &mut usize) { - self.page_setup.page_size.x = read_int(data, seek).to_u16().unwrap(); - self.page_setup.page_size.y = read_int(data, seek).to_u16().unwrap(); - self.page_setup.page_margin.left = read_int(data, seek).to_u16().unwrap(); - self.page_setup.page_margin.right = read_int(data, seek).to_u16().unwrap(); - self.page_setup.page_margin.top = read_int(data, seek).to_u16().unwrap(); - self.page_setup.page_margin.bottom = read_int(data, seek).to_u16().unwrap(); - self.page_setup.score_size_proportion = read_int(data, seek).to_f32().unwrap() / 100.0; - self.page_setup.header_and_footer = read_short(data, seek).to_u16().unwrap(); - self.page_setup.title = read_int_size_string(data, seek); - self.page_setup.subtitle = read_int_size_string(data, seek); - self.page_setup.artist = read_int_size_string(data, seek); - self.page_setup.album = read_int_size_string(data, seek); - self.page_setup.words = read_int_size_string(data, seek); - self.page_setup.music = read_int_size_string(data, seek); - self.page_setup.word_and_music = read_int_size_string(data, seek); - let mut c = read_int_size_string(data, seek); + fn read_page_setup(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()> { + self.page_setup.page_size.x = read_int(data, seek)?.to_u16().unwrap(); + self.page_setup.page_size.y = read_int(data, seek)?.to_u16().unwrap(); + self.page_setup.page_margin.left = read_int(data, seek)?.to_u16().unwrap(); + self.page_setup.page_margin.right = read_int(data, seek)?.to_u16().unwrap(); + self.page_setup.page_margin.top = read_int(data, seek)?.to_u16().unwrap(); + self.page_setup.page_margin.bottom = read_int(data, seek)?.to_u16().unwrap(); + self.page_setup.score_size_proportion = read_int(data, seek)?.to_f32().unwrap() / 100.0; + self.page_setup.header_and_footer = read_short(data, seek)?.to_u16().unwrap(); + self.page_setup.title = read_int_size_string(data, seek)?; + self.page_setup.subtitle = read_int_size_string(data, seek)?; + self.page_setup.artist = read_int_size_string(data, seek)?; + self.page_setup.album = read_int_size_string(data, seek)?; + self.page_setup.words = read_int_size_string(data, seek)?; + self.page_setup.music = read_int_size_string(data, seek)?; + self.page_setup.word_and_music = read_int_size_string(data, seek)?; + let mut c = read_int_size_string(data, seek)?; c.push('\n'); - c.push_str(&read_int_size_string(data, seek)); + c.push_str(&read_int_size_string(data, seek)?); self.page_setup.copyright = c; - self.page_setup.page_number = read_int_size_string(data, seek); + self.page_setup.page_number = read_int_size_string(data, seek)?; + Ok(()) } - pub(crate) fn write_page_setup(&self, data: &mut Vec) { + fn write_page_setup(&self, data: &mut Vec) { write_i32(data, self.page_setup.page_size.x.to_i32().unwrap()); write_i32(data, self.page_setup.page_size.y.to_i32().unwrap()); @@ -126,7 +133,7 @@ impl Song { write_int_byte_size_string(data, &self.page_setup.subtitle); write_int_byte_size_string(data, &self.page_setup.artist); write_int_byte_size_string(data, &self.page_setup.album); - write_int_byte_size_string(data, &self.page_setup.word_and_music); + write_int_byte_size_string(data, &self.page_setup.words); write_int_byte_size_string(data, &self.page_setup.music); write_int_byte_size_string(data, &self.page_setup.word_and_music); let c: Vec<&str> = self.page_setup.copyright.split('\n').collect(); diff --git a/lib/src/rse.rs b/lib/src/model/rse.rs similarity index 66% rename from lib/src/rse.rs rename to lib/src/model/rse.rs index 1973fbd..bcd783c 100644 --- a/lib/src/rse.rs +++ b/lib/src/model/rse.rs @@ -1,6 +1,8 @@ use fraction::ToPrimitive; -use crate::{io::*, gp::*, enums::*, track::*}; +use crate::{io::primitive::*, model::{song::*, enums::*, track::*}}; +use crate::error::GpResult; +// use crate::gp::*; /// Equalizer found in master effect and track effect. /// @@ -42,26 +44,42 @@ pub struct TrackRse { } impl Default for TrackRse { fn default() -> Self { TrackRse {instrument:RseInstrument::default(), humanize:0, auto_accentuation: Accentuation::None, equalizer:RseEqualizer{knobs:vec![0.0;3], ..Default::default()} }}} -impl Song { +pub trait SongRseOps { + fn read_rse_master_effect(&self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_rse_equalizer(&self, data: &[u8], seek: &mut usize, knobs: u8) -> GpResult; + fn unpack_volume_value(&self, value: i8) -> f32; + fn read_track_rse(&mut self, data: &[u8], seek: &mut usize, track: &mut Track) -> GpResult<()>; + fn read_rse_instrument(&mut self, data: &[u8], seek: &mut usize) -> GpResult; + fn read_rse_instrument_effect(&mut self, data: &[u8], seek: &mut usize, instrument: &mut RseInstrument) -> GpResult<()>; + fn write_rse_master_effect(&self, data: &mut Vec); + fn write_equalizer(&self, data: &mut Vec, equalizer: &RseEqualizer); + fn pack_volume_value(&self, value: f32) -> i8; + fn write_master_reverb(&self, data: &mut Vec); + fn write_track_rse(&self, data: &mut Vec, rse: &TrackRse, version: &(u8,u8,u8)); + fn write_rse_instrument(&self, data: &mut Vec, instrument: &RseInstrument, version: &(u8,u8,u8)); + fn write_rse_instrument_effect(&self, data: &mut Vec, instrument: &RseInstrument); +} + +impl SongRseOps for Song { /// Read RSE master effect. Persistence of RSE master effect was introduced in Guitar Pro 5.1. It is read as: /// - Master volume: `int`. Values are in range from 0 to 200. /// - 10-band equalizer. See `read_equalizer()`. - pub(crate) fn read_rse_master_effect(&self, data: &[u8], seek: &mut usize) -> RseMasterEffect { + fn read_rse_master_effect(&self, data: &[u8], seek: &mut usize) -> GpResult { let mut me = RseMasterEffect::default(); if self.version.number > (5,0,0) { - me.volume = read_int(data, seek).to_f32().unwrap(); - read_int(data, seek); //??? - me.equalizer = self.read_rse_equalizer(data, seek, 11); + me.volume = read_int(data, seek)?.to_f32().unwrap(); + read_int(data, seek)?; //??? + me.equalizer = self.read_rse_equalizer(data, seek, 11)?; //println!("read_rse_master_effect(): {:?}", me); } - me + Ok(me) } /// Read equalizer values. Equalizers are used in RSE master effect and Track RSE. They consist of *n* `SignedBytes ` for each *n* bands and one `signed-byte` for gain (PRE) fader. /// Volume values are stored as opposite to actual value. See `unpack_volume_value()`. - fn read_rse_equalizer(&self, data: &[u8], seek: &mut usize, knobs: u8) -> RseEqualizer { + fn read_rse_equalizer(&self, data: &[u8], seek: &mut usize, knobs: u8) -> GpResult { let mut e = RseEqualizer::default(); - for _ in 0..knobs {e.knobs.push(self.unpack_volume_value(read_signed_byte(data, seek)));} //knobs = list(map(self.unpackVolumeValue, self.readSignedByte(count=knobsNumber))) - e //return gp.RSEEqualizer(knobs=knobs[:-1], gain=knobs[-1]) + for _ in 0..knobs {e.knobs.push(self.unpack_volume_value(read_signed_byte(data, seek)?));} //knobs = list(map(self.unpackVolumeValue, self.readSignedByte(count=knobsNumber))) + Ok(e) //return gp.RSEEqualizer(knobs=knobs[:-1], gain=knobs[-1]) } /// Unpack equalizer volume value. Equalizer volumes are float but stored as `SignedBytes `. fn unpack_volume_value(&self, value: i8) -> f32 { -value.to_f32().unwrap() / 10.0 } @@ -72,45 +90,47 @@ impl Song { /// - RSE instrument. See `readRSEInstrument`. /// - 3-band track equalizer. See `read_equalizer()`. /// - RSE instrument effect. See `read_rse_instrument_effect()`. - pub(crate) fn read_track_rse(&mut self, data: &[u8], seek: &mut usize, track: &mut Track) { - track.rse.humanize = read_byte(data, seek); + fn read_track_rse(&mut self, data: &[u8], seek: &mut usize, track: &mut Track) -> GpResult<()> { + track.rse.humanize = read_byte(data, seek)?; //println!("read_track_rse(), humanize: {} \t\t seek: {}", track.rse.humanize, *seek); *seek += 12; //read_int(data, seek); read_int(data, seek); read_int(data, seek); //??? 4 bytes*3 //*seek += 12; *seek += 12; //??? - track.rse.instrument = self.read_rse_instrument(data, seek); + track.rse.instrument = self.read_rse_instrument(data, seek)?; if self.version.number > (5,0,0) { - track.rse.equalizer = self.read_rse_equalizer(data, seek, 4); - self.read_rse_instrument_effect(data, seek, &mut track.rse.instrument); + track.rse.equalizer = self.read_rse_equalizer(data, seek, 4)?; + self.read_rse_instrument_effect(data, seek, &mut track.rse.instrument)?; } + Ok(()) } /// Read RSE instrument. /// - MIDI instrument number: `int`. /// - Unknown `int`. /// - Sound bank: `int`. /// - Effect number: `int`. Vestige of Guitar Pro 5.0 format. - pub(crate) fn read_rse_instrument(&mut self, data: &[u8], seek: &mut usize) -> RseInstrument { - let mut instrument = RseInstrument{instrument: read_int(data, seek).to_i16().unwrap(), ..Default::default()}; - instrument.unknown = read_int(data, seek).to_i16().unwrap(); //??? mostly 1 - instrument.sound_bank = read_int(data, seek).to_i16().unwrap(); + fn read_rse_instrument(&mut self, data: &[u8], seek: &mut usize) -> GpResult { + let mut instrument = RseInstrument{instrument: read_int(data, seek)?.to_i16().unwrap_or(0), ..Default::default()}; + instrument.unknown = read_int(data, seek)?.to_i16().unwrap_or(0); //??? mostly 1 + instrument.sound_bank = read_int(data, seek)?.to_i16().unwrap_or(0); //println!("read_rse_instrument(), instrument: {} {} {} \t\t seek: {}", instrument.instrument, instrument.unknown, instrument.sound_bank, *seek); if self.version.number == (5,0,0) { - instrument.effect_number = read_short(data, seek); + instrument.effect_number = read_short(data, seek)?; *seek += 1; - } else {instrument.effect_number = read_int(data, seek).to_i16().unwrap();} + } else {instrument.effect_number = read_int(data, seek)?.to_i16().unwrap_or(0);} //println!("read_rse_instrument(), instrument.effect_number: {} \t\t seek: {}", instrument.effect_number, *seek); - instrument + Ok(instrument) } /// Read RSE instrument effect name. This feature was introduced in Guitar Pro 5.1. /// - Effect name: `int-byte-size-string`. /// - Effect category: `int-byte-size-string`. - pub(crate) fn read_rse_instrument_effect(&mut self, data: &[u8], seek: &mut usize, instrument: &mut RseInstrument) { + fn read_rse_instrument_effect(&mut self, data: &[u8], seek: &mut usize, instrument: &mut RseInstrument) -> GpResult<()> { if self.version.number > (5,0,0) { - instrument.effect = read_int_byte_size_string(data, seek); - instrument.effect_category = read_int_byte_size_string(data, seek); + instrument.effect = read_int_byte_size_string(data, seek)?; + instrument.effect_category = read_int_byte_size_string(data, seek)?; } + Ok(()) } - pub(crate) fn write_rse_master_effect(&self, data: &mut Vec) { + fn write_rse_master_effect(&self, data: &mut Vec) { write_i32(data, if self.master_effect.volume == 0.0 {100} else {self.master_effect.volume.ceil().to_i32().unwrap()}); write_i32(data, 0); //reverb? self.write_equalizer(data, &self.master_effect.equalizer); @@ -123,11 +143,11 @@ impl Song { fn pack_volume_value(&self, value: f32) -> i8 { (-value * 10f32).round().to_i8().unwrap() //int(-round(value, 1) * 10) } - pub(crate) fn write_master_reverb(&self, data: &mut Vec) { + fn write_master_reverb(&self, data: &mut Vec) { write_i32(data, self.master_effect.reverb.to_i32().unwrap()); } - pub(crate) fn write_track_rse(&self, data: &mut Vec, rse: &TrackRse, version: &(u8,u8,u8)) { + fn write_track_rse(&self, data: &mut Vec, rse: &TrackRse, version: &(u8,u8,u8)) { write_byte(data, rse.humanize); write_i32(data, 0); write_i32(data, 0); write_i32(data, 100); write_placeholder_default(data, 12); @@ -137,7 +157,7 @@ impl Song { self.write_rse_instrument_effect(data, &rse.instrument); } } - pub(crate) fn write_rse_instrument(&self, data: &mut Vec, instrument: &RseInstrument, version: &(u8,u8,u8)) { + fn write_rse_instrument(&self, data: &mut Vec, instrument: &RseInstrument, version: &(u8,u8,u8)) { write_i32(data, instrument.instrument.to_i32().unwrap()); write_i32(data, instrument.unknown.to_i32().unwrap()); write_i32(data, instrument.sound_bank.to_i32().unwrap()); @@ -146,7 +166,7 @@ impl Song { write_placeholder_default(data, 1); } else {write_i32(data, instrument.effect_number.to_i32().unwrap());} } - pub(crate) fn write_rse_instrument_effect(&self, data: &mut Vec, instrument: &RseInstrument) { //version>5.0.0 + fn write_rse_instrument_effect(&self, data: &mut Vec, instrument: &RseInstrument) { //version>5.0.0 write_int_byte_size_string(data, &instrument.effect); write_int_byte_size_string(data, &instrument.effect_category); } diff --git a/lib/src/model/song.rs b/lib/src/model/song.rs new file mode 100644 index 0000000..5a060f0 --- /dev/null +++ b/lib/src/model/song.rs @@ -0,0 +1,342 @@ +use fraction::ToPrimitive; + +use crate::audio::midi::*; +use crate::io::gpif_import::*; +use crate::io::primitive::*; +use crate::model::enums::*; +use crate::model::headers::*; +use crate::model::key_signature::*; +use crate::model::lyric::*; +use crate::model::measure::*; +use crate::model::page::*; +use crate::model::rse::*; +use crate::model::track::*; +use crate::error::GpResult; + +// Struct utility to read file: https://stackoverflow.com/questions/55555538/what-is-the-correct-way-to-read-a-binary-file-in-chunks-of-a-fixed-size-and-stor +#[derive(Debug, Clone)] +pub struct Song { + pub version: Version, + pub clipboard: Option, + + pub name: String, + pub subtitle: String, //Guitar Pro + pub artist: String, + pub album: String, + pub words: String, //GP + pub author: String, //music by + pub date: String, + pub copyright: String, + /// Tab writer + pub writer: String, + pub transcriber: String, + pub instructions: String, + pub comments: String, + pub notice: Vec, + + pub tracks: Vec, + pub measure_headers: Vec, + pub channels: Vec, + pub lyrics: Lyrics, + pub tempo: i16, + pub hide_tempo: bool, + pub tempo_name: String, + pub key: KeySignature, + + pub triplet_feel: TripletFeel, + pub master_effect: RseMasterEffect, + + pub page_setup: PageSetup, + + //Used to read the file + pub current_measure_number: Option, + pub current_track: Option, + pub current_voice_number: Option, + pub current_beat_number: Option, +} + +impl Default for Song { + fn default() -> Self { + Song { + version: Version { + data: String::with_capacity(30), + clipboard: false, + number: (5, 1, 0), + }, + clipboard: None, + name: String::new(), + subtitle: String::new(), + artist: String::new(), + album: String::new(), + words: String::new(), + author: String::new(), + date: String::new(), + copyright: String::new(), + writer: String::new(), + transcriber: String::new(), + comments: String::new(), + notice: Vec::new(), + instructions: String::new(), + tracks: Vec::new(), + measure_headers: Vec::new(), + channels: Vec::with_capacity(64), + lyrics: Lyrics::default(), + tempo: 120, + hide_tempo: false, + tempo_name: String::from("Moderate"), + key: KeySignature::default(), + + triplet_feel: TripletFeel::None, + current_measure_number: None, + current_track: None, + current_voice_number: None, + current_beat_number: None, + + page_setup: PageSetup::default(), + + master_effect: RseMasterEffect::default(), + } + } +} +impl Song { + /// Read the song. A song consists of score information, triplet feel, tempo, song key, MIDI channels, measure and track count, measure headers, tracks, measures. + /// - Version: `byte-size-string` of size 30. + /// - Score information. See `readInfo`. + /// - Triplet feel: `bool`. If value is true, then triplet feel is set to eigth. + /// - Tempo: `int`. + /// - Key: `int`. Key signature of the song. + /// - MIDI channels. See `readMidiChannels`. + /// - Number of measures: `int`. + /// - Number of tracks: `int`. + /// - Measure headers. See `readMeasureHeaders`. + /// - Tracks. See `read_tracks()`. + /// - Measures. See `read_measures()`. + pub fn read_gp3(&mut self, data: &[u8]) -> GpResult<()> { + let mut seek: usize = 0; + self.version = read_version_string(data, &mut seek)?; + self.read_info(data, &mut seek)?; + self.triplet_feel = if read_bool(data, &mut seek)? { + TripletFeel::Eighth + } else { + TripletFeel::None + }; + //println!("Triplet feel: {}", self.triplet_feel); + self.tempo = read_int(data, &mut seek)?.to_i16().unwrap(); + self.key.key = read_int(data, &mut seek)?.to_i8().unwrap(); + //println!("Tempo: {} bpm\t\tKey: {}", self.tempo, self.key.to_string()); + self.read_midi_channels(data, &mut seek)?; + let measure_count = read_int(data, &mut seek)?.to_usize().unwrap(); + let track_count = read_int(data, &mut seek)?.to_usize().unwrap(); + //println!("Measures count: {}\tTrack count: {}", measure_count, track_count); + // Read measure headers. The *measures* are written one after another, their number have been specified previously. + self.read_measure_headers(data, &mut seek, measure_count)?; + self.current_measure_number = Some(0); + self.read_tracks(data, &mut seek, track_count)?; + self.read_measures(data, &mut seek)?; + Ok(()) + } + /// Read the song. A song consists of score information, triplet feel, tempo, song key, MIDI channels, measure and track count, measure headers, tracks, measures. + /// - Version: `byte-size-string` of size 30. + /// - Score information. See `readInfo`. + /// - Triplet feel: `bool`. If value is true, then triplet feel is set to eigth. + /// - Lyrics. See `read_lyrics()`. + /// - Tempo: `int`. + /// - Key: `int`. Key signature of the song. + /// - Octave: `signed-byte`. Reserved for future uses. + /// - MIDI channels. See `readMidiChannels`. + /// - Number of measures: `int`. + /// - Number of tracks: `int`. + /// - Measure headers. See `readMeasureHeaders`. + /// - Tracks. See `read_tracks()`. + /// - Measures. See `read_measures()`. + pub fn read_gp4(&mut self, data: &[u8]) -> GpResult<()> { + let mut seek: usize = 0; + self.version = read_version_string(data, &mut seek)?; + self.read_clipboard(data, &mut seek)?; + self.read_info(data, &mut seek)?; + self.triplet_feel = if read_bool(data, &mut seek)? { + TripletFeel::Eighth + } else { + TripletFeel::None + }; + //println!("Triplet feel: {}", self.triplet_feel); + self.lyrics = self.read_lyrics(data, &mut seek)?; //read lyrics + self.tempo = read_int(data, &mut seek)?.to_i16().unwrap(); + self.key.key = read_int(data, &mut seek)?.to_i8().unwrap(); + //println!("Tempo: {} bpm\t\tKey: {}", self.tempo, self.key.to_string()); + read_signed_byte(data, &mut seek)?; //octave + self.read_midi_channels(data, &mut seek)?; + let measure_count = read_int(data, &mut seek)?.to_usize().unwrap(); + let track_count = read_int(data, &mut seek)?.to_usize().unwrap(); + //println!("Measures count: {}\tTrack count: {}", measure_count, track_count); + // Read measure headers. The *measures* are written one after another, their number have been specified previously. + self.read_measure_headers(data, &mut seek, measure_count)?; + //self.current_measure_number = Some(0); + self.read_tracks(data, &mut seek, track_count)?; + self.read_measures(data, &mut seek)?; + Ok(()) + } + pub fn read_gp5(&mut self, data: &[u8]) -> GpResult<()> { + let mut seek: usize = 0; + self.version = read_version_string(data, &mut seek)?; + self.read_clipboard(data, &mut seek)?; + self.read_info(data, &mut seek)?; + self.lyrics = self.read_lyrics(data, &mut seek)?; //read lyrics + self.master_effect = self.read_rse_master_effect(data, &mut seek)?; + self.read_page_setup(data, &mut seek)?; + self.tempo_name = read_int_size_string(data, &mut seek)?; + self.tempo = read_int(data, &mut seek)?.to_i16().unwrap(); + self.hide_tempo = if self.version.number > (5, 0, 0) { + read_bool(data, &mut seek)? + } else { + false + }; + self.key.key = read_signed_byte(data, &mut seek)?; + read_int(data, &mut seek)?; //octave + self.read_midi_channels(data, &mut seek)?; + let directions = self.read_directions(data, &mut seek)?; + self.master_effect.reverb = read_int(data, &mut seek)?.to_f32().unwrap(); + let measure_count = read_int(data, &mut seek)?.to_usize().unwrap(); + let track_count = read_int(data, &mut seek)?.to_usize().unwrap(); + //println!("{} {} {} {:?}", self.tempo_name, self.tempo, self.hide_tempo, self.key.key); //OK + println!( + "Track count: {} \t Measure count: {}", + track_count, measure_count + ); //OK + self.read_measure_headers_v5(data, &mut seek, measure_count, &directions)?; + self.read_tracks_v5(data, &mut seek, track_count)?; + println!("read_gp5(), after tracks \t seek: {}", seek); + self.read_measures(data, &mut seek)?; + println!("read_gp5(), after measures \t seek: {}", seek); + Ok(()) + } + /// Read Guitar Pro 7+ file (.gp) + pub fn read_gp(&mut self, data: &[u8]) -> GpResult<()> { + use crate::io::gpx::read_gp; + let gpif = read_gp(data)?; + self.version.number = (7, 0, 0); // Todo parse from gpif.version + self.read_gpif(&gpif); + Ok(()) + } + /// Read Guitar Pro 6 file (.gpx) + pub fn read_gpx(&mut self, data: &[u8]) -> GpResult<()> { + use crate::io::gpx::read_gpx; + let gpif = read_gpx(data)?; + self.version.number = (6, 0, 0); + self.read_gpif(&gpif); + Ok(()) + } + + /// Read information (name, artist, ...) + fn read_info(&mut self, data: &[u8], seek: &mut usize) -> GpResult<()> { + self.name = read_int_byte_size_string(data, seek)?; //.replace("\r", " ").replace("\n", " ").trim().to_owned(); + self.subtitle = read_int_byte_size_string(data, seek)?; + self.artist = read_int_byte_size_string(data, seek)?; + self.album = read_int_byte_size_string(data, seek)?; + self.words = read_int_byte_size_string(data, seek)?; //music + self.author = if self.version.number.0 < 5 { + self.words.clone() + } else { + read_int_byte_size_string(data, seek)? + }; + self.copyright = read_int_byte_size_string(data, seek)?; + self.writer = read_int_byte_size_string(data, seek)?; //tabbed by + self.instructions = read_int_byte_size_string(data, seek)?; //instructions + //notices + let nc = read_int(data, seek)?.to_usize().unwrap(); //notes count + if nc > 0 { + for i in 0..nc { + self.notice.push(read_int_byte_size_string(data, seek)?); + println!(" {}\t\t{}", i, self.notice[self.notice.len() - 1]); + } + } + Ok(()) + } + + /*pub const _MAX_STRINGS: i32 = 25; + pub const _MIN_STRINGS: i32 = 1; + pub const _MAX_OFFSET: i32 = 24; + pub const _MIN_OFFSET: i32 = -24;*/ + + /// Write data to a Vec, you are free to use the encoded data to write it in a file or in a database or do something else. + pub fn write(&self, version: (u8, u8, u8), clipboard: Option) -> Vec { + let mut data: Vec = Vec::with_capacity(8388608); //capacity of 8MB, should be sufficient + write_version(&mut data, version); + if clipboard.is_some() && clipboard.unwrap() && version.0 >= 4 { + self.write_clipboard(&mut data, &version); + } + self.write_info(&mut data, version); + if version.0 < 5 { + write_bool(&mut data, self.triplet_feel != TripletFeel::None); + } + if version.0 >= 4 { + self.write_lyrics(&mut data); + } + if version > (5, 0, 0) { + self.write_rse_master_effect(&mut data); + } + if version.0 >= 5 { + self.write_page_setup(&mut data); + write_int_byte_size_string(&mut data, &self.tempo_name); + } + write_i32(&mut data, self.tempo.to_i32().unwrap()); + if version > (5, 0, 0) { + write_bool(&mut data, self.hide_tempo); + } + write_i32(&mut data, self.key.key.to_i32().unwrap()); + + if version.0 >= 4 { + write_signed_byte(&mut data, 0); + } //octave + self.write_midi_channels(&mut data); //TODO: fixme for writing + //return data; + + if version.0 == 5 { + self.write_directions(&mut data); + self.write_master_reverb(&mut data); + } + + write_i32(&mut data, self.tracks[0].measures.len().to_i32().unwrap()); + write_i32(&mut data, self.tracks.len().to_i32().unwrap()); + self.write_measure_headers(&mut data, &version); + self.write_tracks(&mut data, &version); + self.write_measures(&mut data, &version); + write_i32(&mut data, 0); + data + } + fn write_info(&self, data: &mut Vec, version: (u8, u8, u8)) { + write_int_byte_size_string(data, &self.name); + write_int_byte_size_string(data, &self.subtitle); + write_int_byte_size_string(data, &self.artist); + write_int_byte_size_string(data, &self.album); + if version.0 < 5 { + write_int_byte_size_string(data, &self.pack_author()); + } else { + write_int_byte_size_string(data, &self.words); + write_int_byte_size_string(data, &self.author); + } + write_int_byte_size_string(data, &self.copyright); + write_int_byte_size_string(data, &self.writer); + write_int_byte_size_string(data, &self.instructions); + write_i32(data, self.notice.len().to_i32().unwrap()); + for i in 0..self.notice.len() { + write_int_byte_size_string(data, &self.notice[i]); + } + } + fn pack_author(&self) -> String { + if !self.words.is_empty() && !self.author.is_empty() { + if self.words != self.author { + let mut s = self.words.clone(); + s.push_str(", "); + s.push_str(&self.author); + s + } else { + self.words.clone() + } + } else { + let mut s = self.words.clone(); + s.push_str(&self.author); + s + } + } +} diff --git a/lib/src/track.rs b/lib/src/model/track.rs similarity index 82% rename from lib/src/track.rs rename to lib/src/model/track.rs index 79ae267..b099255 100644 --- a/lib/src/track.rs +++ b/lib/src/model/track.rs @@ -1,6 +1,7 @@ use fraction::ToPrimitive; -use crate::{io::*, gp::*, enums::*, rse::*, measure::*}; +use crate::{io::primitive::*, model::{song::*, enums::*, measure::*, rse::*}, audio::midi::*}; +use crate::error::GpResult; /// Settings of the track. #[derive(Debug,Clone)] @@ -41,6 +42,7 @@ pub struct Track { pub mute: bool, pub visible: bool, pub name: String, + pub short_name: String, /// A guitar string with a special tuning. pub strings: Vec<(i8, i8)>, pub color: i32, @@ -54,6 +56,12 @@ pub struct Track { pub rse: TrackRse, pub measures: Vec, pub settings: TrackSettings, + /// MIDI program from GPIF (GP6/GP7) + pub midi_program_gpif: Option, + /// Chromatic transposition (GP6/GP7) + pub transpose_chromatic: i32, + /// Octave transposition (GP6/GP7) + pub transpose_octave: i32, } impl Default for Track { fn default() -> Self { Track { @@ -62,6 +70,7 @@ impl Default for Track { channel_index: 0, //channel_id: 25, solo: false, mute: false, visible: true, name: String::from("Track 1"), + short_name: String::new(), strings: vec![(1, 64), (2, 59), (3, 55), (4, 50), (5, 45), (6, 40)], banjo_track: false, twelve_stringed_guitar_track: false, percussion_track: false, fret_count: 24, @@ -71,24 +80,40 @@ impl Default for Track { use_rse: false, rse: TrackRse::default(), measures: Vec::new(), settings: TrackSettings::default(), + midi_program_gpif: None, + transpose_chromatic: 0, + transpose_octave: 0, }} } -impl Song { + +pub trait SongTrackOps { + fn read_tracks(&mut self, data: &[u8], seek: &mut usize, track_count: usize) -> GpResult<()>; + fn read_tracks_v5(&mut self, data: &[u8], seek: &mut usize, track_count: usize) -> GpResult<()>; + fn read_track(&mut self, data: &[u8], seek: &mut usize, number: usize) -> GpResult<()>; + fn read_track_v5(&mut self, data: &[u8], seek: &mut usize, number: usize) -> GpResult<()>; + fn write_tracks(&self, data: &mut Vec, version: &(u8,u8,u8)); + fn write_track(&self, data: &mut Vec, number: usize); + fn write_track_v5(&self, data: &mut Vec, number: usize, version: &(u8,u8,u8)); +} + +impl SongTrackOps for Song { /// Read tracks. The tracks are written one after another, their number having been specified previously in :meth:`GP3File.readSong`. /// - `track_count`: number of tracks to expect. - pub(crate) fn read_tracks(&mut self, data: &[u8], seek: &mut usize, track_count: usize) { + fn read_tracks(&mut self, data: &[u8], seek: &mut usize, track_count: usize) -> GpResult<()> { //println!("read_tracks()"); - for i in 0..track_count {self.read_track(data, seek, i);} + for i in 0..track_count {self.read_track(data, seek, i)?;} + Ok(()) } - pub(crate) fn read_tracks_v5(&mut self, data: &[u8], seek: &mut usize, track_count: usize) { + fn read_tracks_v5(&mut self, data: &[u8], seek: &mut usize, track_count: usize) -> GpResult<()> { //println!("read_tracks_v5(): {:?} {}", self.version.number, self.version.number == (5,1,0)); - for i in 0..track_count { self.read_track_v5(data, seek, i); } + for i in 0..track_count { self.read_track_v5(data, seek, i)?; } *seek += if self.version.number == (5,0,0) {2} else {1}; + Ok(()) } /// Read a track. The first byte is the track's flags. It presides the track's attributes: - /// + /// /// | **bit 7 to 3** | **bit 2** | **bit 1** | **bit 0** | /// |----------------|-------------|--------------------------|-------------| /// | Blank bits | Banjo track | 12 stringed guitar track | Drums track | @@ -104,31 +129,32 @@ impl Song { /// * **Number of frets**: `integer`. The number of frets of the instrument. /// * **Height of the capo**: `integer`. The number of the fret on which a capo is present. If no capo is used, the value is `0x00000000`. /// * **Track's color**: `color`. The track's displayed color in Guitar Pro. - fn read_track(&mut self, data: &[u8], seek: &mut usize, number: usize) { + fn read_track(&mut self, data: &[u8], seek: &mut usize, number: usize) -> GpResult<()> { let mut track = Track{number: number.to_i32().unwrap(), ..Default::default()}; //read the flag - let flags = read_byte(data, seek); + let flags = read_byte(data, seek)?; //println!("read_track(), flags: {}", flags); track.percussion_track = (flags & 0x01) == 0x01; //Drums track track.twelve_stringed_guitar_track = (flags & 0x02) == 0x02; //12 stringed guitar track track.banjo_track = (flags & 0x04) == 0x04; //Banjo track - track.name = read_byte_size_string(data, seek, 40); - let string_count = read_int(data, seek).to_u8().unwrap(); + track.name = read_byte_size_string(data, seek, 40)?; + let string_count = read_int(data, seek)?.to_u8().unwrap(); track.strings.clear(); for i in 0..7i8 { - let i_tuning = read_int(data, seek).to_i8().unwrap(); + let i_tuning = read_int(data, seek)?.to_i8().unwrap(); if string_count.to_i8().unwrap() > i { track.strings.push((i + 1, i_tuning)); } } //println!("tuning: {:?}", track.strings); - track.port = read_int(data, seek).to_u8().unwrap(); - let index = self.read_channel(data, seek); + track.port = read_int(data, seek)?.to_u8().unwrap(); + let index = self.read_channel(data, seek)?; if self.channels[index].channel == 9 {track.percussion_track = true;} - track.fret_count = read_int(data, seek).to_u8().unwrap(); - track.offset = read_int(data, seek); - track.color = read_color(data, seek); - println!("\tInstrument: {} \t Strings: {}/{} ({:?})", self.channels[index].get_instrument_name(), string_count, track.strings.len(), track.strings); + track.fret_count = read_int(data, seek)?.to_u8().unwrap(); + track.offset = read_int(data, seek)?; + track.color = read_color(data, seek)?; + //println!("\tInstrument: {} \t Strings: {}/{} ({:?})", self.channels[index].get_instrument_name(), string_count, track.strings.len(), track.strings); self.tracks.push(track); + Ok(()) } /// Read track. If it's Guitar Pro 5.0 format and track is first then one blank byte is read. Then go track's flags. It presides the track's attributes: @@ -140,7 +166,7 @@ impl Song { /// - *0x20*: track is muted /// - *0x40*: RSE is enabled /// - *0x80*: show tuning in the header of the sheet. - /// + /// /// Flags are followed by: /// - Name: `String`. A 40 characters long string containing the track's name. /// - Number of strings: :ref:`int`. An integer equal to the number of strings of the track. @@ -150,7 +176,7 @@ impl Song { /// - Number of frets: :ref:`int`. The number of frets of the instrument. /// - Height of the capo: :ref:`int`. The number of the fret on which a capo is set. If no capo is used, the value is 0. /// - Track's color. The track's displayed color in Guitar Pro. - /// + /// /// The properties are followed by second set of flags stored in a :ref:`short`: /// - *0x0001*: show tablature /// - *0x0002*: show standard notation @@ -163,15 +189,15 @@ impl Song { /// - *0x0200*: auto let-ring /// - *0x0400*: auto brush /// - *0x0800*: extend rhythmic inside the tab - /// + /// /// Then follow: /// - Auto accentuation: :ref:`byte`. See :class:`guitarpro.models.Accentuation`. /// - MIDI bank: :ref:`byte`. /// - Track RSE. See `readTrackRSE`. - fn read_track_v5(&mut self, data: &[u8], seek: &mut usize, number: usize) { + fn read_track_v5(&mut self, data: &[u8], seek: &mut usize, number: usize) -> GpResult<()> { let mut track = Track{number: number.to_i32().unwrap(), ..Default::default()}; if number == 0 || self.version.number == (5,0,0) {*seek += 1;} //always 0 //missing 3 skips? - let flags1 = read_byte(data, seek); + let flags1 = read_byte(data, seek)?; //println!("read_track_v5(), flags1: {} \t seek: {}", flags1, *seek); track.percussion_track = (flags1 & 0x01) == 0x01; track.banjo_track = (flags1 & 0x02) == 0x02; @@ -180,24 +206,24 @@ impl Song { track.mute = (flags1 & 0x20) == 0x20; track.use_rse = (flags1 & 0x40) == 0x40; track.indicate_tuning = (flags1 & 0x80) == 0x80; - track.name = read_byte_size_string(data, seek, 40); + track.name = read_byte_size_string(data, seek, 40)?; //let string_count = read_int(data, seek).to_u8().unwrap(); - let sc = read_int(data, seek); + let sc = read_int(data, seek)?; //println!("read_track_v5(), track:name: \"{}\", string count: {}", track.name, sc); let string_count = sc.to_u8().unwrap(); track.strings.clear(); for i in 0i8..7i8 { - let i_tuning = read_int(data, seek).to_i8().unwrap(); + let i_tuning = read_int(data, seek)?.to_i8().unwrap(); if string_count.to_i8().unwrap() > i { track.strings.push((i + 1, i_tuning)); } } - track.port = read_int(data, seek).to_u8().unwrap(); - self.read_channel(data, seek); + track.port = read_int(data, seek)?.to_u8().unwrap(); + self.read_channel(data, seek)?; if self.channels[number].channel == 9 {track.percussion_track = true;} - track.fret_count = read_int(data, seek).to_u8().unwrap(); - track.offset = read_int(data, seek); - track.color = read_color(data, seek); + track.fret_count = read_int(data, seek)?.to_u8().unwrap(); + track.offset = read_int(data, seek)?; + track.color = read_color(data, seek)?; - let flags2 = read_short(data, seek); + let flags2 = read_short(data, seek)?; //println!("read_track_v5(), flags2: {}", flags2); track.settings.tablature = (flags2 & 0x0001) == 0x0001; track.settings.notation = (flags2 & 0x0002) == 0x0002; @@ -212,13 +238,14 @@ impl Song { track.settings.auto_brush = (flags2 & 0x0400) == 0x0400; track.settings.extend_rythmic = (flags2 & 0x0800) == 0x0800; - track.rse.auto_accentuation = get_accentuation(read_byte(data, seek)); - self.channels[number].bank = read_byte(data, seek); - self.read_track_rse(data, seek, &mut track); + track.rse.auto_accentuation = get_accentuation(read_byte(data, seek)?)?; + self.channels[number].bank = read_byte(data, seek)?; + self.read_track_rse(data, seek, &mut track)?; self.tracks.push(track); + Ok(()) } - pub(crate) fn write_tracks(&self, data: &mut Vec, version: &(u8,u8,u8)) { + fn write_tracks(&self, data: &mut Vec, version: &(u8,u8,u8)) { for i in 0..self.tracks.len() { //self.current_track = Some(i); if version.0 < 5 {self.write_track(data, i);} diff --git a/lib/src/song.rs b/lib/src/song.rs deleted file mode 100644 index 037df8d..0000000 --- a/lib/src/song.rs +++ /dev/null @@ -1,260 +0,0 @@ - -use fraction::ToPrimitive; - -use crate::enums::*; -use crate::io::*; -use crate::headers::*; -use crate::page::*; -use crate::track::*; -use crate::key_signature::*; -use crate::lyric::*; -use crate::midi::*; -use crate::rse::*; - - -// Struct utility to read file: https://stackoverflow.com/questions/55555538/what-is-the-correct-way-to-read-a-binary-file-in-chunks-of-a-fixed-size-and-stor -#[derive(Debug,Clone)] -pub struct Song { - pub version: Version, - pub clipboard: Option, - - pub name: String, - pub subtitle: String, //Guitar Pro - pub artist: String, - pub album: String, - pub words: String, //GP - pub author: String, //music by - pub date: String, - pub copyright: String, - /// Tab writer - pub writer: String, - pub transcriber: String, - pub instructions: String, - pub comments: String, - pub notice: Vec, - - pub tracks: Vec, - pub measure_headers: Vec, - pub channels: Vec, - pub lyrics: Lyrics, - pub tempo: i16, - pub hide_tempo: bool, - pub tempo_name:String, - pub key: KeySignature, - - pub triplet_feel: TripletFeel, - pub master_effect: RseMasterEffect, - - pub page_setup: PageSetup, - - //Used to read the file - pub current_measure_number: Option, - pub current_track: Option, - pub current_voice_number: Option, - pub current_beat_number: Option, -} - -impl Default for Song { - fn default() -> Self { Song { - version: Version {data: String::with_capacity(30), clipboard: false, number: (5,1,0)}, clipboard: None, - name:String::new(), subtitle: String::new(), artist:String::new(), album: String::new(), - words: String::new(), author:String::new(), date:String::new(), - copyright:String::new(), writer:String::new(), transcriber:String::new(), comments:String::new(), - notice:Vec::new(), - instructions: String::new(), - tracks:Vec::new(), - measure_headers:Vec::new(), - channels:Vec::with_capacity(64), - lyrics: Lyrics::default(), - tempo: 120, hide_tempo: false, tempo_name:String::from("Moderate"), - key: KeySignature::default(), - - triplet_feel: TripletFeel::None, - current_measure_number: None, current_track: None, current_voice_number: None, current_beat_number: None, - - page_setup: PageSetup::default(), - - master_effect: RseMasterEffect::default(), - }} -} -impl Song { - /// Read the song. A song consists of score information, triplet feel, tempo, song key, MIDI channels, measure and track count, measure headers, tracks, measures. - /// - Version: `byte-size-string` of size 30. - /// - Score information. See `readInfo`. - /// - Triplet feel: `bool`. If value is true, then triplet feel is set to eigth. - /// - Tempo: `int`. - /// - Key: `int`. Key signature of the song. - /// - MIDI channels. See `readMidiChannels`. - /// - Number of measures: `int`. - /// - Number of tracks: `int`. - /// - Measure headers. See `readMeasureHeaders`. - /// - Tracks. See `read_tracks()`. - /// - Measures. See `read_measures()`. - pub fn read_gp3(&mut self, data: &[u8]) { - let mut seek: usize = 0; - self.version = read_version_string(data, &mut seek); - self.read_info(data, &mut seek); - self.triplet_feel = if read_bool(data, &mut seek) {TripletFeel::Eighth} else {TripletFeel::None}; - //println!("Triplet feel: {}", self.triplet_feel); - self.tempo = read_int(data, &mut seek).to_i16().unwrap(); - self.key.key = read_int(data, &mut seek).to_i8().unwrap(); - //println!("Tempo: {} bpm\t\tKey: {}", self.tempo, self.key.to_string()); - self.read_midi_channels(data, &mut seek); - let measure_count = read_int(data, &mut seek).to_usize().unwrap(); - let track_count = read_int(data, &mut seek).to_usize().unwrap(); - //println!("Measures count: {}\tTrack count: {}", measure_count, track_count); - // Read measure headers. The *measures* are written one after another, their number have been specified previously. - self.read_measure_headers(data, &mut seek, measure_count); - self.current_measure_number = Some(0); - self.read_tracks(data, &mut seek, track_count); - self.read_measures(data, &mut seek); - } - /// Read the song. A song consists of score information, triplet feel, tempo, song key, MIDI channels, measure and track count, measure headers, tracks, measures. - /// - Version: `byte-size-string` of size 30. - /// - Score information. See `readInfo`. - /// - Triplet feel: `bool`. If value is true, then triplet feel is set to eigth. - /// - Lyrics. See `read_lyrics()`. - /// - Tempo: `int`. - /// - Key: `int`. Key signature of the song. - /// - Octave: `signed-byte`. Reserved for future uses. - /// - MIDI channels. See `readMidiChannels`. - /// - Number of measures: `int`. - /// - Number of tracks: `int`. - /// - Measure headers. See `readMeasureHeaders`. - /// - Tracks. See `read_tracks()`. - /// - Measures. See `read_measures()`. - pub fn read_gp4(&mut self, data: &[u8]) { - let mut seek: usize = 0; - self.version = read_version_string(data, &mut seek); - self.read_clipboard(data, &mut seek); - self.read_info(data, &mut seek); - self.triplet_feel = if read_bool(data, &mut seek) {TripletFeel::Eighth} else {TripletFeel::None}; - //println!("Triplet feel: {}", self.triplet_feel); - self.lyrics = self.read_lyrics(data, &mut seek); //read lyrics - self.tempo = read_int(data, &mut seek).to_i16().unwrap(); - self.key.key = read_int(data, &mut seek).to_i8().unwrap(); - //println!("Tempo: {} bpm\t\tKey: {}", self.tempo, self.key.to_string()); - read_signed_byte(data, &mut seek); //octave - self.read_midi_channels(data, &mut seek); - let measure_count = read_int(data, &mut seek).to_usize().unwrap(); - let track_count = read_int(data, &mut seek).to_usize().unwrap(); - //println!("Measures count: {}\tTrack count: {}", measure_count, track_count); - // Read measure headers. The *measures* are written one after another, their number have been specified previously. - self.read_measure_headers(data, &mut seek, measure_count); - //self.current_measure_number = Some(0); - self.read_tracks(data, &mut seek, track_count); - self.read_measures(data, &mut seek); - } - pub fn read_gp5(&mut self, data: &[u8]) { - let mut seek: usize = 0; - self.version = read_version_string(data, &mut seek); - self.read_clipboard(data, &mut seek); - self.read_info(data, &mut seek); - self.lyrics = self.read_lyrics(data, &mut seek); //read lyrics - self.master_effect = self.read_rse_master_effect(data, &mut seek); - self.read_page_setup(data, &mut seek); - self.tempo_name = read_int_size_string(data, &mut seek); - self.tempo = read_int(data, &mut seek).to_i16().unwrap(); - self.hide_tempo = if self.version.number > (5,0,0) {read_bool(data, &mut seek)} else {false}; - self.key.key = read_signed_byte(data, &mut seek); - read_int(data, &mut seek); //octave - self.read_midi_channels(data, &mut seek); - let directions = self.read_directions(data, &mut seek); - self.master_effect.reverb = read_int(data, &mut seek).to_f32().unwrap(); - let measure_count = read_int(data, &mut seek).to_usize().unwrap(); - let track_count = read_int(data, &mut seek).to_usize().unwrap(); - //println!("{} {} {} {:?}", self.tempo_name, self.tempo, self.hide_tempo, self.key.key); //OK - println!("Track count: {} \t Measure count: {}", track_count, measure_count); //OK - self.read_measure_headers_v5(data, &mut seek, measure_count, &directions); - self.read_tracks_v5(data, &mut seek, track_count); - println!("read_gp5(), after tracks \t seek: {}", seek); - self.read_measures(data, &mut seek); - println!("read_gp5(), after measures \t seek: {}", seek); - } - - /// Read information (name, artist, ...) - fn read_info(&mut self, data: &[u8], seek: &mut usize) { - self.name = read_int_byte_size_string(data, seek);//.replace("\r", " ").replace("\n", " ").trim().to_owned(); - self.subtitle = read_int_byte_size_string(data, seek); - self.artist = read_int_byte_size_string(data, seek); - self.album = read_int_byte_size_string(data, seek); - self.words = read_int_byte_size_string(data, seek); //music - self.author = if self.version.number.0 < 5 {self.words.clone()} else {read_int_byte_size_string(data, seek)}; - self.copyright = read_int_byte_size_string(data, seek); - self.writer = read_int_byte_size_string(data, seek); //tabbed by - self.instructions= read_int_byte_size_string(data, seek); //instructions - //notices - let nc = read_int(data, seek).to_usize().unwrap(); //notes count - if nc > 0 { for i in 0..nc { self.notice.push(read_int_byte_size_string(data, seek)); println!(" {}\t\t{}",i, self.notice[self.notice.len()-1]); }} - } - - /*pub const _MAX_STRINGS: i32 = 25; - pub const _MIN_STRINGS: i32 = 1; - pub const _MAX_OFFSET: i32 = 24; - pub const _MIN_OFFSET: i32 = -24;*/ - - /// Write data to a Vec, you are free to use the encoded data to write it in a file or in a database or do something else. - pub fn write(&self, version: (u8,u8,u8), clipboard: Option) ->Vec { - let mut data: Vec = Vec::with_capacity(8388608); //capacity of 8MB, should be sufficient - write_version(&mut data, version); - if clipboard.is_some() && clipboard.unwrap() && version.0 >= 4 {self.write_clipboard(&mut data, &version);} - self.write_info(&mut data, version); - if version.0 < 5 {write_bool(&mut data, self.triplet_feel != TripletFeel::None);} - if version.0 >= 4 {self.write_lyrics(&mut data);} - if version > (5,0,0) {self.write_rse_master_effect(&mut data);} - if version.0 >= 5 { - self.write_page_setup(&mut data); - write_int_byte_size_string(&mut data, &self.tempo_name); - } - write_i32(&mut data, self.tempo.to_i32().unwrap()); - if version > (5,0,0) {write_bool(&mut data, self.hide_tempo);} - write_i32(&mut data, self.key.key.to_i32().unwrap()); - - if version.0 >= 4 {write_signed_byte(&mut data, 0);} //octave - self.write_midi_channels(&mut data); //TODO: fixme for writing - //return data; - - if version.0 == 5 { - self.write_directions(&mut data); - self.write_master_reverb(&mut data); - } - - write_i32(&mut data, self.tracks[0].measures.len().to_i32().unwrap()); - write_i32(&mut data, self.tracks.len().to_i32().unwrap()); - self.write_measure_headers(&mut data, &version); - self.write_tracks(&mut data, &version); - self.write_measures(&mut data, &version); - write_i32(&mut data, 0); - data - } - fn write_info(&self, data: &mut Vec, version: (u8,u8,u8)) { - write_int_byte_size_string(data, &self.name); - write_int_byte_size_string(data, &self.subtitle); - write_int_byte_size_string(data, &self.artist); - write_int_byte_size_string(data, &self.album); - if version.0 < 5 {write_int_byte_size_string(data, &self.pack_author());} - else { - write_int_byte_size_string(data, &self.words); - write_int_byte_size_string(data, &self.author); - } - write_int_byte_size_string(data, &self.copyright); - write_int_byte_size_string(data, &self.writer); - write_int_byte_size_string(data, &self.instructions); - write_i32(data, self.notice.len().to_i32().unwrap()); - for i in 0..self.notice.len() {write_int_byte_size_string(data, &self.notice[i]);} - } - fn pack_author(&self) -> String { - if !self.words.is_empty() && !self.author.is_empty() { - if self.words != self.author { - let mut s = self.words.clone(); - s.push_str(", "); - s.push_str(&self.author); - s - } else {self.words.clone()} - } else { - let mut s = self.words.clone(); - s.push_str(&self.author); - s - } - } -} \ No newline at end of file diff --git a/lib/src/test_audit.rs b/lib/src/test_audit.rs new file mode 100644 index 0000000..312fdc9 --- /dev/null +++ b/lib/src/test_audit.rs @@ -0,0 +1,111 @@ +use crate::Song; +use std::fs; +use std::path::Path; + +#[test] +fn test_audit_all_files() { + let test_dir = Path::new("../test"); + // Handle running from lib or root + let test_dir = if test_dir.exists() { + test_dir + } else { + Path::new("./test") + }; + + if !test_dir.exists() { + eprintln!("Test directory not found!"); + return; + } + + let mut results = Vec::new(); + let mut files: Vec<_> = fs::read_dir(test_dir) + .expect("Cannot read dir") + .map(|e| e.unwrap().path()) + .filter(|p| p.is_file()) + .collect(); + files.sort(); + + for path in files { + let filename = path.file_name().unwrap().to_str().unwrap().to_string(); + let extension = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + + let data = match fs::read(&path) { + Ok(d) => d, + Err(e) => { + results.push(format!("{}: READ_ERROR ({})", filename, e)); + continue; + } + }; + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut song = Song::default(); + match extension.as_str() { + "gp3" => { + let _ = song.read_gp3(&data); + } + "gp4" => { + let _ = song.read_gp4(&data); + } + "gp5" => { + let _ = song.read_gp5(&data); + } + "gp" => { + let _ = song.read_gp(&data); + } + "gpx" => { + let _ = song.read_gpx(&data); + } + _ => return "SKIP".to_string(), + } + "OK".to_string() + })); + + match result { + Ok(status) => results.push(format!("{}: {}", filename, status)), + Err(e) => { + let msg = if let Some(s) = e.downcast_ref::<&str>() { + format!("PANIC: {}", s) + } else if let Some(s) = e.downcast_ref::() { + format!("PANIC: {}", s) + } else { + "PANIC: Unknown".to_string() + }; + results.push(format!("{}: {}", filename, msg)); + } + } + } + + // Write report + let report = results.join("\n"); + fs::write("audit_report.txt", report).expect("Unable to write report"); +} + +#[test] +fn test_let_it_be_gp3() { + let path = Path::new("../test/the-beatles-let_it_be.gp3"); + let path = if path.exists() { + path + } else { + Path::new("./test/the-beatles-let_it_be.gp3") + }; + let data = fs::read(path).expect("File not found"); + let mut song = Song::default(); + let _ = song.read_gp3(&data); +} + +#[test] +fn test_demo_v5_gp5() { + let path = Path::new("../test/Demo v5.gp5"); + let path = if path.exists() { + path + } else { + Path::new("./test/Demo v5.gp5") + }; + let data = fs::read(path).expect("File not found"); + let mut song = Song::default(); + let _ = song.read_gp5(&data); +} diff --git a/lib/src/tests.rs b/lib/src/tests.rs new file mode 100644 index 0000000..42ccb0e --- /dev/null +++ b/lib/src/tests.rs @@ -0,0 +1,1846 @@ +use crate::model::song::Song; +use fraction::ToPrimitive; +use std::{fs, io::Read}; + +fn read_file(path: String) -> Vec { + let test_path = if path.starts_with("test/") { + format!("../{}", path) + } else { + format!("../test/{}", path) + }; + let f = fs::OpenOptions::new() + .read(true) + .open(&test_path) + .unwrap_or_else(|e| panic!("Cannot open file '{}': {}", test_path, e)); + let size: usize = fs::metadata(&test_path) + .unwrap_or_else(|e| panic!("Unable to get file size for '{}': {}", test_path, e)) + .len() + .to_usize() + .unwrap(); + let mut data: Vec = Vec::with_capacity(size); + f.take(size as u64) + .read_to_end(&mut data) + .unwrap_or_else(|e| panic!("Unable to read file contents from '{}': {}", test_path, e)); + data +} + +#[test] +fn test_gp3_chord() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/Chords.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_chord() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Chords.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_chord() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Chords.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_unknown_chord_extension() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Unknown Chord Extension.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_chord_without_notes() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/chord_without_notes.gp5"))) + .unwrap(); + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/001_Funky_Guy.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp3_duration() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/Duration.gp3"))) + .unwrap(); +} + +#[test] +fn test_gp3_effects() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/Effects.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_effects() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Effects.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_effects() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Effects.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp3_harmonics() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/Harmonics.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_harmonics() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Harmonics.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_harmonics() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Harmonics.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp4_key() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Key.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_key() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Key.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp4_repeat() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Repeat.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_repeat() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Repeat.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp5_rse() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/RSE.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp4_slides() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Slides.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_slides() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Slides.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp4_strokes() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Strokes.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_strokes() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Strokes.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp4_vibrato() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/Vibrato.gp4"))) + .unwrap(); +} + +#[test] +fn test_gp5_voices() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Voices.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp5_no_wah() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/No Wah.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_wah() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Wah.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_wah_m() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/Wah-m.gp5"))) + .unwrap(); +} + +#[test] +fn test_gp5_all_percussion() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/all-percussion.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_basic_bend() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/basic-bend.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_beams_sterms_ledger_lines() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from( + "test/beams-stems-ledger-lines.gp5", + ))) + .unwrap(); +} +#[test] +fn test_gp5_brush() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/brush.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_capo_fret() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/capo-fret.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_capo_fret() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/capo-fret.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_capo_fret() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/capo-fret.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_copyright() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/copyright.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_copyright() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/copyright.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_copyright() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/copyright.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_dotted_gliss() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/dotted-gliss.gp3"))) + .unwrap(); +} +#[test] +fn test_gp5_dotted_tuplets() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/dotted-tuplets.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_dynamic() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/dynamic.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_fade_in() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/fade-in.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_fade_in() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/fade-in.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_fingering() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/fingering.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_fingering() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/fingering.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_fret_diagram() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/fret-diagram.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_fret_diagram() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/fret-diagram.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_ghost_note() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/ghost_note.gp3"))) + .unwrap(); +} +#[test] +fn test_gp5_grace() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/grace.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_heavy_accent() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/heavy-accent.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_high_pitch() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/high-pitch.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_keysig() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/keysig.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_keysig() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/keysig.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_legato_slide() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/legato-slide.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_legato_slide() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/legato-slide.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_let_ring() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/let-ring.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_let_ring() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/let-ring.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_palm_mute() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/palm-mute.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_palm_mute() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/palm-mute.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_pick_up_down() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/pick-up-down.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_pick_up_down() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/pick-up-down.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_rest_centered() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/rest-centered.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_rest_centered() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/rest-centered.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_sforzato() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/sforzato.gp4"))) + .unwrap(); +} +#[test] +fn test_gp4_shift_slide() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/shift-slide.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_shift_slide() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/shift-slide.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_slide_in_above() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/slide-in-above.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_slide_in_above() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/slide-in-above.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_slide_in_below() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/slide-in-below.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_slide_in_below() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/slide-in-below.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_slide_out_down() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/slide-out-down.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_slide_out_down() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/slide-out-down.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_slide_out_up() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/slide-out-up.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_slide_out_up() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/slide-out-up.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_slur() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/slur.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_slur_notes_effect_mask() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/slur-notes-effect-mask.gp5"))) + .unwrap(); +} +#[test] +fn test_gp5_tap_slap_pop() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/tap-slap-pop.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_tempo() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/tempo.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_tempo() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/tempo.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_tempo() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/tempo.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_test_irr_tuplet() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/testIrrTuplet.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_tremolos() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/tremolos.gp5"))) + .unwrap(); +} +#[test] +fn test_gp4_trill() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/trill.gp4"))) + .unwrap(); +} +#[test] +fn test_gp4_tuplet_with_slur() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/tuplet-with-slur.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_vibrato() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/vibrato.gp5"))) + .unwrap(); +} +#[test] +fn test_gp3_volta() { + let mut song: Song = Song::default(); + song.read_gp3(&read_file(String::from("test/volta.gp3"))) + .unwrap(); +} +#[test] +fn test_gp4_volta() { + let mut song: Song = Song::default(); + song.read_gp4(&read_file(String::from("test/volta.gp4"))) + .unwrap(); +} +#[test] +fn test_gp5_volta() { + let mut song: Song = Song::default(); + song.read_gp5(&read_file(String::from("test/volta.gp5"))) + .unwrap(); +} + +// ==================== GPX (Guitar Pro 6) tests ==================== + +fn read_gpx(filename: &str) -> Song { + let mut song = Song::default(); + song.read_gpx(&read_file(String::from(filename))).unwrap(); + song +} + +#[test] +fn test_gpx_keysig() { + let song = read_gpx("test/keysig.gpx"); + assert_eq!(song.tracks.len(), 1); + assert_eq!(song.measure_headers.len(), 32); +} +#[test] +fn test_gpx_copyright() { + let song = read_gpx("test/copyright.gpx"); + assert!(!song.tracks.is_empty()); + assert!( + !song.copyright.is_empty(), + "copyright field should be populated" + ); +} +#[test] +fn test_gpx_tempo() { + let song = read_gpx("test/tempo.gpx"); + assert!(!song.measure_headers.is_empty()); + assert!(song.tempo > 0, "tempo should be parsed from automations"); +} +#[test] +fn test_gpx_rest_centered() { + let song = read_gpx("test/rest-centered.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_dotted_tuplets() { + let song = read_gpx("test/dotted-tuplets.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_tuplets() { + let song = read_gpx("test/tuplets.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_tuplets2() { + let song = read_gpx("test/tuplets2.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_test_irr_tuplet() { + let song = read_gpx("test/testIrrTuplet.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_repeats() { + let song = read_gpx("test/repeats.gpx"); + assert!(!song.measure_headers.is_empty()); + let has_repeat = song + .measure_headers + .iter() + .any(|mh| mh.repeat_open || mh.repeat_close > 0); + assert!( + has_repeat, + "repeats.gpx should have at least one repeat marker" + ); +} +#[test] +fn test_gpx_repeated_bars() { + let song = read_gpx("test/repeated-bars.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_volta() { + let song = read_gpx("test/volta.gpx"); + assert!(!song.measure_headers.is_empty()); + let has_volta = song + .measure_headers + .iter() + .any(|mh| mh.repeat_alternative > 0); + assert!( + has_volta, + "volta.gpx should have at least one alternate ending" + ); +} +#[test] +fn test_gpx_multivoices() { + let song = read_gpx("test/multivoices.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_double_bar() { + let song = read_gpx("test/double-bar.gpx"); + assert!(!song.measure_headers.is_empty()); + let has_double_bar = song.measure_headers.iter().any(|mh| mh.double_bar); + assert!( + has_double_bar, + "double-bar.gpx should have at least one double bar" + ); +} +#[test] +fn test_gpx_clefs() { + let song = read_gpx("test/clefs.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_bend() { + let song = read_gpx("test/bend.gpx"); + assert!(!song.tracks.is_empty()); + // Verify that at least one note has a bend effect + let has_bend = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.bend.is_some())) + }) + }) + }); + assert!( + has_bend, + "bend.gpx should contain at least one note with a bend effect" + ); +} +#[test] +fn test_gpx_basic_bend() { + let song = read_gpx("test/basic-bend.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_vibrato() { + let song = read_gpx("test/vibrato.gpx"); + assert!(!song.tracks.is_empty()); + let has_vibrato = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.vibrato)) + }) + }) + }); + assert!( + has_vibrato, + "vibrato.gpx should contain at least one note with vibrato" + ); +} +#[test] +fn test_gpx_let_ring() { + let song = read_gpx("test/let-ring.gpx"); + assert!(!song.tracks.is_empty()); + let has_let_ring = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.let_ring)) + }) + }) + }); + assert!( + has_let_ring, + "let-ring.gpx should contain at least one let-ring note" + ); +} +#[test] +fn test_gpx_palm_mute() { + let song = read_gpx("test/palm-mute.gpx"); + assert!(!song.tracks.is_empty()); + let has_palm_mute = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.palm_mute)) + }) + }) + }); + assert!( + has_palm_mute, + "palm-mute.gpx should contain at least one palm-muted note" + ); +} +#[test] +fn test_gpx_accent() { + let song = read_gpx("test/accent.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_sforzato() { + let song = read_gpx("test/sforzato.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_heavy_accent() { + let song = read_gpx("test/heavy-accent.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_ghost_note() { + let song = read_gpx("test/ghost-note.gpx"); + assert!(!song.tracks.is_empty()); + let has_ghost = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.ghost_note)) + }) + }) + }); + assert!( + has_ghost, + "ghost-note.gpx should contain at least one ghost note" + ); +} +#[test] +fn test_gpx_dead_note() { + use crate::model::enums::NoteType; + let song = read_gpx("test/dead-note.gpx"); + assert!(!song.tracks.is_empty()); + let has_dead = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.kind == NoteType::Dead)) + }) + }) + }); + assert!( + has_dead, + "dead-note.gpx should contain at least one dead note" + ); +} +#[test] +fn test_gpx_trill() { + let song = read_gpx("test/trill.gpx"); + assert!(!song.tracks.is_empty()); + let has_trill = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.trill.is_some())) + }) + }) + }); + assert!( + has_trill, + "trill.gpx should contain at least one trill note" + ); +} +#[test] +fn test_gpx_tremolos() { + let song = read_gpx("test/tremolos.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_grace() { + let song = read_gpx("test/grace.gpx"); + assert!(!song.tracks.is_empty()); + let has_grace = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.grace.is_some())) + }) + }) + }); + assert!( + has_grace, + "grace.gpx should contain at least one grace note" + ); +} +#[test] +fn test_gpx_grace_before_beat() { + let song = read_gpx("test/grace-before-beat.gpx"); + assert!(!song.tracks.is_empty()); + let has_grace_before = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats.iter().any(|b| { + b.notes + .iter() + .any(|n| n.effect.grace.as_ref().is_some_and(|g| !g.is_on_beat)) + }) + }) + }) + }); + assert!( + has_grace_before, + "grace-before-beat.gpx should contain a grace note before the beat" + ); +} +#[test] +fn test_gpx_grace_on_beat() { + let song = read_gpx("test/grace-on-beat.gpx"); + assert!(!song.tracks.is_empty()); + let has_grace_on = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats.iter().any(|b| { + b.notes + .iter() + .any(|n| n.effect.grace.as_ref().is_some_and(|g| g.is_on_beat)) + }) + }) + }) + }); + assert!( + has_grace_on, + "grace-on-beat.gpx should contain a grace note on the beat" + ); +} +#[test] +fn test_gpx_artificial_harmonic() { + let song = read_gpx("test/artificial-harmonic.gpx"); + assert!(!song.tracks.is_empty()); + let has_harmonic = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.harmonic.is_some())) + }) + }) + }); + assert!( + has_harmonic, + "artificial-harmonic.gpx should contain at least one harmonic note" + ); +} +#[test] +fn test_gpx_high_pitch() { + let song = read_gpx("test/high-pitch.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_shift_slide() { + let song = read_gpx("test/shift-slide.gpx"); + assert!(!song.tracks.is_empty()); + let has_slide = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| !n.effect.slides.is_empty())) + }) + }) + }); + assert!( + has_slide, + "shift-slide.gpx should contain at least one note with slide effect" + ); +} +#[test] +fn test_gpx_legato_slide() { + let song = read_gpx("test/legato-slide.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slide_out_down() { + let song = read_gpx("test/slide-out-down.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slide_out_up() { + let song = read_gpx("test/slide-out-up.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slide_in_below() { + let song = read_gpx("test/slide-in-below.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slide_in_above() { + let song = read_gpx("test/slide-in-above.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_brush() { + let song = read_gpx("test/brush.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_arpeggio() { + let song = read_gpx("test/arpeggio.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_rasg() { + let song = read_gpx("test/rasg.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_fade_in() { + let song = read_gpx("test/fade-in.gpx"); + assert!(!song.tracks.is_empty()); + let has_fade_in = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices + .iter() + .any(|v| v.beats.iter().any(|b| b.effect.fade_in)) + }) + }); + assert!( + has_fade_in, + "fade-in.gpx should contain at least one beat with fade-in" + ); +} +#[test] +fn test_gpx_volume_swell() { + let song = read_gpx("test/volume-swell.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_pick_up_down() { + let song = read_gpx("test/pick-up-down.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slur() { + let song = read_gpx("test/slur.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slur_hammer_slur() { + let song = read_gpx("test/slur_hammer_slur.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slur_slur_hammer() { + let song = read_gpx("test/slur_slur_hammer.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slur_over_3_measures() { + let song = read_gpx("test/slur_over_3_measures.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slur_voices() { + let song = read_gpx("test/slur_voices.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_slur_notes_effect_mask() { + let song = read_gpx("test/slur-notes-effect-mask.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_dotted_gliss() { + let song = read_gpx("test/dotted-gliss.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_ottava1() { + let song = read_gpx("test/ottava1.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_ottava2() { + let song = read_gpx("test/ottava2.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_ottava3() { + let song = read_gpx("test/ottava3.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_ottava4() { + let song = read_gpx("test/ottava4.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_ottava5() { + let song = read_gpx("test/ottava5.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_mordents() { + let song = read_gpx("test/mordents.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_turn() { + let song = read_gpx("test/turn.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_barre() { + let song = read_gpx("test/barre.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_fingering() { + let song = read_gpx("test/fingering.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_fret_diagram() { + let song = read_gpx("test/fret-diagram.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_fret_diagram_2instruments() { + let song = read_gpx("test/fret-diagram_2instruments.gpx"); + assert!(song.tracks.len() >= 2); +} +#[test] +fn test_gpx_text() { + let song = read_gpx("test/text.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_timer() { + let song = read_gpx("test/timer.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_directions() { + let song = read_gpx("test/directions.gpx"); + assert!(!song.measure_headers.is_empty()); +} +#[test] +fn test_gpx_fermata() { + let song = read_gpx("test/fermata.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_free_time() { + let song = read_gpx("test/free-time.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_dynamic() { + let song = read_gpx("test/dynamic.gpx"); + assert!(!song.tracks.is_empty()); + // Verify that notes have varying velocities (not all the same default) + let velocities: Vec = song + .tracks + .iter() + .flat_map(|t| { + t.measures.iter().flat_map(|m| { + m.voices.iter().flat_map(|v| { + v.beats + .iter() + .flat_map(|b| b.notes.iter().map(|n| n.velocity)) + }) + }) + }) + .collect(); + assert!(!velocities.is_empty(), "dynamic.gpx should contain notes"); + let has_varying = velocities.iter().any(|&v| v != velocities[0]); + assert!( + has_varying, + "dynamic.gpx should have varying velocities across notes" + ); +} +#[test] +fn test_gpx_crescendo_diminuendo() { + let song = read_gpx("test/crescendo-diminuendo.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_wah() { + let song = read_gpx("test/wah.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_all_percussion() { + let song = read_gpx("test/all-percussion.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_beams_stems_ledger_lines() { + let song = read_gpx("test/beams-stems-ledger-lines.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_chordnames_keyboard() { + let song = read_gpx("test/chordnames_keyboard.gpx"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gpx_tuplet_with_slur() { + let song = read_gpx("test/tuplet-with-slur.gpx"); + assert!(!song.tracks.is_empty()); +} + +#[test] +fn test_gpx_all_files_parse() { + use std::fs; + let test_dir = "../test"; + let mut pass = 0; + let mut failures: Vec = Vec::new(); + for entry in fs::read_dir(test_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|e| e == "gpx") { + let fname = path.file_name().unwrap().to_str().unwrap().to_string(); + let data = fs::read(&path).unwrap(); + let mut song = Song::default(); + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + song.read_gpx(&data).unwrap(); + })) { + Ok(_) => { + pass += 1; + } + Err(e) => { + let msg = if let Some(s) = e.downcast_ref::() { + s.clone() + } else if let Some(s) = e.downcast_ref::<&str>() { + s.to_string() + } else { + "unknown".to_string() + }; + let short = &msg[..msg.len().min(100)]; + failures.push(format!("{}: {}", fname, short)); + } + } + } + } + if !failures.is_empty() { + for f in &failures { + eprintln!("FAIL: {}", f); + } + } + eprintln!( + "{} pass, {} fail out of {}", + pass, + failures.len(), + pass + failures.len() + ); + assert!( + failures.is_empty(), + "{} files failed to parse", + failures.len() + ); +} + +// ==================== GP7 (Guitar Pro 7+) tests ==================== + +fn read_gp7(filename: &str) -> Song { + let mut song = Song::default(); + song.read_gp(&read_file(String::from(filename))).unwrap(); + song +} + +#[test] +fn test_gp7_keysig() { + let song = read_gp7("test/keysig.gp"); + assert_eq!(song.tracks.len(), 1); + assert_eq!(song.measure_headers.len(), 32); +} +#[test] +fn test_gp7_copyright() { + let song = read_gp7("test/copyright.gp"); + assert!(!song.tracks.is_empty()); + assert!( + !song.copyright.is_empty(), + "copyright field should be populated" + ); +} +#[test] +fn test_gp7_tempo() { + let song = read_gp7("test/tempo.gp"); + assert!(!song.measure_headers.is_empty()); + assert!(song.tempo > 0, "tempo should be parsed from automations"); +} +#[test] +fn test_gp7_rest_centered() { + let song = read_gp7("test/rest-centered.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_dotted_tuplets() { + let song = read_gp7("test/dotted-tuplets.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_test_irr_tuplet() { + let song = read_gp7("test/testIrrTuplet.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_repeats() { + let song = read_gp7("test/repeats.gp"); + assert!(!song.measure_headers.is_empty()); + let has_repeat = song + .measure_headers + .iter() + .any(|mh| mh.repeat_open || mh.repeat_close > 0); + assert!( + has_repeat, + "repeats.gp should have at least one repeat marker" + ); +} +#[test] +fn test_gp7_repeated_bars() { + let song = read_gp7("test/repeated-bars.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_volta() { + let song = read_gp7("test/volta.gp"); + assert!(!song.measure_headers.is_empty()); + let has_volta = song + .measure_headers + .iter() + .any(|mh| mh.repeat_alternative > 0); + assert!( + has_volta, + "volta.gp should have at least one alternate ending" + ); +} +#[test] +fn test_gp7_multivoices() { + let song = read_gp7("test/multivoices.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_double_bar() { + let song = read_gp7("test/double-bar.gp"); + assert!(!song.measure_headers.is_empty()); + let has_double_bar = song.measure_headers.iter().any(|mh| mh.double_bar); + assert!( + has_double_bar, + "double-bar.gp should have at least one double bar" + ); +} +#[test] +fn test_gp7_clefs() { + let song = read_gp7("test/clefs.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_bend() { + let song = read_gp7("test/bend.gp"); + assert!(!song.tracks.is_empty()); + let has_bend = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.bend.is_some())) + }) + }) + }); + assert!( + has_bend, + "bend.gp should contain at least one note with a bend effect" + ); +} +#[test] +fn test_gp7_basic_bend() { + let song = read_gp7("test/basic-bend.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_vibrato() { + let song = read_gp7("test/vibrato.gp"); + assert!(!song.tracks.is_empty()); + let has_vibrato = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.vibrato)) + }) + }) + }); + assert!( + has_vibrato, + "vibrato.gp should contain at least one note with vibrato" + ); +} +#[test] +fn test_gp7_let_ring() { + let song = read_gp7("test/let-ring.gp"); + assert!(!song.tracks.is_empty()); + let has_let_ring = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.let_ring)) + }) + }) + }); + assert!( + has_let_ring, + "let-ring.gp should contain at least one let-ring note" + ); +} +#[test] +fn test_gp7_palm_mute() { + let song = read_gp7("test/palm-mute.gp"); + assert!(!song.tracks.is_empty()); + let has_palm_mute = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.palm_mute)) + }) + }) + }); + assert!( + has_palm_mute, + "palm-mute.gp should contain at least one palm-muted note" + ); +} +#[test] +fn test_gp7_accent() { + let song = read_gp7("test/accent.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_sforzato() { + let song = read_gp7("test/sforzato.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_heavy_accent() { + let song = read_gp7("test/heavy-accent.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_ghost_note() { + let song = read_gp7("test/ghost-note.gp"); + assert!(!song.tracks.is_empty()); + let has_ghost = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.ghost_note)) + }) + }) + }); + assert!( + has_ghost, + "ghost-note.gp should contain at least one ghost note" + ); +} +#[test] +fn test_gp7_dead_note() { + use crate::model::enums::NoteType; + let song = read_gp7("test/dead-note.gp"); + assert!(!song.tracks.is_empty()); + let has_dead = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.kind == NoteType::Dead)) + }) + }) + }); + assert!( + has_dead, + "dead-note.gp should contain at least one dead note" + ); +} +#[test] +fn test_gp7_trill() { + let song = read_gp7("test/trill.gp"); + assert!(!song.tracks.is_empty()); + let has_trill = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.trill.is_some())) + }) + }) + }); + assert!(has_trill, "trill.gp should contain at least one trill note"); +} +#[test] +fn test_gp7_tremolos() { + let song = read_gp7("test/tremolos.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_grace() { + let song = read_gp7("test/grace.gp"); + assert!(!song.tracks.is_empty()); + let has_grace = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.grace.is_some())) + }) + }) + }); + assert!(has_grace, "grace.gp should contain at least one grace note"); +} +#[test] +fn test_gp7_grace_before_beat() { + let song = read_gp7("test/grace-before-beat.gp"); + assert!(!song.tracks.is_empty()); + let has_grace_before = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats.iter().any(|b| { + b.notes + .iter() + .any(|n| n.effect.grace.as_ref().is_some_and(|g| !g.is_on_beat)) + }) + }) + }) + }); + assert!( + has_grace_before, + "grace-before-beat.gp should contain a grace note before the beat" + ); +} +#[test] +fn test_gp7_grace_on_beat() { + let song = read_gp7("test/grace-on-beat.gp"); + assert!(!song.tracks.is_empty()); + let has_grace_on = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats.iter().any(|b| { + b.notes + .iter() + .any(|n| n.effect.grace.as_ref().is_some_and(|g| g.is_on_beat)) + }) + }) + }) + }); + assert!( + has_grace_on, + "grace-on-beat.gp should contain a grace note on the beat" + ); +} +#[test] +fn test_gp7_artificial_harmonic() { + let song = read_gp7("test/artificial-harmonic.gp"); + assert!(!song.tracks.is_empty()); + let has_harmonic = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| n.effect.harmonic.is_some())) + }) + }) + }); + assert!( + has_harmonic, + "artificial-harmonic.gp should contain at least one harmonic note" + ); +} +#[test] +fn test_gp7_high_pitch() { + let song = read_gp7("test/high-pitch.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_shift_slide() { + let song = read_gp7("test/shift-slide.gp"); + assert!(!song.tracks.is_empty()); + let has_slide = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices.iter().any(|v| { + v.beats + .iter() + .any(|b| b.notes.iter().any(|n| !n.effect.slides.is_empty())) + }) + }) + }); + assert!( + has_slide, + "shift-slide.gp should contain at least one note with slide effect" + ); +} +#[test] +fn test_gp7_legato_slide() { + let song = read_gp7("test/legato-slide.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slide_out_down() { + let song = read_gp7("test/slide-out-down.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slide_out_up() { + let song = read_gp7("test/slide-out-up.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slide_in_below() { + let song = read_gp7("test/slide-in-below.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slide_in_above() { + let song = read_gp7("test/slide-in-above.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_brush() { + let song = read_gp7("test/brush.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_arpeggio() { + let song = read_gp7("test/arpeggio.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_rasg() { + let song = read_gp7("test/rasg.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_fade_in() { + let song = read_gp7("test/fade-in.gp"); + assert!(!song.tracks.is_empty()); + let has_fade_in = song.tracks.iter().any(|t| { + t.measures.iter().any(|m| { + m.voices + .iter() + .any(|v| v.beats.iter().any(|b| b.effect.fade_in)) + }) + }); + assert!( + has_fade_in, + "fade-in.gp should contain at least one beat with fade-in" + ); +} +#[test] +fn test_gp7_volume_swell() { + let song = read_gp7("test/volume-swell.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_pick_up_down() { + let song = read_gp7("test/pick-up-down.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slur() { + let song = read_gp7("test/slur.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slur_hammer_slur() { + let song = read_gp7("test/slur_hammer_slur.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slur_slur_hammer() { + let song = read_gp7("test/slur_slur_hammer.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slur_over_3_measures() { + let song = read_gp7("test/slur_over_3_measures.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slur_voices() { + let song = read_gp7("test/slur_voices.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_slur_notes_effect_mask() { + let song = read_gp7("test/slur-notes-effect-mask.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_dotted_gliss() { + let song = read_gp7("test/dotted-gliss.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_ottava1() { + let song = read_gp7("test/ottava1.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_ottava2() { + let song = read_gp7("test/ottava2.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_ottava3() { + let song = read_gp7("test/ottava3.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_ottava4() { + let song = read_gp7("test/ottava4.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_ottava5() { + let song = read_gp7("test/ottava5.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_mordents() { + let song = read_gp7("test/mordents.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_turn() { + let song = read_gp7("test/turn.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_barre() { + let song = read_gp7("test/barre.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_fingering() { + let song = read_gp7("test/fingering.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_fret_diagram() { + let song = read_gp7("test/fret-diagram.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_fret_diagram_2instruments() { + let song = read_gp7("test/fret-diagram_2instruments.gp"); + assert!(song.tracks.len() >= 2); +} +#[test] +fn test_gp7_text() { + let song = read_gp7("test/text.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_timer() { + let song = read_gp7("test/timer.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_directions() { + let song = read_gp7("test/directions.gp"); + assert!(!song.measure_headers.is_empty()); +} +#[test] +fn test_gp7_fermata() { + let song = read_gp7("test/fermata.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_free_time() { + let song = read_gp7("test/free-time.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_dynamic() { + let song = read_gp7("test/dynamic.gp"); + assert!(!song.tracks.is_empty()); + let velocities: Vec = song + .tracks + .iter() + .flat_map(|t| { + t.measures.iter().flat_map(|m| { + m.voices.iter().flat_map(|v| { + v.beats + .iter() + .flat_map(|b| b.notes.iter().map(|n| n.velocity)) + }) + }) + }) + .collect(); + assert!(!velocities.is_empty(), "dynamic.gp should contain notes"); + let has_varying = velocities.iter().any(|&v| v != velocities[0]); + assert!( + has_varying, + "dynamic.gp should have varying velocities across notes" + ); +} +#[test] +fn test_gp7_crescendo_diminuendo() { + let song = read_gp7("test/crescendo-diminuendo.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_wah() { + let song = read_gp7("test/wah.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_all_percussion() { + let song = read_gp7("test/all-percussion.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_beams_stems_ledger_lines() { + let song = read_gp7("test/beams-stems-ledger-lines.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_chordnames_keyboard() { + let song = read_gp7("test/chordnames_keyboard.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_tuplet_with_slur() { + let song = read_gp7("test/tuplet-with-slur.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_tap_slap_pop() { + let song = read_gp7("test/tap-slap-pop.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_tremolo_bar() { + let song = read_gp7("test/tremolo-bar.gp"); + assert!(!song.tracks.is_empty()); +} +#[test] +fn test_gp7_test() { + let song = read_gp7("test/test.gp"); + assert!(!song.tracks.is_empty()); +} + +#[test] +fn test_gp7_all_files_parse() { + use std::fs; + let test_dir = "../test"; + let mut pass = 0; + let mut failures: Vec = Vec::new(); + for entry in fs::read_dir(test_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|e| e == "gp") { + let fname = path.file_name().unwrap().to_str().unwrap().to_string(); + let data = fs::read(&path).unwrap(); + let mut song = Song::default(); + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + song.read_gp(&data).unwrap(); + })) { + Ok(_) => { + pass += 1; + } + Err(e) => { + let msg = if let Some(s) = e.downcast_ref::() { + s.clone() + } else if let Some(s) = e.downcast_ref::<&str>() { + s.to_string() + } else { + "unknown".to_string() + }; + let short = &msg[..msg.len().min(100)]; + failures.push(format!("{}: {}", fname, short)); + } + } + } + } + if !failures.is_empty() { + for f in &failures { + eprintln!("FAIL: {}", f); + } + } + eprintln!( + "{} pass, {} fail out of {}", + pass, + failures.len(), + pass + failures.len() + ); + assert!( + failures.is_empty(), + "{} files failed to parse", + failures.len() + ); +} diff --git a/test/iron-maiden-doctor_doctor.gp5 b/test/iron-maiden-doctor_doctor.gp5 new file mode 100644 index 0000000..3042045 Binary files /dev/null and b/test/iron-maiden-doctor_doctor.gp5 differ diff --git a/test/led-zeppelin-babe_i_m_gonna_leave_you.gp4 b/test/led-zeppelin-babe_i_m_gonna_leave_you.gp4 new file mode 100644 index 0000000..f76002e Binary files /dev/null and b/test/led-zeppelin-babe_i_m_gonna_leave_you.gp4 differ diff --git a/test/rage-against-the-machine_bombtrack-official-2210247.gpx b/test/rage-against-the-machine_bombtrack-official-2210247.gpx new file mode 100644 index 0000000..6c640c5 Binary files /dev/null and b/test/rage-against-the-machine_bombtrack-official-2210247.gpx differ diff --git a/test/the-beatles-let_it_be.gp3 b/test/the-beatles-let_it_be.gp3 new file mode 100644 index 0000000..fcb6bbd Binary files /dev/null and b/test/the-beatles-let_it_be.gp3 differ diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..37db6eb --- /dev/null +++ b/todo.md @@ -0,0 +1,86 @@ +Rapport d'analyse — guitarproparser +Structure du workspace +Crate Binaire Rôle État +lib (scorelib) — Bibliothèque de parsing Production +cli (score_tool) score_tool Interface CLI Fonctionnel +web_server score_server Serveur web Vide (stub) +~7 500 lignes de code dans la bibliothèque, 161 tests, 248 fichiers de test. + +Support des formats +Lecture +Format Extension État Méthode +GP3 .gp3 ✅ Complet Parsing binaire +GP4 .gp4 ✅ Complet Parsing binaire +GP5 .gp5 ✅ Complet Parsing binaire +GP6 .gpx ⚠️ Fonctionnel BCFZ + GPIF XML +GP7+ .gp ⚠️ Fonctionnel ZIP + GPIF XML +GP6 et GP7 sont fonctionnels mais pas exhaustifs — certaines fonctionnalités avancées du XML GPIF ne sont pas encore mappées. + +Écriture +Format État Couverture +GP3/4/5 ✅ Partiel ~80% (métadonnées, pistes, mesures, notes) +GP6/GP7 ❌ Aucune Non implémenté +Un TODO existe dans song.rs pour l'écriture des canaux MIDI. + +Modèle de données +La hiérarchie Song → Track → Measure → Voice → Beat → Note est complète : + +Song : métadonnées complètes (titre, artiste, album, auteur, etc.) +Track : nom, cordes, frettes, canal MIDI, percussions, paramètres RSE +Measure/MeasureHeader : tempo, signature, tonalité, répétitions, marqueurs +Beat : durée, effets, tuples, accords +Note : vélocité, frette, 12 types d'effets (bend, slide, harmonique, grace, hammer, etc.) +Effets : BendEffect (avec points), GraceEffect, HarmonicEffect (6 types), TrillEffect, SlideType (6 types), tremolo picking, vibrato, palm mute, let ring, staccato, ghost, dead notes +Partiellement implémenté : RSE (Realistic Sound Engine) — parsé mais pas pleinement exploité. + +Tests (161 tests) +Format Nombre de tests Couverture +GPX 69 Effets, slides, harmoniques, accords, percussions +GP5 42 Couverture complète des fonctionnalités +GP4 28 Complète sauf fonctionnalités GP5 +GP3 10 Fonctionnalités de base +GP7 1 Lecture basique uniquement +Multi-format 11 Accords, comparaisons cross-format +Points forts : slides (12 tests), effets (11 tests), harmoniques (9 tests), accords (7 tests). + +CLI (score_tool) +Fonctionnalités : + +Affichage des métadonnées (titre, artiste, album, version, etc.) +Génération de tablature ASCII +Supporte GP3, GP4, GP5, GPX, GP +Limite de fichier : 16 Mo +Limitations : + +Tablature ASCII pour la première piste uniquement +Pas d'export MIDI +Pas de conversion de format +Pas de traitement par lots +Gestion des erreurs +État actuel : hybride + +La couche I/O (gpx.rs, gpif.rs) utilise Result +Le CLI et certains chemins de parsing utilisent panic!() / unwrap() +Pas de type d'erreur unifié dans la bibliothèque +Ce qui reste à faire +Priorité Tâche Détails +Haute Compléter le support GP6/GP7 Mapper tous les attributs GPIF (wah, auto-brush, rasgueado, etc.) +Haute Gestion d'erreurs unifiée Remplacer panic!() par des Result avec un type d'erreur dédié +Haute Tests GP7 1 seul test actuellement vs 74 fichiers de test disponibles +Moyenne Compléter l'écriture GP3/4/5 Finaliser les ~20% manquants (canaux MIDI, etc.) +Moyenne Écriture GP6/GP7 Non implémenté — nécessite génération XML GPIF +Moyenne RSE complet Données parsées mais pas entièrement exploitées +Basse Web server Complètement vide — main.rs ne contient que fn main() {} +Basse CLI amélioré Multi-pistes, export MIDI, conversion de format +Basse Support MuseScore (.mscz) Mentionné nulle part dans le code mais potentiellement intéressant +Basse Message d'erreur CLI Le message d'erreur à la ligne 57 du CLI ne mentionne pas GPX dans les formats supportés +Points forts du projet +Architecture trait-based propre (13 traits spécialisés) +Modèle de données complet et fidèle à la structure musicale +Excellente couverture de tests pour GP5 et GPX +Support des formats legacy (GP3) jusqu'aux modernes (GP7) +Code propre, passe clippy sans warnings +Points d'attention +Le format GP7 n'a qu'un seul test malgré 74 fichiers de test disponibles — c'est le plus gros gap de couverture +L'absence de Result unifié rend la bibliothèque difficile à intégrer proprement dans d'autres projets +Le web server est un placeholder vide \ No newline at end of file diff --git a/web_server/README.md b/web_server/README.md new file mode 100644 index 0000000..36fe952 --- /dev/null +++ b/web_server/README.md @@ -0,0 +1,16 @@ +# Web Server (Experimental) + +This is an experimental web server for searching and browsing music scores parsed by `scorelib`. + +## Current Status + +- **Project Phase**: Early Prototype. +- **Goal**: Provide a web-based interface and API for score libraries. + +## Development + +To run the web server: + +```bash +cargo run -p web_server +```