Full Code of agourlay/ruxguitar for AI

master 2bf2167bffcb cached
30 files
311.8 KB
77.8k tokens
439 symbols
1 requests
Download .txt
Showing preview only (324K chars total). Download the full file or copy to clipboard to get everything.
Repository: agourlay/ruxguitar
Branch: master
Commit: 2bf2167bffcb
Files: 30
Total size: 311.8 KB

Directory structure:
gitextract_md1x0dqc/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── resources/
│   └── TimGM6mb.sf2
└── src/
    ├── audio/
    │   ├── midi_builder.rs
    │   ├── midi_event.rs
    │   ├── midi_player.rs
    │   ├── midi_player_params.rs
    │   ├── midi_sequencer.rs
    │   ├── mod.rs
    │   └── playback_order.rs
    ├── config.rs
    ├── main.rs
    ├── parser/
    │   ├── mod.rs
    │   ├── music_parser.rs
    │   ├── primitive_parser.rs
    │   ├── song_parser.rs
    │   └── song_parser_tests.rs
    └── ui/
        ├── application.rs
        ├── canvas_measure.rs
        ├── icons.rs
        ├── mod.rs
        ├── picker.rs
        ├── tablature.rs
        ├── tuning.rs
        └── utils.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: agourlay


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [ '*' ]
  pull_request:
    branches: [ '*' ]

env:
  CARGO_TERM_COLOR: always

jobs:
  linux-build:
    name: Linux CI
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Update apt
        run: sudo apt update
      - name: Install alsa
        run: sudo apt-get install libasound2-dev
      - name: Install stable
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - name: Check code formatting
        run: cargo fmt --all -- --check
      - name: Build
        run: cargo build --locked --verbose
      - name: Run tests
        run: cargo test --locked --verbose
      - name: Check cargo clippy warnings
        run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings

  macos-build:
    name: macOS CI
    runs-on: macOS-latest
    steps:
      - uses: actions/checkout@v6
      - name: Install llvm and clang
        run: brew install llvm
      - name: Install stable
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - name: Build
        run: cargo build --locked --verbose
      - name: Run tests
        run: cargo test --locked --verbose
      - name: Check cargo clippy warnings
        run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings

  windows-build:
    name: windows CI
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v6
      - name: Install ASIO SDK
        env:
          LINK: https://www.steinberg.net/asiosdk
        run: |
          curl -L -o asio.zip $env:LINK
          7z x -oasio asio.zip
          move asio\*\* asio\
      - name: Install ASIO4ALL
        run: choco install asio4all
      - name: Install llvm and clang
        run: choco install llvm
      - name: Install stable
        uses: dtolnay/rust-toolchain@stable
        with:
          target: x86_64-pc-windows-msvc
          components: clippy
      - name: Build
        run: cargo build --locked --verbose
      - name: Run tests
        run: cargo test --locked --verbose
      - name: Check cargo clippy warnings
        run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings

================================================
FILE: .github/workflows/release.yml
================================================
name: release binaries

on:
  release:
    types: [created]

permissions:
  contents: write

jobs:
  upload-bins:
    name: "Upload release binaries"
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
          - target: aarch64-pc-windows-msvc
            os: windows-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v6
      # Install dependencies per OS
      # copy the dependencies step from the ci.yml file
      - if: matrix.os == 'ubuntu-latest'
        name: Install dependencies (ubuntu-latest)
        run: |
          sudo apt update
          sudo apt-get install libasound2-dev

      - if: matrix.os == 'macOS-latest'
        name: Install dependencies (macOS-latest)
        run: |
          brew install llvm

      - if: matrix.os == 'windows-latest'
        name: Install ASIO SDK
        env:
          LINK: https://www.steinberg.net/asiosdk
        run: |
          curl -L -o asio.zip $env:LINK
          7z x -oasio asio.zip
          move asio\*\* asio\
          choco install asio4all
          choco install llvm

      - if: matrix.os == 'windows-latest'
        uses: dtolnay/rust-toolchain@stable
        with:
          target: x86_64-pc-windows-msvc

      - if: matrix.os != 'windows-latest'
        uses: dtolnay/rust-toolchain@stable

      # All
      - uses: taiki-e/upload-rust-binary-action@v1
        with:
          target: ${{ matrix.target }}
          bin: ruxguitar
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  publish-crate:
    name: "Publish on crates.io"
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v6
      # Install dependencies per OS
      # copy the dependencies step from the ci.yml file
      - name: Install dependencies (ubuntu-latest)
        run: |
          sudo apt update
          sudo apt-get install libasound2-dev
      - name: Publish
        uses: actions-rs/cargo@v1
        with:
          command: publish
          args: --token ${{ secrets.CARGO_TOKEN }}

================================================
FILE: .gitignore
================================================
/target
.idea
/test-files/

================================================
FILE: Cargo.toml
================================================
[package]
name = "ruxguitar"
version = "0.8.1"
edition = "2024"
authors = ["Arnaud Gourlay <arnaud.gourlay@gmail.com>"]
description = "Guitar pro tablature player"
repository = "https://github.com/agourlay/ruxguitar"
license = "Apache-2.0"
readme = "README.md"
categories = ["multimedia"]
keywords = ["guitar", "tablature", "music"]

[lints.clippy]
cast_lossless = "warn"
doc_link_with_quotes = "warn"
enum_glob_use = "warn"
explicit_into_iter_loop = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
from_iter_instead_of_collect = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
inefficient_to_string = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn"
needless_continue = "warn"
needless_raw_string_hashes = "warn"
ptr_as_ptr = "warn"
ref_option_ref = "warn"
uninlined_format_args = "warn"
unnecessary_wraps = "warn"
unused_self = "warn"
used_underscore_binding = "warn"
match_wildcard_for_single_variants = "warn"
needless_pass_by_ref_mut = "warn"
missing_const_for_fn = "warn"
redundant_closure_for_method_calls = "warn"
semicolon_if_nothing_returned = "warn"
unreadable_literal = "warn"
unused_async = "warn"

[dependencies]
nom = "8.0.0"
encoding_rs = "0.8.35"
iced = { version = "0.14.0", features = [
    "advanced",
    "canvas",
    "tokio",
    "selector",
] }
tokio = { version = "1.52.1", features = ["fs", "sync"] }
rfd = "0.17.2"
log = "0.4.29"
env_logger = "0.11.10"
rustysynth = "1.3.6"
cpal = "0.17.3"
thiserror = "2.0.18"
clap = { version = "4.6.1", features = ["derive", "cargo"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"

[profile.release]
lto = "fat"
codegen-units = 1

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# ruxguitar

[![Build status](https://github.com/agourlay/ruxguitar/actions/workflows/ci.yml/badge.svg)](https://github.com/agourlay/ruxguitar/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/ruxguitar.svg)](https://crates.io/crates/ruxguitar)

A guitar pro tablature player.

The design of the application is described in details in the blog article "[Playing guitar tablatures in Rust](https://agourlay.github.io/ruxguitar-tablature-player/)".

![capture](ruxguitar.gif)

## Features

- GP4 and GP5 file support (drag-and-drop supported)
- MIDI playback with embedded soundfont (or custom soundfont)
- Repeat sections with alternative endings
- Tempo control (25% to 200%)
- Solo mode (isolate single track)
- Track selection
- Keyboard shortcuts:
    - `Space` play/pause
    - `Ctrl+Up` / `Ctrl+Down` tempo up/down
    - `Left` / `Right` previous/next measure
    - `S` toggle solo
    - `F11` toggle fullscreen

## Limitations

- no editing capabilities (read-only player)
- no score notation (tablature only)
- supports only GP5 and GP4 files

## Usage

```bash
./ruxguitar --help
Guitar pro tablature player

Usage: ruxguitar [OPTIONS]

Options:
      --sound-font-file <SOUND_FONT_FILE>  Optional path to a sound font file
      --tab-file-path <TAB_FILE_PATH>      Optional path to tab file to by-pass the file picker
      --no-antialiasing                    Disable antialiasing
  -h, --help                               Print help
  -V, --version                            Print version
```

A basic soundfont is embedded in the binary for a plug and play experience, however it is possible to provide a larger soundfont file to get better sound quality.

For instance I like to use `FluidR3_GM.sf2` which is present on most systems and easy to find online ([here](https://musical-artifacts.com/artifacts/738) or [there](https://member.keymusician.com/Member/FluidR3_GM/index.html)).

```bash
./ruxguitar --sound-font-file /usr/share/sounds/sf2/FluidR3_GM.sf2
```

## FAQ

- **Where can I find guitar pro files?**
  - You can find a lot of guitar pro files on the internet. For instance on [Ultimate Guitar](https://www.ultimate-guitar.com/).

- **Why is the sound quality so bad?**
  - The default soundfont is very basic. You can provide a better soundfont file using the `--sound-font-file` option.

- **Which dependencies are needed to run the application?**
  - Check the necessary dependencies for your system from the [CI configuration](https://github.com/agourlay/ruxguitar/blob/master/.github/workflows/ci.yml).

- **Why is the file picker not opening on Linux?**
  - Install the `XDG Destop Portal` package for your [desktop environment](https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces).

- **Why are the strings not rendered on the tablature?**
  - You might need to disable antialiasing using the `--no-antialiasing` option.

- **Does it run on Windows 7 or Windows 8?**
  - The last compatible release with those versions of Windows is [v0.6.3](https://github.com/agourlay/ruxguitar/releases/tag/v0.6.3).

- **Why is the sound not working on Linux?**
  - Getting the error `The requested device is no longer available. For example, it has been unplugged`.
  - You are most likely using `PulseAudio` or `Pipewire` which are not supported.
  - Install compatibility packages `pulseaudio-alsa` or `pipewire-alsa` (requires a restart of the audio service).

## Installation

### Releases

Using the provided binaries in https://github.com/agourlay/ruxguitar/releases

### Crates.io

Using Cargo via [crates.io](https://crates.io/crates/ruxguitar).

```bash
cargo install ruxguitar
```

### Build

Make sure to check the necessary dependencies for your system from the [CI configuration](https://github.com/agourlay/ruxguitar/blob/master/.github/workflows/ci.yml).

## Acknowledgements

This project is heavily inspired by the great [TuxGuitar](https://github.com/helge17/tuxguitar) project.

================================================
FILE: src/audio/midi_builder.rs
================================================
/// Thanks to `TuxGuitar` for the reference implementation in `MidiSequenceParser.java`
use crate::audio::FIRST_TICK;
use crate::audio::midi_event::MidiEvent;
use crate::parser::song_parser::{
    Beat, BeatStrokeDirection, BendEffect, BendPoint, HarmonicType, MIN_VELOCITY, Measure,
    MeasureHeader, MidiChannel, Note, NoteType, QUARTER_TIME, SEMITONE_LENGTH, Song, Track,
    TremoloBarEffect, TripletFeel, VELOCITY_INCREMENT,
};
use std::rc::Rc;

#[cfg(test)]
use crate::audio::playback_order::compute_playback_order;

const DEFAULT_DURATION_DEAD: u32 = 30;
const DEFAULT_DURATION_PM: u32 = 60;
const DEFAULT_BEND: f32 = 64.0;
const DEFAULT_BEND_SEMI_TONE: f32 = 2.75;

pub const NATURAL_FREQUENCIES: [(i32, i32); 6] = [
    (12, 12), //AH12 (+12 frets)
    (9, 28),  //AH9 (+28 frets)
    (5, 24),  //AH5 (+24 frets)
    (7, 19),  //AH7 (+19 frets)
    (4, 28),  //AH4 (+28 frets)
    (3, 31),  //AH3 (+31 frets)
];

pub struct MidiBuilder {
    events: Vec<MidiEvent>, // events accumulated during build
}

impl MidiBuilder {
    pub const fn new() -> Self {
        Self { events: Vec::new() }
    }

    /// Parse song and record events, computing playback order internally.
    #[cfg(test)]
    pub fn build_for_song(self, song: &Rc<Song>) -> Vec<MidiEvent> {
        let playback_order = compute_playback_order(&song.measure_headers);
        self.build_for_song_with_order(song, &playback_order)
    }

    /// Parse song and record events using a pre-computed playback order.
    pub fn build_for_song_with_order(
        mut self,
        song: &Rc<Song>,
        playback_order: &[(usize, i64)],
    ) -> Vec<MidiEvent> {
        for (track_id, track) in song.tracks.iter().enumerate() {
            log::debug!("building events for track {track_id}");
            let midi_channel = song
                .midi_channels
                .iter()
                .find(|c| c.channel_id == track.channel_id)
                .unwrap_or_else(|| {
                    panic!(
                        "midi channel {} not found for track {}",
                        track.channel_id, track_id
                    )
                });
            self.add_track_events(
                song.tempo.value,
                track_id,
                track,
                &song.measure_headers,
                playback_order,
                midi_channel,
            );
        }
        // Sort events by tick
        self.events.sort_by_key(|event| event.tick);
        self.events
    }

    fn add_track_events(
        &mut self,
        song_tempo: u32,
        track_id: usize,
        track: &Track,
        measure_headers: &[MeasureHeader],
        playback_order: &[(usize, i64)],
        midi_channel: &MidiChannel,
    ) {
        // add MIDI control events for the track channel
        self.add_track_channel_midi_control(track_id, midi_channel);

        let strings = &track.strings;
        let mut prev_tempo = song_tempo;
        assert_eq!(track.measures.len(), measure_headers.len());
        for (measure_index, tick_offset) in playback_order {
            let measure = &track.measures[*measure_index];
            let measure_header = &measure_headers[*measure_index];

            // add song info events once for all tracks
            if track_id == 0 {
                // change tempo if necessary
                let measure_tempo = measure_header.tempo.value;
                if measure_tempo != prev_tempo {
                    let tick = (i64::from(measure_header.start) + tick_offset) as u32;
                    self.add_tempo_change(tick, measure_tempo);
                    prev_tempo = measure_tempo;
                }
            }

            // record event count to shift new events by tick_offset
            let event_start = self.events.len();
            self.add_beat_events(
                track_id,
                track,
                measure,
                measure_header,
                midi_channel,
                strings,
            );
            // shift events generated for this measure by tick_offset
            if *tick_offset != 0 {
                for event in &mut self.events[event_start..] {
                    event.tick = (i64::from(event.tick) + tick_offset) as u32;
                }
            }
        }
    }

    fn add_beat_events(
        &mut self,
        track_id: usize,
        track: &Track,
        measure: &Measure,
        measure_header: &MeasureHeader,
        midi_channel: &MidiChannel,
        strings: &[(i32, i32)],
    ) {
        let measure_id = measure.voices[0].measure_index as usize;
        for voice in &measure.voices {
            let beats = &voice.beats;
            for (beat_id, beat) in beats.iter().enumerate() {
                if beat.empty || beat.notes.is_empty() {
                    continue;
                }
                // extract surrounding beats
                let previous_beat = if beat_id == 0 {
                    None
                } else {
                    beats.get(beat_id - 1)
                };
                let next_beat = beats.get(beat_id + 1).or_else(|| {
                    // check next measure if it was the last beat
                    track
                        .measures
                        .get(voice.measure_index as usize + 1)
                        .and_then(|next_measure| next_measure.voices[0].beats.first())
                });
                // apply triplet feel adjustment to beat timing
                let triplet_adj =
                    apply_triplet_feel(beat, previous_beat, next_beat, measure_header.triplet_feel);
                self.add_notes(
                    track_id,
                    track,
                    measure_id,
                    measure_header,
                    midi_channel,
                    previous_beat,
                    beat_id,
                    beat,
                    next_beat,
                    strings,
                    triplet_adj,
                );
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn add_notes(
        &mut self,
        track_id: usize,
        track: &Track,
        measure_id: usize,
        measure_header: &MeasureHeader,
        midi_channel: &MidiChannel,
        previous_beat: Option<&Beat>,
        beat_id: usize,
        beat: &Beat,
        next_beat: Option<&Beat>,
        strings: &[(i32, i32)],
        triplet_adj: TripletAdjustment,
    ) {
        let channel_id = midi_channel.channel_id;
        let tempo = measure_header.tempo.value;
        // GP files define an effect channel per track, but TuxGuitar doesn't use it for playback.
        assert!(channel_id < 16);
        let track_offset = track.offset;
        let beat_duration = triplet_adj.duration;
        let stroke = &beat.effect.stroke;
        let stroke_increment = stroke.increment_for_duration(beat_duration);
        // pre-compute per-string stroke offsets (only for strings with non-tied notes)
        let stroke_offsets = compute_stroke_offsets(beat, stroke_increment, strings.len());
        for note in &beat.notes {
            if note.kind != NoteType::Tie {
                let (string_id, string_tuning) = strings[note.string as usize - 1];
                assert_eq!(string_id, i32::from(note.string));

                // note starts on beat (adjusted for triplet feel)
                let mut note_start = triplet_adj.start;

                // apply effects on duration
                let mut duration = apply_duration_effect(
                    track,
                    measure_id,
                    beat_id,
                    note,
                    next_beat,
                    tempo,
                    beat_duration,
                );
                assert_ne!(duration, 0);

                // apply stroke effect: stagger note start times across strings
                let stroke_offset = stroke_offsets[note.string as usize - 1];
                if stroke_offset > 0 {
                    note_start += stroke_offset;
                    duration = duration.saturating_sub(stroke_offset);
                }

                // surrounding notes on the same string on the previous & next beat
                let previous_note =
                    previous_beat.and_then(|b| b.notes.iter().find(|n| n.string == note.string));
                let next_note =
                    next_beat.and_then(|b| b.notes.iter().find(|n| n.string == note.string));

                // pack with beat to propagate duration
                let next_note = next_beat.zip(next_note);

                // apply effects on velocity
                let velocity = apply_velocity_effect(note, previous_note, midi_channel);

                // apply effects on key
                if let Some(key) = self.add_key_effect(
                    track_id,
                    track_offset,
                    string_tuning,
                    &mut note_start,
                    &mut duration,
                    tempo,
                    note,
                    next_note,
                    velocity,
                    midi_channel,
                ) {
                    self.add_note(
                        track_id,
                        key,
                        note_start,
                        duration,
                        velocity,
                        i32::from(channel_id),
                    );
                }
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn add_key_effect(
        &mut self,
        track_id: usize,
        track_offset: i32,
        string_tuning: i32,
        note_start: &mut u32,
        duration: &mut u32,
        tempo: u32,
        note: &Note,
        next_note_beat: Option<(&Beat, &Note)>,
        velocity: i16,
        midi_channel: &MidiChannel,
    ) -> Option<i32> {
        let channel_id = i32::from(midi_channel.channel_id);
        let is_percussion = midi_channel.is_percussion();

        // compute key without effect
        let initial_key = track_offset + i32::from(note.value) + string_tuning;

        // key with effect
        let mut key = initial_key;

        // fade in
        if note.effect.fade_in {
            let mut expression = 31;
            let expression_increment = 1;
            let mut tick = *note_start;
            let tick_increment = *duration / ((127 - expression) / expression_increment);
            while tick < (*note_start + *duration) && expression < 127 {
                self.add_expression(tick, track_id, channel_id, expression as i32);
                tick += tick_increment;
                expression += expression_increment;
            }
            // normalize the expression
            self.add_expression(*note_start + *duration, track_id, channel_id, 127);
        }

        // grace note
        if let Some(grace) = &note.effect.grace {
            let grace_key = track_offset + i32::from(grace.fret) + string_tuning;
            let grace_length = grace.duration_time() as u32;
            let grace_velocity = grace.velocity;
            let grace_duration = if grace.is_dead {
                apply_static_duration(tempo, DEFAULT_DURATION_DEAD, grace_length)
            } else {
                grace_length
            };
            let on_beat_duration = *note_start - grace_length;
            if grace.is_on_beat || on_beat_duration < QUARTER_TIME {
                *note_start = note_start.saturating_add(grace_length);
                *duration = duration.saturating_sub(grace_length);
            }
            self.add_note(
                track_id,
                grace_key,
                *note_start - grace_length,
                grace_duration,
                grace_velocity,
                channel_id,
            );
        }

        // trill
        if let Some(trill) = &note.effect.trill
            && !is_percussion
        {
            let trill_key = track_offset + i32::from(trill.fret) + string_tuning;
            let mut trill_length = trill.duration.time();

            let trill_tick_limit = *note_start + *duration;
            let mut real_key = false;
            let mut tick = *note_start;

            let mut counter = 0;
            while tick + 10 < trill_tick_limit {
                if tick + trill_length >= trill_tick_limit {
                    trill_length = trill_tick_limit - tick - 1;
                }
                let iter_key = if real_key { initial_key } else { trill_key };
                self.add_note(track_id, iter_key, tick, trill_length, velocity, channel_id);
                real_key = !real_key;
                tick += trill_length;
                counter += 1;
            }
            assert!(
                counter > 0,
                "No trill notes published! trill_length: {trill_length}, tick: {tick}, trill_tick_limit: {trill_tick_limit}"
            );

            // all notes published - the caller does not need to publish the note
            return None;
        }

        // tremolo picking
        if let Some(tremolo_picking) = &note.effect.tremolo_picking {
            let mut tp_length = tremolo_picking.duration.time();
            let mut tick = *note_start;
            let tp_tick_limit = *note_start + *duration;
            let mut counter = 0;
            while tick + 10 < tp_tick_limit {
                if tick + tp_length >= tp_tick_limit {
                    tp_length = tp_tick_limit - tick - 1;
                }
                self.add_note(track_id, initial_key, tick, tp_length, velocity, channel_id);
                tick += tp_length;
                counter += 1;
            }
            assert!(
                counter > 0,
                "No tremolo notes published! tp_length: {tp_length}, tick: {tick}, tp_tick_limit: {tp_tick_limit}"
            );
            // all notes published - the caller does not need to publish the note
            return None;
        }

        // bend
        if let Some(bend_effect) = &note.effect.bend
            && !is_percussion
        {
            self.add_bend(track_id, *note_start, *duration, channel_id, bend_effect);
        }

        // tremolo bar
        if let Some(tremolo_bar) = &note.effect.tremolo_bar
            && !is_percussion
        {
            self.add_tremolo_bar(track_id, *note_start, *duration, channel_id, tremolo_bar);
        }

        // slide
        if let Some(_slide) = &note.effect.slide
            && !is_percussion
            && let Some((next_beat, next_note)) = next_note_beat
        {
            let value_1 = i32::from(note.value);
            let value_2 = i32::from(next_note.value);

            let tick1 = *note_start;
            let tick2 = next_beat.start;

            // make slide
            let distance: i32 = value_2 - value_1;
            let length: i32 = (tick2 - tick1) as i32;
            let points = length / (QUARTER_TIME / 8) as i32;
            for p_offset in 1..=points {
                let tone = ((length / points) * p_offset) * distance / length;
                let bend = DEFAULT_BEND + (tone as f32 * DEFAULT_BEND_SEMI_TONE * 2.0);
                let bend_tick = tick1 as i32 + (length / points) * p_offset;
                self.add_pitch_bend(bend_tick as u32, track_id, channel_id, bend as i32);
            }

            // normalise the bend
            self.add_pitch_bend(tick2, track_id, channel_id, DEFAULT_BEND as i32);
        }

        // vibrato
        if note.effect.vibrato && !is_percussion {
            self.add_vibrato(track_id, *note_start, *duration, channel_id);
        }

        // harmonic
        if let Some(harmonic) = &note.effect.harmonic
            && !is_percussion
        {
            match harmonic.kind {
                HarmonicType::Natural => {
                    for (harmonic_value, harmonic_frequency) in NATURAL_FREQUENCIES {
                        if note.value % 12 == (harmonic_value % 12) as i16 {
                            key = (initial_key + harmonic_frequency) - i32::from(note.value);
                            break;
                        }
                    }
                }
                HarmonicType::Semi => {
                    let velocity = MIN_VELOCITY.max(velocity - VELOCITY_INCREMENT * 3);
                    self.add_note(
                        track_id,
                        initial_key,
                        *note_start,
                        *duration,
                        velocity,
                        channel_id,
                    );
                    key = initial_key + NATURAL_FREQUENCIES[0].1;
                }
                HarmonicType::Artificial | HarmonicType::Pinch => {
                    key = initial_key + NATURAL_FREQUENCIES[0].1;
                }
                HarmonicType::Tapped => {
                    if let Some(right_hand_fret) = harmonic.right_hand_fret {
                        for (harmonic_value, harmonic_frequency) in NATURAL_FREQUENCIES {
                            if i16::from(right_hand_fret) - note.value == harmonic_value as i16 {
                                key = initial_key + harmonic_frequency;
                                break;
                            }
                        }
                    }
                }
            }
            if key - 12 > 0 {
                let velocity = MIN_VELOCITY.max(velocity - VELOCITY_INCREMENT * 4);
                self.add_note(
                    track_id,
                    key - 12,
                    *note_start,
                    *duration,
                    velocity,
                    channel_id,
                );
            }
        }

        Some(key)
    }

    fn add_vibrato(&mut self, track_id: usize, start: u32, duration: u32, channel_id: i32) {
        let end = start + duration;
        let mut next_start = start;
        while next_start < end {
            next_start = if next_start + 160 > end {
                end
            } else {
                next_start + 160
            };
            self.add_pitch_bend(next_start, track_id, channel_id, DEFAULT_BEND as i32);

            next_start = if next_start + 160 > end {
                end
            } else {
                next_start + 160
            };
            let value = DEFAULT_BEND + DEFAULT_BEND_SEMI_TONE / 2.0;
            self.add_pitch_bend(next_start, track_id, channel_id, value as i32);
        }
        self.add_pitch_bend(next_start, track_id, channel_id, DEFAULT_BEND as i32);
    }

    fn add_bend(
        &mut self,
        track_id: usize,
        start: u32,
        duration: u32,
        channel_id: i32,
        bend: &BendEffect,
    ) {
        for (point_id, point) in bend.points.iter().enumerate() {
            let value =
                DEFAULT_BEND + (f32::from(point.value) * DEFAULT_BEND_SEMI_TONE / SEMITONE_LENGTH);
            let value = value.clamp(0.0, 127.0) as i32;
            let bend_start = start + point.get_time(duration);
            self.add_pitch_bend(bend_start, track_id, channel_id, value);

            // look ahead to next bend point
            if let Some(next_point) = bend.points.get(point_id + 1) {
                let next_value = DEFAULT_BEND
                    + (f32::from(next_point.value) * DEFAULT_BEND_SEMI_TONE / SEMITONE_LENGTH);
                self.process_next_bend_values(
                    track_id,
                    channel_id,
                    value,
                    next_value as i32,
                    bend_start,
                    start,
                    next_point,
                    duration,
                );
            }
        }
        self.add_pitch_bend(start + duration, track_id, channel_id, DEFAULT_BEND as i32);
    }

    #[allow(clippy::too_many_arguments)]
    fn process_next_bend_values(
        &mut self,
        track_id: usize,
        channel_id: i32,
        mut value: i32,
        next_value: i32,
        mut bend_start: u32,
        start: u32,
        next_point: &BendPoint,
        duration: u32,
    ) {
        if value != next_value {
            let next_bend_start = start + next_point.get_time(duration);
            let width = (next_bend_start - bend_start) as f32 / (next_value - value).abs() as f32;
            let width = width as u32;
            // ascending
            if value < next_value {
                while value < next_value {
                    value += 1;
                    bend_start += width;
                    // clamp to 127
                    let value = value.min(127);
                    self.add_pitch_bend(bend_start, track_id, channel_id, value);
                }
            }
            // descending
            if value > next_value {
                while value > next_value {
                    value -= 1;
                    bend_start += width;
                    // clamp to 0
                    let value = value.max(0);
                    self.add_pitch_bend(bend_start, track_id, channel_id, value);
                }
            }
        }
    }

    fn add_tremolo_bar(
        &mut self,
        track_id: usize,
        start: u32,
        duration: u32,
        channel_id: i32,
        tremolo_bar: &TremoloBarEffect,
    ) {
        for (point_id, point) in tremolo_bar.points.iter().enumerate() {
            let value = DEFAULT_BEND + (f32::from(point.value) * DEFAULT_BEND_SEMI_TONE * 2.0);
            let value = value.clamp(0.0, 127.0) as i32;
            let bend_start = start + point.get_time(duration);
            self.add_pitch_bend(bend_start, track_id, channel_id, value);

            // look ahead to next bend point
            if let Some(next_point) = tremolo_bar.points.get(point_id + 1) {
                let next_value =
                    DEFAULT_BEND + (f32::from(next_point.value) * DEFAULT_BEND_SEMI_TONE * 2.0);
                self.process_next_bend_values(
                    track_id,
                    channel_id,
                    value,
                    next_value as i32,
                    bend_start,
                    start,
                    next_point,
                    duration,
                );
            }
        }
        self.add_pitch_bend(start + duration, track_id, channel_id, DEFAULT_BEND as i32);
    }

    fn add_note(
        &mut self,
        track_id: usize,
        key: i32,
        start: u32,
        duration: u32,
        velocity: i16,
        channel: i32,
    ) {
        let note_on = MidiEvent::new_note_on(start, track_id, key, velocity, channel);
        self.add_event(note_on);
        if duration > 0 {
            let tick = start + duration;
            let note_off = MidiEvent::new_note_off(tick, track_id, key, channel);
            self.add_event(note_off);
        }
    }

    fn add_tempo_change(&mut self, tick: u32, tempo: u32) {
        let event = MidiEvent::new_tempo_change(tick, tempo);
        self.add_event(event);
    }

    fn add_bank_selection(&mut self, tick: u32, track_id: usize, channel: i32, bank: i32) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x00, bank);
        self.add_event(event);
    }

    fn add_volume_selection(&mut self, tick: u32, track_id: usize, channel: i32, volume: i32) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x27, volume);
        self.add_event(event);
    }

    fn add_expression_selection(
        &mut self,
        tick: u32,
        track_id: usize,
        channel: i32,
        expression: i32,
    ) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x2B, expression);
        self.add_event(event);
    }

    fn add_chorus_selection(&mut self, tick: u32, track_id: usize, channel: i32, chorus: i32) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x5D, chorus);
        self.add_event(event);
    }

    fn add_reverb_selection(&mut self, tick: u32, track_id: usize, channel: i32, reverb: i32) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x5B, reverb);
        self.add_event(event);
    }

    fn add_pitch_bend(&mut self, tick: u32, track_id: usize, channel: i32, value: i32) {
        // GP uses a value between 0 and 128
        // MIDI uses a value between 0 and 16383 (128 * 128)
        let midi_value = value * 128;

        // the bend value must be split into two bytes and sent to the synthesizer.
        let data1 = midi_value & 0x7F;
        let data2 = midi_value >> 7;
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xE0, data1, data2);
        self.add_event(event);
    }

    fn add_expression(&mut self, tick: u32, track_id: usize, channel: i32, expression: i32) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x0B, expression);
        self.add_event(event);
    }

    fn add_program_selection(&mut self, tick: u32, track_id: usize, channel: i32, program: i32) {
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xC0, program, 0);
        self.add_event(event);
    }

    fn add_pitch_bend_range(&mut self, tick: u32, track_id: usize, channel: i32) {
        // RPN MSB: Select RPN group (usually 0)
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x65, 0);
        self.add_event(event);

        // RPN LSB: Select RPN 0/0 (Pitch Bend Sensitivity)
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x64, 0);
        self.add_event(event);

        // Data Entry MSB: Set the value (Pitch Bend Range)
        // 12 semitones for the guitar
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x06, 12);
        self.add_event(event);

        // Data Entry LSB: Cents (usually 0)
        let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x26, 0);
        self.add_event(event);
    }

    fn add_track_channel_midi_control(&mut self, track_id: usize, midi_channel: &MidiChannel) {
        let channel_id = midi_channel.channel_id;
        // publish MIDI control messages for the track channel at the start
        let info_tick = FIRST_TICK;
        self.add_volume_selection(
            info_tick,
            track_id,
            i32::from(channel_id),
            i32::from(midi_channel.volume),
        );
        self.add_expression_selection(info_tick, track_id, i32::from(channel_id), 127);
        self.add_chorus_selection(
            info_tick,
            track_id,
            i32::from(channel_id),
            i32::from(midi_channel.chorus),
        );
        self.add_reverb_selection(
            info_tick,
            track_id,
            i32::from(channel_id),
            i32::from(midi_channel.reverb),
        );
        self.add_bank_selection(
            info_tick,
            track_id,
            i32::from(channel_id),
            i32::from(midi_channel.bank),
        );
        self.add_program_selection(
            info_tick,
            track_id,
            i32::from(channel_id),
            midi_channel.instrument,
        );
        self.add_pitch_bend_range(info_tick, track_id, i32::from(channel_id));
    }

    fn add_event(&mut self, event: MidiEvent) {
        self.events.push(event);
    }
}

fn apply_velocity_effect(
    note: &Note,
    previous_note: Option<&Note>,
    midi_channel: &MidiChannel,
) -> i16 {
    let effect = &note.effect;
    let mut velocity = note.velocity;

    if !midi_channel.is_percussion() && previous_note.is_some_and(|n| n.effect.hammer) {
        velocity = MIN_VELOCITY.max(velocity - 25);
    }

    if effect.ghost_note {
        velocity = MIN_VELOCITY.max(velocity - VELOCITY_INCREMENT);
    } else if effect.accentuated_note {
        velocity = MIN_VELOCITY.max(velocity + VELOCITY_INCREMENT);
    } else if effect.heavy_accentuated_note {
        velocity = MIN_VELOCITY.max(velocity + VELOCITY_INCREMENT * 2);
    }
    velocity.min(127)
}

fn apply_duration_effect(
    track: &Track,
    measure_id: usize,
    beat_id: usize,
    note: &Note,
    first_next_beat: Option<&Beat>,
    tempo: u32,
    mut duration: u32,
) -> u32 {
    let note_type = &note.kind;
    let next_beats_in_next_measures = track.measures[measure_id..]
        .iter()
        .flat_map(|m| m.voices[0].beats.iter())
        .skip(beat_id + 1); // skip current and previous beats

    // handle chains of tie notes
    for next_beat in next_beats_in_next_measures {
        // filter for only next notes on matching string
        if let Some(next_note) = next_beat.notes.iter().find(|n| n.string == note.string) {
            if next_note.kind == NoteType::Tie {
                duration += next_beat.duration.time();
            } else {
                // stop chain
                break;
            }
        } else {
            // break chain of tie notes
            break;
        }
    }
    // hande let-ring
    if let Some(first_next_beat) = first_next_beat
        && note.effect.let_ring
    {
        duration += first_next_beat.duration.time();
    }
    if note_type == &NoteType::Dead {
        return apply_static_duration(tempo, DEFAULT_DURATION_DEAD, duration);
    }
    if note.effect.palm_mute {
        return apply_static_duration(tempo, DEFAULT_DURATION_PM, duration);
    }
    if note.effect.staccato {
        return (duration as f32 * 50.0 / 100.00) as u32;
    }
    duration
}

fn apply_static_duration(tempo: u32, duration: u32, maximum: u32) -> u32 {
    let value = tempo * duration / 60;
    value.min(maximum)
}

/// Triplet feel adjustment for a beat's start and duration.
struct TripletAdjustment {
    start: u32,
    duration: u32,
}

/// Apply triplet feel (swing) to a beat's timing.
/// Pairs of equal-duration notes are converted to a long-short triplet pattern.
fn apply_triplet_feel(
    beat: &Beat,
    previous_beat: Option<&Beat>,
    next_beat: Option<&Beat>,
    triplet_feel: TripletFeel,
) -> TripletAdjustment {
    let beat_start = beat.start;
    let beat_duration = beat.duration.time();

    match triplet_feel {
        TripletFeel::None => TripletAdjustment {
            start: beat_start,
            duration: beat_duration,
        },
        TripletFeel::Eighth => apply_triplet_feel_for_duration(
            beat_start,
            beat_duration,
            previous_beat,
            next_beat,
            QUARTER_TIME / 2,
            QUARTER_TIME,
        ),
        TripletFeel::Sixteenth => apply_triplet_feel_for_duration(
            beat_start,
            beat_duration,
            previous_beat,
            next_beat,
            QUARTER_TIME / 4,
            QUARTER_TIME / 2,
        ),
    }
}

/// Apply triplet feel for a specific note duration level.
/// `target_duration` is the straight note duration to match (e.g., 480 for eighth, 240 for sixteenth).
/// `boundary` is the rhythmic boundary for pairing (e.g., 960 for eighth pairs, 480 for sixteenth pairs).
fn apply_triplet_feel_for_duration(
    beat_start: u32,
    beat_duration: u32,
    previous_beat: Option<&Beat>,
    next_beat: Option<&Beat>,
    target_duration: u32,
    boundary: u32,
) -> TripletAdjustment {
    if beat_duration != target_duration {
        return TripletAdjustment {
            start: beat_start,
            duration: beat_duration,
        };
    }

    // triplet duration = target_duration * 2 / 3
    let triplet_duration = target_duration * 2 / 3;

    // first beat of pair: on the boundary
    if beat_start.is_multiple_of(boundary) {
        // check that next beat is also the same duration (forming a pair)
        let next_qualifies = next_beat.is_none_or(|nb| {
            nb.start > beat_start + beat_duration || nb.duration.time() == target_duration
        });
        if next_qualifies {
            return TripletAdjustment {
                start: beat_start,
                duration: triplet_duration * 2, // long note
            };
        }
    }
    // second beat of pair: on the half-boundary
    else if beat_start.is_multiple_of(boundary / 2) {
        // check that previous beat is also the same duration
        let prev_qualifies = previous_beat.is_none_or(|pb| {
            pb.start < beat_start - beat_duration || pb.duration.time() == target_duration
        });
        if prev_qualifies {
            let adjusted_start = (beat_start - beat_duration) + triplet_duration * 2;
            return TripletAdjustment {
                start: adjusted_start,
                duration: triplet_duration, // short note
            };
        }
    }

    TripletAdjustment {
        start: beat_start,
        duration: beat_duration,
    }
}

/// Compute per-string stroke offsets for a beat, following TuxGuitar's approach:
/// only strings with non-tied notes receive incremental offsets.
fn compute_stroke_offsets(beat: &Beat, stroke_increment: u32, string_count: usize) -> Vec<u32> {
    let mut offsets = vec![0_u32; string_count];
    if stroke_increment == 0 || beat.effect.stroke.direction == BeatStrokeDirection::None {
        return offsets;
    }

    // build bitmask of strings that have non-tied notes
    let mut strings_used: u32 = 0;
    for note in &beat.notes {
        if note.kind != NoteType::Tie {
            strings_used |= 1 << (note.string as u32 - 1);
        }
    }

    // assign cumulative offsets in stroke direction order
    let mut stroke_move: u32 = 0;
    for i in 0..string_count {
        let index = match beat.effect.stroke.direction {
            BeatStrokeDirection::Down => (string_count - 1) - i,
            BeatStrokeDirection::Up => i,
            BeatStrokeDirection::None => unreachable!(),
        };
        if strings_used & (1 << index) != 0 {
            offsets[index] = stroke_move;
            stroke_move += stroke_increment;
        }
    }

    offsets
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audio::midi_event::MidiEventType;
    use crate::parser::song_parser::{DURATION_EIGHTH, DURATION_SIXTEENTH, NoteEffect, NoteType};
    use crate::parser::song_parser_tests::parse_gp_file;
    use std::collections::HashSet;
    use std::io::Write;
    use std::path::{Path, PathBuf};

    #[test]
    fn test_midi_events_for_all_files() {
        let test_dir = Path::new("test-files");
        let gold_dir = Path::new("test-files/gold-generated-midi");
        for entry in std::fs::read_dir(test_dir).unwrap() {
            let entry = entry.unwrap();
            let path = entry.path();
            if path.is_dir() {
                continue;
            }
            let extension = path.extension().unwrap();
            if extension != "gp5" && extension != "gp4" {
                continue;
            }
            let file_name = path.file_name().unwrap().to_str().unwrap();
            eprintln!("Parsing file: {file_name}");
            let file_path = path.to_str().unwrap();
            let song = parse_gp_file(file_path)
                .unwrap_or_else(|err| panic!("Failed to parse file: {file_name}\n{err}"));
            let song = Rc::new(song);
            let builder = MidiBuilder::new();
            let events = builder.build_for_song(&song);
            assert!(!events.is_empty(), "No events found for {file_name}");

            // assert sorted by tick
            assert!(events.windows(2).all(|w| w[0].tick <= w[1].tick));
            assert_eq!(events[0].tick, 1);

            // check against golden file
            let gold_file_path = gold_dir.join(format!("{file_name}.txt"));
            if !gold_file_path.exists() {
                // create gold file
                let mut file = std::fs::File::create(&gold_file_path).unwrap();
                for event in &events {
                    writeln!(file, "{}", print_event(event)).unwrap();
                }
            }

            // verify against gold file
            validate_gold_rendered_result(&events, gold_file_path);
        }
    }

    fn print_event(event: &MidiEvent) -> String {
        format!("{:?} {:?} {:?}", event.tick, event.event, event.track)
    }

    fn validate_gold_rendered_result(events: &[MidiEvent], gold_path: PathBuf) {
        let gold = std::fs::read_to_string(&gold_path).expect("gold file not found!");
        let mut expected_lines = events.iter().map(print_event);
        for (i1, l1) in gold.lines().enumerate() {
            let l2 = expected_lines.next().unwrap();
            if l1.trim_end() != l2.trim_end() {
                println!("## GOLD line {} ##", i1 + 1);
                println!("{}", l1.trim_end());
                println!("## ACTUAL ##");
                println!("{}", l2.trim_end());
                println!("#####");
                assert_eq!(l1, l2, "line {i1} failed for {gold_path:?}");
            }
        }
    }

    #[test]
    fn test_midi_events_for_demo_song() {
        const FILE_PATH: &str = "test-files/Demo v5.gp5";
        let song = parse_gp_file(FILE_PATH).unwrap();
        let song = Rc::new(song);
        let builder = MidiBuilder::new();
        let events = builder.build_for_song(&song);

        assert_eq!(events.len(), 4693);
        assert_eq!(events[0].tick, 1);

        // assert number of tracks
        let track_count = song.tracks.len();
        let unique_tracks: HashSet<_> = events.iter().map(|event| event.track).collect();
        assert_eq!(unique_tracks.len(), track_count + 1); // plus None for info events

        // skip MIDI program messages
        let rhythm_track_events: Vec<_> = events
            .iter()
            .filter(|e| e.track == Some(0))
            .skip(10)
            .collect();

        // print 20 first for debugging
        // for (i, event) in rhythm_track_events.iter().enumerate().take(20) {
        //     eprintln!("{} {:?}", i, event);
        // }

        // C5 ON
        let event = &rhythm_track_events[0];
        assert_eq!(event.tick, 960);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 60, 95)));

        let event = &rhythm_track_events[1];
        assert_eq!(event.tick, 960);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 55, 95)));

        let event = &rhythm_track_events[2];
        assert_eq!(event.tick, 960);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 127)));

        // C5 OFF
        let event = &rhythm_track_events[3];
        assert_eq!(event.tick, 1440);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 60)));

        let event = &rhythm_track_events[4];
        assert_eq!(event.tick, 1440);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 55)));

        let event = &rhythm_track_events[5];
        assert_eq!(event.tick, 1440);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 48)));

        // single note `3` on string `1` (E2)
        let event = &rhythm_track_events[6];
        assert_eq!(event.tick, 1440);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 95)));

        // single note OFF (palm mute)
        let event = &rhythm_track_events[7];
        assert_eq!(event.tick, 1605);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 48)));

        // single note `3` on string `1` (E2)
        let event = &rhythm_track_events[8];
        assert_eq!(event.tick, 1920);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 95)));

        // single note OFF (palm mute)
        let event = &rhythm_track_events[9];
        assert_eq!(event.tick, 2085);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 48)));

        // C5 ON
        let event = &rhythm_track_events[10];
        assert_eq!(event.tick, 2400);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 60, 95)));

        let event = &rhythm_track_events[11];
        assert_eq!(event.tick, 2400);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 55, 95)));

        let event = &rhythm_track_events[12];
        assert_eq!(event.tick, 2400);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 127)));

        // skip MIDI program messages
        let solo_track_events: Vec<_> = events
            .iter()
            .filter(|e| e.track == Some(1))
            .skip(10)
            .collect();

        //print 100 first for debugging
        for (i, event) in solo_track_events.iter().enumerate().take(100) {
            eprintln!("{i} {event:?}");
        }

        // trill ON
        let event = &solo_track_events[0];
        assert_eq!(event.tick, 12480);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 72, 95)));

        // trill OFF
        let event = &solo_track_events[1];
        assert_eq!(event.tick, 12720);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 72)));

        // trill ON
        let event = &solo_track_events[2];
        assert_eq!(event.tick, 12720);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 69, 95)));

        // trill OFF
        let event = &solo_track_events[3];
        assert_eq!(event.tick, 12960);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 69)));

        // trill ON
        let event = &solo_track_events[4];
        assert_eq!(event.tick, 12960);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 72, 95)));

        // trill OFF
        let event = &solo_track_events[5];
        assert_eq!(event.tick, 13200);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 72)));

        // pass some trill notes...

        // trill ON
        let event = &solo_track_events[30];
        assert_eq!(event.tick, 16080);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 69, 95)));

        // trill OFF
        let event = &solo_track_events[31];
        assert_eq!(event.tick, 16319);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 69)));

        // tremolo ON (repeated section)
        let event = &solo_track_events[32];
        assert_eq!(event.tick, 27840);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 60, 95)));

        // tremolo OFF
        let event = &solo_track_events[33];
        assert_eq!(event.tick, 27960);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 60)));

        // note ON (after all tremolo and repeated sections)
        let event = &solo_track_events[64];
        assert_eq!(event.tick, 77760);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 63, 95)));

        // note OFF
        let event = &solo_track_events[65];
        assert_eq!(event.tick, 78240);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 63)));

        // note ON hammer
        let event = &solo_track_events[66];
        assert_eq!(event.tick, 78240);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOn(2, 65, 70)));

        // note OFF hammer
        let event = &solo_track_events[67];
        assert_eq!(event.tick, 78720);
        assert_eq!(event.track, Some(1));
        assert!(matches!(event.event, MidiEventType::NoteOff(2, 65)));
    }

    #[test]
    fn test_midi_events_for_bleed() {
        const FILE_PATH: &str = "test-files/Meshuggah - Bleed.gp5";
        let song = parse_gp_file(FILE_PATH).unwrap();
        let song = Rc::new(song);
        let builder = MidiBuilder::new();
        let events = builder.build_for_song(&song);

        assert_eq!(events.len(), 44442);
        assert_eq!(events[0].tick, 1);

        // assert number of tracks
        let track_count = song.tracks.len();
        let unique_tracks: HashSet<_> = events.iter().map(|event| event.track).collect();
        assert_eq!(unique_tracks.len(), track_count);

        // skip MIDI program messages
        let rhythm_track_events: Vec<_> = events
            .iter()
            .filter(|e| e.track == Some(0))
            .skip(10)
            .collect();

        // print 60 first for debugging
        // for (i, event) in rhythm_track_events.iter().enumerate().take(100) {
        //     eprintln!("{} {:?}", i, event);
        // }

        let event = &rhythm_track_events[44];
        assert_eq!(event.tick, 4800);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[45];
        assert_eq!(event.tick, 4915);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 39)));

        let event = &rhythm_track_events[46];
        assert_eq!(event.tick, 5040);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[47];
        assert_eq!(event.tick, 5155);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 39)));

        let event = &rhythm_track_events[48];
        assert_eq!(event.tick, 5280);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[49];
        assert_eq!(event.tick, 5395);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 39)));

        let event = &rhythm_track_events[50];
        assert_eq!(event.tick, 5400);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[51];
        assert_eq!(event.tick, 5515);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 39)));

        let event = &rhythm_track_events[52];
        assert_eq!(event.tick, 5520);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[50];
        assert_eq!(event.tick, 5400);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[51];
        assert_eq!(event.tick, 5515);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 39)));

        let event = &rhythm_track_events[52];
        assert_eq!(event.tick, 5520);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOn(0, 39, 95)));

        let event = &rhythm_track_events[53];
        assert_eq!(event.tick, 5635);
        assert_eq!(event.track, Some(0));
        assert!(matches!(event.event, MidiEventType::NoteOff(0, 39)));
    }

    #[test]
    fn playback_order_damage_control() {
        const FILE_PATH: &str = "test-files/John Petrucci - Damage Control (ver 6 by Feio666).gp5";
        let song = parse_gp_file(FILE_PATH).unwrap();
        let headers = &song.measure_headers;

        // discover repeat structure
        let repeats: Vec<(usize, bool, i8, u8)> = headers
            .iter()
            .enumerate()
            .filter(|(_, h)| h.repeat_open || h.repeat_close > 0 || h.repeat_alternative > 0)
            .map(|(i, h)| (i, h.repeat_open, h.repeat_close, h.repeat_alternative))
            .collect();
        assert!(!repeats.is_empty(), "Expected repeat markers");

        let order = compute_playback_order(headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();

        // playback order should be longer than header count due to repeats
        assert!(
            order.len() > headers.len(),
            "Playback order ({}) should be > header count ({})",
            order.len(),
            headers.len()
        );

        // verify the first repeat section has alternative endings
        // find first measure with repeat_alternative
        let first_alt = repeats.iter().find(|(_, _, _, alt)| *alt > 0);
        assert!(first_alt.is_some(), "Expected alternative endings");

        // verify repeated measures appear multiple times in playback order
        let first_repeat_open = repeats.iter().find(|(_, open, _, _)| *open).unwrap().0;
        let appearances = indices
            .iter()
            .filter(|&&idx| idx == first_repeat_open)
            .count();
        assert!(
            appearances > 1,
            "First repeated measure should appear more than once"
        );

        // verify all playback ticks are monotonically increasing
        let playback_ticks: Vec<i64> = order
            .iter()
            .map(|(idx, offset)| i64::from(headers[*idx].start) + offset)
            .collect();
        for window in playback_ticks.windows(2) {
            assert!(
                window[0] < window[1],
                "Playback ticks not monotonically increasing: {} >= {}",
                window[0],
                window[1]
            );
        }

        // verify all measure indices are valid
        for (idx, _) in &order {
            assert!(*idx < headers.len(), "Invalid measure index {idx}");
        }

        // build MIDI events and verify they are sorted
        let song = Rc::new(song);
        let builder = MidiBuilder::new();
        let events = builder.build_for_song(&song);
        assert!(!events.is_empty());
        assert!(
            events.windows(2).all(|w| w[0].tick <= w[1].tick),
            "Events not sorted by tick"
        );
    }

    #[test]
    fn triplet_feel_guthrie_eric() {
        const FILE_PATH: &str = "test-files/Guthrie Govan - Eric.gp5";
        let song = parse_gp_file(FILE_PATH).unwrap();

        // verify triplet feel is parsed
        let triplet_measure_indices: Vec<usize> = song
            .measure_headers
            .iter()
            .enumerate()
            .filter(|(_, h)| h.triplet_feel != TripletFeel::None)
            .map(|(i, _)| i)
            .collect();
        assert!(
            !triplet_measure_indices.is_empty(),
            "Expected triplet feel measures in Guthrie Govan - Eric"
        );

        let first_triplet_idx = triplet_measure_indices[0];
        let measure_start = song.measure_headers[first_triplet_idx].start;
        let measure_end = measure_start + song.measure_headers[first_triplet_idx].length();

        // build events and verify they are sorted
        let song = Rc::new(song);
        let builder = MidiBuilder::new();
        let events = builder.build_for_song(&song);
        assert!(!events.is_empty());
        assert!(
            events.windows(2).all(|w| w[0].tick <= w[1].tick),
            "Events not sorted by tick"
        );

        let note_ons: Vec<u32> = events
            .iter()
            .filter(|e| {
                e.tick >= measure_start
                    && e.tick < measure_end
                    && matches!(e.event, MidiEventType::NoteOn(_, _, _))
            })
            .map(|e| e.tick)
            .collect();

        // if there are consecutive eighth notes, the gaps should be uneven (640 + 320)
        // rather than even (480 + 480)
        if note_ons.len() >= 3 {
            let gaps: Vec<u32> = note_ons.windows(2).map(|w| w[1] - w[0]).collect();
            let has_uneven_gaps = gaps.windows(2).any(|w| w[0] != w[1]);
            // at least some gaps should differ (swing feel)
            assert!(
                has_uneven_gaps || gaps.iter().all(|&g| g != 480),
                "Expected uneven note spacing from triplet feel in measure {} (gaps: {gaps:?})",
                first_triplet_idx + 1
            );
        }
    }

    #[test]
    fn triplet_feel_none_no_change() {
        let beat = Beat {
            start: 960,
            ..Beat::default()
        };
        let adj = apply_triplet_feel(&beat, None, None, TripletFeel::None);
        assert_eq!(adj.start, 960);
        assert_eq!(adj.duration, beat.duration.time());
    }

    #[test]
    fn triplet_feel_eighth_first_beat() {
        // first eighth note on quarter boundary → extended to 2/3 triplet * 2
        let mut beat = Beat {
            start: 960,
            ..Beat::default()
        };
        beat.duration.value = u16::from(DURATION_EIGHTH);
        let adj = apply_triplet_feel(&beat, None, None, TripletFeel::Eighth);
        // triplet_duration = 480 * 2 / 3 = 320, long note = 640
        assert_eq!(adj.start, 960);
        assert_eq!(adj.duration, 640);
    }

    #[test]
    fn triplet_feel_eighth_second_beat() {
        // second eighth note on half-quarter boundary → shortened to 1/3 triplet
        let mut beat = Beat {
            start: 960 + 480, // half-quarter boundary
            ..Beat::default()
        };
        beat.duration.value = u16::from(DURATION_EIGHTH);
        let adj = apply_triplet_feel(&beat, None, None, TripletFeel::Eighth);
        // triplet_duration = 320, short note, start shifts to 960 + 640 = 1600
        assert_eq!(adj.start, 1600);
        assert_eq!(adj.duration, 320);
    }

    #[test]
    fn triplet_feel_preserves_total_time() {
        // first + second beat durations should sum to the original pair
        let mut first = Beat {
            start: 960,
            ..Beat::default()
        };
        first.duration.value = u16::from(DURATION_EIGHTH);
        let mut second = Beat {
            start: 960 + 480,
            ..Beat::default()
        };
        second.duration.value = u16::from(DURATION_EIGHTH);
        let adj1 = apply_triplet_feel(&first, None, Some(&second), TripletFeel::Eighth);
        let adj2 = apply_triplet_feel(&second, Some(&first), None, TripletFeel::Eighth);
        // total should be 960 (one quarter note)
        assert_eq!(adj1.duration + adj2.duration, 960);
        // second starts where first ends
        assert_eq!(adj2.start, adj1.start + adj1.duration);
    }

    #[test]
    fn triplet_feel_wrong_duration_no_change() {
        // quarter note should not be affected by eighth triplet feel
        let beat = Beat {
            start: 960,
            ..Beat::default()
        };
        // default duration is quarter (960), not eighth
        let adj = apply_triplet_feel(&beat, None, None, TripletFeel::Eighth);
        assert_eq!(adj.start, 960);
        assert_eq!(adj.duration, 960);
    }

    #[test]
    fn triplet_feel_sixteenth_pair() {
        // sixteenth pair on eighth-note boundary
        // target_duration = 240, boundary = 480
        // triplet_duration = 240 * 2 / 3 = 160
        let mut first = Beat {
            start: 960,
            ..Beat::default()
        };
        first.duration.value = u16::from(DURATION_SIXTEENTH);
        let mut second = Beat {
            start: 960 + 240,
            ..Beat::default()
        };
        second.duration.value = u16::from(DURATION_SIXTEENTH);
        let adj1 = apply_triplet_feel(&first, None, Some(&second), TripletFeel::Sixteenth);
        let adj2 = apply_triplet_feel(&second, Some(&first), None, TripletFeel::Sixteenth);
        assert_eq!(adj1.start, 960);
        assert_eq!(adj1.duration, 320); // long: 160 * 2
        assert_eq!(adj2.start, 1280); // 960 + 320
        assert_eq!(adj2.duration, 160); // short: 160
        assert_eq!(adj1.duration + adj2.duration, 480); // total = one eighth note
    }

    fn make_note(string: i8) -> Note {
        let mut note = Note::new(NoteEffect::default());
        note.string = string;
        note.kind = NoteType::Normal;
        note
    }

    #[test]
    fn stroke_offsets_no_stroke() {
        let beat = Beat::default();
        let offsets = compute_stroke_offsets(&beat, 0, 6);
        assert_eq!(offsets, vec![0, 0, 0, 0, 0, 0]);
    }

    #[test]
    fn stroke_offsets_down_stroke() {
        // down stroke: thickest string (6, index 5) plays first
        let mut beat = Beat::default();
        beat.effect.stroke.direction = BeatStrokeDirection::Down;
        beat.notes = vec![make_note(1), make_note(3), make_note(5)];
        let increment = 10;
        let offsets = compute_stroke_offsets(&beat, increment, 6);
        // string 5 (index 4) plays first (offset 0), string 3 (index 2) second, string 1 (index 0) third
        assert_eq!(offsets[4], 0); // string 5: first
        assert_eq!(offsets[2], 10); // string 3: second
        assert_eq!(offsets[0], 20); // string 1: third
        // strings without notes have 0 offset
        assert_eq!(offsets[1], 0);
        assert_eq!(offsets[3], 0);
        assert_eq!(offsets[5], 0);
    }

    #[test]
    fn stroke_offsets_up_stroke() {
        // up stroke: thinnest string (1, index 0) plays first
        let mut beat = Beat::default();
        beat.effect.stroke.direction = BeatStrokeDirection::Up;
        beat.notes = vec![make_note(1), make_note(3), make_note(5)];
        let increment = 10;
        let offsets = compute_stroke_offsets(&beat, increment, 6);
        assert_eq!(offsets[0], 0); // string 1: first
        assert_eq!(offsets[2], 10); // string 3: second
        assert_eq!(offsets[4], 20); // string 5: third
    }
}


================================================
FILE: src/audio/midi_event.rs
================================================
/// A MIDI event.
/// Try to keep this struct as small as possible because there will be a lot of them.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct MidiEvent {
    /// The tick at which the event occurs.
    pub tick: u32,
    /// The type of the event.
    pub event: MidiEventType,
    /// The track number of the event. None = info event.
    pub track: Option<u8>,
}

impl MidiEvent {
    pub const fn is_midi_message(&self) -> bool {
        matches!(self.event, MidiEventType::MidiMessage(_, _, _, _))
    }

    pub const fn is_note_event(&self) -> bool {
        matches!(
            self.event,
            MidiEventType::NoteOn(_, _, _) | MidiEventType::NoteOff(_, _)
        )
    }

    pub const fn new_note_on(
        tick: u32,
        track: usize,
        key: i32,
        velocity: i16,
        channel: i32,
    ) -> Self {
        let event = MidiEventType::note_on(channel, key, velocity);
        Self {
            tick,
            event,
            track: Some(track as u8),
        }
    }

    pub const fn new_note_off(tick: u32, track: usize, key: i32, channel: i32) -> Self {
        let event = MidiEventType::note_off(channel, key);
        Self {
            tick,
            event,
            track: Some(track as u8),
        }
    }

    pub const fn new_tempo_change(tick: u32, tempo: u32) -> Self {
        let event = MidiEventType::tempo_change(tempo);
        Self {
            tick,
            event,
            track: None,
        }
    }

    pub const fn new_midi_message(
        tick: u32,
        track: usize,
        channel: i32,
        command: i32,
        data1: i32,
        data2: i32,
    ) -> Self {
        let event = MidiEventType::midi_message(channel, command, data1, data2);
        Self {
            tick,
            event,
            track: Some(track as u8),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum MidiEventType {
    NoteOn(i32, i32, i16),           // channel, note, velocity
    NoteOff(i32, i32),               // channel, note
    TempoChange(u32),                // tempo in BPM
    MidiMessage(i32, i32, i32, i32), // channel: i32, command: i32, data1: i32, data2: i32
}

impl MidiEventType {
    const fn note_on(channel: i32, key: i32, velocity: i16) -> Self {
        Self::NoteOn(channel, key, velocity)
    }

    const fn note_off(channel: i32, key: i32) -> Self {
        Self::NoteOff(channel, key)
    }

    const fn tempo_change(tempo: u32) -> Self {
        Self::TempoChange(tempo)
    }

    const fn midi_message(channel: i32, command: i32, data1: i32, data2: i32) -> Self {
        Self::MidiMessage(channel, command, data1, data2)
    }
}


================================================
FILE: src/audio/midi_player.rs
================================================
use crate::audio::FIRST_TICK;
use crate::audio::midi_builder::MidiBuilder;
use crate::audio::midi_event::MidiEventType;
use crate::audio::midi_player_params::MidiPlayerParams;
use crate::audio::midi_sequencer::MidiSequencer;
use crate::parser::song_parser::Song;
use cpal::DefaultStreamConfigError;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use rustysynth::{SoundFont, Synthesizer, SynthesizerSettings};
use std::fs::File;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::Notify;

const DEFAULT_SAMPLE_RATE: u32 = 44100; // number of samples per second

/// Default sound font file is embedded in the binary (6MB)
const TIMIDITY_SOUND_FONT: &[u8] = include_bytes!("../../resources/TimGM6mb.sf2");

pub struct AudioPlayer {
    is_playing: bool,
    song: Rc<Song>,                       // Song to play (shared with app)
    stream: Option<Rc<cpal::Stream>>,     // Stream is not Send & Sync
    sequencer: Arc<Mutex<MidiSequencer>>, // Need a handle to reset sequencer
    player_params: Arc<MidiPlayerParams>, // Lock-free playback parameters
    synthesizer: Arc<Mutex<Synthesizer>>, // Synthesizer for audio output
    sound_font: Arc<SoundFont>,           // Sound font for synthesizer
    current_tick: Arc<AtomicU32>,         // Latest tick reached by the audio callback
    beat_notify: Arc<Notify>,             // Wake UI when current_tick changes
    measure_playback_ticks: Vec<u32>,     // first playback tick per measure (for seeking)
}

impl AudioPlayer {
    pub fn new(
        song: Rc<Song>,
        song_tempo: u32,
        tempo_percentage: u32,
        sound_font_file: Option<PathBuf>,
        current_tick: Arc<AtomicU32>,
        beat_notify: Arc<Notify>,
        playback_order: &[(usize, i64)],
    ) -> Result<Self, AudioPlayerError> {
        // default to no solo track
        let solo_track_id = None;

        // player params
        let player_params = Arc::new(MidiPlayerParams::new(
            song_tempo,
            tempo_percentage,
            solo_track_id,
        ));

        // midi sequencer initialization
        let builder = MidiBuilder::new();
        let events = builder.build_for_song_with_order(&song, playback_order);

        // build first-playback-tick lookup per measure (for seeking)
        let measure_count = song.measure_headers.len();
        let mut measure_playback_ticks = vec![0_u32; measure_count];
        let mut seen = vec![false; measure_count];
        for &(measure_index, tick_offset) in playback_order {
            if !seen[measure_index] {
                seen[measure_index] = true;
                let header = &song.measure_headers[measure_index];
                measure_playback_ticks[measure_index] =
                    (i64::from(header.start) + tick_offset) as u32;
            }
        }

        // sound font setup
        let sound_font = if let Some(ref sound_font_file) = sound_font_file {
            let mut sf2 = File::open(sound_font_file).map_err(|e| {
                AudioPlayerError::SoundFontFileError(format!("{}: {e}", sound_font_file.display()))
            })?;
            SoundFont::new(&mut sf2).map_err(|e| {
                AudioPlayerError::SoundFontLoadError(format!("{}: {e}", sound_font_file.display()))
            })?
        } else {
            let mut sf2 = TIMIDITY_SOUND_FONT;
            SoundFont::new(&mut sf2)
                .map_err(|e| AudioPlayerError::SoundFontLoadError(format!("embedded: {e}")))?
        };
        let sound_font = Arc::new(sound_font);

        // build new default synthesizer for the stream
        let synthesizer = Self::make_synthesizer(sound_font.clone(), DEFAULT_SAMPLE_RATE)?;
        let midi_sequencer = MidiSequencer::new(events);

        let synthesizer = Arc::new(Mutex::new(synthesizer));
        let sequencer = Arc::new(Mutex::new(midi_sequencer));
        Ok(Self {
            is_playing: false,
            song,
            stream: None,
            sequencer,
            player_params,
            synthesizer,
            sound_font,
            current_tick,
            beat_notify,
            measure_playback_ticks,
        })
    }

    fn make_synthesizer(
        sound_font: Arc<SoundFont>,
        sample_rate: u32,
    ) -> Result<Synthesizer, AudioPlayerError> {
        let synthesizer_settings = SynthesizerSettings::new(sample_rate as i32);
        let synthesizer_settings = Arc::new(synthesizer_settings);
        debug_assert_eq!(synthesizer_settings.sample_rate, sample_rate as i32);
        Synthesizer::new(&sound_font, &synthesizer_settings)
            .map_err(|e| AudioPlayerError::SynthesizerError(e.to_string()))
    }

    pub const fn is_playing(&self) -> bool {
        self.is_playing
    }

    pub fn solo_track_id(&self) -> Option<usize> {
        self.player_params.solo_track_id()
    }

    pub fn toggle_solo_mode(&self, new_track_id: usize) {
        if self.player_params.solo_track_id() == Some(new_track_id) {
            log::info!("Disable solo mode on track {new_track_id}");
            self.player_params.set_solo_track_id(None);
        } else {
            log::info!("Enable solo mode on track {new_track_id}");
            self.player_params.set_solo_track_id(Some(new_track_id));
        }
    }

    pub fn set_tempo_percentage(&self, new_tempo_percentage: u32) {
        self.player_params
            .set_tempo_percentage(new_tempo_percentage);
    }

    pub fn master_volume(&self) -> f32 {
        self.player_params.master_volume()
    }

    pub fn set_master_volume(&self, volume: f32) {
        self.player_params.set_master_volume(volume);
    }

    pub fn stop(&mut self) {
        // Pause stream
        if let Some(stream) = &self.stream {
            log::info!("Stopping audio stream");
            stream.pause().unwrap();
        }
        self.is_playing = false;

        // reset ticks
        let mut sequencer_guard = self.sequencer.lock().unwrap();
        sequencer_guard.reset_last_time();
        sequencer_guard.reset_ticks();
        drop(sequencer_guard);

        // stop all sound in synthesizer
        let mut synthesizer_guard = self.synthesizer.lock().unwrap();
        synthesizer_guard.note_off_all(false);
        drop(synthesizer_guard);

        // reset the UI cursor to the first playable tick so the measure lookup resolves cleanly
        self.current_tick.store(FIRST_TICK, Ordering::Relaxed);
        self.beat_notify.notify_one();

        // Drop stream
        self.stream.take();
    }

    /// Toggle play/pause. Returns an error message if playback fails.
    pub fn toggle_play(&mut self) -> Option<String> {
        log::info!("Toggle audio stream");
        if let Some(ref stream) = self.stream {
            if self.is_playing {
                self.is_playing = false;
                if let Err(err) = stream.pause() {
                    return Some(format!("Failed to pause audio stream: {err}"));
                }
            } else {
                self.is_playing = true;
                // reset last time to not advance time too fast on resume
                self.sequencer.lock().unwrap().reset_last_time();
                if let Err(err) = stream.play() {
                    return Some(format!("Failed to resume audio stream: {err}"));
                }
            }
        } else {
            self.is_playing = true;

            // Initialize audio output stream
            let stream = new_output_stream(
                self.sequencer.clone(),
                self.player_params.clone(),
                self.synthesizer.clone(),
                self.sound_font.clone(),
                self.current_tick.clone(),
                self.beat_notify.clone(),
            );

            match stream {
                Ok(stream) => {
                    self.stream = Some(Rc::new(stream));
                }
                Err(err) => {
                    self.is_playing = false;
                    self.stream = None;
                    return Some(format!("Failed to create audio stream: {err}"));
                }
            }
        }
        None
    }

    pub fn focus_measure(&self, measure_id: usize) {
        log::debug!("Focus audio player on measure:{measure_id}");
        let measure = &self.song.measure_headers[measure_id];
        let measure_start_tick = self.measure_playback_ticks[measure_id];
        let tempo = measure.tempo.value;

        // move sequencer to measure start tick
        let mut sequencer_guard = self.sequencer.lock().unwrap();
        sequencer_guard.set_tick(measure_start_tick);
        drop(sequencer_guard);

        // stop current sound
        let mut synthesizer_guard = self.synthesizer.lock().unwrap();
        synthesizer_guard.note_off_all(false);
        drop(synthesizer_guard);

        // set tempo for focuses measure
        self.player_params.set_tempo(tempo);
    }
}

#[derive(Debug, thiserror::Error)]
pub enum AudioPlayerError {
    #[error("audio device not found")]
    CpalDeviceNotFound,
    #[error("no output configuration found: {0}")]
    CpalOutputConfigNotFound(DefaultStreamConfigError),
    #[error("failed to open sound font file: {0}")]
    SoundFontFileError(String),
    #[error("failed to load sound font: {0}")]
    SoundFontLoadError(String),
    #[error("failed to create synthesizer: {0}")]
    SynthesizerError(String),
    #[error("failed to create audio stream: {0}")]
    StreamError(String),
}

/// Create a new output stream for audio playback.
fn new_output_stream(
    sequencer: Arc<Mutex<MidiSequencer>>,
    player_params: Arc<MidiPlayerParams>,
    synthesizer: Arc<Mutex<Synthesizer>>,
    sound_font: Arc<SoundFont>,
    current_tick: Arc<AtomicU32>,
    beat_notify: Arc<Notify>,
) -> Result<cpal::Stream, AudioPlayerError> {
    let host = cpal::default_host();
    let Some(device) = host.default_output_device() else {
        return Err(AudioPlayerError::CpalDeviceNotFound);
    };

    let config = device
        .default_output_config()
        .map_err(AudioPlayerError::CpalOutputConfigNotFound)?;

    if !config.sample_format().is_float() {
        return Err(AudioPlayerError::StreamError(format!(
            "Unsupported sample format {}",
            config.sample_format()
        )));
    }
    let stream_config: cpal::StreamConfig = config.into();
    let sample_rate = stream_config.sample_rate;

    log::info!("Audio output stream config: {stream_config:?}");

    let mut synthesizer_guard = synthesizer.lock().unwrap();
    if sample_rate != DEFAULT_SAMPLE_RATE {
        // audio output is not using the default sample rate - recreate synthesizer with proper sample rate
        let new_synthesizer = AudioPlayer::make_synthesizer(sound_font, sample_rate)?;
        *synthesizer_guard = new_synthesizer;
    }

    // Apply events at tick=FIRST_TICK to set up synthesizer state
    // otherwise clicking on a measure *before* playing does not produce the correct instrument sound
    sequencer
        .lock()
        .unwrap()
        .events()
        .iter()
        .take_while(|event| event.tick == FIRST_TICK)
        .filter(|event| event.is_midi_message())
        .for_each(|event| {
            if let MidiEventType::MidiMessage(channel, command, data1, data2) = event.event {
                synthesizer_guard.process_midi_message(channel, command, data1, data2);
            }
        });

    drop(synthesizer_guard);

    // Size left and right buffers according to sample rate.
    // The buffer accounts for 0.1 second of audio.
    // e.g. 4410 samples at 44100 Hz is 0.1 second
    let channel_sample_count = sample_rate / 10;

    // reuse buffer for left and right channels across all calls
    let mut left: Vec<f32> = vec![0_f32; channel_sample_count as usize];
    let mut right: Vec<f32> = vec![0_f32; channel_sample_count as usize];

    let err_fn = |err| log::error!("an error occurred on stream: {err}");

    let stream = device.build_output_stream(
        &stream_config,
        move |output: &mut [f32], _: &cpal::OutputCallbackInfo| {
            let mut sequencer_guard = sequencer.lock().unwrap();
            sequencer_guard.advance(player_params.adjusted_tempo());
            let mut synthesizer_guard = synthesizer.lock().unwrap();
            // process midi events for current tick
            if let Some(events) = sequencer_guard.get_next_events() {
                let tick = sequencer_guard.get_tick();
                let last_tick = sequencer_guard.get_last_tick();
                if !events.is_empty() {
                    log::debug!(
                        "---> Increase {} ticks [{} -> {}] ({} events)",
                        tick - last_tick,
                        last_tick,
                        tick,
                        events.len()
                    );
                }
                let solo_track_id = player_params.solo_track_id();
                if events
                    .iter()
                    .any(super::midi_event::MidiEvent::is_note_event)
                {
                    current_tick.store(tick, Ordering::Release);
                    beat_notify.notify_one();
                }
                for midi_event in events {
                    match midi_event.event {
                        MidiEventType::NoteOn(channel, key, velocity) => {
                            if let Some(track_id) = solo_track_id {
                                // skip note on events for other tracks in solo mode
                                if midi_event.track != Some(track_id as u8) {
                                    continue;
                                }
                            }
                            log::debug!(
                                "[{}] Note on: channel={}, key={}, velocity={}",
                                midi_event.tick,
                                channel,
                                key,
                                velocity
                            );
                            synthesizer_guard.note_on(channel, key, i32::from(velocity));
                        }
                        MidiEventType::NoteOff(channel, key) => {
                            log::debug!(
                                "[{}] Note off: channel={}, key={}",
                                midi_event.tick,
                                channel,
                                key
                            );
                            synthesizer_guard.note_off(channel, key);
                        }
                        MidiEventType::TempoChange(tempo) => {
                            log::info!("Tempo changed to {tempo}");
                            player_params.set_tempo(tempo);
                        }
                        MidiEventType::MidiMessage(channel, command, data1, data2) => {
                            log::debug!(
                                "[{}] Midi message: channel={}, command={}, data1={}, data2={}",
                                midi_event.tick,
                                channel,
                                command,
                                data1,
                                data2
                            );
                            synthesizer_guard.process_midi_message(channel, command, data1, data2);
                        }
                    }
                }
            }
            // Split buffer for this run between left and right
            let mut output_channel_len = output.len() / 2;

            if left.len() < output_channel_len || right.len() < output_channel_len {
                log::info!(
                    "Output buffer larger than expected channel size {} > {}",
                    output_channel_len,
                    left.len()
                );
                output_channel_len = left.len();
            }

            // Render the waveform.
            synthesizer_guard.render(
                &mut left[..output_channel_len],
                &mut right[..output_channel_len],
            );

            let master_volume = player_params.master_volume();

            // Drop locks
            drop(sequencer_guard);
            drop(synthesizer_guard);

            // Interleave the left and right channels into the output buffer.
            for i in 0..output_channel_len {
                output[i * 2] = left[i] * master_volume;
                output[i * 2 + 1] = right[i] * master_volume;
            }
        },
        err_fn,
        None, // blocking stream
    );
    let stream = stream.map_err(|e| AudioPlayerError::StreamError(e.to_string()))?;
    stream
        .play()
        .map_err(|e| AudioPlayerError::StreamError(e.to_string()))?;
    Ok(stream)
}


================================================
FILE: src/audio/midi_player_params.rs
================================================
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};

const SOLO_NONE: i32 = -1;

/// Playback parameters shared lock-free between UI and audio callback.
pub struct MidiPlayerParams {
    tempo: AtomicU32,
    tempo_percentage: AtomicU32,
    solo_track_id: AtomicI32, // -1 == None
    master_volume: AtomicU32, // f32 bits
}

impl MidiPlayerParams {
    pub fn new(tempo: u32, tempo_percentage: u32, solo_track_id: Option<usize>) -> Self {
        Self {
            tempo: AtomicU32::new(tempo),
            tempo_percentage: AtomicU32::new(tempo_percentage),
            solo_track_id: AtomicI32::new(solo_track_id.map_or(SOLO_NONE, |id| id as i32)),
            master_volume: AtomicU32::new(1.0_f32.to_bits()),
        }
    }

    pub fn master_volume(&self) -> f32 {
        f32::from_bits(self.master_volume.load(Ordering::Relaxed))
    }

    pub fn set_master_volume(&self, volume: f32) {
        self.master_volume
            .store(volume.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
    }

    pub fn solo_track_id(&self) -> Option<usize> {
        match self.solo_track_id.load(Ordering::Relaxed) {
            SOLO_NONE => None,
            id => Some(id as usize),
        }
    }

    pub fn set_solo_track_id(&self, solo_track_id: Option<usize>) {
        self.solo_track_id.store(
            solo_track_id.map_or(SOLO_NONE, |id| id as i32),
            Ordering::Relaxed,
        );
    }

    pub fn adjusted_tempo(&self) -> u32 {
        let tempo = self.tempo.load(Ordering::Relaxed);
        let pct = self.tempo_percentage.load(Ordering::Relaxed);
        (tempo as f32 * pct as f32 / 100.0) as u32
    }

    pub fn set_tempo(&self, tempo: u32) {
        self.tempo.store(tempo, Ordering::Relaxed);
    }

    pub fn set_tempo_percentage(&self, tempo_percentage: u32) {
        self.tempo_percentage
            .store(tempo_percentage, Ordering::Relaxed);
    }
}


================================================
FILE: src/audio/midi_sequencer.rs
================================================
use crate::audio::midi_event::MidiEvent;
use std::time::Instant;

const QUARTER_TIME: f32 = 960.0; // 1 quarter note = 960 ticks

pub struct MidiSequencer {
    current_tick: u32,             // current Midi tick
    last_tick: u32,                // last Midi tick
    last_time: Instant,            // last time in milliseconds
    sorted_events: Vec<MidiEvent>, // sorted Midi events
}

impl MidiSequencer {
    pub fn new(sorted_events: Vec<MidiEvent>) -> Self {
        // events are sorted by tick
        assert!(
            sorted_events
                .as_slice()
                .windows(2)
                .all(|w| w[0].tick <= w[1].tick)
        );
        Self {
            current_tick: 0,
            last_tick: 0,
            last_time: Instant::now(),
            sorted_events,
        }
    }

    #[allow(clippy::missing_const_for_fn)]
    pub fn events(&self) -> &[MidiEvent] {
        &self.sorted_events
    }

    #[allow(clippy::missing_const_for_fn)]
    pub fn set_tick(&mut self, tick: u32) {
        // set last_tick before the target so get_next_events includes events at target tick
        // set current_tick = last_tick so advance() triggers the init path (bumps by 1)
        let adjusted = tick.saturating_sub(1);
        self.last_tick = adjusted;
        self.current_tick = adjusted;
    }

    pub fn reset_last_time(&mut self) {
        self.last_time = Instant::now();
    }

    #[allow(clippy::missing_const_for_fn)]
    pub fn reset_ticks(&mut self) {
        self.current_tick = 0;
        self.last_tick = 0;
    }

    pub const fn get_tick(&self) -> u32 {
        self.current_tick
    }

    pub const fn get_last_tick(&self) -> u32 {
        self.last_tick
    }

    pub fn get_next_events(&self) -> Option<&[MidiEvent]> {
        // do not return events if tick did not change
        if self.last_tick == self.current_tick {
            return Some(&[]);
        }

        assert!(self.last_tick <= self.current_tick);

        // get all events between last tick and next tick using binary search
        // TODO could be improved by saving `end_index` to the next `start_index`
        let start_index = match self
            .sorted_events
            .binary_search_by_key(&self.last_tick, |event| event.tick)
        {
            Ok(position) => position + 1,
            Err(position) => {
                // exit if end reached
                if position == self.sorted_events.len() {
                    return None;
                }
                position
            }
        };

        let end_index = match self.sorted_events[start_index..]
            .binary_search_by_key(&self.current_tick, |event| event.tick)
        {
            Ok(next_position) => start_index + next_position,
            Err(next_position) => {
                if next_position == 0 {
                    // no matching elements
                    return Some(&[]);
                }
                // return slice until the last event
                start_index + next_position - 1
            }
        };
        Some(&self.sorted_events[start_index..=end_index])
    }

    pub fn advance(&mut self, tempo: u32) {
        // init sequencer if first advance after reset
        if self.current_tick == self.last_tick {
            self.current_tick += 1;
            self.last_time = Instant::now();
            return;
        }
        // check how many ticks have passed since last advance
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_time);
        let elapsed_secs = elapsed.as_secs_f32();
        let tick_increase = tick_increase(tempo, elapsed_secs);
        self.last_time = now;
        self.last_tick = self.current_tick;
        self.current_tick += tick_increase;
    }

    #[cfg(test)]
    #[allow(clippy::missing_const_for_fn)]
    pub fn advance_tick(&mut self, tick: u32) {
        self.last_tick = self.current_tick;
        self.current_tick += tick;
    }
}

fn tick_increase(tempo_bpm: u32, elapsed_seconds: f32) -> u32 {
    let tempo_bps = tempo_bpm as f32 / 60.0;
    let bump = QUARTER_TIME * tempo_bps * elapsed_seconds;
    bump as u32
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audio::midi_builder::MidiBuilder;
    use crate::audio::midi_event::MidiEventType;
    use crate::parser::song_parser_tests::parse_gp_file;
    use std::rc::Rc;
    use std::time::Duration;

    #[test]
    fn test_tick_increase() {
        let tempo = 100;
        let elapsed = Duration::from_millis(32);
        let result = tick_increase(tempo, elapsed.as_secs_f32());
        assert_eq!(result, 51);
    }

    #[test]
    fn test_tick_increase_bis() {
        let tempo = 120;
        let elapsed = Duration::from_millis(100);
        let result = tick_increase(tempo, elapsed.as_secs_f32());
        assert_eq!(result, 192);
    }
    #[test]
    fn test_sequence_demo_song() {
        const FILE_PATH: &str = "test-files/Demo v5.gp5";
        let song = parse_gp_file(FILE_PATH).unwrap();
        let song = Rc::new(song);
        let builder = MidiBuilder::new();
        let events = builder.build_for_song(&song);
        let events_len = 4693;
        assert_eq!(events.len(), events_len);
        assert_eq!(events[0].tick, 1);
        let mut sequencer = MidiSequencer::new(events.clone());

        // last_tick:0 current_tick:0
        let batch = sequencer.get_next_events().unwrap();
        assert_eq!(batch.len(), 0);

        // advance time by 1 tick
        sequencer.advance_tick(1);

        // last_tick:0 current_tick:1
        let batch = sequencer.get_next_events().unwrap();
        let count_1 = batch.len();
        assert_eq!(&events[0..count_1], batch);
        assert!(batch.iter().all(MidiEvent::is_midi_message));

        let mut pos = count_1;
        loop {
            let prev_tick = sequencer.get_tick();
            // advance time by 112 tick
            sequencer.advance_tick(112);
            let next_tick = sequencer.get_tick();
            assert_eq!(next_tick - prev_tick, 112);

            if let Some(batch) = sequencer.get_next_events() {
                let count = batch.len();
                assert_eq!(&events[pos..pos + count], batch);
                pos += count;
            } else {
                break;
            }
        }
        assert_eq!(pos, events.len());
    }

    #[test]
    fn set_tick_includes_events_at_target() {
        // events at ticks 100, 200, 300
        let events = vec![
            MidiEvent {
                tick: 100,
                event: MidiEventType::NoteOn(0, 60, 95),
                track: Some(0),
            },
            MidiEvent {
                tick: 200,
                event: MidiEventType::NoteOn(0, 62, 95),
                track: Some(0),
            },
            MidiEvent {
                tick: 300,
                event: MidiEventType::NoteOn(0, 64, 95),
                track: Some(0),
            },
        ];
        let mut sequencer = MidiSequencer::new(events);

        // seek to tick 200 — set_tick sets both ticks to 199
        sequencer.set_tick(200);
        // first advance_tick triggers init: last_tick stays 199, current_tick becomes 200
        sequencer.advance_tick(1);
        let batch = sequencer.get_next_events().unwrap();

        // should include the event at tick 200
        assert!(
            batch.iter().any(|e| e.tick == 200),
            "set_tick should include events at the target tick, got: {batch:?}"
        );
        // should NOT include event at tick 100 (before target)
        assert!(
            !batch.iter().any(|e| e.tick == 100),
            "set_tick should not include events before target tick"
        );
    }

    #[test]
    fn set_tick_on_song_with_repeats() {
        // verify seeking works correctly with repeat-expanded events
        const FILE_PATH: &str = "test-files/John Petrucci - Damage Control (ver 6 by Feio666).gp5";
        let song = parse_gp_file(FILE_PATH).unwrap();
        let playback_order =
            crate::audio::playback_order::compute_playback_order(&song.measure_headers);

        // build measure_playback_ticks (same logic as AudioPlayer::new)
        let measure_count = song.measure_headers.len();
        let mut measure_playback_ticks = vec![0_u32; measure_count];
        let mut seen = vec![false; measure_count];
        for &(measure_index, tick_offset) in &playback_order {
            if !seen[measure_index] {
                seen[measure_index] = true;
                let header = &song.measure_headers[measure_index];
                measure_playback_ticks[measure_index] =
                    (i64::from(header.start) + tick_offset) as u32;
            }
        }

        let song = Rc::new(song);
        let builder = MidiBuilder::new();
        let events = builder.build_for_song(&song);
        let mut sequencer = MidiSequencer::new(events.clone());

        // seek to measure 5 (index 4)
        let target_measure = 4;
        let target_tick = measure_playback_ticks[target_measure];
        assert!(
            target_tick > 0,
            "Measure 5 should have a non-zero playback tick"
        );

        sequencer.set_tick(target_tick);
        sequencer.advance_tick(1);
        let batch = sequencer.get_next_events().unwrap();

        // verify we get events at or near the target tick, not from earlier measures
        if !batch.is_empty() {
            let min_tick = batch.iter().map(|e| e.tick).min().unwrap();
            assert!(
                min_tick >= target_tick,
                "After seeking to tick {target_tick}, got events at tick {min_tick}"
            );
        }
    }
}


================================================
FILE: src/audio/mod.rs
================================================
pub mod midi_builder;
pub mod midi_event;
pub mod midi_player;
mod midi_player_params;
pub mod midi_sequencer;
pub mod playback_order;

/// First tick of a song
pub const FIRST_TICK: u32 = 1;


================================================
FILE: src/audio/playback_order.rs
================================================
use crate::parser::song_parser::{MeasureHeader, QUARTER_TIME};
use std::collections::HashMap;

/// Tracks the state of repeat section navigation during playback order computation.
struct RepeatState {
    start_stack: Vec<usize>,    // stack of repeat_open indices (for nesting)
    visits: HashMap<usize, i8>, // how many times each repeat_close has been hit
    current_repetition: i8,     // 0-based: 0 = first play, 1 = first repeat, etc.
    jumping_back: bool,         // true when looping back to a repeat_open
}

impl RepeatState {
    fn new() -> Self {
        Self {
            start_stack: vec![0], // implicit start at measure 0
            visits: HashMap::new(),
            current_repetition: 0,
            jumping_back: false,
        }
    }

    /// Process a repeat_open marker. Pushes onto the stack on first entry,
    /// skips the push when looping back (to preserve the repetition counter).
    fn enter_repeat(&mut self, measure_index: usize) {
        if !self.jumping_back {
            self.current_repetition = 0;
            self.start_stack.push(measure_index);
        }
        self.jumping_back = false;
    }

    /// Check if the current repetition matches an alternative ending bitmask.
    fn matches_alternative(&self, repeat_alternative: u8) -> bool {
        // clamp to 7 to avoid shift overflow on u8
        let clamped = self.current_repetition.min(7);
        let bit = 1_u8 << clamped;
        repeat_alternative & bit != 0
    }

    /// Process a repeat_close marker. Returns the index to jump back to,
    /// or None if all repetitions are done.
    fn close_repeat(&mut self, measure_index: usize, repeat_close: i8) -> Option<usize> {
        let visits = self.visits.entry(measure_index).or_insert(0);
        if *visits < repeat_close {
            *visits += 1;
            self.current_repetition += 1;
            self.jumping_back = true;
            let repeat_start = *self.start_stack.last().unwrap_or(&0);
            // clear visit counts for inner repeats so they replay on next outer pass
            self.visits
                .retain(|&k, _| k <= repeat_start || k >= measure_index);
            Some(repeat_start)
        } else {
            // done repeating
            self.visits.remove(&measure_index);
            self.start_stack.pop();
            None
        }
    }
}

/// Compute the playback order of measures, expanding repeats and alternative endings.
///
/// Used by the MIDI builder to generate events at the correct ticks,
/// and by the tablature to map playback ticks back to visual measures.
/// Returns a Vec of (measure_index, tick_offset) pairs.
/// The tick_offset is the difference between the playback tick and the original measure tick.
pub fn compute_playback_order(headers: &[MeasureHeader]) -> Vec<(usize, i64)> {
    let mut order: Vec<(usize, i64)> = Vec::new();
    let mut running_tick: u32 = QUARTER_TIME; // same starting tick as parser
    let mut repeat = RepeatState::new();
    let mut i = 0;

    while i < headers.len() {
        let header = &headers[i];

        if header.repeat_open {
            repeat.enter_repeat(i);
        }

        // check alternative ending: skip this measure if it doesn't match current repetition
        if header.repeat_alternative != 0 && !repeat.matches_alternative(header.repeat_alternative)
        {
            // still check for repeat_close on this skipped measure
            if header.repeat_close > 0
                && let Some(jump_to) = repeat.close_repeat(i, header.repeat_close)
            {
                i = jump_to;
                continue;
            }
            i += 1;
            continue;
        }

        // add this measure to the playback order
        let tick_offset = i64::from(running_tick) - i64::from(header.start);
        order.push((i, tick_offset));
        running_tick += header.length();

        // handle repeat close
        if header.repeat_close > 0
            && let Some(jump_to) = repeat.close_repeat(i, header.repeat_close)
        {
            i = jump_to;
            continue;
        }

        i += 1;
    }

    order
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_header(start: u32, repeat_open: bool, repeat_close: i8) -> MeasureHeader {
        MeasureHeader {
            start,
            repeat_open,
            repeat_close,
            ..MeasureHeader::default()
        }
    }

    #[test]
    fn no_repeats() {
        let headers = vec![
            make_header(960, false, 0),
            make_header(4800, false, 0),
            make_header(8640, false, 0),
        ];
        let order = compute_playback_order(&headers);
        assert_eq!(order.len(), 3);
        assert_eq!(order[0], (0, 0));
        assert_eq!(order[1], (1, 0));
        assert_eq!(order[2], (2, 0));
    }

    #[test]
    fn simple_repeat() {
        // |: M0 | M1 :|  M2
        // Plays: M0 M1 M0 M1 M2
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 0),
            make_header(960 + measure_len, false, 1),
            make_header(960 + measure_len * 2, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 1, 0, 1, 2]);

        // tick offsets: first pass is 0, second pass shifts by 2 measures
        assert_eq!(order[0].1, 0);
        assert_eq!(order[1].1, 0);
        assert_eq!(order[2].1, i64::from(measure_len) * 2);
        assert_eq!(order[3].1, i64::from(measure_len) * 2);
        assert_eq!(order[4].1, i64::from(measure_len) * 2);
    }

    #[test]
    fn repeat_three_times() {
        // |: M0 :| x3  M1
        // Plays: M0 M0 M0 M1
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 2),
            make_header(960 + measure_len, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 0, 0, 1]);
    }

    #[test]
    fn two_repeat_sections() {
        // |: M0 :|  |: M1 :|  M2
        // Plays: M0 M0 M1 M1 M2
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 1),
            make_header(960 + measure_len, true, 1),
            make_header(960 + measure_len * 2, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 0, 1, 1, 2]);
    }

    #[test]
    fn alternative_endings() {
        // |: M0 | M1[1.] | M2[2.] :|  M3
        // Plays: M0 M1 M0 M2 M3
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 0),
            MeasureHeader {
                start: 960 + measure_len,
                repeat_alternative: 1,
                ..MeasureHeader::default()
            },
            MeasureHeader {
                start: 960 + measure_len * 2,
                repeat_alternative: 2,
                repeat_close: 1,
                ..MeasureHeader::default()
            },
            make_header(960 + measure_len * 3, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 1, 0, 2, 3]);
    }

    #[test]
    fn three_alternatives() {
        // |: M0 | M1[1.] | M2[2.] | M3[3.] :|x3  M4
        // Plays: M0 M1 M0 M2 M0 M3 M4
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 0),
            MeasureHeader {
                start: 960 + measure_len,
                repeat_alternative: 1,
                ..MeasureHeader::default()
            },
            MeasureHeader {
                start: 960 + measure_len * 2,
                repeat_alternative: 2,
                ..MeasureHeader::default()
            },
            MeasureHeader {
                start: 960 + measure_len * 3,
                repeat_alternative: 4,
                repeat_close: 2,
                ..MeasureHeader::default()
            },
            make_header(960 + measure_len * 4, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 1, 0, 2, 0, 3, 4]);
    }

    #[test]
    fn nested_repeats() {
        // |: M0 |: M1 :| M2 :|  M3
        // Plays: M0 M1 M1 M2 M0 M1 M1 M2 M3
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 0),
            make_header(960 + measure_len, true, 1),
            make_header(960 + measure_len * 2, false, 1),
            make_header(960 + measure_len * 3, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 1, 1, 2, 0, 1, 1, 2, 3]);
    }

    #[test]
    fn repeat_close_without_open() {
        // M0 | M1 :|  M2
        // Plays: M0 M1 M0 M1 M2
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, false, 0),
            make_header(960 + measure_len, false, 1),
            make_header(960 + measure_len * 2, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 1, 0, 1, 2]);
    }

    #[test]
    fn single_measure_repeat() {
        // |: M0 :|
        // Plays: M0 M0
        let headers = vec![make_header(960, true, 1)];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 0]);
    }

    #[test]
    fn tick_offsets_are_consistent() {
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 0),
            make_header(960 + measure_len, false, 1),
            make_header(960 + measure_len * 2, false, 0),
        ];
        let order = compute_playback_order(&headers);
        let playback_ticks: Vec<i64> = order
            .iter()
            .map(|(idx, offset)| i64::from(headers[*idx].start) + offset)
            .collect();
        assert!(playback_ticks.windows(2).all(|w| w[0] < w[1]));
    }

    #[test]
    fn alternative_on_last_pass_with_close() {
        // |: M0 | M1[1.+2.] :|
        // Plays: M0 M1 M0 M1
        let measure_len = 3840_u32;
        let headers = vec![
            make_header(960, true, 0),
            MeasureHeader {
                start: 960 + measure_len,
                repeat_alternative: 3,
                repeat_close: 1,
                ..MeasureHeader::default()
            },
        ];
        let order = compute_playback_order(&headers);
        let indices: Vec<usize> = order.iter().map(|(i, _)| *i).collect();
        assert_eq!(indices, vec![0, 1, 0, 1]);
    }

    #[test]
    fn empty() {
        let headers: Vec<MeasureHeader> = vec![];
        let order = compute_playback_order(&headers);
        assert!(order.is_empty());
    }
}


================================================
FILE: src/config.rs
================================================
use std::{
    env::home_dir,
    fs::{File, create_dir_all},
    io::{BufReader, Write},
    path::PathBuf,
};

use serde::{Deserialize, Serialize};

use crate::RuxError;

#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    tabs_folder: Option<PathBuf>,
}

impl Config {
    // folder placed in $HOME directory
    const FOLDER: &'static str = ".config/ruxguitar";

    pub fn get_tabs_folder(&self) -> Option<PathBuf> {
        self.tabs_folder.clone()
    }

    pub fn set_tabs_folder(&mut self, new_tabs_folder: Option<PathBuf>) -> Result<(), RuxError> {
        if self.tabs_folder == new_tabs_folder {
            // no op
            Ok(())
        } else {
            self.tabs_folder = new_tabs_folder;
            self.save_config()
        }
    }

    fn get_base_path() -> Result<PathBuf, RuxError> {
        let home = home_dir()
            .ok_or_else(|| RuxError::ConfigError("Could not find home directory".to_string()))?;
        let path = home.join(Self::FOLDER);
        Ok(path)
    }

    fn get_path() -> Result<PathBuf, RuxError> {
        let base = Self::get_base_path()?;
        Ok(base.join("config.json"))
    }

    /// Creates config if it does not exist
    pub fn read_config() -> Result<Self, RuxError> {
        let base_path = Self::get_base_path()?;
        if !base_path.exists() {
            create_dir_all(base_path)?;
        }
        let config_path = Self::get_path()?;
        if !config_path.exists() {
            // create empty config
            Config::default().save_config()?;
        }
        let file = File::open(&config_path)?;
        let reader = BufReader::new(file);
        match serde_json::from_reader(reader) {
            Ok(config) => Ok(config),
            Err(err) => {
                log::warn!(
                    "Could not read local configuration {}: {err}, resetting to default",
                    config_path.display()
                );
                let default_config = Config::default();
                default_config.save_config()?;
                Ok(default_config)
            }
        }
    }

    /// Assumes the config folder exists
    pub fn save_config(&self) -> Result<(), RuxError> {
        let config_path = Self::get_path()?;
        let json = serde_json::to_string_pretty(self).map_err(|err| {
            RuxError::ConfigError(format!("Could not save local configuration {err:}"))
        })?;
        let mut file = File::create(config_path)?;
        file.write_all(json.as_bytes())?;
        Ok(())
    }
}


================================================
FILE: src/main.rs
================================================
use crate::RuxError::ConfigError;
use crate::ui::application::RuxApplication;
use clap::Parser;
use config::Config;
use std::io;
use std::path::PathBuf;

mod audio;
mod config;
mod parser;
mod ui;

fn main() {
    let result = main_result();
    std::process::exit(match result {
        Ok(()) => 0,
        Err(err) => {
            // use Display instead of Debug for user friendly error messages
            log::error!("{err}");
            1
        }
    });
}

pub fn main_result() -> Result<(), RuxError> {
    // setup logging
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("ruxguitar=info"))
        .init();

    // args
    let mut args = CliArgs::parse();
    let sound_font_file = args.sound_font_file.take();
    let tab_file_path = args.tab_file_path.take();

    // check if sound font file exists
    if let Some(sound_font_file) = &sound_font_file {
        if !sound_font_file.exists() {
            let err = ConfigError(format!("Sound font file not found {sound_font_file:?}"));
            return Err(err);
        }
        log::info!("Starting with custom sound font file {sound_font_file:?}");
    }

    // check if tab file exists
    if let Some(tab_file_path) = &tab_file_path {
        if !tab_file_path.exists() {
            let err = ConfigError(format!("Tab file not found {tab_file_path:?}"));
            return Err(err);
        }
        log::info!("Starting with tab file {tab_file_path:?}");
    }

    // read local config
    let local_config = Config::read_config()?;

    // bundle application args
    let args = ApplicationArgs {
        sound_font_bank: sound_font_file,
        tab_file_path,
        no_antialiasing: args.no_antialiasing,
        local_config,
    };

    // go!
    RuxApplication::start(args)?;
    Ok(())
}

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct CliArgs {
    /// Optional path to a sound font file.
    #[arg(long)]
    sound_font_file: Option<PathBuf>,
    /// Optional path to tab file to by-pass the file picker.
    #[arg(long)]
    tab_file_path: Option<PathBuf>,
    /// Disable antialiasing.
    #[arg(long, default_value_t = false)]
    no_antialiasing: bool,
}

#[derive(Debug, Clone)]
pub struct ApplicationArgs {
    sound_font_bank: Option<PathBuf>,
    tab_file_path: Option<PathBuf>,
    no_antialiasing: bool,
    local_config: Config,
}

#[derive(Debug, thiserror::Error)]
pub enum RuxError {
    #[error("iced error: {0}")]
    IcedError(iced::Error),
    #[error("configuration error: {0}")]
    ConfigError(String),
    #[error("parsing error: {0}")]
    ParsingError(String),
    #[error("other error: {0}")]
    OtherError(String),
}

impl From<iced::Error> for RuxError {
    fn from(error: iced::Error) -> Self {
        Self::IcedError(error)
    }
}

impl From<io::Error> for RuxError {
    fn from(error: io::Error) -> Self {
        Self::OtherError(error.to_string())
    }
}


================================================
FILE: src/parser/mod.rs
================================================
mod music_parser;
mod primitive_parser;
pub mod song_parser;
pub mod song_parser_tests;


================================================
FILE: src/parser/music_parser.rs
================================================
use crate::parser::primitive_parser::{
    parse_byte_size_string, parse_i8, parse_int, parse_int_byte_sized_string, parse_u8, skip,
};
use crate::parser::song_parser::{
    Beat, GpVersion, MAX_VOICES, Measure, Note, NoteEffect, NoteType, QUARTER_TIME, Song, Track,
    Voice, convert_velocity, parse_beat_effects, parse_chord, parse_color, parse_duration,
    parse_measure_headers, parse_note_effects,
};
use nom::multi::count;
use nom::{IResult, Parser};

pub struct MusicParser {
    song: Song,
}

impl MusicParser {
    pub const fn new(song: Song) -> Self {
        Self { song }
    }
    pub fn take_song(&mut self) -> Song {
        std::mem::take(&mut self.song)
    }

    pub fn parse_music_data<'a>(&'a mut self, i: &'a [u8]) -> IResult<&'a [u8], ()> {
        let mut i = i;
        let song_version = self.song.version;

        if song_version >= GpVersion::GP5 {
            // skip directions & master reverb
            i = skip(i, 42);
        }

        let (i, (measure_count, track_count)) = (
            parse_int, // Measure count
            parse_int, // Track count
        )
            .parse(i)?;

        log::debug!(
            "Parsing music data -> track_count: {track_count} measure_count {measure_count}"
        );

        let song_tempo = self.song.tempo.value;
        let (i, measure_headers) =
            parse_measure_headers(measure_count, song_tempo, song_version)(i)?;
        self.song.measure_headers = measure_headers;

        let (i, tracks) = self.parse_tracks(track_count as usize)(i)?;
        self.song.tracks = tracks;

        let (i, _measures) = self.parse_measures(measure_count, track_count)(i)?;

        Ok((i, ()))
    }

    fn parse_tracks(
        &mut self,
        tracks_count: usize,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], Vec<Track>> + '_ {
        move |i| {
            log::debug!("Parsing {tracks_count} tracks");
            let mut i = i;
            let mut tracks = Vec::with_capacity(tracks_count);
            for index in 1..=tracks_count {
                let (inner, track) = self.parse_track(index)(i)?;
                i = inner;
                tracks.push(track);
            }
            // tracks done
            if self.song.version == GpVersion::GP5 {
                i = skip(i, 2);
            }

            if self.song.version > GpVersion::GP5 {
                i = skip(i, 1);
            }

            Ok((i, tracks))
        }
    }

    fn parse_track(&mut self, number: usize) -> impl FnMut(&[u8]) -> IResult<&[u8], Track> + '_ {
        move |i| {
            log::debug!("--------");
            log::debug!("Parsing track {number}");
            let mut i = skip(i, 1);
            let mut track = Track::default();

            if self.song.version >= GpVersion::GP5
                && (number == 1 || self.song.version == GpVersion::GP5)
            {
                i = skip(i, 1);
            };

            track.number = number as i32;

            // track name
            let (inner, name) = parse_byte_size_string(40)(i)?;
            i = inner;
            log::debug!("Track name:{name}");
            track.name = name;

            // string count
            let (inner, string_count) = parse_int(i)?;
            i = inner;
            log::debug!("String count: {string_count}");
            assert!(string_count > 0);

            // tunings
            let (inner, tunings) = count(parse_int, 7).parse(i)?;
            i = inner;
            log::debug!("Tunings: {tunings:?}");
            track.strings = tunings
                .iter()
                .enumerate()
                .filter(|(i, _)| (*i as i32) < string_count)
                .map(|(i, &t)| (i as i32 + 1, t))
                .collect();

            // midi port
            let (inner, port) = parse_int(i)?;
            log::debug!("Midi port: {port:?}");
            i = inner;
            track.midi_port = port as u8;

            // parse track channel info
            let (inner, channel_id) = self.parse_track_channel()(i)?;
            log::debug!("Midi channel id: {channel_id:?}");
            track.channel_id = channel_id as u8;
            i = inner;

            // fret
            let (inner, fret_count) = parse_int(i)?;
            log::debug!("Fret count: {fret_count:?}");
            i = inner;
            track.fret_count = fret_count as u8;

            // offset
            let (inner, offset) = parse_int(i)?;
            log::debug!("Offset: {offset:?}");
            i = inner;
            track.offset = offset;

            // color
            let (inner, color) = parse_color(i)?;
            log::debug!("Color: {color:?}");
            i = inner;
            track.color = color;

            if self.song.version == GpVersion::GP5 {
                // skip 44
                i = skip(i, 44);
            } else if self.song.version == GpVersion::GP5_10 {
                // skip 49
                i = skip(i, 49);
            };

            if self.song.version > GpVersion::GP5 {
                let (inner, _) = parse_int_byte_sized_string(i)?;
                i = inner;
                let (inner, _) = parse_int_byte_sized_string(i)?;
                i = inner;
            };
            Ok((i, track))
        }
    }

    /// 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 parse_track_channel(&mut self) -> impl FnMut(&[u8]) -> IResult<&[u8], i32> + '_ {
        log::debug!("Parsing track channel");
        |i| {
            let (i, (mut gm_channel_1, mut gm_channel_2)) = (parse_int, parse_int).parse(i)?;
            gm_channel_1 -= 1;
            gm_channel_2 -= 1;

            log::debug!("Track channel gm1: {gm_channel_1} gm2: {gm_channel_2}");

            if let Some(channel) = self.song.midi_channels.get_mut(gm_channel_1 as usize) {
                // if not percussion - set effect channel
                if channel.channel_id != 9 {
                    channel.effect_channel_id = gm_channel_2 as u8;
                }
            } else {
                log::debug!("channel {gm_channel_1} not found");
                debug_assert!(false, "channel {gm_channel_1} not found");
            }
            Ok((i, gm_channel_1))
        }
    }

    /// 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 parse_measures(
        &mut self,
        measure_count: i32,
        track_count: i32,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + '_ {
        move |i: &[u8]| {
            log::debug!("--------");
            log::debug!("Parsing measures");
            let mut start = QUARTER_TIME;
            let mut i = i;
            for measure_index in 0..measure_count as usize {
                // set header start
                self.song.measure_headers[measure_index].start = start;
                for track_index in 0..track_count as usize {
                    let (inner, measure) =
                        self.parse_measure(start, measure_index, track_index)(i)?;
                    i = inner;
                    // push measure on track
                    self.song.tracks[track_index].measures.push(measure);
                    if self.song.version >= GpVersion::GP5 {
                        i = skip(i, 1);
                    }
                }
                // update start with measure length
                let measure_length = self.song.measure_headers[measure_index].length();
                assert!(measure_length > 0, "Measure length is 0");
                start += measure_length;
            }
            Ok((i, ()))
        }
    }

    fn parse_measure(
        &mut self,
        measure_start: u32,
        measure_index: usize,
        track_index: usize,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], Measure> + '_ {
        move |i: &[u8]| {
            log::debug!("--------");
            log::debug!("Parsing measure {measure_index} for track {track_index}");
            let mut i = i;
            let mut measure = Measure {
                header_index: measure_index,
                track_index,
                ..Default::default()
            };
            let voice_count = if self.song.version >= GpVersion::GP5 {
                MAX_VOICES
            } else {
                1
            };
            for voice_index in 0..voice_count {
                // voices have the same start value
                let beat_start = measure_start;
                log::debug!("--------");
                log::debug!("Parsing voice {voice_index}");
                let (inner, voice) = self.parse_voice(beat_start, track_index, measure_index)(i)?;
                i = inner;
                measure.voices.push(voice);
            }
            Ok((i, measure))
        }
    }

    fn parse_voice(
        &mut self,
        mut beat_start: u32,
        track_index: usize,
        measure_index: usize,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], Voice> + '_ {
        move |i: &[u8]| {
            let mut i = i;
            let (inner, beats) = parse_int(i)?;
            i = inner;
            let mut voice = Voice {
                measure_index: measure_index as i16,
                ..Default::default()
            };
            log::debug!("--------");
            log::debug!("...with {beats} beats");
            for b in 1..=beats {
                log::debug!("--------");
                log::debug!("Parsing beat {b}");
                let (inner, beat) = self.parse_beat(beat_start, track_index, measure_index)(i)?;
                if !beat.empty {
                    beat_start += beat.duration.time();
                }
                i = inner;
                voice.beats.push(beat);
            }
            Ok((i, voice))
        }
    }

    fn parse_beat(
        &mut self,
        start: u32,
        track_index: usize,
        measure_index: usize,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], Beat> + '_ {
        move |i: &[u8]| {
            let mut i = i;
            let (inner, flags) = parse_u8(i)?;
            i = inner;

            // make new beat at starting time
            let mut beat = Beat {
                start,
                ..Default::default()
            };

            // beat type
            if (flags & 0x40) != 0 {
                let (inner, beat_type) = parse_u8(i)?;
                i = inner;
                beat.empty = beat_type & 0x02 == 0;
            }

            // beat duration is an eighth note
            let (inner, duration) = parse_duration(flags)(i)?;
            beat.duration = duration;
            i = inner;

            // beat chords
            if (flags & 0x02) != 0 {
                let track = &self.song.tracks[track_index];
                let (inner, chord) = parse_chord(track.strings.len() as u8)(i)?;
                i = inner;
                beat.effect.chord = Some(chord);
            }

            // beat text
            if (flags & 0x04) != 0 {
                let (inner, text) = parse_int_byte_sized_string(i)?;
                i = inner;
                log::debug!("Beat text: {text}");
                beat.text = text;
            }

            let mut note_effect = NoteEffect::default();
            // beat effect
            if (flags & 0x08) != 0 {
                let (inner, ()) = parse_beat_effects(&mut beat, &mut note_effect)(i)?;
                i = inner;
            }

            // parse mix change
            if (flags & 0x10) != 0 {
                let (inner, ()) = self.parse_mix_change(measure_index)(i)?;
                i = inner;
            }

            // parse notes
            let (inner, string_flags) = parse_u8(i)?;
            i = inner;
            let track = &self.song.tracks[track_index];
            log::debug!(
                "Parsing notes for beat strings:{}, flags:{string_flags:08b}",
                track.strings.len()
            );
            assert!(!track.strings.is_empty());
            for (string_id, string_value) in track.strings.iter().enumerate() {
                if string_flags & (1 << (7 - string_value.0)) > 0 {
                    log::debug!("Parsing note for string {}", string_id + 1);
                    let mut note = Note::new(note_effect.clone());
                    let (inner, ()) = self.parse_note(&mut note, string_value, track_index)(i)?;
                    i = inner;
                    beat.notes.push(note);
                }
            }

            if self.song.version >= GpVersion::GP5 {
                i = skip(i, 1);
                let (inner, read) = parse_u8(i)?;
                i = inner;
                if (read & 0x08) != 0 {
                    i = skip(i, 1);
                }
            }
            Ok((i, beat))
        }
    }

    /// Get note value of tied note
    fn get_tied_note_value(&self, string_index: i8, track_index: usize) -> i16 {
        let track = &self.song.tracks[track_index];
        for m in (0usize..track.measures.len()).rev() {
            for v in (0usize..track.measures[m].voices.len()).rev() {
                for b in 0..track.measures[m].voices[v].beats.len() {
                    for n in 0..track.measures[m].voices[v].beats[b].notes.len() {
                        if track.measures[m].voices[v].beats[b].notes[n].string == string_index {
                            return track.measures[m].voices[v].beats[b].notes[n].value;
                        }
                    }
                }
            }
        }
        -1
    }

    fn parse_mix_change(
        &mut self,
        measure_index: usize,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + '_ {
        move |i: &[u8]| {
            log::debug!("Parsing mix change");
            let mut i = i;

            // instrument
            let (inner, _) = parse_i8(i)?;
            i = inner;

            if self.song.version >= GpVersion::GP5 {
                i = skip(i, 16);
            }

            let (inner, (volume, pan, chorus, reverb, phaser, tremolo)) =
                (parse_i8, parse_i8, parse_i8, parse_i8, parse_i8, parse_i8).parse(i)?;
            i = inner;

            let tempo_name = if self.song.version >= GpVersion::GP5 {
                let (inner, tempo_name_tmp) = parse_int_byte_sized_string(i)?;
                log::debug!("Tempo name: {tempo_name_tmp}");
                i = inner;
                tempo_name_tmp
            } else {
                String::new()
            };

            let (inner, tempo_value) = parse_int(i)?;
            i = inner;

            if volume >= 0 {
                i = skip(i, 1);
            }
            if pan >= 0 {
                i = skip(i, 1);
            }
            if chorus >= 0 {
                i = skip(i, 1);
            }
            if reverb >= 0 {
                i = skip(i, 1);
            }
            if phaser >= 0 {
                i = skip(i, 1);
            }
            if tremolo >= 0 {
                i = skip(i, 1);
            }

            if tempo_value >= 0 {
                // update tempo value for all next measure headers
                self.song.measure_headers[measure_index..]
                    .iter_mut()
                    .for_each(|mh| {
                        mh.tempo.value = tempo_value as u32;
                        mh.tempo.name = Some(tempo_name.clone());
                    });
                i = skip(i, 1);
                if self.song.version > GpVersion::GP5 {
                    i = skip(i, 1);
                }
            }

            i = skip(i, 1);

            if self.song.version >= GpVersion::GP5 {
                i = skip(i, 1);
                if self.song.version > GpVersion::GP5 {
                    let (inner, _) =
                        (parse_int_byte_sized_string, parse_int_byte_sized_string).parse(i)?;
                    i = inner;
                }
            }

            Ok((i, ()))
        }
    }

    fn parse_note<'a>(
        &'a self,
        note: &'a mut Note,
        guitar_string: &'a (i32, i32),
        track_index: usize,
    ) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + 'a {
        move |i| {
            log::debug!("Parsing note {guitar_string:?}");
            let mut i = i;
            let (inner, flags) = parse_u8(i)?;
            i = inner;
            let string = guitar_string.0 as i8;
            note.string = string;
            note.effect.heavy_accentuated_note = (flags & 0x02) == 0x02;
            note.effect.ghost_note = (flags & 0x04) == 0x04;
            note.effect.accentuated_note = (flags & 0x40) == 0x40;

            // note type
            if (flags & 0x20) != 0 {
                let (inner, note_type) = parse_u8(i)?;
                i = inner;
                note.kind = NoteType::get_note_type(note_type);
            }

            // duration percent GP4
            if (flags & 0x01) != 0 && self.song.version <= GpVersion::GP4_06 {
                i = skip(i, 2);
            }

            // note velocity
            if (flags & 0x10) != 0 {
                let (inner, velocity) = parse_i8(i)?;
                i = inner;
                note.velocity = convert_velocity(i16::from(velocity));
            }

            // note value
            if (flags & 0x20) != 0 {
                let (inner, fret) = parse_i8(i)?;
                i = inner;

                let value = if note.kind == NoteType::Tie {
                    self.get_tied_note_value(string, track_index)
                } else {
                    i16::from(fret)
                };
                // value is between 0 and 99
                if (0..100).contains(&value) {
                    note.value = value;
                } else {
                    note.value = 0;
                }
            }

            // fingering
            if (flags & 0x80) != 0 {
                i = skip(i, 2);
            }

            if self.song.version >= GpVersion::GP5 {
                // duration percent GP5
                if (flags & 0x01) != 0 {
                    i = skip(i, 8);
                }

                // swap accidentals
                let (inner, swap) = parse_u8(i)?;
                i = inner;
                note.swap_accidentals = swap & 0x02 == 0x02;
            }

            if (flags & 0x08) != 0 {
                let (inner, ()) = parse_note_effects(note, self.song.version)(i)?;
                i = inner;
            }

            Ok((i, ()))
        }
    }
}


================================================
FILE: src/parser/primitive_parser.rs
================================================
use encoding_rs::WINDOWS_1252;
use nom::combinator::{flat_map, map};
use nom::{IResult, Parser, bytes, number};

/// Parse signed byte
pub fn parse_i8(i: &[u8]) -> IResult<&[u8], i8> {
    number::complete::le_i8(i)
}

/// Parse unsigned byte
pub fn parse_u8(i: &[u8]) -> IResult<&[u8], u8> {
    number::complete::le_u8(i)
}

/// Parse signed 32
pub fn parse_int(i: &[u8]) -> IResult<&[u8], i32> {
    number::complete::le_i32(i)
}

/// Parse bool
pub fn parse_bool(i: &[u8]) -> IResult<&[u8], bool> {
    map(number::complete::le_u8, |b| b == 1).parse(i)
}

/// Parse signed short
pub fn parse_short(i: &[u8]) -> IResult<&[u8], i16> {
    number::complete::le_i16(i)
}

/// Skip `n` bytes.
pub fn skip(i: &[u8], n: usize) -> &[u8] {
    if i.is_empty() {
        return i;
    }
    log::debug!("skip: {n}");
    &i[n..]
}

/// Materialize properly encoded String
fn make_string(i: &[u8]) -> String {
    let (cow, encoding_used, had_errors) = WINDOWS_1252.decode(i);
    if had_errors {
        log::debug!("Error parsing string with {encoding_used:?}");
        match std::str::from_utf8(i) {
            Ok(s) => s.to_string(),
            Err(e) => {
                log::debug!("Error UTF-8 string parsing:{e}");
                String::new()
            }
        }
    } else {
        cow.to_string()
    }
}

/// Parse string of length `len`.
fn parse_string(len: i32) -> impl FnMut(&[u8]) -> IResult<&[u8], String> {
    parse_string_field(len as usize, len as usize)
}

/// Parse string field of length `string_len` with total size to consume `field_size`
fn parse_string_field(
    field_size: usize,
    string_len: usize,
) -> impl FnMut(&[u8]) -> IResult<&[u8], String> {
    move |i: &[u8]| {
        log::debug!("Parsing string field: field_size={field_size}, string_len={string_len}");

        // Read exactly the field size
        let (rest, field) = bytes::complete::take(field_size)(i)?;

        log::debug!("Raw field raw={field:02X?}");

        // Decode only the meaningful string bytes
        let string = make_string(&field[..std::cmp::min(string_len, field_size)]);

        Ok((rest, string))
    }
}

/// Size of string encoded as Int.
/// [i32 string_len][size bytes field]
pub fn parse_int_sized_string(i: &[u8]) -> IResult<&[u8], String> {
    flat_map(parse_int, parse_string).parse(i)
}

/// Size of Strings provided
/// `size`:   real string length
/// `length`: optional provided length (in case of blank chars after the string)
pub fn parse_byte_size_string(size: usize) -> impl FnMut(&[u8]) -> IResult<&[u8], String> {
    move |i: &[u8]| {
        let (i, length) = parse_u8(i)?;
        log::debug!("Parsing byte sized string of length {length} for String size {size}");
        parse_string_field(size, length as usize)(i)
    }
}

/// Size of string encoded as Int, but the size is encoded as a byte.
pub fn parse_int_byte_sized_string(i: &[u8]) -> IResult<&[u8], String> {
    flat_map(parse_int, |len| {
        flat_map(parse_u8, move |str_len| {
            log::debug!("Parsing int byte sized string int_len={len} u8_len={str_len}");
            parse_string_field(len as usize - 1, str_len as usize)
        })
    })
    .parse(i)
}

#[cfg(test)]
mod tests {
    use crate::parser::primitive_parser::parse_byte_size_string;

    #[test]
    fn test_read_byte_size_string() {
        let data: Vec<u8> = vec![
            0x18, 0x46, 0x49, 0x43, 0x48, 0x49, 0x45, 0x52, 0x20, 0x47, 0x55, 0x49, 0x54, 0x41,
            0x52, 0x20, 0x50, 0x52, 0x4f, 0x20, 0x76, 0x33, 0x2e, 0x30, 0x30, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00,
        ];
        let (_rest, res) = parse_byte_size_string(30)(&data).unwrap();
        assert_eq!(res, "FICHIER GUITAR PRO v3.00");
    }
}


================================================
FILE: src/parser/song_parser.rs
================================================
use crate::RuxError;
use crate::parser::music_parser::MusicParser;
use crate::parser::primitive_parser::{
    parse_bool, parse_byte_size_string, parse_i8, parse_int, parse_int_byte_sized_string,
    parse_int_sized_string, parse_short, parse_u8, skip,
};
use nom::IResult;
use nom::Parser;
use nom::bytes::complete::take;
use nom::combinator::{cond, flat_map, map};
use nom::multi::count;
use nom::sequence::preceded;
use std::fmt::Debug;

// GP4 docs at <https://dguitar.sourceforge.net/GP4format.html>
// GP5 docs thanks to Tuxguitar and <https://github.com/slundi/guitarpro> for the help

pub const MAX_VOICES: u32 = 2;

pub const QUARTER_TIME: u32 = 960;
pub const QUARTER: u16 = 4;

pub const DURATION_EIGHTH: u8 = 8;
pub const DURATION_SIXTEENTH: u8 = 16;
pub const DURATION_THIRTY_SECOND: u8 = 32;
pub const DURATION_SIXTY_FOURTH: u8 = 64;

pub const BEND_EFFECT_MAX_POSITION_LENGTH: f32 = 12.0;

pub const SEMITONE_LENGTH: f32 = 1.0;
pub const GP_BEND_SEMITONE: f32 = 25.0;
pub const GP_BEND_POSITION: f32 = 60.0;

pub const SHARP_NOTES: [&str; 12] = [
    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];

pub const DEFAULT_PERCUSSION_BANK: u8 = 128;

pub const DEFAULT_BANK: u8 = 0;

pub const MIN_VELOCITY: i16 = 15;
pub const VELOCITY_INCREMENT: i16 = 16;
pub const DEFAULT_VELOCITY: i16 = MIN_VELOCITY + VELOCITY_INCREMENT * 5; // FORTE

/// Convert Guitar Pro dynamic value to raw MIDI velocity
pub const fn convert_velocity(v: i16) -> i16 {
    MIN_VELOCITY + (VELOCITY_INCREMENT * v) - VELOCITY_INCREMENT
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Default)]
pub enum GpVersion {
    #[default]
    GP3,
    GP4,
    GP4_06,
    GP5,
    GP5_10,
}

#[derive(Debug, PartialEq, Default)]
pub struct Song {
    pub version: GpVersion,
    pub song_info: SongInfo,
    pub triplet_feel: Option<bool>, // only < GP5
    pub lyrics: Option<Lyrics>,
    pub page_setup: Option<PageSetup>,
    pub tempo: Tempo,
    pub hide_tempo: Option<bool>,
    pub key_signature: i8,
    pub octave: Option<i8>,
    pub midi_channels: Vec<MidiChannel>,
    pub measure_headers: Vec<MeasureHeader>,
    pub tracks: Vec<Track>,
}

#[derive(Debug, PartialEq, Eq)]
pub struct MidiChannel {
    pub channel_id: u8,
    pub effect_channel_id: 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 MidiChannel {
    pub const fn is_percussion(&self) -> bool {
        self.bank == DEFAULT_PERCUSSION_BANK
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Padding {
    pub right: i32,
    pub top: i32,
    pub left: i32,
    pub bottom: i32,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Point {
    pub x: i32,
    pub y: i32,
}

#[derive(Debug, PartialEq)]
pub struct PageSetup {
    pub page_size: Point,
    pub page_margin: Padding,
    pub score_size_proportion: f32,
    pub header_and_footer: i16,
    pub title: String,
    pub subtitle: String,
    pub artist: String,
    pub album: String,
    pub words: String,
    pub music: String,
    pub word_and_music: String,
    pub copyright: String,
    pub page_number: String,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Lyrics {
    pub track_choice: i32,
    pub lines: Vec<(i32, String)>,
}

#[derive(Debug, PartialEq, Eq, Default)]
pub struct SongInfo {
    pub name: String,
    pub subtitle: String,
    pub artist: String,
    pub album: String,
    pub author: String,
    pub words: Option<String>,
    pub copyright: String,
    pub writer: String,
    pub instructions: String,
    pub notices: Vec<String>,
}

#[derive(Debug, PartialEq, Eq)]
pub struct Marker {
    pub title: String,
    pub color: i32,
}

pub const KEY_SIGNATURES: [&str; 34] = [
    "F♭ major",
    "C♭ major",
    "G♭ major",
    "D♭ major",
    "A♭ major",
    "E♭ major",
    "B♭ major",
    "F major",
    "C major",
    "G major",
    "D major",
    "A major",
    "E major",
    "B major",
    "F# major",
    "C# major",
    "G# major",
    "D♭ minor",
    "A♭ minor",
    "E♭ minor",
    "B♭ minor",
    "F minor",
    "C minor",
    "G minor",
    "D minor",
    "A minor",
    "E minor",
    "B minor",
    "F# minor",
    "C# minor",
    "G# minor",
    "D# minor",
    "A# minor",
    "E# minor",
];

#[derive(Debug, PartialEq, Eq)]
pub struct KeySignature {
    pub key: i8,
    pub is_minor: bool,
}

impl KeySignature {
    pub const fn new(key: i8, is_minor: bool) -> Self {
        KeySignature { key, is_minor }
    }
}

impl std::fmt::Display for KeySignature {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let index: usize = if self.is_minor {
            (23i8 + self.key) as usize
        } else {
            (8i8 + self.key) as usize
        };
        write!(f, "{}", KEY_SIGNATURES[index])
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TripletFeel {
    None,
    Eighth,
    Sixteenth,
}

#[derive(Debug, PartialEq, Eq)]
pub struct Tempo {
    pub value: u32,
    pub name: Option<String>,
}

impl Tempo {
    const fn new(value: u32, name: Option<String>) -> Self {
        Tempo { value, name }
    }
}

impl Default for Tempo {
    fn default() -> Self {
        Tempo {
            value: 120,
            name: None,
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct MeasureHeader {
    pub start: u32,
    pub time_signature: TimeSignature,
    pub tempo: Tempo,
    pub marker: Option<Marker>,
    pub repeat_open: bool,
    pub repeat_alternative: u8,
    pub repeat_close: i8,
    pub triplet_feel: TripletFeel,
    pub key_signature: KeySignature,
}

impl Default for MeasureHeader {
    fn default() -> Self {
        MeasureHeader {
            start: QUARTER_TIME,
            time_signature: TimeSignature::default(),
            tempo: Tempo::default(),
            marker: None,
            repeat_open: false,
            repeat_alternative: 0,
            repeat_close: 0,
            triplet_feel: TripletFeel::None,
            key_signature: KeySignature::new(0, false),
        }
    }
}

impl MeasureHeader {
    pub fn length(&self) -> u32 {
        let numerator = u32::from(self.time_signature.numerator);
        let denominator = self.time_signature.denominator.time();
        numerator * denominator
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TimeSignature {
    pub numerator: u8,
    pub denominator: Duration,
}

impl Default for TimeSignature {
    fn default() -> Self {
        TimeSignature {
            numerator: 4,
            denominator: Duration::default(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Duration {
    pub value: u16,
    pub dotted: bool,
    pub double_dotted: bool,
    pub tuplet_enters: u8,
    pub tuplet_times: u8,
}

impl Default for Duration {
    fn default() -> Self {
        Duration {
            value: QUARTER,
            dotted: false,
            double_dotted: false,
            tuplet_enters: 1,
            tuplet_times: 1,
        }
    }
}

impl Duration {
    pub fn convert_time(&self, time: u32) -> u32 {
        log::debug!(
            "time:{} tuplet_times:{} tuplet_enters:{}",
            time,
            self.tuplet_times,
            self.tuplet_enters
        );
        time * u32::from(self.tuplet_times) / u32::from(self.tuplet_enters)
    }

    pub fn time(&self) -> u32 {
        let mut time = QUARTER_TIME as f32 * (4.0 / f32::from(self.value));
        if self.dotted {
            time += time / 2.0;
        } else if self.double_dotted {
            time += (time / 4.0) * 3.0;
        }
        self.convert_time(time as u32)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BendPoint {
    pub position: u8,
    pub value: i8,
}

impl BendPoint {
    pub fn get_time(&self, duration: u32) -> u32 {
        let time = duration as f32 * f32::from(self.position) / BEND_EFFECT_MAX_POSITION_LENGTH;
        time as u32
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct BendEffect {
    pub points: Vec<BendPoint>,
}

impl BendEffect {
    pub fn direction(&self) -> isize {
        if self.points.len() < 2 {
            return 0;
        }
        let first = self.points[0].value;
        for p in &self.points[1..] {
            match first.cmp(&p.value) {
                std::cmp::Ordering::Greater => return -1,
                std::cmp::Ordering::Less => return 1,
                std::cmp::Ordering::Equal => (),
            }
        }
        0
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TremoloBarEffect {
    pub points: Vec<BendPoint>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraceEffect {
    pub duration: u8,
    pub fret: i8,
    pub is_dead: bool,
    pub is_on_beat: bool,
    pub transition: GraceEffectTransition,
    pub velocity: i16,
}

impl GraceEffect {
    pub fn duration_time(&self) -> f32 {
        (QUARTER_TIME as f32 / 16.00) * f32::from(self.duration)
    }
}

impl Default for GraceEffect {
    fn default() -> Self {
        GraceEffect {
            duration: 0,
            fret: 0,
            is_dead: false,
            is_on_beat: false,
            transition: GraceEffectTransition::None,
            velocity: 0,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraceEffectTransition {
    /// No transition
    None = 0,
    /// Slide from the grace note to the real one.
    Slide,
    /// Perform a bend from the grace note to the real one.
    Bend,
    /// Perform a hammer on.
    Hammer,
}

impl GraceEffectTransition {
    pub fn get_grace_effect_transition(value: i8) -> Self {
        match value {
            0 => Self::None,
            1 => Self::Slide,
            2 => Self::Bend,
            3 => Self::Hammer,
            _ => panic!("Cannot get transition for the grace effect"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PitchClass {
    pub note: String,
    pub just: i8,
    /// flat (-1), none (0) or sharp (1).
    pub accidental: i8,
    pub value: i8,
    pub sharp: bool,
}

impl PitchClass {
    pub fn from(just: i8, accidental: Option<i8>, sharp: Option<bool>) -> PitchClass {
        let mut p = PitchClass {
            just,
            accidental: 0,
            value: -1,
            sharp: true,
            note: String::with_capacity(2),
        };
        let pitch: i8;
        let accidental2: i8;
        if let Some(a) = accidental {
            pitch = p.just;
            accidental2 = a;
        } else {
            let value = p.just % 12;
            p.note = if value >= 0 {
                String::from(SHARP_NOTES[value as usize])
            } else {
                String::from(SHARP_NOTES[(12 + value) as usize])
            };
            if p.note.ends_with('b') {
                accidental2 = -1;
                p.sharp = false;
            } else if p.note.ends_with('#') {
                accidental2 = 1;
            } else {
                accidental2 = 0;
            }
            pitch = value - accidental2;
        }
        p.just = pitch % 12;
        p.accidental = accidental2;
        p.value = p.just + accidental2;
        if sharp.is_none() {
            p.sharp = p.accidental >= 0;
        }
        p
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HarmonicType {
    Natural,
    Artificial,
    Tapped,
    Pinch,
    Semi,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Octave {
    None,
    Ottava,
    Quindicesima,
    OttavaBassa,
    QuindicesimaBassa,
}

impl Octave {
    pub fn get_octave(value: u8) -> Self {
        match value {
            0 => Self::None,
            1 => Self::Ottava,
            2 => Self::Quindicesima,
            3 => Self::OttavaBassa,
            4 => Self::QuindicesimaBassa,
            _ => panic!("Cannot get octave value"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HarmonicEffect {
    pub kind: HarmonicType,
    // artificial harmonic
    pub pitch: Option<PitchClass>,
    pub octave: Option<Octave>,
    // tapped harmonic
    pub right_hand_fret: Option<i8>,
}

impl Default for HarmonicEffect {
    fn default() -> Self {
        HarmonicEffect {
            kind: HarmonicType::Natural,
            pitch: None,
            octave: None,
            right_hand_fret: None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlideType {
    IntoFromAbove,
    IntoFromBelow,
    ShiftSlideTo,
    LegatoSlideTo,
    OutDownwards,
    OutUpWards,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TrillEffect {
    pub fret: i8,
    pub duration: Duration,
}

impl TrillEffect {
    fn from_trill_period(period: i8) -> u16 {
        match period {
            1 => u16::from(DURATION_SIXTEENTH),
            2 => u16::from(DURATION_THIRTY_SECOND),
            3 => u16::from(DURATION_SIXTY_FOURTH),
            other => panic!("Cannot get trill period - got {other}"),
        }
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TremoloPickingEffect {
    pub duration: Duration,
}

impl TremoloPickingEffect {
    fn from_tremolo_value(value: i8) -> u16 {
        match value {
            1 => u16::from(DURATION_EIGHTH),
            3 => u16::from(DURATION_SIXTEENTH),
            2 => u16::from(DURATION_THIRTY_SECOND),
            other => panic!("Cannot get tremolo value - got {other}"),
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum NoteType {
    Rest,
    Normal,
    Tie,
    Dead,
    Unknown(u8),
}

impl NoteType {
    pub const fn get_note_type(value: u8) -> Self {
        match value {
            0 => Self::Rest,
            1 => Self::Normal,
            2 => Self::Tie,
            3 => Self::Dead,
            _ => Self::Unknown(value),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoteEffect {
    pub accentuated_note: bool,
    pub bend: Option<BendEffect>,
    pub ghost_note: bool,
    pub grace: Option<GraceEffect>,
    pub hammer: bool,
    pub harmonic: Option<HarmonicEffect>,
    pub heavy_accentuated_note: bool,
    pub let_ring: bool,
    pub palm_mute: bool,
    pub slide: Option<SlideType>,
    pub staccato: bool,
    pub tremolo_picking: Option<TremoloPickingEffect>,
    pub trill: Option<TrillEffect>,
    pub fade_in: bool,
    pub vibrato: bool,
    pub slap: SlapEffect,
    pub tremolo_bar: Option<TremoloBarEffect>,
}

impl Default for NoteEffect {
    fn default() -> Self {
        NoteEffect {
            accentuated_note: false,
            bend: None,
            ghost_note: false,
            grace: None,
            hammer: false,
            harmonic: None,
            heavy_accentuated_note: false,
            let_ring: false,
            palm_mute: false,
            slide: None,
            staccato: false,
            tremolo_picking: None,
            trill: None,
            fade_in: false,
            vibrato: false,
            slap: SlapEffect::None,
            tremolo_bar: None,
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Chord {
    pub length: u8,
    pub sharp: Option<bool>,
    pub root: Option<PitchClass>,
    pub bass: Option<PitchClass>,
    pub add: Option<bool>,
    pub name: String,
    pub first_fret: Option<u32>,
    pub strings: Vec<i8>,
    pub omissions: Vec<bool>,
    pub show: Option<bool>,
    pub new_format: Option<bool>,
}

#[derive(Debug, PartialEq, Eq)]
pub enum BeatStrokeDirection {
    None,
    Up,
    Down,
}

#[derive(Debug, PartialEq, Eq)]
pub struct BeatStroke {
    pub direction: BeatStrokeDirection,
    pub value: u16,
}

impl BeatStroke {
    pub fn is_empty(&self) -> bool {
        self.direction == BeatStrokeDirection::None || self.value == 0
    }

    // A small time increment that depends on note duration and stroke intensity.
    pub fn increment_for_duration(&self, beat_duration: u32) -> u32 {
        if self.value == 0 {
            return 0;
        }
        // stroke speed is based on the smallest rhythmic value
        let duration = beat_duration.min(QUARTER_TIME);
        ((duration as f32 / 8.0) * (4.0 / f32::from(self.value))).round() as u32
    }
}

/// Convert raw GP stroke byte to a duration value.
const fn to_stroke_value(raw: i8) -> u16 {
    match raw {
        1 | 2 => DURATION_SIXTY_FOURTH as u16,
        3 => DURATION_THIRTY_SECOND as u16,
        4 => DURATION_SIXTEENTH as u16,
        5 => DURATION_EIGHTH as u16,
        6 => QUARTER,
        _ => DURATION_SIXTY_FOURTH as u16,
    }
}

impl Default for BeatStroke {
    fn default() -> Self {
        BeatStroke {
            direction: BeatStrokeDirection::None,
            value: 0,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlapEffect {
    None,
    Tapping,
    Slapping,
    Popping,
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct BeatEffects {
    pub stroke: BeatStroke,
    pub chord: Option<Chord>,
}

#[derive(Debug, PartialEq, Eq)]
pub struct Note {
    pub value: i16,
    pub velocity: i16,
    pub string: i8,
    pub effect: NoteEffect,
    pub swap_accidentals: bool,
    pub kind: NoteType,
    tuplet: Option<i8>,
}

impl Note {
    pub const fn new(note_effect: NoteEffect) -> Self {
        Note {
            value: 0,
            velocity: DEFAULT_VELOCITY,
            string: 1,
            effect: note_effect,
            swap_accidentals: false,
            kind: NoteType::Rest,
            tuplet: None,
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Beat {
    pub notes: Vec<Note>,
    pub duration: Duration,
    pub empty: bool,
    pub text: String,
    pub start: u32,
    pub effect: BeatEffects,
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Voice {
    pub measure_index: i16,
    pub beats: Vec<Beat>,
}

#[derive(Debug, PartialEq, Eq)]
pub struct Measure {
    pub key_signature: KeySignature,
    pub time_signature: TimeSignature,
    pub track_index: usize,
    pub header_index: usize,
    pub voices: Vec<Voice>,
}

impl Default for Measure {
    fn default() -> Self {
        Measure {
            key_signature: KeySignature::new(0, false),
            time_signature: TimeSignature::default(),
            track_index: 0,
            header_index: 0,
            voices: vec![],
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Track {
    pub number: i32,
    pub offset: i32,
    pub channel_id: u8,
    pub solo: bool,
    pub mute: bool,
    pub visible: bool,
    pub name: String,
    pub strings: Vec<(i32, i32)>,
    pub color: i32,
    pub midi_port: u8,
    pub fret_count: u8,
    pub measures: Vec<Measure>,
}

impl Default for Track {
    fn default() -> Self {
        Track {
            number: 1,
            offset: 0,
            channel_id: 0,
            solo: false,
            mute: false,
            visible: true,
            name: String::new(),
            strings: vec![],
            color: 0,
            midi_port: 0,
            fret_count: 24,
            measures: vec![],
        }
    }
}

pub fn parse_chord(string_count: u8) -> impl FnMut(&[u8]) -> IResult<&[u8], Chord> {
    move |i| {
        log::debug!("Parsing chords for {string_count} strings");
        let mut i = i;
        let mut chord = Chord {
            strings: vec![-1; string_count.into()],
            ..Default::default()
        };
        let (inner, chord_gp4_header) = parse_u8(i)?;
        i = inner;

        // chord header defines the version as well
        if (chord_gp4_header & 0x01) == 0 {
            log::debug!("Parsing simple chord");
            let (inner, chord_name) = parse_int_byte_sized_string(i)?;
            log::debug!("Chord name {chord_name}");
            i = inner;
            chord.name = chord_name;
            let (inner, first_fret) = parse_int(i)?;
            i = inner;
            log::debug!("Chord first fret {first_fret}");
            chord.first_fret = Some(first_fret as u32);
            if first_fret != 0 {
                for c in 0..6 {
                    let (inner, fret) = parse_int(i)?;
                    if c < string_count {
                        chord.strings[c as usize] = fret as i8;
                    }
                    i = inner;
                }
            }
        } else {
            log::debug!("Parsing diagram style chord");
            i = skip(i, 16);
            let (inner, chord_name) = parse_byte_size_string(21)(i)?;
            i = inner;
            log::debug!("Chord name {chord_name}");
            chord.name = chord_name;
            i = skip(i, 4);
            let (inner, first_fret) = parse_int(i)?;
            i = inner;
            log::debug!("Chord first fret {first_fret}");
            chord.first_fret = Some(first_fret as u32);
            for c in 0..7 {
                let (inner, fret) = parse_int(i)?;
                i = inner;
                log::debug!("Chord fret {c}:{fret}");
                if c < string_count {
                    chord.strings[c as usize] = fret as i8;
                }
            }
            i = skip(i, 32);
        }
        Ok((i, chord))
    }
}

pub fn parse_note_effects(
    note: &mut Note,
    version: GpVersion,
) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + '_ {
    move |i| {
        log::debug!("Parsing note effects");
        let mut i = i;
        let (inner, (flags1, flags2)) = (parse_u8, parse_u8).parse(i)?;
        i = inner;
        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) != 0 {
            let (inner, bend_effect) = parse_bend_effect(i)?;
            i = inner;
            note.effect.bend = Some(bend_effect);
        }

        if (flags1 & 0x10) != 0 {
            let (inner, grace_effect) = parse_grace_effect(version)(i)?;
            i = inner;
            note.effect.grace = Some(grace_effect);
        }

        if (flags2 & 0x04) != 0 {
            let (inner, tremolo_picking) = parse_tremolo_picking(i)?;
            i = inner;
            note.effect.tremolo_picking = Some(tremolo_picking);
        }

        if (flags2 & 0x08) != 0 {
            let (inner, slide_type) = parse_slide_type(i)?;
            i = inner;
            note.effect.slide = slide_type;
        }

        if (flags2 & 0x10) != 0 {
            let (inner, harmonic_effect) = parse_harmonic_effect(version)(i)?;
            i = inner;
            note.effect.harmonic = Some(harmonic_effect);
        }

        if (flags2 & 0x20) != 0 {
            let (inner, trill_effect) = parse_trill_effect(i)?;
            i = inner;
            note.effect.trill = Some(trill_effect);
        }

        Ok((i, ()))
    }
}

pub fn parse_trill_effect(i: &[u8]) -> IResult<&[u8], TrillEffect> {
    log::debug!("Parsing trill effect");
    let mut trill_effect = TrillEffect::default();
    let (inner, (fret, period)) = (parse_i8, parse_i8).parse(i)?;
    trill_effect.fret = fret;
    trill_effect.duration.value = TrillEffect::from_trill_period(period);
    Ok((inner, trill_effect))
}

pub fn parse_harmonic_effect(
    version: GpVersion,
) -> impl FnMut(&[u8]) -> IResult<&[u8], HarmonicEffect> {
    move |i| {
        let mut i = i;
        let mut he = HarmonicEffect::default();
        let (inner, harmonic_type) = parse_i8(i)?;
        i = inner;
        log::debug!("Parsing harmonic effect {harmonic_type}");
        match harmonic_type {
            1 => he.kind = HarmonicType::Natural,
            2 => {
                he.kind = HarmonicType::Artificial;
                if version >= GpVersion::GP5 {
                    let (inner, (semitone, accidental, octave)) =
                        (parse_u8, parse_i8, parse_u8).parse(i)?;
                    i = inner;
                    he.pitch = Some(PitchClass::from(semitone as i8, Some(accidental), None));
                    he.octave = Some(Octave::get_octave(octave));
                }
            }
            3 => {
                he.kind = HarmonicType::Tapped;
                if version >= GpVersion::GP5 {
                    let (inner, fret) = parse_u8(i)?;
                    i = inner;
                    he.right_hand_fret = Some(fret as i8);
                }
            }
            4 => he.kind = HarmonicType::Pinch,
            5 => he.kind = HarmonicType::Semi,
            15 => {
                assert!(
                    version < GpVersion::GP5,
                    "Cannot read artificial harmonic type for GP4"
                );
                he.kind = HarmonicType::Artificial;
            }
            17 => {
                assert!(
                    version < GpVersion::GP5,
                    "Cannot read artificial harmonic type for GP4"
                );
                he.kind = HarmonicType::Artificial;
            }
            22 => {
                assert!(
                    version < GpVersion::GP5,
                    "Cannot read artificial harmonic type for GP4"
                );
                he.kind = HarmonicType::Artificial;
            }
            x => panic!("Cannot read harmonic type {x}"),
        };
        Ok((i, he))
    }
}

pub fn parse_slide_type(i: &[u8]) -> IResult<&[u8], Option<SlideType>> {
    map(parse_i8, |t| {
        log::debug!("Parsing slide type {t}");
        if (t & 0x01) == 0x01 {
            Some(SlideType::ShiftSlideTo)
        } else if (t & 0x02) == 0x02 {
            Some(SlideType::LegatoSlideTo)
        } else if (t & 0x04) == 0x04 {
            Some(SlideType::OutDownwards)
        } else if (t & 0x08) == 0x08 {
            Some(SlideType::OutUpWards)
        } else if (t & 0x10) == 0x10 {
            Some(SlideType::IntoFromBelow)
        } else if (t & 0x20) == 0x20 {
            Some(SlideType::IntoFromAbove)
        } else {
            None
        }
    })
    .parse(i)
}

pub fn parse_tremolo_picking(i: &[u8]) -> IResult<&[u8], TremoloPickingEffect> {
    log::debug!("Parsing tremolo picking");
    map(parse_u8, |tp| {
        let value = TremoloPickingEffect::from_tremolo_value(tp as i8);
        let mut tremolo_picking_effect = TremoloPickingEffect::default();
        tremolo_picking_effect.duration.value = value;
        tremolo_picking_effect
    })
    .parse(i)
}

pub fn parse_grace_effect(version: GpVersion) -> impl FnMut(&[u8]) -> IResult<&[u8], GraceEffect> {
    move |i| {
        log::debug!("Parsing grace effect");
        let mut i = i;
        let mut grace_effect = GraceEffect::default();

        // fret
        let (inner, fret) = parse_u8(i)?;
        i = inner;
        grace_effect.fret = fret as i8;

        // velocity
        let (inner, velocity) = parse_u8(i)?;
        i = inner;
        grace_effect.velocity = convert_velocity(i16::from(velocity));

        // transition
        let (inner, transition) = parse_i8(i)?;
        i = inner;
        grace_effect.transition = GraceEffectTransition::get_grace_effect_transition(transition);

        // duration
        let (inner, duration) = parse_u8(i)?;
        i = inner;
        grace_effect.duration = duration;

        if version >= GpVersion::GP5 {
            // flags
            let (inner, flags) = parse_u8(i)?;
            i = inner;
            grace_effect.is_dead = (flags & 0x01) == 0x01;
            grace_effect.is_on_beat = (flags & 0x02) == 0x02;
        }

        Ok((i, grace_effect))
    }
}

pub fn parse_beat_effects<'a>(
    beat: &'a mut Beat,
    note_effect: &'a mut NoteEffect,
) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + 'a {
    move |i| {
        log::debug!("Parsing beat effects");
        let mut i = i;
        let (inner, (flags1, flags2)) = (parse_u8, parse_u8).parse(i)?;
        i = inner;

        note_effect.fade_in = flags1 & 0x10 != 0;
        note_effect.vibrato = flags1 & 0x02 != 0;

        if flags1 & 0x20 != 0 {
            let (inner, effect) = parse_u8(i)?;
            i = inner;
            log::debug!("Parsing tapping effect {effect}");
            note_effect.slap = match effect {
                1 => SlapEffect::Tapping,
                2 => SlapEffect::Slapping,
                3 => SlapEffect::Popping,
                _ => SlapEffect::None,
            };
        }

        if flags2 & 0x04 != 0 {
            let (inner, effect) = parse_tremolo_bar(i)?;
            i = inner;
            note_effect.tremolo_bar = Some(effect);
        }

        if flags1 & 0x40 != 0 {
            log::debug!("Parsing stroke effect");
            let (inner, (stroke_up, stroke_down)) = (parse_i8, parse_i8).parse(i)?;
            i = inner;
            if stroke_up > 0 {
                beat.effect.stroke.value = to_stroke_value(stroke_up);
                beat.effect.stroke.direction = BeatStrokeDirection::Up;
            }
            if stroke_down > 0 {
                beat.effect.stroke.value = to_stroke_value(stroke_down);
                beat.effect.stroke.direction = BeatStrokeDirection::Down;
            }
        }

        if flags2 & 0x02 != 0 {
            i = skip(i, 1);
        }

        Ok((i, ()))
    }
}

pub fn parse_bend_effect(i: &[u8]) -> IResult<&[u8], BendEffect> {
    log::debug!("Parsing bend effect");
    let mut i = skip(i, 5);
    let mut bend_effect = BendEffect::default();
    let (inner, num_points) = parse_int(i)?;
    i = inner;
    for _ in 0..num_points {
        let (inner, (bend_position, bend_value, _vibrato)) =
            (parse_int, parse_int, parse_i8).parse(i)?;
        i = inner;

        let point_position =
            bend_position as f32 * BEND_EFFECT_MAX_POSITION_LENGTH / GP_BEND_POSITION;
        let point_value = bend_value as f32 * SEMITONE_LENGTH / GP_BEND_SEMITONE;
        bend_effect.points.push(BendPoint {
            position: point_position.round() as u8,
            value: point_value.round() as i8,
        });
    }
    Ok((i, bend_effect))
}

pub fn parse_tremolo_bar(i: &[u8]) -> IResult<&[u8], TremoloBarEffect> {
    log::debug!("Parsing tremolo bar");
    let mut i = skip(i, 5);
    let mut tremolo_bar_effect = TremoloBarEffect::default();
    let (inner, num_points) = parse_int(i)?;
    i = inner;
    for _ in 0..num_points {
        let (inner, (position, value, _vibrato)) = (parse_int, parse_int, parse_i8).parse(i)?;
        i = inner;

        let point_position = position as f32 * BEND_EFFECT_MAX_POSITION_LENGTH / GP_BEND_POSITION;
        let point_value = value as f32 / GP_BEND_SEMITONE * 2.0f32;
        tremolo_bar_effect.points.push(BendPoint {
            position: point_position.round() as u8,
            value: point_value.round() as i8,
        });
    }
    Ok((i, tremolo_bar_effect))
}

/// Read beat duration.
/// Duration is composed of byte signifying duration and an integer that maps to `Tuplet`. The byte maps to following values:
///
/// * *-2*: whole note
/// * *-1*: half note
/// * *0*: quarter note
/// * *1*: eighth note
/// * *2*: sixteenth note
/// * *3*: thirty-second note
///
/// If flag at *0x20* is true, the tuplet is read
pub fn parse_duration(flags: u8) -> impl FnMut(&[u8]) -> IResult<&[u8], Duration> {
    move |i: &[u8]| {
        log::debug!("Parsing duration");
        let mut i = i;
        let mut d = Duration::default();
        let (inner, value) = parse_i8(i)?;
        i = inner;
        d.value = (2_u32.pow((value + 4) as u32) / 4) as u16;
        log::debug!("Duration value: {}", d.value);
        d.dotted = flags & 0x01 != 0;

        if (flags & 0x20) != 0 {
            let (inner, i_tuplet) = parse_int(i)?;
            i = inner;

            match i_tuplet {
                3 => {
                    d.tuplet_enters = i_tuplet as u8;
                    d.tuplet_times = 2;
                }
                5..=7 => {
                    d.tuplet_enters = i_tuplet as u8;
                    d.tuplet_times = 4;
                }
                9..=13 => {
                    d.tuplet_enters = i_tuplet as u8;
                    d.tuplet_times = 8;
                }
                x => log::debug!("Unknown tuplet: {x}"),
            }
        }

        Ok((i, d))
    }
}

pub fn parse_color(i: &[u8]) -> IResult<&[u8], i32> {
    log::debug!("Parsing RGB color");
    map(
        (parse_u8, parse_u8, parse_u8, parse_u8),
        |(r, g, b, _alpha)| (i32::from(r) << 16) | (i32::from(g) << 8) | i32::from(b),
    )
    .parse(i)
}

pub fn parse_marker(i: &[u8]) -> IResult<&[u8], Marker> {
    log::debug!("Parsing marker");
    map(
        (parse_int_byte_sized_string, parse_color),
        |(title, color)| Marker { title, color },
    )
    .parse(i)
}

pub fn parse_triplet_feel(i: &[u8]) -> IResult<&[u8], TripletFeel> {
    log::debug!("Parsing triplet feel");
    map(parse_i8, |triplet_feel| match triplet_feel {
        0 => TripletFeel::None,
        1 => TripletFeel::Eighth,
        2 => TripletFeel::Sixteenth,
        x => panic!("Unknown triplet feel: {x}"),
    })
    .parse(i)
}

/// Parse measure header.
/// the time signature is propagated to the next measure
pub fn parse_measure_header(
    previous_time_signature: TimeSignature,
    song_tempo: u32,
    song_version: GpVersion,
) -> impl FnMut(&[u8]) -> IResult<&[u8], MeasureHeader> {
    move |i: &[u8]| {
        log::debug!("Parsing measure header");
        let (mut i, flags) = parse_u8(i)?;
        log::debug!("Flags: {flags:08b}");
        let mut mh = MeasureHeader::default();
        mh.tempo.value = song_tempo; // value updated later when parsing beats
        mh.repeat_open = (flags & 0x04) != 0;
        // propagate time signature
        mh.time_signature = previous_time_signature.clone();

        // Numerator of the (key) signature
        if (flags & 0x01) != 0 {
            log::debug!("Parsing numerator");
            let (inner, numerator) = parse_i8(i)?;
            i = inner;
            mh.time_signature.numerator = numerator as u8;
        }

        // Denominator of the (key) signature
        if (flags & 0x02) != 0 {
            log::debug!("Parsing denominator");
            let (inner, denominator_value) = parse_i8(i)?;
            i = inner;
            let denominator = Duration {
                value: denominator_value as u16,
                ..Default::default()
            };
            mh.time_signature.denominator = denominator;
        }

        if song_version >= GpVersion::GP5 {
            // Beginning of repeat
            if (flags & 0x08) != 0 {
                log::debug!("Parsing repeat close");
                let (inner, repeat_close) = parse_i8(i)?;
                i = inner;
                mh.repeat_close = repeat_close - 1; // GP5 specific logic
            }

            // Presence of a marker
            if (flags & 0x20) != 0 {
                let (inner, marker) = parse_marker(i)?;
                i = inner;
                mh.marker = Some(marker);
            }

            // Tonality of the measure
            if (flags & 0x40) != 0 {
                log::debug!("Parsing key signature");
                let (inner, key_signature) = parse_i8(i)?;
                mh.key_signature.key = key_signature;
                i = inner;
                let (inner, is_minor) = parse_i8(i)?;
                i = inner;
                mh.key_signature.is_minor = is_minor != 0;
            }

            if (flags & 0x01) != 0 || (flags & 0x02) != 0 {
                log::debug!("Skip 4");
                i = skip(i, 4);
            }

            // Number of alternate ending
            if (flags & 0x10) != 0 {
                log::debug!("Parsing repeat alternative");
                let (inner, alternative) = parse_u8(i)?;
                i = inner;
                mh.repeat_alternative = alternative;
            }

            if (flags & 0x10) == 0 {
                log::debug!("Skip one");
                i = skip(i, 1);
            }

            // Triplet feel
            let (inner, triplet_feel) = parse_triplet_feel(i)?;
            i = inner;
            mh.triplet_feel = triplet_feel;
        } else if song_version <= GpVersion::GP4_06 {
            // Beginning of repeat
            if (flags & 0x08) != 0 {
                log::debug!("Parsing repeat close");
                let (inner, repeat_close) = parse_i8(i)?;
                i = inner;
                mh.repeat_close = repeat_close;
            }

            // Number of alternate ending
            if (flags & 0x10) != 0 {
                log::debug!("Parsing repeat alternative");
                let (inner, alternative) = parse_u8(i)?;
                i = inner;
                mh.repeat_alternative = alternative;
            }

            // Presence of a marker
            if (flags & 0x20) != 0 {
                let (inner, marker) = parse_marker(i)?;
                i = inner;
                mh.marker = Some(marker);
            }

            // Tonality of the measure
            if (flags & 0x40) != 0 {
                log::debug!("Parsing key signature");
                let (inner, key_signature) = parse_i8(i)?;
                mh.key_signature.key = key_signature;
                i = inner;
                let (inner, is_minor) = parse_i8(i)?;
                i = inner;
                mh.key_signature.is_minor = is_minor != 0;
            }
        }

        log::debug!("{mh:?}");

        Ok((i, mh))
    }
}

pub fn parse_measure_headers(
    measure_count: i32,
    song_tempo: u32,
    version: GpVersion,
) -> impl FnMut(&[u8]) -> IResult<&[u8], Vec<MeasureHeader>> {
    move |i: &[u8]| {
        log::debug!("Parsing {measure_count} measure headers");
        // parse first header to account for the byte in between each header in GP5
        let (mut i, first_header) =
            parse_measure_header(TimeSignature::default(), song_tempo, version)(i)?;
        let mut previous_time_signature = first_header.time_signature.clone();
        let mut headers = vec![first_header];
        for _ in 1..measure_count {
            let (rest, header) = preceded(
                cond(version >= GpVersion::GP5, parse_u8),
                parse_measure_header(previous_time_signature, song_tempo, version),
            )
            .parse(i)?;
            // propagate time signature
            previous_time_signature = header.time_signature.clone();
            i = rest;
            headers.push(header);
        }
        debug_assert_eq!(headers.len(), measure_count as usize);
        Ok((i, headers))
    }
}

pub fn parse_midi_channels(i: &[u8]) -> IResult<&[u8], Vec<MidiChannel>> {
    log::debug!("Parsing midi channels");
    let mut channels = Vec::with_capacity(64);
    let mut i = i;
    for channel_index in 0..64 {
        let (inner, channel) = parse_midi_channel(channel_index)(i)?;
        i = inner;
        channels.push(channel);
    }
    Ok((i, channels))
}

pub fn parse_midi_channel(channel_id: i32) -> impl FnMut(&[u8]) -> IResult<&[u8], MidiChannel> {
    move |i: &[u8]| {
        map(
            (
                parse_int, parse_i8, parse_i8, parse_i8, parse_i8, parse_i8, parse_i8, parse_u8,
                parse_u8,
            ),
            |(
                mut instrument,
                volume,
                balance,
                chorus,
                reverb,
                phaser,
                tremolo,
                _blank,
                _blank2,
            )| {
                let bank = if channel_id == 9 {
                    DEFAULT_PERCUSSION_BANK
                } else {
                    DEFAULT_BANK
                };
                if instrument < 0 {
                    instrument = 0;
                }
                MidiChannel {
                    channel_id: channel_id as u8,
                    effect_channel_id: 0, // filled at the track level
                    instrument,
                    volume,
                    balance,
                    chorus,
                    reverb,
                    phaser,
                    tremolo,
                    bank,
                }
            },
        )
        .parse(i)
    }
}

pub fn parse_page_setup(i: &[u8]) -> IResult<&[u8], PageSetup> {
    log::debug!("Parsing page setup");
    map(
        (
            parse_point,
            parse_padding,
            parse_int,
            parse_short,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
            parse_int_sized_string,
        ),
        |(
            page_size,
            page_margin,
            score_size_proportion,
            header_and_footer,
            title,
            subtitle,
            artist,
            album,
            words,
            music,
            word_and_music,
            copyright_1,
            copyright_2,
            page_number,
        )| PageSetup {
            page_size,
            page_margin,
            score_size_proportion: score_size_proportion as f32 / 100.0,
            header_and_footer,
            title,
            subtitle,
            artist,
            album,
            words,
            music,
            word_and_music,
            copyright: format!("{copyright_1}\n{copyright_2}"),
            page_number,
        },
    )
    .parse(i)
}

pub fn parse_point(i: &[u8]) -> IResult<&[u8], Point> {
    log::debug!("Parsing point");
    map((parse_int, parse_int), |(x, y)| Point { x, y }).parse(i)
}

pub fn parse_padding(i: &[u8]) -> IResult<&[u8], Padding> {
    log::debug!("Parsing padding");
    map(
        (parse_int, parse_int, parse_int, parse_int),
        |(right, top, left, bottom)| Padding {
            right,
            top,
            left,
            bottom,
        },
    )
    .parse(i)
}

pub fn parse_lyrics(i: &[u8]) -> IResult<&[u8], Lyrics> {
    log::debug!("Parsing lyrics");
    map(
        (parse_int, count((parse_int, parse_int_sized_string), 5)),
        |(track_choice, lines)| Lyrics {
            track_choice,
            lines,
        },
    )
    .parse(i)
}

/// Parse the version string from the file header.
///
/// 30 character string (not counting the byte announcing the real length of the string)
///
/// <https://dguitar.sourceforge.net/GP4format.html#VERSIONS>
pub fn parse_gp_version(i: &[u8]) -> IResult<&[u8], GpVersion> {
    log::debug!("Parsing GP version");
    parse_byte_size_string(30)(i).map(|(i, version_string)| match version_string.as_str() {
        "FICHIER GUITAR PRO v3.00" => (i, GpVersion::GP3),
        "FICHIER GUITAR PRO v4.00" => (i, GpVersion::GP4),
        "FICHIER GUITAR PRO v4.06" => (i, GpVersion::GP4_06),
        "FICHIER GUITAR PRO v5.00" => (i, GpVersion::GP5),
        "FICHIER GUITAR PRO v5.10" => (i, GpVersion::GP5_10),
        _ => panic!("Unsupported GP version: {version_string}"),
    })
}

fn parse_notices(i: &[u8]) -> IResult<&[u8], Vec<String>> {
    flat_map(parse_int, |notice_count| {
        log::debug!("Notice count: {notice_count}");
        count(parse_int_byte_sized_string, notice_count as usize)
    })
    .parse(i)
}

/// Par information about the piece of music.
/// <https://dguitar.sourceforge.net/GP4format.html#Information_About_the_Piece>
fn parse_info(version: GpVersion) -> impl FnMut(&[u8]) -> IResult<&[u8], SongInfo> {
    move |i: &[u8]| {
        log::debug!("Parsing song info");
        map(
            (
                parse_int_byte_sized_string,
                parse_int_byte_sized_string,
                parse_int_byte_sized_string,
                parse_int_byte_sized_string,
                parse_int_byte_sized_string,
                cond(version >= GpVersion::GP5, parse_int_byte_sized_string),
                parse_int_byte_sized_string,
                parse_int_byte_sized_string,
                parse_int_byte_sized_string,
                parse_notices,
            ),
            |(
                name,
                subtitle,
                artist,
                album,
                author,
                words,
                copyright,
                writer,
                instructions,
                notices,
            )| {
                SongInfo {
                    name,
                    subtitle,
                    artist,
                    album,
                    author,
                    words,
                    copyright,
                    writer,
                    instructions,
                    notices,
                }
            },
        )
        .parse(i)
    }
}

pub fn parse_gp_data(file_data: &[u8]) -> Result<Song, RuxError> {
    let (rest, base_song) = flat_map(parse_gp_version, |version| {
        map(
            (
                parse_info(version),                                     // Song info
                cond(version < GpVersion::GP5, parse_bool),              // Triplet feel
                cond(version >= GpVersion::GP4, parse_lyrics),           // Lyrics
                cond(version >= GpVersion::GP5_10, take(19usize)),       // Skip RSE master effect
                cond(version >= GpVersion::GP5, parse_page_setup),       // Page setup
                cond(version >= GpVersion::GP5, parse_int_sized_string), // Tempo name
                parse_int,                                               // Tempo value
                cond(version > GpVersion::GP5, parse_bool),              // Tempo hide
                parse_i8,                                                // Key signature
                take(3usize),                                            // unknown
                cond(version > GpVersion::GP3, parse_i8),                // Octave
                parse_midi_channels,                                     // Midi channels
            ),
            move |(
                song_info,
                triplet_feel,
                lyrics,
                _master_effect,
                page_setup,
                tempo_name,
                tempo,
                hide_tempo,
                key_signature,
                _unknown,
                octave,
                midi_channels,
            )| {
                // init base song
                let tempo = Tempo::new(tempo as u32, tempo_name);
                Song {
                    version,
                    song_info,
                    triplet_feel,
                    lyrics,
                    page_setup,
                    tempo,
                    hide_tempo,
                    key_signature,
                    octave,
                    midi_cha
Download .txt
gitextract_md1x0dqc/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── resources/
│   └── TimGM6mb.sf2
└── src/
    ├── audio/
    │   ├── midi_builder.rs
    │   ├── midi_event.rs
    │   ├── midi_player.rs
    │   ├── midi_player_params.rs
    │   ├── midi_sequencer.rs
    │   ├── mod.rs
    │   └── playback_order.rs
    ├── config.rs
    ├── main.rs
    ├── parser/
    │   ├── mod.rs
    │   ├── music_parser.rs
    │   ├── primitive_parser.rs
    │   ├── song_parser.rs
    │   └── song_parser_tests.rs
    └── ui/
        ├── application.rs
        ├── canvas_measure.rs
        ├── icons.rs
        ├── mod.rs
        ├── picker.rs
        ├── tablature.rs
        ├── tuning.rs
        └── utils.rs
Download .txt
SYMBOL INDEX (439 symbols across 20 files)

FILE: src/audio/midi_builder.rs
  constant DEFAULT_DURATION_DEAD (line 14) | const DEFAULT_DURATION_DEAD: u32 = 30;
  constant DEFAULT_DURATION_PM (line 15) | const DEFAULT_DURATION_PM: u32 = 60;
  constant DEFAULT_BEND (line 16) | const DEFAULT_BEND: f32 = 64.0;
  constant DEFAULT_BEND_SEMI_TONE (line 17) | const DEFAULT_BEND_SEMI_TONE: f32 = 2.75;
  constant NATURAL_FREQUENCIES (line 19) | pub const NATURAL_FREQUENCIES: [(i32, i32); 6] = [
  type MidiBuilder (line 28) | pub struct MidiBuilder {
    method new (line 33) | pub const fn new() -> Self {
    method build_for_song (line 39) | pub fn build_for_song(self, song: &Rc<Song>) -> Vec<MidiEvent> {
    method build_for_song_with_order (line 45) | pub fn build_for_song_with_order(
    method add_track_events (line 76) | fn add_track_events(
    method add_beat_events (line 125) | fn add_beat_events(
    method add_notes (line 175) | fn add_notes(
    method add_key_effect (line 265) | fn add_key_effect(
    method add_vibrato (line 480) | fn add_vibrato(&mut self, track_id: usize, start: u32, duration: u32, ...
    method add_bend (line 502) | fn add_bend(
    method process_next_bend_values (line 537) | fn process_next_bend_values(
    method add_tremolo_bar (line 575) | fn add_tremolo_bar(
    method add_note (line 608) | fn add_note(
    method add_tempo_change (line 626) | fn add_tempo_change(&mut self, tick: u32, tempo: u32) {
    method add_bank_selection (line 631) | fn add_bank_selection(&mut self, tick: u32, track_id: usize, channel: ...
    method add_volume_selection (line 636) | fn add_volume_selection(&mut self, tick: u32, track_id: usize, channel...
    method add_expression_selection (line 641) | fn add_expression_selection(
    method add_chorus_selection (line 652) | fn add_chorus_selection(&mut self, tick: u32, track_id: usize, channel...
    method add_reverb_selection (line 657) | fn add_reverb_selection(&mut self, tick: u32, track_id: usize, channel...
    method add_pitch_bend (line 662) | fn add_pitch_bend(&mut self, tick: u32, track_id: usize, channel: i32,...
    method add_expression (line 674) | fn add_expression(&mut self, tick: u32, track_id: usize, channel: i32,...
    method add_program_selection (line 679) | fn add_program_selection(&mut self, tick: u32, track_id: usize, channe...
    method add_pitch_bend_range (line 684) | fn add_pitch_bend_range(&mut self, tick: u32, track_id: usize, channel...
    method add_track_channel_midi_control (line 703) | fn add_track_channel_midi_control(&mut self, track_id: usize, midi_cha...
    method add_event (line 741) | fn add_event(&mut self, event: MidiEvent) {
  function apply_velocity_effect (line 746) | fn apply_velocity_effect(
  function apply_duration_effect (line 768) | fn apply_duration_effect(
  function apply_static_duration (line 816) | fn apply_static_duration(tempo: u32, duration: u32, maximum: u32) -> u32 {
  type TripletAdjustment (line 822) | struct TripletAdjustment {
  function apply_triplet_feel (line 829) | fn apply_triplet_feel(
  function apply_triplet_feel_for_duration (line 865) | fn apply_triplet_feel_for_duration(
  function compute_stroke_offsets (line 919) | fn compute_stroke_offsets(beat: &Beat, stroke_increment: u32, string_cou...
  function test_midi_events_for_all_files (line 961) | fn test_midi_events_for_all_files() {
  function print_event (line 1003) | fn print_event(event: &MidiEvent) -> String {
  function validate_gold_rendered_result (line 1007) | fn validate_gold_rendered_result(events: &[MidiEvent], gold_path: PathBu...
  function test_midi_events_for_demo_song (line 1024) | fn test_midi_events_for_demo_song() {
  function test_midi_events_for_bleed (line 1223) | fn test_midi_events_for_bleed() {
  function playback_order_damage_control (line 1317) | fn playback_order_damage_control() {
  function triplet_feel_guthrie_eric (line 1389) | fn triplet_feel_guthrie_eric() {
  function triplet_feel_none_no_change (line 1445) | fn triplet_feel_none_no_change() {
  function triplet_feel_eighth_first_beat (line 1456) | fn triplet_feel_eighth_first_beat() {
  function triplet_feel_eighth_second_beat (line 1470) | fn triplet_feel_eighth_second_beat() {
  function triplet_feel_preserves_total_time (line 1484) | fn triplet_feel_preserves_total_time() {
  function triplet_feel_wrong_duration_no_change (line 1505) | fn triplet_feel_wrong_duration_no_change() {
  function triplet_feel_sixteenth_pair (line 1518) | fn triplet_feel_sixteenth_pair() {
  function make_note (line 1541) | fn make_note(string: i8) -> Note {
  function stroke_offsets_no_stroke (line 1549) | fn stroke_offsets_no_stroke() {
  function stroke_offsets_down_stroke (line 1556) | fn stroke_offsets_down_stroke() {
  function stroke_offsets_up_stroke (line 1574) | fn stroke_offsets_up_stroke() {

FILE: src/audio/midi_event.rs
  type MidiEvent (line 4) | pub struct MidiEvent {
    method is_midi_message (line 14) | pub const fn is_midi_message(&self) -> bool {
    method is_note_event (line 18) | pub const fn is_note_event(&self) -> bool {
    method new_note_on (line 25) | pub const fn new_note_on(
    method new_note_off (line 40) | pub const fn new_note_off(tick: u32, track: usize, key: i32, channel: ...
    method new_tempo_change (line 49) | pub const fn new_tempo_change(tick: u32, tempo: u32) -> Self {
    method new_midi_message (line 58) | pub const fn new_midi_message(
  type MidiEventType (line 76) | pub enum MidiEventType {
    method note_on (line 84) | const fn note_on(channel: i32, key: i32, velocity: i16) -> Self {
    method note_off (line 88) | const fn note_off(channel: i32, key: i32) -> Self {
    method tempo_change (line 92) | const fn tempo_change(tempo: u32) -> Self {
    method midi_message (line 96) | const fn midi_message(channel: i32, command: i32, data1: i32, data2: i...

FILE: src/audio/midi_player.rs
  constant DEFAULT_SAMPLE_RATE (line 17) | const DEFAULT_SAMPLE_RATE: u32 = 44100;
  constant TIMIDITY_SOUND_FONT (line 20) | const TIMIDITY_SOUND_FONT: &[u8] = include_bytes!("../../resources/TimGM...
  type AudioPlayer (line 22) | pub struct AudioPlayer {
    method new (line 36) | pub fn new(
    method make_synthesizer (line 107) | fn make_synthesizer(
    method is_playing (line 118) | pub const fn is_playing(&self) -> bool {
    method solo_track_id (line 122) | pub fn solo_track_id(&self) -> Option<usize> {
    method toggle_solo_mode (line 126) | pub fn toggle_solo_mode(&self, new_track_id: usize) {
    method set_tempo_percentage (line 136) | pub fn set_tempo_percentage(&self, new_tempo_percentage: u32) {
    method master_volume (line 141) | pub fn master_volume(&self) -> f32 {
    method set_master_volume (line 145) | pub fn set_master_volume(&self, volume: f32) {
    method stop (line 149) | pub fn stop(&mut self) {
    method toggle_play (line 177) | pub fn toggle_play(&mut self) -> Option<String> {
    method focus_measure (line 220) | pub fn focus_measure(&self, measure_id: usize) {
  type AudioPlayerError (line 242) | pub enum AudioPlayerError {
  function new_output_stream (line 258) | fn new_output_stream(

FILE: src/audio/midi_player_params.rs
  constant SOLO_NONE (line 3) | const SOLO_NONE: i32 = -1;
  type MidiPlayerParams (line 6) | pub struct MidiPlayerParams {
    method new (line 14) | pub fn new(tempo: u32, tempo_percentage: u32, solo_track_id: Option<us...
    method master_volume (line 23) | pub fn master_volume(&self) -> f32 {
    method set_master_volume (line 27) | pub fn set_master_volume(&self, volume: f32) {
    method solo_track_id (line 32) | pub fn solo_track_id(&self) -> Option<usize> {
    method set_solo_track_id (line 39) | pub fn set_solo_track_id(&self, solo_track_id: Option<usize>) {
    method adjusted_tempo (line 46) | pub fn adjusted_tempo(&self) -> u32 {
    method set_tempo (line 52) | pub fn set_tempo(&self, tempo: u32) {
    method set_tempo_percentage (line 56) | pub fn set_tempo_percentage(&self, tempo_percentage: u32) {

FILE: src/audio/midi_sequencer.rs
  constant QUARTER_TIME (line 4) | const QUARTER_TIME: f32 = 960.0;
  type MidiSequencer (line 6) | pub struct MidiSequencer {
    method new (line 14) | pub fn new(sorted_events: Vec<MidiEvent>) -> Self {
    method events (line 31) | pub fn events(&self) -> &[MidiEvent] {
    method set_tick (line 36) | pub fn set_tick(&mut self, tick: u32) {
    method reset_last_time (line 44) | pub fn reset_last_time(&mut self) {
    method reset_ticks (line 49) | pub fn reset_ticks(&mut self) {
    method get_tick (line 54) | pub const fn get_tick(&self) -> u32 {
    method get_last_tick (line 58) | pub const fn get_last_tick(&self) -> u32 {
    method get_next_events (line 62) | pub fn get_next_events(&self) -> Option<&[MidiEvent]> {
    method advance (line 102) | pub fn advance(&mut self, tempo: u32) {
    method advance_tick (line 121) | pub fn advance_tick(&mut self, tick: u32) {
  function tick_increase (line 127) | fn tick_increase(tempo_bpm: u32, elapsed_seconds: f32) -> u32 {
  function test_tick_increase (line 143) | fn test_tick_increase() {
  function test_tick_increase_bis (line 151) | fn test_tick_increase_bis() {
  function test_sequence_demo_song (line 158) | fn test_sequence_demo_song() {
  function set_tick_includes_events_at_target (line 202) | fn set_tick_includes_events_at_target() {
  function set_tick_on_song_with_repeats (line 242) | fn set_tick_on_song_with_repeats() {

FILE: src/audio/mod.rs
  constant FIRST_TICK (line 9) | pub const FIRST_TICK: u32 = 1;

FILE: src/audio/playback_order.rs
  type RepeatState (line 5) | struct RepeatState {
    method new (line 13) | fn new() -> Self {
    method enter_repeat (line 24) | fn enter_repeat(&mut self, measure_index: usize) {
    method matches_alternative (line 33) | fn matches_alternative(&self, repeat_alternative: u8) -> bool {
    method close_repeat (line 42) | fn close_repeat(&mut self, measure_index: usize, repeat_close: i8) -> ...
  function compute_playback_order (line 68) | pub fn compute_playback_order(headers: &[MeasureHeader]) -> Vec<(usize, ...
  function make_header (line 118) | fn make_header(start: u32, repeat_open: bool, repeat_close: i8) -> Measu...
  function no_repeats (line 128) | fn no_repeats() {
  function simple_repeat (line 142) | fn simple_repeat() {
  function repeat_three_times (line 164) | fn repeat_three_times() {
  function two_repeat_sections (line 178) | fn two_repeat_sections() {
  function alternative_endings (line 193) | fn alternative_endings() {
  function three_alternatives (line 218) | fn three_alternatives() {
  function nested_repeats (line 248) | fn nested_repeats() {
  function repeat_close_without_open (line 264) | fn repeat_close_without_open() {
  function single_measure_repeat (line 279) | fn single_measure_repeat() {
  function tick_offsets_are_consistent (line 289) | fn tick_offsets_are_consistent() {
  function alternative_on_last_pass_with_close (line 305) | fn alternative_on_last_pass_with_close() {
  function empty (line 324) | fn empty() {

FILE: src/config.rs
  type Config (line 13) | pub struct Config {
    constant FOLDER (line 19) | const FOLDER: &'static str = ".config/ruxguitar";
    method get_tabs_folder (line 21) | pub fn get_tabs_folder(&self) -> Option<PathBuf> {
    method set_tabs_folder (line 25) | pub fn set_tabs_folder(&mut self, new_tabs_folder: Option<PathBuf>) ->...
    method get_base_path (line 35) | fn get_base_path() -> Result<PathBuf, RuxError> {
    method get_path (line 42) | fn get_path() -> Result<PathBuf, RuxError> {
    method read_config (line 48) | pub fn read_config() -> Result<Self, RuxError> {
    method save_config (line 75) | pub fn save_config(&self) -> Result<(), RuxError> {

FILE: src/main.rs
  function main (line 13) | fn main() {
  function main_result (line 25) | pub fn main_result() -> Result<(), RuxError> {
  type CliArgs (line 71) | pub struct CliArgs {
  type ApplicationArgs (line 84) | pub struct ApplicationArgs {
  type RuxError (line 92) | pub enum RuxError {
    method from (line 104) | fn from(error: iced::Error) -> Self {
    method from (line 110) | fn from(error: io::Error) -> Self {

FILE: src/parser/music_parser.rs
  type MusicParser (line 12) | pub struct MusicParser {
    method new (line 17) | pub const fn new(song: Song) -> Self {
    method take_song (line 20) | pub fn take_song(&mut self) -> Song {
    method parse_music_data (line 24) | pub fn parse_music_data<'a>(&'a mut self, i: &'a [u8]) -> IResult<&'a ...
    method parse_tracks (line 56) | fn parse_tracks(
    method parse_track (line 82) | fn parse_track(&mut self, number: usize) -> impl FnMut(&[u8]) -> IResu...
    method parse_track_channel (line 170) | fn parse_track_channel(&mut self) -> impl FnMut(&[u8]) -> IResult<&[u8...
    method parse_measures (line 206) | fn parse_measures(
    method parse_measure (line 238) | fn parse_measure(
    method parse_voice (line 271) | fn parse_voice(
    method parse_beat (line 301) | fn parse_beat(
    method get_tied_note_value (line 391) | fn get_tied_note_value(&self, string_index: i8, track_index: usize) ->...
    method parse_mix_change (line 407) | fn parse_mix_change(
    method parse_note (line 487) | fn parse_note<'a>(

FILE: src/parser/primitive_parser.rs
  function parse_i8 (line 6) | pub fn parse_i8(i: &[u8]) -> IResult<&[u8], i8> {
  function parse_u8 (line 11) | pub fn parse_u8(i: &[u8]) -> IResult<&[u8], u8> {
  function parse_int (line 16) | pub fn parse_int(i: &[u8]) -> IResult<&[u8], i32> {
  function parse_bool (line 21) | pub fn parse_bool(i: &[u8]) -> IResult<&[u8], bool> {
  function parse_short (line 26) | pub fn parse_short(i: &[u8]) -> IResult<&[u8], i16> {
  function skip (line 31) | pub fn skip(i: &[u8], n: usize) -> &[u8] {
  function make_string (line 40) | fn make_string(i: &[u8]) -> String {
  function parse_string (line 57) | fn parse_string(len: i32) -> impl FnMut(&[u8]) -> IResult<&[u8], String> {
  function parse_string_field (line 62) | fn parse_string_field(
  function parse_int_sized_string (line 83) | pub fn parse_int_sized_string(i: &[u8]) -> IResult<&[u8], String> {
  function parse_byte_size_string (line 90) | pub fn parse_byte_size_string(size: usize) -> impl FnMut(&[u8]) -> IResu...
  function parse_int_byte_sized_string (line 99) | pub fn parse_int_byte_sized_string(i: &[u8]) -> IResult<&[u8], String> {
  function test_read_byte_size_string (line 114) | fn test_read_byte_size_string() {

FILE: src/parser/song_parser.rs
  constant MAX_VOICES (line 18) | pub const MAX_VOICES: u32 = 2;
  constant QUARTER_TIME (line 20) | pub const QUARTER_TIME: u32 = 960;
  constant QUARTER (line 21) | pub const QUARTER: u16 = 4;
  constant DURATION_EIGHTH (line 23) | pub const DURATION_EIGHTH: u8 = 8;
  constant DURATION_SIXTEENTH (line 24) | pub const DURATION_SIXTEENTH: u8 = 16;
  constant DURATION_THIRTY_SECOND (line 25) | pub const DURATION_THIRTY_SECOND: u8 = 32;
  constant DURATION_SIXTY_FOURTH (line 26) | pub const DURATION_SIXTY_FOURTH: u8 = 64;
  constant BEND_EFFECT_MAX_POSITION_LENGTH (line 28) | pub const BEND_EFFECT_MAX_POSITION_LENGTH: f32 = 12.0;
  constant SEMITONE_LENGTH (line 30) | pub const SEMITONE_LENGTH: f32 = 1.0;
  constant GP_BEND_SEMITONE (line 31) | pub const GP_BEND_SEMITONE: f32 = 25.0;
  constant GP_BEND_POSITION (line 32) | pub const GP_BEND_POSITION: f32 = 60.0;
  constant SHARP_NOTES (line 34) | pub const SHARP_NOTES: [&str; 12] = [
  constant DEFAULT_PERCUSSION_BANK (line 38) | pub const DEFAULT_PERCUSSION_BANK: u8 = 128;
  constant DEFAULT_BANK (line 40) | pub const DEFAULT_BANK: u8 = 0;
  constant MIN_VELOCITY (line 42) | pub const MIN_VELOCITY: i16 = 15;
  constant VELOCITY_INCREMENT (line 43) | pub const VELOCITY_INCREMENT: i16 = 16;
  constant DEFAULT_VELOCITY (line 44) | pub const DEFAULT_VELOCITY: i16 = MIN_VELOCITY + VELOCITY_INCREMENT * 5;
  function convert_velocity (line 47) | pub const fn convert_velocity(v: i16) -> i16 {
  type GpVersion (line 52) | pub enum GpVersion {
  type Song (line 62) | pub struct Song {
  type MidiChannel (line 78) | pub struct MidiChannel {
    method is_percussion (line 92) | pub const fn is_percussion(&self) -> bool {
  type Padding (line 98) | pub struct Padding {
  type Point (line 105) | pub struct Point {
  type PageSetup (line 111) | pub struct PageSetup {
  type Lyrics (line 127) | pub struct Lyrics {
  type SongInfo (line 133) | pub struct SongInfo {
  type Marker (line 147) | pub struct Marker {
  constant KEY_SIGNATURES (line 152) | pub const KEY_SIGNATURES: [&str; 34] = [
  type KeySignature (line 190) | pub struct KeySignature {
    method new (line 196) | pub const fn new(key: i8, is_minor: bool) -> Self {
    method fmt (line 202) | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
  type TripletFeel (line 213) | pub enum TripletFeel {
  type Tempo (line 220) | pub struct Tempo {
    method new (line 226) | const fn new(value: u32, name: Option<String>) -> Self {
  method default (line 232) | fn default() -> Self {
  type MeasureHeader (line 241) | pub struct MeasureHeader {
    method length (line 270) | pub fn length(&self) -> u32 {
  method default (line 254) | fn default() -> Self {
  type TimeSignature (line 278) | pub struct TimeSignature {
  method default (line 284) | fn default() -> Self {
  type Duration (line 293) | pub struct Duration {
    method convert_time (line 314) | pub fn convert_time(&self, time: u32) -> u32 {
    method time (line 324) | pub fn time(&self) -> u32 {
  method default (line 302) | fn default() -> Self {
  type BendPoint (line 336) | pub struct BendPoint {
    method get_time (line 342) | pub fn get_time(&self, duration: u32) -> u32 {
  type BendEffect (line 349) | pub struct BendEffect {
    method direction (line 354) | pub fn direction(&self) -> isize {
  type TremoloBarEffect (line 371) | pub struct TremoloBarEffect {
  type GraceEffect (line 376) | pub struct GraceEffect {
    method duration_time (line 386) | pub fn duration_time(&self) -> f32 {
  method default (line 392) | fn default() -> Self {
  type GraceEffectTransition (line 405) | pub enum GraceEffectTransition {
    method get_grace_effect_transition (line 417) | pub fn get_grace_effect_transition(value: i8) -> Self {
  type PitchClass (line 429) | pub struct PitchClass {
    method from (line 439) | pub fn from(just: i8, accidental: Option<i8>, sharp: Option<bool>) -> ...
  type HarmonicType (line 480) | pub enum HarmonicType {
  type Octave (line 489) | pub enum Octave {
    method get_octave (line 498) | pub fn get_octave(value: u8) -> Self {
  type HarmonicEffect (line 511) | pub struct HarmonicEffect {
  method default (line 521) | fn default() -> Self {
  type SlideType (line 532) | pub enum SlideType {
  type TrillEffect (line 542) | pub struct TrillEffect {
    method from_trill_period (line 548) | fn from_trill_period(period: i8) -> u16 {
  type TremoloPickingEffect (line 559) | pub struct TremoloPickingEffect {
    method from_tremolo_value (line 564) | fn from_tremolo_value(value: i8) -> u16 {
  type NoteType (line 575) | pub enum NoteType {
    method get_note_type (line 584) | pub const fn get_note_type(value: u8) -> Self {
  type NoteEffect (line 596) | pub struct NoteEffect {
  method default (line 617) | fn default() -> Self {
  type Chord (line 641) | pub struct Chord {
  type BeatStrokeDirection (line 656) | pub enum BeatStrokeDirection {
  type BeatStroke (line 663) | pub struct BeatStroke {
    method is_empty (line 669) | pub fn is_empty(&self) -> bool {
    method increment_for_duration (line 674) | pub fn increment_for_duration(&self, beat_duration: u32) -> u32 {
  function to_stroke_value (line 685) | const fn to_stroke_value(raw: i8) -> u16 {
  method default (line 697) | fn default() -> Self {
  type SlapEffect (line 706) | pub enum SlapEffect {
  type BeatEffects (line 714) | pub struct BeatEffects {
  type Note (line 720) | pub struct Note {
    method new (line 731) | pub const fn new(note_effect: NoteEffect) -> Self {
  type Beat (line 745) | pub struct Beat {
  type Voice (line 755) | pub struct Voice {
  type Measure (line 761) | pub struct Measure {
  method default (line 770) | fn default() -> Self {
  type Track (line 782) | pub struct Track {
  method default (line 798) | fn default() -> Self {
  function parse_chord (line 816) | pub fn parse_chord(string_count: u8) -> impl FnMut(&[u8]) -> IResult<&[u...
  function parse_note_effects (line 873) | pub fn parse_note_effects(
  function parse_trill_effect (line 929) | pub fn parse_trill_effect(i: &[u8]) -> IResult<&[u8], TrillEffect> {
  function parse_harmonic_effect (line 938) | pub fn parse_harmonic_effect(
  function parse_slide_type (line 996) | pub fn parse_slide_type(i: &[u8]) -> IResult<&[u8], Option<SlideType>> {
  function parse_tremolo_picking (line 1018) | pub fn parse_tremolo_picking(i: &[u8]) -> IResult<&[u8], TremoloPickingE...
  function parse_grace_effect (line 1029) | pub fn parse_grace_effect(version: GpVersion) -> impl FnMut(&[u8]) -> IR...
  function parse_beat_effects (line 1067) | pub fn parse_beat_effects<'a>(
  function parse_bend_effect (line 1120) | pub fn parse_bend_effect(i: &[u8]) -> IResult<&[u8], BendEffect> {
  function parse_tremolo_bar (line 1142) | pub fn parse_tremolo_bar(i: &[u8]) -> IResult<&[u8], TremoloBarEffect> {
  function parse_duration (line 1173) | pub fn parse_duration(flags: u8) -> impl FnMut(&[u8]) -> IResult<&[u8], ...
  function parse_color (line 1209) | pub fn parse_color(i: &[u8]) -> IResult<&[u8], i32> {
  function parse_marker (line 1218) | pub fn parse_marker(i: &[u8]) -> IResult<&[u8], Marker> {
  function parse_triplet_feel (line 1227) | pub fn parse_triplet_feel(i: &[u8]) -> IResult<&[u8], TripletFeel> {
  function parse_measure_header (line 1240) | pub fn parse_measure_header(
  function parse_measure_headers (line 1366) | pub fn parse_measure_headers(
  function parse_midi_channels (line 1394) | pub fn parse_midi_channels(i: &[u8]) -> IResult<&[u8], Vec<MidiChannel>> {
  function parse_midi_channel (line 1406) | pub fn parse_midi_channel(channel_id: i32) -> impl FnMut(&[u8]) -> IResu...
  function parse_page_setup (line 1450) | pub fn parse_page_setup(i: &[u8]) -> IResult<&[u8], PageSetup> {
  function parse_point (line 1503) | pub fn parse_point(i: &[u8]) -> IResult<&[u8], Point> {
  function parse_padding (line 1508) | pub fn parse_padding(i: &[u8]) -> IResult<&[u8], Padding> {
  function parse_lyrics (line 1522) | pub fn parse_lyrics(i: &[u8]) -> IResult<&[u8], Lyrics> {
  function parse_gp_version (line 1539) | pub fn parse_gp_version(i: &[u8]) -> IResult<&[u8], GpVersion> {
  function parse_notices (line 1551) | fn parse_notices(i: &[u8]) -> IResult<&[u8], Vec<String>> {
  function parse_info (line 1561) | fn parse_info(version: GpVersion) -> impl FnMut(&[u8]) -> IResult<&[u8],...
  function parse_gp_data (line 1607) | pub fn parse_gp_data(file_data: &[u8]) -> Result<Song, RuxError> {
  function test_gp_ordering (line 1689) | fn test_gp_ordering() {

FILE: src/parser/song_parser_tests.rs
  function parse_gp_file (line 9) | pub fn parse_gp_file(file_path: &str) -> Result<Song, RuxError> {
  function init_logger (line 24) | fn init_logger() {
  function parse_all_files_successfully (line 31) | fn parse_all_files_successfully(with_extension: &str) {
  function parse_all_gp5_files_successfully (line 88) | fn parse_all_gp5_files_successfully() {
  function parse_all_gp4_files_successfully (line 93) | fn parse_all_gp4_files_successfully() {
  function parse_gp4_06_canon_rock (line 98) | fn parse_gp4_06_canon_rock() {
  function parse_gp5_00_demo (line 116) | fn parse_gp5_00_demo() {
  function parse_gp5_10_bleed (line 310) | fn parse_gp5_10_bleed() {
  function parse_gp5_10_ghost (line 627) | fn parse_gp5_10_ghost() {
  function gp_version_ordering (line 768) | fn gp_version_ordering() {

FILE: src/ui/application.rs
  constant ICONS_FONT (line 29) | const ICONS_FONT: &[u8] = include_bytes!("../../resources/icons.ttf");
  type RuxApplication (line 31) | pub struct RuxApplication {
    method new (line 186) | fn new(sound_font_file: Option<PathBuf>, config: Config) -> Self {
    method boot (line 205) | fn boot(args: &ApplicationArgs) -> (Self, Task<Message>) {
    method start (line 215) | pub fn start(args: ApplicationArgs) -> iced::Result {
    method title (line 229) | fn title(&self) -> String {
    method focus_measure_with_scroll (line 236) | fn focus_measure_with_scroll(&mut self, measure_id: usize) -> Task<Mes...
    method update (line 251) | fn update(&mut self, message: Message) -> Task<Message> {
    method view (line 514) | fn view(&self) -> Element<'_, Message> {
    method theme (line 684) | const fn theme(&self) -> Theme {
    method audio_player_beat_subscription (line 688) | fn audio_player_beat_subscription(
    method subscription (line 704) | fn subscription(&self) -> Subscription<Message> {
  type SongDisplayInfo (line 49) | struct SongDisplayInfo {
    method new (line 62) | fn new(song: &Song, file_name: String) -> Self {
    method metadata_line (line 78) | fn metadata_line(&self) -> Option<String> {
  type TempoSelection (line 98) | pub struct TempoSelection {
    method new (line 109) | const fn new(percentage: u32) -> Self {
    constant PRESET (line 113) | const PRESET: [Self; 9] = {
  method default (line 103) | fn default() -> Self {
  method fmt (line 129) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  type TrackSelection (line 135) | pub struct TrackSelection {
    method new (line 142) | const fn new(index: usize, name: String, tuning: Option<String>) -> Se...
  method fmt (line 152) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  type Message (line 162) | pub enum Message {
  function song_time_up_to_measure (line 761) | fn song_time_up_to_measure(headers: &[MeasureHeader], measure_idx: usize...
  function format_mmss (line 776) | fn format_mmss(seconds: f32) -> String {
  type BeatSubscriptionData (line 781) | struct BeatSubscriptionData(Arc<AtomicU32>, Arc<Notify>);
    method hash (line 784) | fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
  method eq (line 790) | fn eq(&self, _other: &Self) -> bool {

FILE: src/ui/canvas_measure.rs
  constant TEMPO_SIGN (line 16) | const TEMPO_SIGN: char = '\u{1D15F}';
  constant VIBRATO (line 17) | const VIBRATO: char = '\u{301C}';
  constant HAMMER_ON (line 18) | const HAMMER_ON: char = '\u{25E0}';
  constant HORIZONTAL_BAR (line 19) | const HORIZONTAL_BAR: char = '\u{2015}';
  constant SHIFT_SLIDE (line 20) | const SHIFT_SLIDE: char = '\u{27CD}';
  constant LEGATO_SLIDE (line 21) | const LEGATO_SLIDE: char = '\u{27CB}';
  constant ARROW_UP (line 22) | const ARROW_UP: char = '\u{2191}';
  constant ARROW_DOWN (line 23) | const ARROW_DOWN: char = '\u{2193}';
  constant TIE (line 24) | const TIE: char = '\u{2323}';
  constant MEASURE_ANNOTATION_Y (line 35) | const MEASURE_ANNOTATION_Y: f32 = 3.0;
  constant CHORD_ANNOTATION_Y (line 36) | const CHORD_ANNOTATION_Y: f32 = 15.0;
  constant NOTE_EFFECT_ANNOTATION_Y (line 37) | const NOTE_EFFECT_ANNOTATION_Y: f32 = 27.0;
  constant BEAT_TEXT_ANNOTATION_Y (line 38) | const BEAT_TEXT_ANNOTATION_Y: f32 = 38.0;
  constant FIRST_STRING_Y (line 39) | const FIRST_STRING_Y: f32 = 60.0;
  constant BOTTOM_PADDING (line 42) | const BOTTOM_PADDING: f32 = 16.0;
  constant STRING_LINE_HEIGHT (line 45) | const STRING_LINE_HEIGHT: f32 = 13.0;
  constant MEASURE_NOTES_PADDING (line 48) | const MEASURE_NOTES_PADDING: f32 = 20.0;
  constant BEAT_LENGTH (line 51) | const BEAT_LENGTH: f32 = 24.0;
  constant HALF_BEAT_LENGTH (line 53) | const HALF_BEAT_LENGTH: f32 = BEAT_LENGTH / 2.0 + 1.0;
  constant MIN_MEASURE_WIDTH (line 56) | const MIN_MEASURE_WIDTH: f32 = 60.0;
  type CanvasMeasure (line 59) | pub struct CanvasMeasure {
    method new (line 74) | pub fn new(
    method set_first_on_line (line 119) | pub const fn set_first_on_line(&mut self, value: bool) {
    method view (line 123) | pub fn view(&self) -> Element<'_, Message> {
    method view_fill (line 132) | pub fn view_fill(&self) -> Element<'_, Message> {
    method overhead_width (line 142) | fn overhead_width(&self) -> f32 {
    method toggle_focused (line 146) | pub fn toggle_focused(&mut self) {
    method focus_beat (line 154) | pub fn focus_beat(&mut self, beat_id: usize) {
    method clear_canvas_cache (line 161) | pub fn clear_canvas_cache(&self) {
    type State (line 174) | type State = MeasureInteraction;
    method update (line 176) | fn update(
    method draw (line 193) | fn draw(
    method mouse_interaction (line 421) | fn mouse_interaction(
  type MeasureInteraction (line 167) | pub enum MeasureInteraction {
  function draw_focused_box (line 431) | fn draw_focused_box(
  function draw_measure_vertical_line (line 460) | fn draw_measure_vertical_line(
  function draw_beat (line 474) | fn draw_beat(
  function draw_note (line 552) | fn draw_note(
  function draw_open_section (line 594) | fn draw_open_section(
  function draw_open_repeat (line 618) | fn draw_open_repeat(
  function draw_close_repeat (line 639) | fn draw_close_repeat(
  function draw_stroke_arrow (line 671) | fn draw_stroke_arrow(
  function draw_alternative_ending (line 720) | fn draw_alternative_ending(
  function draw_repeat_dots (line 765) | fn draw_repeat_dots(
  function draw_end_section (line 792) | fn draw_end_section(
  function draw_time_signature (line 815) | fn draw_time_signature(
  function above_note_effect_annotation (line 849) | fn above_note_effect_annotation(note_effect: &NoteEffect) -> Vec<String> {
  function inlined_note_effect_annotation (line 894) | fn inlined_note_effect_annotation(note_effect: &NoteEffect) -> String {
  function note_value (line 922) | fn note_value(note: &Note) -> String {

FILE: src/ui/icons.rs
  function open_icon (line 6) | pub fn open_icon<'a, Message>() -> Element<'a, Message> {
  function solo_icon (line 10) | pub fn solo_icon<'a, Message>() -> Element<'a, Message> {
  function pause_icon (line 14) | pub fn pause_icon<'a, Message>() -> Element<'a, Message> {
  function play_icon (line 18) | pub fn play_icon<'a, Message>() -> Element<'a, Message> {
  function stop_icon (line 22) | pub fn stop_icon<'a, Message>() -> Element<'a, Message> {
  function icon (line 26) | fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> {

FILE: src/ui/picker.rs
  type FilePickerError (line 4) | pub enum FilePickerError {
  function open_file_dialog (line 12) | pub async fn open_file_dialog(
  function load_file (line 33) | pub async fn load_file(

FILE: src/ui/tablature.rs
  constant INNER_PADDING (line 9) | const INNER_PADDING: f32 = 10.0;
  constant SCROLLBAR_WIDTH (line 10) | const SCROLLBAR_WIDTH: f32 = 10.0;
  type Tablature (line 12) | pub struct Tablature {
    method new (line 24) | pub fn new(
    method load_measures (line 52) | pub fn load_measures(&mut self) {
    method update_container_width (line 88) | pub fn update_container_width(&mut self, width: f32) {
    method update_first_on_line (line 100) | fn update_first_on_line(&mut self) {
    method get_measure_beat_indexes_for_tick (line 134) | pub fn get_measure_beat_indexes_for_tick(&self, track_id: usize, tick:...
    method focus_on_tick (line 164) | pub fn focus_on_tick(&mut self, tick: u32) -> Option<f32> {
    method focus_on_measure (line 189) | pub fn focus_on_measure(&mut self, new_measure_id: usize) {
    method focused_measure (line 200) | pub const fn focused_measure(&self) -> usize {
    method measure_count (line 204) | pub const fn measure_count(&self) -> usize {
    method scroll_offset_for_measure (line 208) | pub fn scroll_offset_for_measure(&self, measure_id: usize) -> Option<f...
    method view (line 217) | pub fn view(&self) -> Element<'_, Message> {
    method update_track (line 266) | pub fn update_track(&mut self, track: usize) {
  type LineTracker (line 276) | struct LineTracker {
    method make (line 282) | pub fn make(measures: &[CanvasMeasure], tablature_container_width: f32...
    method make_from_widths (line 287) | fn make_from_widths(widths: &[f32], tablature_container_width: f32) ->...
    method get_line (line 305) | pub fn get_line(&self, measure_id: usize) -> u32 {
  function line_tracker_single_line (line 315) | fn line_tracker_single_line() {
  function line_tracker_wraps_to_multiple_lines (line 324) | fn line_tracker_wraps_to_multiple_lines() {
  function line_tracker_exact_fit_stays (line 335) | fn line_tracker_exact_fit_stays() {
  function line_tracker_single_wide_measure (line 345) | fn line_tracker_single_wide_measure() {
  function line_tracker_varying_widths (line 355) | fn line_tracker_varying_widths() {
  function line_tracker_empty (line 370) | fn line_tracker_empty() {
  function first_on_line_detection (line 377) | fn first_on_line_detection() {

FILE: src/ui/tuning.rs
  function tuning_label (line 3) | pub fn tuning_label(strings: &[(i32, i32)]) -> Option<String> {
  function preset_name (line 23) | fn preset_name(pitches_sorted: &[i32]) -> Option<&'static str> {
  function note_name (line 60) | fn note_name(midi_pitch: i32) -> String {
  function standard_e_guitar (line 74) | fn standard_e_guitar() {
  function drop_d_guitar (line 80) | fn drop_d_guitar() {
  function standard_d_guitar (line 86) | fn standard_d_guitar() {
  function standard_c_sharp_guitar (line 93) | fn standard_c_sharp_guitar() {
  function standard_c_guitar (line 100) | fn standard_c_guitar() {
  function standard_a_sharp_guitar (line 107) | fn standard_a_sharp_guitar() {
  function standard_b_guitar (line 114) | fn standard_b_guitar() {
  function drop_a_7_string (line 121) | fn drop_a_7_string() {
  function standard_a_7_string (line 136) | fn standard_a_7_string() {
  function standard_f_7_string (line 151) | fn standard_f_7_string() {
  function standard_bass (line 166) | fn standard_bass() {
  function open_d_minor_high_e (line 172) | fn open_d_minor_high_e() {
  function drop_a_6_string (line 182) | fn drop_a_6_string() {
  function drop_a_5_string_bass (line 189) | fn drop_a_5_string_bass() {
  function standard_d_bass (line 196) | fn standard_d_bass() {
  function standard_b_6_string_bass (line 203) | fn standard_b_6_string_bass() {
  function empty_strings_returns_none (line 210) | fn empty_strings_returns_none() {
  function unknown_tuning_falls_back_to_notes (line 215) | fn unknown_tuning_falls_back_to_notes() {
  function note_name_e2 (line 225) | fn note_name_e2() {
  function note_name_middle_c (line 230) | fn note_name_middle_c() {

FILE: src/ui/utils.rs
  constant COLOR_GRAY (line 8) | pub const COLOR_GRAY: Color = Color::from_rgb8(0x40, 0x44, 0x4B);
  constant COLOR_DARK_RED (line 9) | pub const COLOR_DARK_RED: Color = Color::from_rgb8(200, 50, 50);
  function untitled_text_table_box (line 11) | pub fn untitled_text_table_box() -> Container<'static, Message> {
  function action_gated (line 26) | pub fn action_gated<'a, Message: Clone + 'a>(
  function action_toggle (line 46) | pub fn action_toggle<'a, Message: Clone + 'a>(
  function modal (line 69) | pub fn modal<'a, Message>(
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (331K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "github: agourlay\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2264,
    "preview": "name: CI\n\non:\n  push:\n    branches: [ '*' ]\n  pull_request:\n    branches: [ '*' ]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2327,
    "preview": "name: release binaries\n\non:\n  release:\n    types: [created]\n\npermissions:\n  contents: write\n\njobs:\n  upload-bins:\n    na"
  },
  {
    "path": ".gitignore",
    "chars": 26,
    "preview": "/target\n.idea\n/test-files/"
  },
  {
    "path": "Cargo.toml",
    "chars": 1672,
    "preview": "[package]\nname = \"ruxguitar\"\nversion = \"0.8.1\"\nedition = \"2024\"\nauthors = [\"Arnaud Gourlay <arnaud.gourlay@gmail.com>\"]\n"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 3973,
    "preview": "# ruxguitar\n\n[![Build status](https://github.com/agourlay/ruxguitar/actions/workflows/ci.yml/badge.svg)](https://github."
  },
  {
    "path": "src/audio/midi_builder.rs",
    "chars": 58153,
    "preview": "/// Thanks to `TuxGuitar` for the reference implementation in `MidiSequenceParser.java`\nuse crate::audio::FIRST_TICK;\nus"
  },
  {
    "path": "src/audio/midi_event.rs",
    "chars": 2684,
    "preview": "/// A MIDI event.\n/// Try to keep this struct as small as possible because there will be a lot of them.\n#[derive(Debug, "
  },
  {
    "path": "src/audio/midi_player.rs",
    "chars": 16810,
    "preview": "use crate::audio::FIRST_TICK;\nuse crate::audio::midi_builder::MidiBuilder;\nuse crate::audio::midi_event::MidiEventType;\n"
  },
  {
    "path": "src/audio/midi_player_params.rs",
    "chars": 1899,
    "preview": "use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};\n\nconst SOLO_NONE: i32 = -1;\n\n/// Playback parameters shared loc"
  },
  {
    "path": "src/audio/midi_sequencer.rs",
    "chars": 9689,
    "preview": "use crate::audio::midi_event::MidiEvent;\nuse std::time::Instant;\n\nconst QUARTER_TIME: f32 = 960.0; // 1 quarter note = 9"
  },
  {
    "path": "src/audio/mod.rs",
    "chars": 192,
    "preview": "pub mod midi_builder;\npub mod midi_event;\npub mod midi_player;\nmod midi_player_params;\npub mod midi_sequencer;\npub mod p"
  },
  {
    "path": "src/audio/playback_order.rs",
    "chars": 11323,
    "preview": "use crate::parser::song_parser::{MeasureHeader, QUARTER_TIME};\nuse std::collections::HashMap;\n\n/// Tracks the state of r"
  },
  {
    "path": "src/config.rs",
    "chars": 2553,
    "preview": "use std::{\n    env::home_dir,\n    fs::{File, create_dir_all},\n    io::{BufReader, Write},\n    path::PathBuf,\n};\n\nuse ser"
  },
  {
    "path": "src/main.rs",
    "chars": 2952,
    "preview": "use crate::RuxError::ConfigError;\nuse crate::ui::application::RuxApplication;\nuse clap::Parser;\nuse config::Config;\nuse "
  },
  {
    "path": "src/parser/mod.rs",
    "chars": 88,
    "preview": "mod music_parser;\nmod primitive_parser;\npub mod song_parser;\npub mod song_parser_tests;\n"
  },
  {
    "path": "src/parser/music_parser.rs",
    "chars": 18859,
    "preview": "use crate::parser::primitive_parser::{\n    parse_byte_size_string, parse_i8, parse_int, parse_int_byte_sized_string, par"
  },
  {
    "path": "src/parser/primitive_parser.rs",
    "chars": 3730,
    "preview": "use encoding_rs::WINDOWS_1252;\nuse nom::combinator::{flat_map, map};\nuse nom::{IResult, Parser, bytes, number};\n\n/// Par"
  },
  {
    "path": "src/parser/song_parser.rs",
    "chars": 48205,
    "preview": "use crate::RuxError;\nuse crate::parser::music_parser::MusicParser;\nuse crate::parser::primitive_parser::{\n    parse_bool"
  },
  {
    "path": "src/parser/song_parser_tests.rs",
    "chars": 31572,
    "preview": "#[cfg(test)]\nuse crate::RuxError;\n#[cfg(test)]\nuse crate::parser::song_parser::{Song, parse_gp_data};\n#[cfg(test)]\nuse s"
  },
  {
    "path": "src/ui/application.rs",
    "chars": 30407,
    "preview": "use iced::advanced::text::Shaping::Auto;\nuse iced::widget::operation::scroll_to;\nuse iced::widget::space::horizontal;\nus"
  },
  {
    "path": "src/ui/canvas_measure.rs",
    "chars": 31733,
    "preview": "use crate::parser::song_parser::{\n    Beat, BeatStrokeDirection, HarmonicType, Note, NoteEffect, NoteType, SlapEffect, S"
  },
  {
    "path": "src/ui/icons.rs",
    "chars": 688,
    "preview": "//! Icons coming from <https://fontello.com/>\n\nuse iced::widget::text;\nuse iced::{Element, Font};\n\npub fn open_icon<'a, "
  },
  {
    "path": "src/ui/mod.rs",
    "chars": 102,
    "preview": "pub mod application;\nmod canvas_measure;\nmod icons;\nmod picker;\nmod tablature;\nmod tuning;\nmod utils;\n"
  },
  {
    "path": "src/ui/picker.rs",
    "chars": 2011,
    "preview": "use std::path::PathBuf;\n\n#[derive(Debug, Clone, thiserror::Error)]\npub enum FilePickerError {\n    #[error(\"dialog window"
  },
  {
    "path": "src/ui/tablature.rs",
    "chars": 14331,
    "preview": "use crate::parser::song_parser::Song;\nuse crate::ui::application::Message;\nuse crate::ui::canvas_measure::CanvasMeasure;"
  },
  {
    "path": "src/ui/tuning.rs",
    "chars": 7108,
    "preview": "/// Returns a human-readable tuning label for a stringed track.\n/// Returns `None` for tracks with no strings (non-strin"
  },
  {
    "path": "src/ui/utils.rs",
    "chars": 2577,
    "preview": "use crate::ui::application::Message;\nuse iced::widget::{\n    Container, Text, button, center, container, mouse_area, opa"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the agourlay/ruxguitar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (311.8 KB), approximately 77.8k tokens, and a symbol index with 439 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!