[
  {
    "path": ".all-contributorsrc",
    "content": "{\n  \"files\": [\n    \"README.md\"\n  ],\n  \"imageSize\": 50,\n  \"commit\": false,\n  \"contributors\": [\n    {\n      \"login\": \"nickberry17\",\n      \"name\": \"nickberry17\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/18670565?v=4\",\n      \"profile\": \"https://github.com/nickberry17\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"zacanger\",\n      \"name\": \"zacanger\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/12520493?v=4\",\n      \"profile\": \"https://github.com/zacanger\",\n      \"contributions\": [\n        \"bug\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"ttaschke\",\n      \"name\": \"Tim\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/7067750?v=4\",\n      \"profile\": \"https://github.com/ttaschke\",\n      \"contributions\": [\n        \"doc\",\n        \"code\",\n        \"maintenance\"\n      ]\n    }\n  ],\n  \"contributorsPerLine\": 7,\n  \"projectName\": \"PyFLP\",\n  \"projectOwner\": \"demberto\",\n  \"repoType\": \"github\",\n  \"repoHost\": \"https://github.com\",\n  \"skipCi\": true\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\ncharset = utf-8\ninsert_final_newline = true\nindent_size = 2\nindent_style = space\ntrim_trailing_whitespace = true\n\n# 4 space indentation\n[*.{cfg,py}]\nindent_size = 4\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: File a bug report\ntitle: \"🐞 \"\nlabels: [\"bug\"]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the issue\n      description: A clear and a concise description of what happened.\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: What version of PyFLP are you using?\n    validations:\n      required: true\n  - type: textarea\n    id: code\n    attributes:\n      label: What code caused this issue?\n      render: python3\n    validations:\n      required: true\n  - type: textarea\n    id: additional\n    attributes:\n      label: Screenshots, Additional info\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Code of Conduct\n      description: By submitting this issue, you agree to follow PyFLP's [Code of Conduct](https://github.com/demberto/PyFLP/blob/master/CODE_OF_CONDUCT.md)\n      options:\n        - label: I agree to follow this project's Code of Conduct\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "contact_links:\n  - name: Discussions\n    url: https://github.com/demberto/PyFLP/discussions\n    about: Please ask and answer questions here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: ✨ I want a new feature\ntitle: \"✨ \"\nlabels: [\"enhancement\"]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the feature\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: What version of PyFLP are you using?\n    validations:\n      required: true\n  - type: textarea\n    id: additional\n    attributes:\n      label: Screenshots, Additional info\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Code of Conduct\n      description: By submitting this issue, you agree to follow PyFLP's [Code of Conduct](https://github.com/demberto/PyFLP/blob/master/CODE_OF_CONDUCT.md)\n      options:\n        - label: I agree to follow this project's Code of Conduct\n          required: true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: pip\n    directory: / # Location of package manifests\n    schedule:\n      interval: weekly\n    assignees:\n      - demberto\n    ignore:\n      - dependency-name: m2r2\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ \"master\" ]\n  schedule:\n    - cron: '36 19 * * 2'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'python' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n    #   If the Autobuild fails above, remove it and uncomment the following three lines.\n    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n    # - run: |\n    #   echo \"Run, Build Application using script\"\n    #   ./location_of_script_within_repo/buildscript.sh\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: publish\n\non:\n  push:\n    tags:\n      - v*\n  workflow_dispatch:\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.x\"\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install build twine\n      - name: Build package\n        run: python -m build\n      - name: Twine check\n        run: twine check dist/*\n      - name: Get changelog for release\n        id: changelog\n        uses: mindsers/changelog-reader-action@v2\n      - name: Create release\n        uses: ncipollo/release-action@v1\n        with:\n          artifacts: \"dist/*\"\n          body: ${{ steps.changelog.outputs.changes }}\n      - name: Publish package\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          user: __token__\n          password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        python-version:\n          [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"pypy3.8\", \"pypy3.9\"]\n        os: [\"macos-latest\", \"windows-latest\", \"ubuntu-latest\"]\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: \"pip\"\n      - name: Install dependencies\n        run: |\n          python -m pip install -U pip\n          pip install tox tox-gh\n      - name: Test with tox\n        run: tox\n      - name: Upload coverage artifacts\n        uses: actions/upload-artifact@v3\n        with:\n          name: coverage-artifacts\n          path: ./.coverage.*\n  upload-to-codecov:\n    needs: test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python 3.10\n        uses: actions/setup-python@v4\n        with:\n          python-version: \"3.10\"\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install coverage[toml]\n      - name: Download artifacts\n        uses: actions/download-artifact@v3\n        with:\n          name: coverage-artifacts\n      - name: Coverage data preparation for shitty codecov\n        run: coverage combine\n      - name: Upload to Codecov\n        uses: codecov/codecov-action@v3\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# setuptools_scm\npyflp/_version.py\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\ndocs/_images/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# Ruff\n.ruff_cache/\n\n# Just easier than running tests\nmain.py\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.4.0\n    hooks:\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n      - id: check-toml\n      - id: check-yaml\n      - id: check-added-large-files\n      - id: requirements-txt-fixer\n      - id: check-vcs-permalinks\n  - repo: https://github.com/psf/black\n    rev: 23.3.0\n    hooks:\n      - id: black\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.0.277\n    hooks:\n      - id: ruff\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.9.0\n    hooks:\n      - id: pyupgrade\n        args: [\"--py38-plus\"]\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the version of Python and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3\"\n\n# Build documentation in this directory with Sphinx\nsphinx:\n  configuration: docs/conf.py\n\n# Optionally declare the Python requirements required to build your docs\npython:\n  install:\n    - path: . # Required by importlib.version for Sphinx (setuptools_scm)\n    - requirements: docs/requirements.txt\n    - requirements: requirements.txt\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"aaron-bond.better-comments\", // Highlighting annotated comments\n    \"bierner.markdown-preview-github-styles\", // Github-style markdown preview\n    \"charliermarsh.ruff\", // Ruff, btw\n    \"DavidAnson.vscode-markdownlint\", // Lint markdown files, just like code\n    \"EditorConfig.EditorConfig\", // For .editorconfig\n    \"leonhard-s.python-sphinx-highlight\", // Highlight RST elements in docstrings\n    \"ms-vscode.hexeditor\", // Useful if you don't have a hex editor\n    \"ms-python.python\", // Pyright, docstrings\n    \"njpwerner.autodocstring\", // Create docstrings quickly\n    \"redhat.vscode-yaml\", // For YAML files\n    \"swyddfa.esbonio\",  // RST language server\n    \"tamasfe.even-better-toml\", // For pyproject.toml\n    \"trond-snekvik.simple-rst\", // Sphinx rST docs\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "// Suggested settings for contributors using VSCode.\n{\n  \"git.enableCommitSigning\": true, // nice \"Verified\" badges @ GH\n  \"python.analysis.autoImportCompletions\": false, // almost always wrong\n  \"python.analysis.importFormat\": \"relative\", // from .module import ...\n  \"python.analysis.inlayHints.functionReturnTypes\": true, // time saver\n  \"python.analysis.inlayHints.variableTypes\": false, // almost a PITA\n  \"python.analysis.typeCheckingMode\": \"strict\", // pylance strict mode\n  \"python.formatting.provider\": \"black\", // the best out there\n  \"python.languageServer\": \"Pylance\", // obviously\n  \"python.linting.enabled\": true,\n  \"python.linting.mypyEnabled\": true, // ah ofc this shitty typechecker\n  \"python.terminal.activateEnvironment\": false, // venvs don't deactivate properly\n  \"python.terminal.activateEnvInCurrentTerminal\": true, // save my time instead\n  \"python.testing.pytestEnabled\": true, // use pytest, and...\n  \"python.testing.unittestEnabled\": false // not unittest\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=733558\n  // for the documentation about the tasks.json format\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"coverage: all\",\n      \"type\": \"shell\",\n      \"command\": \"coverage run -m pytest && coverage combine && coverage report && coverage html && start htmlcov/index.html\",\n      \"problemMatcher\": []\n    },\n    {\n      \"label\": \"sphinx: clean run\",\n      \"type\": \"shell\",\n      \"command\": \"./docs/make.bat clean && ./docs/make.bat html && start ./docs/_build/index.html\",\n      \"problemMatcher\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<!-- markdownlint-disable no-duplicate-heading -->\n<!-- markdownlint-disable link-image-reference-definitions -->\n\n# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [2.2.1] - 2023-06-05\n\n### Fixed\n\n- Python 3.8 compatibility.\n\n## [2.2.0] - 2023-05-28\n\n### Changed\n\n- All event parsing happens during `pyflp.parse` itself.\n- `colour.Color` replaced with `pyflp.types.RGBA`.\n- Increase *line-length* of 100.\n\n### Fixed\n\n- Backtracking issues in nested dictionaries.\n\n### Removed\n\n- Python 3.7 support.\n- Bunch of intermediate `EventBase` subclasses.\n- Removed dependency on `colour` library.\n\n## [2.1.1] - 2023-05-24\n\n### Changed\n\n- Refactored `VSTPluginEvent` sub-event handling into `_VSTPluginProp`.\n- All `VSTPluginEvent` string sub-events decoded as UTF8.\n\n### Fixed\n\n- `VSTPlugin.name` encoded in UTF8 [#150].\n\n[#150]: https://github.com/demberto/PyFLP/issues/150\n\n## [2.1.0] - 2023-04-18\n\n### Added\n\n- Plugin data parsers: `FruitKick` and `Plucked`.\n- `ArrangementsID.PLSelection` [#132].\n\n### Changed\n\n- Unbound descriptors return `self` - more `property`*esque* behaviour.\n  This is primarily done to allow `flpinspect` to inspect descriptor types.\n- Moved `Sampler.pitch_shift` upto its base class `_SamplerInstrument`.\n\n### Deprecated\n\n- `ArrangementID.LoopPos` [#132].\n\n[#132]: https://github.com/demberto/PyFLP/issues/132\n\n## [2.0.0] - 2023-03-18\n\nWelcome PyFLP 2.0 🎉\nRead the previous changelogs to get the complete list of changes.\n\n### Added\n\n- `FruityBloodOverdrive` - thanks to @@ttaschke [#120].\n\n### Changed\n\n- Docs are way more easier to navigate now.\n\n### Fixed\n\n- `VSTPluginEvent.__setitem__` and `_VSTPluginProp._set` [#113].\n\n### Removed\n\n- Support for PyPy 3.7 (unable to run tox, cannot find a download).\n\n[#113]: https://github.com/demberto/PyFLP/issues/113\n[#120]: https://github.com/demberto/PyFLP/pull/120\n\n## [2.0.0a7] - 2022-12-19\n\n### Added\n\n- `Pattern` timemarkers [#27].\n- Low-level API support for FL Studio 21's `PlaylistEvent` [#108].\n\n### Changed\n\n- Renamed `PlaylistEvent.track_index` to `PlaylistEvent.track_rvidx`.\n- Optimized `Arrangement.tracks` iteration logic - 50% lesser time to run tests.\n- `StructEventBase.value` raises `NotImplementedError`.\n- Ambiguous `Pattern.__iter__` refactored into a property `Pattern.notes`.\n- `Pattern.index` renamed to `Pattern.iid`.\n- Improved `__repr__` strings; replaced with `ModelReprMixin` at some places\n  use `__str__` for a more human readable representation.\n\n### Fixed\n\n- `Patterns.__getitem__` didn't work with pattern names as documented.\n\n### Removed\n\n- Ambiguous `__index__` methods from a bunch of model classes.\n- Unimplemented `Slot.controllers`.\n\n[#27]: https://github.com/demberto/PyFLP/issues/27\n[#108]: https://github.com/demberto/PyFLP/issues/108\n\n## [2.0.0a6] - 2022-11-19\n\n### Added\n\n- `Keyboard.main_pitch`, `Keyboard.add_root`, `Keyboard.key_region` [#92].\n- `Sampler.filter` and `Filter` [#99].\n\n### Changed\n\n- `Channel.group` becomes a read-only property (modify event to change channel group).\n- `PLItemBase.offsets` and its fields in `PlaylistEvent` are [float32](https://stackoverflow.com/a/74247360/)\n  Thanks to `chrslg` from Stackoverflow and @jubabrut.\n- `Track.height` returns an `str` of its percentage e.g. `100%`.\n- `Instrument.plugin` and `Slot.plugin` return `_PluginBase` for unimplemented\n  native plugins [#102].\n- Reimplemented `EventTree` to use a list and got a 10+% perf boost in unit tests.\n\n### Fixed\n\n- `Channel.group` remained unitialised [#100].\n- `Chanel.plugin` failed due to base class type parameter check [#101].\n\n### Removed\n\n- `Track.locked_height` as what this quantity stores is unknown to me yet.\n- Use of fixture factories in unittests [#74].\n\n[#74]: https://github.com/demberto/PyFLP/issues/74\n[#92]: https://github.com/demberto/PyFLP/issues/92\n[#99]: https://github.com/demberto/PyFLP/issues/99\n[#100]: https://github.com/demberto/PyFLP/issues/100\n[#101]: https://github.com/demberto/PyFLP/issues/101\n[#102]: https://github.com/demberto/PyFLP/issues/102\n\n## [2.0.0a5.post] - 2022-10-31\n\n### Changed\n\n- Upgrade `construct-typing` to 0.5.3.\n\n## [2.0.0a5] - 2022-10-28\n\n### Added\n\n- Implementation for `Channel` and `Pattern` playlist items [#84].\n- `FX.remove_dc`, `FX.trim`, `FX.fix_trim`, `FX.crossfade`,\n  `FX.length`, `FX.normalize`, `FX.inverted`, `FX.start` [#55].\n- Normalized linear values for certain properties, more user friendly to deal with.\n  The required encode / decode is done at event level itself.\n- `TimeStretching.time`, `TimeStretching.pitch`, `TimeStretching.multiplier` [#87].\n- (Undiscovered) `MIDIControllerEvent`.\n- `Delay.mod_x`, `Delay.mod_y`, `Delay.fat_mode` and `Delay.ping_pong` [#88].\n- Improve enum performance by using `f-enum` library (`pyflp.parse` is 50% faster).\n- `Time.gate`, `Time.shift` and `Time.full_porta` [#89].\n- *Experimental* Python 3.11 support is back.\n- A shit ton of flags in `VSTPlugin` and refactoring [#95].\n- `WrapperEvent.page`, `WrapperEvent.height`, `WrapperEvent.width` [#93].\n- `ItemModel.__setitem__` propagates back changes to owner event [#97].\n\n### Changed\n\n- `PlaylistItemBase.offsets` now returns start and end offsets.\n- Use git commit for `construct-typing` which has fixed certain bugs.\n- Rename `PlaylistItemBase` to `PLItemBase` and `PatternPlaylistItem` to `PatternPLItem`.\n- Rename `Polyphony` members `is_mono` to `mono` and `is_porta` to `porta`.\n- `NoModelsFound` also bases `LookupError` now.\n- Compiled `VSTPluginEvent.STRUCT`.\n\n### Fixed\n\n- `EventTree.divide` fails to yield the only element [#90].\n- `TrackID.Name` events were grouped instead of getting divided [#96].\n- `PropBase.__set__` always raises `PropertyCannotBeSet` [#97].\n\n### Removed\n\n- `PlaylistItemBase.start_offset` and `PlaylistItemBase.end_offset`.\n- Redundant exceptions `ExpectedValue`, `UnexpectedType`.\n- Undiscovered `num_inputs`, `num_outputs` and `vst_number` from `VSTPlugin`.\n\n[#55]: https://github.com/demberto/PyFLP/issues/55\n[#84]: https://github.com/demberto/PyFLP/issues/84\n[#87]: https://github.com/demberto/PyFLP/issues/87\n[#88]: https://github.com/demberto/PyFLP/issues/88\n[#89]: https://github.com/demberto/PyFLP/issues/89\n[#90]: https://github.com/demberto/PyFLP/issues/90\n[#93]: https://github.com/demberto/PyFLP/issues/93\n[#95]: https://github.com/demberto/PyFLP/issues/95\n[#96]: https://github.com/demberto/PyFLP/issues/96\n[#97]: https://github.com/demberto/PyFLP/issues/97\n\n## [2.0.0a4] - 2022-10-22\n\nThe way models were passed events has changed. I designed a new data structure\ncalled `EventTree` (check `pyflp._events`) to allow the insertion and\ndeletion of events like a list while preserving the speed of a dict lookups.\n\nSounds *awfully* like `multidict` except that it doesn't allow mutable views.\n`EventTree` knows its parents and any attempt to insert or delete an event\nfrom it will also affect its parents *and vice-versa*. Took quite some to do.\n\n`EventTree` will allow for insertion / removal of events when corresponding\ndescriptor setters / deleters (yet to implement) are invoked. This can allow\nfor wonderful things like creating new channels, moving inserts etc.\n\n### Added\n\n- A multidict with mutable dict view `EventTree`.\n- PyPy 3.7+ support [#77].\n- Slicing for ModelBase collections [#31].\n- Fruity Center parser [#42].\n- Dependency on `sortedcontainers` library for `EventTree`.\n- Remaining and some new images for docstrings [#47].\n- GUI locations of descriptors (w.r.t. FL 20.8.4) [#80].\n\n### Changed\n\n- Simplified some `__repr__` strings.\n- Event IDs are all `EventEnum` members (better repr-strings).\n- PyFLP is guaranteed to be not thread-safe.\n- Moved up `Sampler.cut_group` to `_SamplerInstrument`.\n\n### Fixed\n\n- `ModelReprMixin`.\n\n### Removed\n\n- `Track.index` in favour of the redundant `Track.__index__`.\n- `Track.items`. Iterate over a track, to get them now.\n- Subclassing of protocol classes keeping [PEP544] in mind [#50].\n- Models are no longer hashable as events were made unhashable previously.\n- Commented out currently unimplemented `Channel.controllers`.\n\n[#31]: https://github.com/demberto/PyFLP/issues/31\n[#42]: https://github.com/demberto/PyFLP/discussions/42\n[#47]: https://github.com/demberto/PyFLP/issues/47\n[#50]: https://github.com/demberto/PyFLP/issues/50\n[#77]: https://github.com/demberto/PyFLP/issues/77\n[#80]: https://github.com/demberto/PyFLP/issues/80\n[PEP544]: https://peps.python.org/pep-0544\n\n## [2.0.0a3] - 2022-10-08\n\n### Added\n\n- 100% mypy tested *for all you mypy geeks*. It makes me play cat-and-mouse.\n- `Automation` points and LFO, via [#29].\n\n### Changed\n\n- All `StructBaseEvent` classes overhauled to use the `construct` library.\n- `EventBase.__len__` is now `EventBase.size`, a property.\n- Shift all subclass event parsing to `PODEventBase`.\n- Replace all uses of `bytesioex` with equivalents from `construct`.\n- Struct definitions moved to `StructEventBase` itself.\n- Enums used in structs directly now inherit from `construct_typed.EnumBase`.\n- `LFO` renamed to `SamplerLFO` to be distinguishable from `AutomationLFO`.\n\n### Fixed\n\n- `InsertEQ` was't working [#46].\n- Negative `FileFormat` weren't being read.\n- Incorrect event size calculation in `StructEventBase` [#72].\n- `Pattern.__repr__` failed for empty patterns.\n\n### Removed\n\n- `_StructMeta` (voodoo magic) and `StructBase` from `pyflp._events`.\n- `SoundgoodizerMode`, `FruityFastDistKind`, `StereoEnhancerInvertPosition`,\n  `StereoEnhancerEffectPosition` from `pyflp.plugin` in favour of equivalent\n  string literals.\n- Protocol subclassing of `EventBase` hierarchy.\n- Faulty `EventBase.__hash__`.\n- Python 3.11 support due to <https://github.com/timrid/construct-typing/issues/15>\n- Incomplete support for `Sequence` in model collections.\n\n[#29]: https://github.com/demberto/PyFLP/issues/29\n[#46]: https://github.com/demberto/PyFLP/issues/46\n[#72]: https://github.com/demberto/PyFLP/issues/72\n\n## [2.0.0a2] - 2022-10-01\n\n### Added\n\n- `FX.clip`, `FX.fade_stereo`, `FX.freq_tilt`, `FX.pogo`, `FX.ringmod`,\n  `FX.swap_stereo` & `FX.reverse` [#55].\n- `TimeStretching.mode` and `StretchMode` [#56].\n- `Playback.start_offset` [#57].\n- `Content.declick_mode` and `DeclickMode` [#58].\n- User guide and contibutor's guide.\n- Official support for Python 3.11.\n- Super basic `__repr__` for `StructBase` to ease debugging.\n- `Envelope.amount`, `Envelope.synced`, `LFO.amount`,\n  `LFO.attack`, `LFO.predelay` & `LFO.speed` [#69].\n\n### Changed\n\n- Moved `stretching` to `Sampler`, instruments don't have it.\n- `Note.key` now returns a note name with octave [#66].\n- A cleaner implementation of `MixerParamsEvent`.\n- `Layer.__repr__` now shows the number of children also.\n- Separated test assets into presets for better isolation of results [#6].\n- Renamed `LFO.is_synced` to `LFO.synced` and `LFO.is_retrig` to `LFO.retring`.\n- `StructBase` and `ListEventBase` are lazily evaluated now.\n- Model collections are indexable by item names as well [#45].\n\n### Fixed\n\n- String are decoded as UTF16 when version is 11.5+ now [#65].\n- `Insert.stereo_separation` docstring for maximum, minimum value.\n- `U16TupleEvent.value` [#68].\n- Minimum and maximum value docstrings for certain `FX` properties.\n- `Sampler.pitch_shift` internal representation.\n\n### Removed\n\n- Images for individual FX properties as they were redundant.\n- Redundant member `_SamplerInstrument.flags`.\n\n[#6]: https://github.com/demberto/PyFLP/issues/6\n[#45]: https://github.com/demberto/PyFLP/issues/45\n[#55]: https://github.com/demberto/PyFLP/issues/55\n[#56]: https://github.com/demberto/PyFLP/issues/56\n[#57]: https://github.com/demberto/PyFLP/issues/57\n[#58]: https://github.com/demberto/PyFLP/issues/58\n[#65]: https://github.com/demberto/PyFLP/issues/65\n[#66]: https://github.com/demberto/PyFLP/issues/66\n[#68]: https://github.com/demberto/PyFLP/issues/68\n[#69]: https://github.com/demberto/PyFLP/issues/69\n\n## [2.0.0a1] - 2022-09-21\n\n### Added\n\n- `PlaylistItemBase.group` for `ChannelPlaylistItem` and `PatternPlaylistItem` [#36].\n- More info in contributor's guide.\n- VSCode Python extension configuration, recommended extensions and tasks.\n- `ChannelRack.height` which tells the height of the channel rack in pixels.\n- `Track[x]` returns `Track.items[x]`.\n- `Patterns` warns when tried to be accessed with an index of 0.\n- `Note.group`, a number which notes of the same group share [#28].\n- `Note.slide` which indicates whether a note is a sliding note.\n- Plugin wrapper properties to docs.\n- A user guide section in docs.\n- `Sampler.content`, `Layer.random` & `Layer.crossfade` [#24].\n- `Playback.ping_pong_loop`.\n\n### Changed\n\n- `Pattern.notes` refactored into `Pattern.__iter__`.\n- `Sampler.sample_path` returns `pathlib.Path` instead of `str` now [#41].\n- `PluginID.Data` events get parsed during event collection itself.\n- All models are now equatable and hashable.\n\n### Fixed\n\n- `Arrangement` parsing logic is incorrect [#32].\n- `Track.color` returns `int` instead of `colour.Color` [#33].\n- `_PlaylistItemStruct.track_index` should be 2 bytes [#36].\n- Tracks don't get assigned playlist items [#37].\n- KeyError when accessing `Track.content_locked` [#38].\n- Channel type wasn't correctly detected at times [#40].\n- `Arrangements.height` was actually `ChannelRack.height` [#43].\n- TypeError when accessing `Insert.dock` [#44].\n- `Pattern.note` and `Pattern.controllers` [#48].\n- `Track.items` [#49]\n- Certain properties of `Note` were interpreted incorrectly.\n- `Slot.plugin` wasn't working at all (events, properties, repr) [#53].\n- `FruitySend.send_to` was interepreted incorrectly.\n- `Instrument.plugin` and `Slot.plugin` setter.\n- `Playback.use_loop_points`.\n\n### Removed\n\n- `Arrangements.height`.\n\n[#24]: https://github.com/demberto/PyFLP/issues/24\n[#28]: https://github.com/demberto/PyFLP/issues/28\n[#32]: https://github.com/demberto/PyFLP/issues/32\n[#33]: https://github.com/demberto/PyFLP/issues/33\n[#36]: https://github.com/demberto/PyFLP/issues/36\n[#37]: https://github.com/demberto/PyFLP/issues/37\n[#38]: https://github.com/demberto/PyFLP/issues/38\n[#40]: https://github.com/demberto/PyFLP/issues/40\n[#41]: https://github.com/demberto/PyFLP/issues/41\n[#43]: https://github.com/demberto/PyFLP/issues/43\n[#44]: https://github.com/demberto/PyFLP/issues/44\n[#48]: https://github.com/demberto/PyFLP/issues/48\n[#49]: https://github.com/demberto/PyFLP/issues/49\n[#53]: https://github.com/demberto/PyFLP/issues/53\n\n## [2.0.0a0] - 2022-09-14\n\nPyFLP has been rewritten ✨\n\nHighlights:\n\n1. Richer events: Variable data events now parse their structure themselves.\n   Fixed size events are categorized closely to the data they represent.\n2. Lazy evaluation: Properties are evaluated as lazily as possible to prevent\n   the use of private variables and keep them synced with event data.\n3. Neatly organised models: Appropriate use of composition and subclassing.\n4. Zero pre-parse field validation: Makes sense for an undocumented format.\n5. Fully type hinted: Ensures strict adherence with pyright.\n6. Simplified single-level module hierarchy to ease imports.\n7. Docs now contain images for corresponding model types.\n\n*The major version number bump indicates a breaking change, however I would highly\nencourage you to upgrade to this version. **I WILL NOT BE MAINTAINING OLDER VERSIONS.***\n\n## 1.1.2 - Unreleased\n\n### Fixed\n\n- [#9](https://github.com/demberto/PyFLP/pull/9), thanks to @zacanger.\n\n## [1.1.1] - 2022-07-10\n\n### Added\n\n- Avoid mkdocs warnings in tox.\n\n### Changed\n\n- `_FLObject._save` always returns a list now.\n- CI: Merge `dev` and `publish` workflows into one.\n\n### Fixed\n\n- [#8](https://github.com/demberto/PyFLP/issues/8).\n- Type hints and type variables are much better.\n- `FSoftClipper` property setter typo caused it to be set to zero.\n- `ChannelParameters._save()` didn't return an event.\n\n### Removed\n\n- Wait action in CI workflow.\n- `setup-cfg-fmt` pre-commit hook, [why?](https://github.com/asottile/setup-cfg-fmt/issues/147)\n\n## [1.1.0] - 2022-05-29\n\n### Added\n\n- Support for Fruity Stereo Enhancer @@nickberry17\n- Instructions for alternate methods to install PyFLP.\n\n### Changed\n\n- Improvements to CI\n\n### Fixed\n\n- Incorrect encoding used to dump UTF-16 strings in `_TextEvent`.\n- [#4](https://github.com/demberto/PyFLP/issues/4).\n\n### Removed\n\n- `_FLObject.max_count`, `MaxInstancesError`, `test_flobject.py` and\n  `_MaxInstancedFLObject`.\n- Gitter links from README and room itself, due to inactivity.\n\n## [1.0.1] - 2022-04-02\n\nThis update is more about QOL improvements, testing and refactoring. Few bugs\nhave been fixed as well, while Python 3.6 support has been deprecated.\n\n### Added\n\n- Adopted `bandit`.\n- `_MaxInstancedFLObject`: `FLObject` with a limit on number of instances.\n- GPL3 short license headers.\n- Missing docs about `PatternNote` and `PatternController` events.\n- Exceptions: `InvalidHeaderSizeError`, `InvalidMagicError` and `MaxInstancesError`.\n- Import statements in submodules to simplify import process externally.\n- Test validators and properties and project version setter.\n- OTT plugin to test project to test VST plugins.\n\n### Changed\n\n- All use of `assert` has been replaced by exceptions (bandit: assert-used).\n- Version links in changelog now show changes.\n- LF line endings used and enforced everywhere.\n- `ppq` field moved to `_FLObject` from `Playlist`.\n- Much improved `tox.ini` and pre-commit configuration.\n- Modules which aren't meant for external use are prefixed with a _.\n- Simplified property declaration.\n\n### Deprecated\n\n- Python 3.6 support will be dropped in a future major release.\n\n### Fixed\n\n- All this time, `VSTPluginEvent` was never getting created/saved.\n- Lint errors reported by flake8, pylint and bandit.\n- Just realised `__setattr__` works only on instances 😅, came up with\n  `_FLObjectMeta` which is the metaclass used by `_FLObject`.\n\n### Removed\n\n- Redundant `__repr__` from `PatternNote`.\n\n## [1.0.0] - 2021-11-12\n\n### **Highlights**\n\n- The entire module hierarchy of PyFLP has been simplified.\n- Internal/abstract base classes have bee renamed to start with _.\n- `repr` for `_FLObject` subclasses.\n- The way properties are handled is now completely changed.\n- Data events get parsed by a `DataEvent` subclass.\n- Way better testing, with a coverage of whooping 79%.\n- `color` properties now return a `colour.Color` object.\n- Almost everything has a docstring now, even enum members.\n- PyFLP has adopted Contributor Covenant Code of Conduct v2.1.\n\n### Added\n\n- `__repr__()` for all `_FLObject` subclasses.\n- `Channel.color`, `Insert.color` and `Pattern.color` now return `colour.Color`.\n  This is implemented by `ColorEvent` (*which subclasses `DWordEvent`*).\n- New event implementations for `ChannelFX.EventID` (`Cutoff`, `Fadein`, `Fadeout` and more).\n- New event implementations for `Channel.EventID` (`ChannelTracking`,\n  `ChannelLevels`, `ChannelLevelOffsets`, `ChannelPolyphony` and more).\n- `Channel.cut_group` implementing `Channel.EventID.CutSelfCutBy`.\n- Remote controllers (`RemoteController`). Accessible from `Project.controllers`.\n- Saving for `VSTPlugin`.\n- All enum members used by `FLObject` subclasses now have a docstring.\n- Added links in docstrings to official FL Studio Manual wherever possible.\n- `Parser.__build_event_store()` uses inner methods now to parse different kind of events; very helpful for the new `DataEvents`.\n- Added support for pattern controller events (`PatternController`, `PatternControllerEvent` who implement `PatternEventID.Controllers`).\n- Many attribute docstrings now include minimum, maximum and default values. **These limits are enforced by setters**.\n- Added `.editorconfig`, *using CRLF line endings btw*.\n- Added `test_parser.py` and `test_events.py`.\n- `Parser.parse_zip` now accepts a `bytes` object for `zip_file` parameter.\n- `Misc.registered` for `Misc.EventID.Registered`.\n\n### Changed\n\n- All `_FLObject` subclasses have been moved to parent `pyflp/` from `pyflp/flobject/` to ease import names.\n- All `Event` subclasses have been moved in a single `event.py` and `event/` folder is removed.\n- All event ID enum names are now inner classes of `_FLObject` subclasses.\n- Constructor of `Project` has been simplified.\n- `VSTPlugin`'s underlying event now supports saving, it has been refactored out of `_parse_data_event` also.\n- `InsertParametersEvent` to replace the equivalent parsing in `Insert._parse_data_event`.\n- The `TODO` *(deleted now)* has been changed to reflect the type of goals.\n- `_FLObject.save` is now `_FLObject._save`.\n- Some constants present in `utils.py` have been moved to `constants.py`.\n- Docs include a brief summary of the underlying data event wherever applicable.\n- Minor property name changes; made them more concise.\n- Absolute imports are used everywhere now.\n\n### Fixed\n\n- `ChannelFXReverb` was not getting initialised.\n- `InsertParamsEvent` was not getting initialised.\n- Syntax is highlighted in the [docs](https://pyflp.rtfd.io/) as expected now.\n- `FNotebook2` text parsing.\n- `Insert.routing` returned `True` for all tracks.\n- `Misc.start_date` and `Misc.work_time` parsing.\n\n### Removed\n\n- Any and all sort of logging, not useful anymore. Haven't seen any 3rd party\n  Python library ever using it. Used `warnings` wherever necessary.\n- `mypy`. Its useless tbh, I will use types as I see fit.\n- Setters for all properties containing `_FLObject` (or any sort of a collection of them), *e.g. Arrangement.tracks*.\n\n## [0.2.0]\n\n### **Highlights**\n\n- **PyFLP has passed the null test for a full project of mine (FL 20.7.2) 🥳**.\n- This library uses code from [FLParser](https://github.com/monadgroup/FLParser),\n  a GPL license project, PyFLP is now under GPL.\n- API reference documentation is complete now.\n- Few new events implemented for `Channel`.\n- Refactored `FLObject` and `Plugin`.\n\n#### `FLObject` refactoring\n\n- `parseprop` is now `_parseprop`.\n- All `_parseprop` delegates are now \"protected\" as well.\n- `setprop` is now `_setprop`.\n\n### Added\n\n- `ChannelEvent.Delay` is implemented by `ChannelDelay` and `Channel.delay`.\n- `Event.to_raw` and `Event.dump` now log when they are called.\n- Exceptions `DataCorruptionDetected` and `OperationNotPermitted`.\n\n### Fixed\n\n- Can definitely say, all naming inconsistencies have been fixed.\n- Fixed `TimeMarker` assign to `Arrangement` logic in `Parser`.\n- Extraneous data dumped sometimes by `InsertSlotEvent.Plugin`, caused due to\n  double dumping of same events.\n- Empty pattern events, `PatternEvent.Name` and `PatternEvent.Color` don't get saved.\n\n---\n\n❗ These versions below don't work due to naming inconsistencies 😅, you will not find them 👇\n\n## [0.1.2]\n\n### Added\n\n- More docs.\n- Add some new properties/events to `Channel`.\n- A sample empty FLP has been provided to allow running tests.\n- All `FLObject` subclasses now have a basic `__repr__` method.\n\n### Fixed\n\n- Improve the GitHub workflow action, uploads to PyPI will not happen unless the test is passed.\n- ~~Fix all naming inconsistencies caused due to migration to [`BytesIOEx`](https://github.com/demberto/BytesIOEx)~~ Not all.\n\n### Known issues\n\nSame as in 0.1.1\n\n## [0.1.1]\n\n~~The first version of PyFLP that works correctly 🥳~~ No, unfortunately\n\n### **Highlights**\n\n- Changed documentation from Sphinx to MkDocs.\n- [FLPInfo](https://github.com/demberto/FLPInfo) is now a separate package.\n- FLPInspect is now a separate package.\n- PyFLP now uses [BytesIOEx](https://github.com/demberto/BytesIOEx/) as an external dependency.\n\n### Fixed\n\n- `ByteEvent`, `WordEvent` and `DWordEvent` now raise a `TypeError`.\n  when they are initialised with the wrong size of data.\n- Fix setup.cfg, project structure is now as expected, imports will work.\n- [Docs](https://pyflp.rtfd.io/) are now up and running.\n\n### Known issues\n\n- Extraneous data dumped sometimes by `InsertSlotEvent.Plugin`, why this is caused is not known.\n\n---\n\n**❗ These versions below don't work because I didn't know how to configure `setup.cfg` properly 😅**\n\n## 0.1.0\n\n- `flpinspect` - An FLP Event Viewer made using Tkinter.\n- `flpinfo` - A CLI utility to get basic information about an FLP.\n- Switched to MIT License.\n\n### Added\n\n- Lots of changes, refactoring and code cleanup of `pyflp`.\n- New docs.\n- Changes to `README`.\n- Adopted [`black`](https://github.com/psf/black) coding style.\n- Added a `log_level` argument to `Parser`.\n- `Project.create_zip` copies stock samples as well now.\n- `Project.get_events` for getting just the events; they are not parsed.\n  Read [docs](https://pyflp.rtfd.io) for more info about this.\n- `Event` classes now have an `__eq__` and `__repr__` method.\n\n### Fixed\n\n- Tests don't give module import errors.\n- `Pattern` event parsing.\n- Initialise `_count` to 0, everytime `Parser` is initialised.\n- `Project.create_zip` now works as intended.\n- Overhauled logging.\n- A lot of potential bugs in `FLObject` subclasses.\n\n### Known issues\n\n- `flpinfo` doesn't output correctly sometimes due to long strings.\n- Extraneous data dumped sometimes by `InsertSlotEvent.Plugin`, why this is caused is not known.\n\n[2.2.1]: https://github.com/demberto/PyFLP/compare/v2.2.0...v2.2.1\n[2.2.0]: https://github.com/demberto/PyFLP/compare/v2.1.1...v2.2.0\n[2.1.1]: https://github.com/demberto/PyFLP/compare/v2.1.0...v2.1.1\n[2.1.0]: https://github.com/demberto/PyFLP/compare/v2.0.0...v2.1.0\n[2.0.0]: https://github.com/demberto/PyFLP/compare/v2.0.0a7.post0...v2.0.0\n[2.0.0a7]: https://github.com/demberto/PyFLP/compare/v2.0.0a6...v2.0.0a7\n[2.0.0a6]: https://github.com/demberto/PyFLP/compare/v2.0.0a5.post...v2.0.0a6\n[2.0.0a5.post]: https://github.com/demberto/PyFLP/compare/v2.0.0a5...v2.0.0a5.post\n[2.0.0a5]: https://github.com/demberto/PyFLP/compare/v2.0.0a4...v2.0.0a5\n[2.0.0a4]: https://github.com/demberto/PyFLP/compare/v2.0.0a3...v2.0.0a4\n[2.0.0a3]: https://github.com/demberto/PyFLP/compare/v2.0.0a2...v2.0.0a3\n[2.0.0a2]: https://github.com/demberto/PyFLP/compare/v2.0.0a1...v2.0.0a2\n[2.0.0a1]: https://github.com/demberto/PyFLP/compare/v2.0.0a0...v2.0.0a1\n[2.0.0a0]: https://github.com/demberto/PyFLP/compare/v1.1.1...v2.0.0a0\n[1.1.1]: https://github.com/demberto/PyFLP/compare/1.1.0...v1.1.1\n[1.1.0]: https://github.com/demberto/PyFLP/compare/1.0.1...1.1.0\n[1.0.1]: https://github.com/demberto/PyFLP/compare/1.0.0...1.0.1\n[1.0.0]: https://github.com/demberto/PyFLP/compare/0.2.0...1.0.0\n[0.2.0]: https://github.com/demberto/PyFLP/compare/0.1.2...0.2.0\n[0.1.2]: https://github.com/demberto/PyFLP/compare/0.1.1...0.1.2\n[0.1.1]: https://github.com/demberto/PyFLP/releases/tag/0.1.1\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n[demberto@protonmail.com](demberto@protonmail.com).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\nCommunity Impact Guidelines were inspired by\n[Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][FAQ]. Translations are available\nat [https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "prune .github\nexclude .all-contributorsrc\nexclude .gitignore\nexclude .pre-commit-config.yaml\nexclude .readthedocs.yaml\n"
  },
  {
    "path": "README.md",
    "content": "# PyFLP\n\nPyFLP is an unofficial parser for [FL Studio](https://www.image-line.com/fl-studio/)\nproject and preset files written in Python.\n\n<!-- SHIELDS -->\n<!-- markdownlint-disable -->\n<table>\n  <colgroup>\n    <col style=\"width: 10%;\"/>\n    <col style=\"width: 90%;\"/>\n  </colgroup>\n  <tbody>\n    <tr>\n      <th>CI</th>\n      <td>\n        <a href=\"https://pyflp.readthedocs.io/en/latest/\">\n          <img alt=\"Documentation Build Status\" src=\"https://img.shields.io/readthedocs/pyflp/latest?logo=read-the-docs\"/>\n        </a>\n        <a href=\"https://results.pre-commit.ci/latest/github/demberto/PyFLP/master\">\n          <img alt=\"pre-commit-ci\" src=\"https://results.pre-commit.ci/badge/github/demberto/PyFLP/master.svg\"/>\n        </a>\n      </td>\n    </tr>\n    <tr>\n      <th>PyPI</th>\n      <td>\n        <a href=\"https://pypi.org/project/PyFLP\">\n          <img alt=\"PyPI - Package Version\" src=\"https://img.shields.io/pypi/v/PyFLP\"/>\n        </a>\n        <a href=\"https://pypi.org/project/PyFLP\">\n          <img alt=\"PyPI - Supported Python Versions\" src=\"https://img.shields.io/pypi/pyversions/PyFLP?logo=python&amp;logoColor=white\"/>\n        </a>\n        <a href=\"https://pypi.org/project/PyFLP\">\n          <img alt=\"PyPI - Supported Implementations\" src=\"https://img.shields.io/pypi/implementation/PyFLP\"/>\n        </a>\n        <a href=\"https://pypi.org/project/PyFLP\">\n          <img alt=\"PyPI - Wheel\" src=\"https://img.shields.io/pypi/wheel/PyFLP\"/>\n        </a>\n      </td>\n    </tr>\n    <tr>\n      <th>Activity</th>\n      <td>\n        <img alt=\"Maintenance\" src=\"https://img.shields.io/maintenance/yes/2023\"/>\n        <a href=\"https://pypistats.org/packages/pyflp\">\n          <img alt=\"PyPI - Downloads\" src=\"https://img.shields.io/pypi/dm/PyFLP\"/>\n        </a>\n      </td>\n    </tr>\n    <tr>\n      <th>QA</th>\n      <td>\n        <a href=\"https://codecov.io/gh/demberto/PyFLP\">\n          <img alt=\"codecov\" src=\"https://codecov.io/gh/demberto/PyFLP/branch/master/graph/badge.svg?token=RGSRMMF8PF\"/>\n        </a>\n        <a href=\"https://codefactor.io/repository/github/demberto/PyFLP\">\n          <img alt=\"CodeFactor Grade\" src=\"https://img.shields.io/codefactor/grade/github/demberto/PyFLP?logo=codefactor\"/>\n        </a>\n        <a href=\"http://mypy-lang.org/\">\n          <img alt=\"Checked with mypy\" src=\"http://www.mypy-lang.org/static/mypy_badge.svg\">\n        </a>\n        <a href=\"https://github.com/pre-commit/pre-commit\">\n          <img alt=\"pre-commit\" src=\"https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&amp;logoColor=white\"/>\n        </a>\n        <a href=\"https://github.com/PyCQA/bandit\">\n          <img alt=\"Security Status\" src=\"https://img.shields.io/badge/security-bandit-yellow.svg\"/>\n        </a>\n      </td>\n    </tr>\n    <tr>\n      <th>Other</th>\n      <td>\n        <a href=\"https://github.com/demberto/PyFLP/blob/master/LICENSE\">\n          <img alt=\"License\" src=\"https://img.shields.io/github/license/demberto/PyFLP\"/>\n        </a>\n        <img alt=\"GitHub top language\" src=\"https://img.shields.io/github/languages/top/demberto/PyFLP\"/>\n        <a href=\"https://github.com/psf/black\">\n          <img alt=\"Code Style: Black\" src=\"https://img.shields.io/badge/code%20style-black-black\"/>\n        </a>\n        <a href=\"https://github.com/demberto/PyFLP/blob/master/CODE_OF_CONDUCT.md\">\n          <img alt=\"covenant\" src=\"https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg\"/>\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n<!-- markdownlint-restore -->\n\nFrom a very general point-of-view, this is the state of what is currently\nimplemented. Click on a link to go to the documentation for that feature.\n\n<!-- FEATURE TABLE -->\n<!-- markdownlint-disable -->\n<table>\n  <tr>\n    <th>Group</th>\n    <th>Feature</th>\n    <th>Issues</th>\n  </tr>\n  <tr>\n    <td rowspan=\"3\">\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/arrangements.html\">Arrangements</a><br/>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Aarrangement-general\">\n        <img alt=\"open arrangement-general issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/arrangement-general?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Aarrangement-general\">\n        <img alt=\"closed arrangement-general issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/arrangement-general?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/arrangements.html#playlist\">🎼 Playlist</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Aarrangement-playlist\">\n        <img alt=\"open arrangement-playlist issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/arrangement-playlist?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Aarrangement-playlist\">\n        <img alt=\"closed arrangement-playlist issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/arrangement-playlist?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr></tr> <!-- only for formatting --->\n  <tr>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/arrangements.html#track\">🎞️ Tracks</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Aarrangement-track\">\n        <img alt=\"open arrangement-track issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/arrangement-track?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Aarrangement-track\">\n        <img alt=\"closed arrangement-track issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/arrangement-track?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"4\">\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/channels.html\">Channel Rack</a><br/>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Achannel-general\">\n        <img alt=\"open channel-general issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/channel-general?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Achannel-general\">\n        <img alt=\"closed channel-general issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/channel-general?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/channels.html#pyflp.channel.Automation\">📈 Automations</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%channel-automation\">\n        <img alt=\"open channel-automation issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/channel-automation?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Achannel-automation\">\n        <img alt=\"closed channel-automation issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/channel-automation?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/channels.html#pyflp.channel.Instrument\">🎹 Instruments</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Achannel-instrument\">\n        <img alt=\"channel-instrument issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/channel-instrument?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Achannel-instrument\">\n        <img alt=\"closed channel-instrument issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/channel-instrument?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/channels.html#pyflp.channel.Layer\">📚 Layer</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Achannel-layer\">\n        <img alt=\"open channel-layer issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/channel-layer?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Achannel-layer\">\n        <img alt=\"closed channel-layer issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/channel-layer?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/channels.html#pyflp.channel.Sampler\">📁 Sampler</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Achannel-sampler\">\n        <img alt=\"open channel-sampler issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/channel-sampler?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Achannel-sampler\">\n        <img alt=\"closed channel-sampler issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/channel-sampler?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/mixer.html\">Mixer</a><br/>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Amixer-general\">\n        <img alt=\"open mixer-general issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/mixer-general?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Amixer-general\">\n        <img alt=\"closed mixer-general issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/mixer-general?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/mixer.html#pyflp.mixer.Insert\">🎚️ Inserts</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Amixer-insert\">\n        <img alt=\"open mixer-insert issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/mixer-insert?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Amixer-insert\">\n        <img alt=\"closed mixer-insert issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/mixer-insert?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n    <tr>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/mixer.html#pyflp.mixer.Slot\">🎰 Effect slots</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Amixer-slot\">\n        <img alt=\"open mixer-slot issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/mixer-slot?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Amixer-slot\">\n        <img alt=\"closed mixer-slot issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/mixer-slot?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"3\">\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/patterns.html\">🎶 Patterns</a><br/>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Apattern-general\">\n        <img alt=\"open pattern-general issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/pattern-general?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Apattern-general\">\n        <img alt=\"closed pattern-general issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/pattern-general?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/patterns.html#pyflp.pattern.Controller\">🎛 Controllers</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Apattern-controller\">\n        <img alt=\"open pattern-controller issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/pattern-controller?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Apattern-controller\">\n        <img alt=\"closed pattern-controller issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/pattern-controller?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n    <tr>\n    <td><a href=\"https://pyflp.readthedocs.io/en/latest/reference/patterns.html#pyflp.pattern.Note\">🎵 Notes</a></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Apattern-note\">\n        <img alt=\"open pattern-note issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/pattern-note?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Apattern-note\">\n        <img alt=\"closed pattern-note issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/pattern-note?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr></tr> <!-- for formatting --->\n  <tr>\n    <td>\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/timemarkers.html\">🚩 Timemarkers</a>\n    </td>\n    <td></td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Atimemarker\">\n        <img alt=\"open timemarker issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/timemarker?label=open&style=flat-square\"/>\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Atimemarker\">\n        <img alt=\"closed timemarker issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/timemarker?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/plugins.html\">Plugins</a>\n    </td>\n    <td>\n      Native -\n      8 <a href=\"https://pyflp.readthedocs.io/en/latest/reference/plugins.html#effects\">effects</a>,\n      1 <a href=\"https://pyflp.readthedocs.io/en/latest/reference/plugins.html#generators\">synth</a>\n    </td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin-native\">\n        <img alt=\"open plugin-native issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/plugin-native?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Aplugin-native\">\n        <img alt=\"closed plugin-native issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/plugin-native?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/plugins.html#pyflp.plugin.VSTPlugin\">VST 2/3</a>\n    </td>\n    <td>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin-3rdparty\">\n        <img alt=\"plugin-3rdparty issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/plugin-3rdparty?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Aplugin-3rdparty\">\n        <img alt=\"closed plugin-3rdparty issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/plugin-3rdparty?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\" colspan=\"2\">\n      <a href=\"https://pyflp.readthedocs.io/en/latest/reference/project.html\">Project</a>\n      - Settings and song metadata\n    </td>\n    <td colspan=\"2\">\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aopen+is%3Aissue+label%3Aproject-general\">\n        <img alt=\"open project-general issues\" src=\"https://img.shields.io/github/issues-raw/demberto/PyFLP/project-general?label=open&style=flat-square\">\n      </a>\n      <a href=\"https://github.com/demberto/PyFLP/issues?q=is%3Aclosed+is%3Aissue+label%3Aproject-general\">\n        <img alt=\"closed project-general issues\" src=\"https://img.shields.io/github/issues-closed-raw/demberto/PyFLP/project-general?label=closed&style=flat-square\"/>\n      </a>\n    </td>\n  </tr>\n</table>\n<!-- markdownlint-restore -->\n\n## ⏬ Installation\n\nCPython 3.8+ / PyPy 3.8+ required.\n\n```none\npython -m pip install -U pyflp\n```\n\n## ▶ Usage\n\n[Load](https://pyflp.readthedocs.io/en/latest/reference.html#pyflp.parse) a project file:\n\n```py\nimport pyflp\nproject = pyflp.parse(\"/path/to/parse.flp\")\n```\n\n> If you get any sort of errors or warnings while doing this, please open an\n> [issue](https://github.com/demberto/PyFLP/issues).\n\n[Save](https://pyflp.readthedocs.io/en/latest/reference.html#pyflp.save) the project:\n\n```py\npyflp.save(project, \"/path/to/save.flp\")\n```\n\n> It is advised to do a backup of your projects before doing any changes.\n> It is also recommended to open the modified project in FL Studio to ensure\n> that it works as intended.\n\nCheck the [reference](https://pyflp.rtfd.io/en/latest/reference.html) for a\ncomplete list of useable features.\n\n## 🙏 Acknowledgements\n\n- Monad.FLParser: <https://github.com/monadgroup/FLParser>\n- FLPEdit (repo deleted by [author](https://github.com/roadcrewworker))\n\n## ✨ Contributors\n\n<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->\n![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)\n<!-- ALL-CONTRIBUTORS-BADGE:END -->\n\nThanks goes to these wonderful people:\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\"><a href=\"https://github.com/nickberry17\"><img src=\"https://avatars.githubusercontent.com/u/18670565?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>nickberry17</b></sub></a><br /><a href=\"https://github.com/demberto/PyFLP/commits?author=nickberry17\" title=\"Code\">💻</a></td>\n      <td align=\"center\"><a href=\"https://github.com/zacanger\"><img src=\"https://avatars.githubusercontent.com/u/12520493?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>zacanger</b></sub></a><br /><a href=\"https://github.com/demberto/PyFLP/issues?q=author%3Azacanger\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/demberto/PyFLP/commits?author=zacanger\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\"><a href=\"https://github.com/ttaschke\"><img src=\"https://avatars.githubusercontent.com/u/7067750?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Tim</b></sub></a><br /><a href=\"https://github.com/demberto/PyFLP/commits?author=ttaschke\" title=\"Documentation\">📖</a> <a href=\"https://github.com/demberto/PyFLP/commits?author=ttaschke\" title=\"Code\">💻</a> <a href=\"#maintenance-ttaschke\" title=\"Maintenance\">🚧</a></td>\n    </tr>\n  </tbody>\n</table>\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\nThis project follows the [all-contributors](https://allcontributors.org/) specification.\nContributions of any kind are welcome!\n\nPlease see the [contributor's guide](https://pyflp.rtfd.io/en/latest/contributing.html)\nfor more information about contributing.\n\n## 📧 Contact\n\nYou can contact me either via [issues](https://github.com/demberto/PyFLP/issues)\nand [discussions](https://github.com/demberto/PyFLP/discussions) or through\nemail via ``demberto(at)proton(dot)me``.\n\n## © License\n\nThe code in this project has been licensed under the\n[GNU Public License v3](https://www.gnu.org/licenses/gpl-3.0.en.html).\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/architecture/flp-format.rst",
    "content": "Part I: FLP Format & Events\n===========================\n\nFLP is a binary format used by Image-Line FL Studio, a music production\nsoftware, to store project files. Instead of using C-style structs entirely,\nthe FLP format has evolved from what once was a MIDI-like format to a really\nbad and messy combination of :wikipedia:`Type-length-value` encoded \"events\"\nand structs.\n\nSpecification\n-------------\n\nAn FLP file contains of basically 2 sections or \"chunks\", one is the header\nand other is the \"data\" section, which contains all the \"events\".\n\nHeader chunk\n^^^^^^^^^^^^\n\n.. tab-set::\n\n   .. tab-item:: C / C++\n\n      .. code-block:: c\n\n         struct {\n             char magic[4];          // 'FLhd'\n             uint32_t size;          // always been 6\n             int16_t format;         // Internal file format\n             uint16_t num_channels;  // Number of channels in channel rack\n             uint16_t ppq;           // Pulses per quarter\n         }\n\n   .. tab-item:: Python\n\n      .. code-block:: python\n\n         class Header:\n             magic: str\n             size: int\n             format: int\n             num_channels: int\n             ppq: int\n\n.. currentmodule:: pyflp.project\n.. seealso::\n\n   :attr:`Project.format`, :attr:`Project.channel_count`, :attr:`Project.ppq`\n\nData chunk\n^^^^^^^^^^\n\n.. code-block:: c\n\n   struct {\n       char magic[4];  // 'FLdt'\n       uint32_t size;  // Total combined size of events\n       void* events;   // Event data\n   }\n\n.. _architecture-event:\n\nEvent\n-----\n\nAn event can be thought of as a \"flattened\" :class:`dict` of attributes\ncomposing a class. It can *roughly* be represented as:\n\n.. tab-set::\n\n   .. tab-item:: C / C++\n\n      .. code-block:: c\n\n         struct {\n             uint8_t type;\n             void* value;\n         }\n\n   .. tab-item:: Python\n\n      .. code-block:: python\n\n         class Event:\n             type: int\n             value: object\n\nTypes\n^^^^^\n\nThere are basically 4 kinds of events depending on the range of ``type``:\n\n+----------+--------------------+-----------------+\n| Event ID | Size of ``value`` | Total event size |\n+==========+===================+==================+\n| 0-63     | 1 byte            | 1 + 1 = **2**    |\n+----------+-------------------+------------------+\n| 64-127   | 2 bytes           | 1 + 2 = **3**    |\n+----------+-------------------+------------------+\n| 128-191  | 4 bytes           | 1 + 4 = **5**    |\n+----------+-------------------+------------------+\n| 192-255  | Length prefixed   | >= 2             |\n+----------+-------------------+------------------+\n\n.. note:: Length prefixed events\n\n   These events store the length of the ``value`` they contain after ``type``\n   in a varint. It can be considered as the only true TLV encoded event type.\n\n   .. code-block:: c\n\n      struct {\n          uint8_t type;     // 192-255\n          uint8_t* length;  // varint\n          void* value;      // string, struct or subevent\n      }\n\nIt should be clearer by now how the FLP format is a misfit for the data it\nrepresents.\n\nRepresentation\n^^^^^^^^^^^^^^\n\nEvent IDs 0-191 are used for storing fixed size data like integers, floats and\nbooleans. IDs from 192-255 are used for storing structs, subevents and strings.\n"
  },
  {
    "path": "docs/architecture/how-it-works.rst",
    "content": "Part II: How PyFLP works\n========================\n\n    💡 You should read Part I before this.\n\nPyFLP's entry-point :meth:`pyflp.parse` verifies the headers and parses all the\nevents. These events are collected into an :class:`pyflp._events.EventTree`.\n\nSchematic diagram\n-----------------\n\n.. svgbob::\n   :align: center\n\n    ┌──────────────────────────────────────────────────────────────────────────┐\n    │     Events - binary representation - low level API - Stage 1 parser      │\n    │                                                                          │\n    │ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────┐  │\n    │ │  Project-wide / 1-time  │ │      Per-instance       │ │    Shared   │  │\n    │ │┌─────────┐   ┌─────────┐│ │┌─────────┐   ┌─────────┐│ │ ┌─────────┐ │  │\n    │ ││ Event 1 │   │ Event 2 ││ ││ Event 3 │   │ Event 4 ││ │ │ Event 5 │ │  │\n    │ ││ id: 199 │ → │ id: 159 ││→││ id: 64  │ → │ id: 215 ││→│ │ id: 225 │ │  │\n    │ ││ string  │   │ integer ││ ││ integer │   │ struct  ││ │ │   AoS   │ │  │\n    │ │└─────────┘   └─────────┘│ │└─────────┘   └─────────┘│ │ └─────────┘ │  │\n    │ └─────│──────────────│────┘ └──────│────────────│─────┘ └──────│──────┘  │\n    │       └──┬───────────╯╭────────────┴────────────╯              │         │\n    │ ┌───────────────┐ ┌───────┬──────────┬──────────────┐ ┌────────────────┐ │\n    │ │    Model A    │ │ Model │ Model B1 │ attr_64: int │ │ Model C1: e[0] │ │\n    │ │ attr_199: str │ │ list  ├──────────┼──────────────┤ ├────────────────┤ │\n    │ │ attr_159: int │ │ of B  │ Model B2 │ attr_215: X  │ │ Model C2: e[1] │ │\n    │ └───────────────┘ └───────┴──────────┴──────────────┘ └────────────────┘ │\n    │                                                                          │\n    │    Models - PyFLP's representation - high level API - Stage 2 parser     │\n    └──────────────────────────────────────────────────────────────────────────┘\n\nPyFLP provides a high-level and a low-level API. Normally the high-level API\nshould get your work done. However, it might be possible that due to a bug or\nsuper old versions of FLPs the high level API fails to parse. In that case,\none can use the low-level API. Using it requires a deeper understanding of\nthe FLP format and how the GUI hierarchies relate to their underlying events.\n\nWhat it does?\n-------------\n\nIn a nutshell, PyFLP parses the events and creates a better semantic structure\nfrom it (as shown in the above diagram; stage 2 parser). I call this a \"model\".\n\n.. _architecture-model:\n\nModel\n-----\n\nA model acts like a \"view\" or alternate representation of the event data. It\nhas no state of its own and its composed of descriptors which get and set\nvalues from the events directly. A model is essentially stateless.\n\nThis has some advantages as compared to stateful models:\n\n1. The underlying event data and the values returned from the model descriptors\n   *i.e. its attributes or properties* always remain in sync with each other.\n2. Since modifying the event data at a binary level means conforming to the\n   various size and range limits imposed by C's data types, it can act as basic\n   validation for no extra cost or implementation.\n3. Avoid the use of private members in the models itself. Private members maybe\n   a good idea in languages which have better implementation of such concepts,\n   but in Python its quite as good as shooting yourself in the foot. Due to\n   Python's do-whatever-you-want nature, it can lead to some very bad coding\n   practices. This is one of the big reasons why PyFLP underwent a rewrite.\n4. Nothing is done in class constructor, so if a particular set of events are\n   out of order or follow a sequence not yet understood by PyFLP, they will\n   fail only for the attributes which use them. Hence, what is *parseable* can\n   still be parsed. This lazy evaluation can be good and bad both, but with\n   adequate unit tests its more good than it is bad.\n\nCreating a model involves a good amount of reverse engineering and insight. The\nmodels PyFLP has are based as close to the GUI objects inside FL Studio. For\ne.g. a pattern is represented by :class:`pyflp.pattern.Pattern`.\n\nA model is constructed with events it requires and additional information (like\nPPQ) its descriptors might need.\n"
  },
  {
    "path": "docs/architecture/reference.rst",
    "content": "Developer Reference\n===================\n\nThis page documents PyFLP's internals which consists of :mod:`pyflp._events`,\n:mod:`pyflp._descriptors` and :mod:`pyflp._models`.\n\n    The content below assumes you have fairly good knowledge of the following:\n\n    - OOP and descriptors, especially\n    - Type annotations\n    - Binary data types and streams\n\nEvents\n------\n\n.. automodule:: pyflp._events\n\nIf you have read Part I, you know how events use a TLV encoding scheme.\n\nType\n^^^^\n\nThe ``type`` represents the event ID. A custom enum class (and a metaclass)\nsupporting unknown IDs and member check using Python's ``... in ...`` syntax is\nused.\n\n.. autoclass:: _EventEnumMeta\n   :members:\n.. autoclass:: EventEnum\n   :members:\n\nLength\n^^^^^^\n\nThe ``length`` is a field prefixed for IDs in the range of 192-255. It is the\nsize of ``value`` and is encoded as a VLQ128 (variable length quantity base-128).\n\nValue\n^^^^^\n\nBelow are the list of classes PyFLP has, grouped w.r.t the ID range.\n\n.. dropdown:: 0-63\n\n    .. autoclass:: ByteEventBase\n    .. autoclass:: U8Event\n    .. autoclass:: BoolEvent\n    .. autoclass:: I8Event\n\n.. dropdown:: 64-127\n\n    .. autoclass:: WordEventBase\n    .. autoclass:: U16Event\n    .. autoclass:: I16Event\n\n.. dropdown:: 128-191\n\n    .. autoclass:: DWordEventBase\n    .. autoclass:: U32Event\n    .. autoclass:: I32Event\n    .. autoclass:: ColorEvent\n    .. autoclass:: U16TupleEvent\n\n.. dropdown:: 192-255\n\n    .. autoclass:: StrEventBase\n    .. autoclass:: AsciiEvent\n    .. autoclass:: UnicodeEvent\n    .. autoclass:: StructEventBase\n    .. autoclass:: ListEventBase\n    .. autoclass:: UnknownDataEvent\n\nEventTree\n^^^^^^^^^\n\n.. autoclass:: EventTree\n   :members:\n\nModels\n------\n\n.. automodule:: pyflp._models\n\nImplementing a model\n^^^^^^^^^^^^^^^^^^^^\n\nA look at the **source code** will definitely help, although these are a few\npoints that must be kept in mind when Implementing a model:\n\n1. Does the model mimic the hierarchy exposed by FL Studio's GUI?\n\n   .. tip::\n\n      Browse through the hierarchies of :class:`pyflp.channel.Channel`\n      subclasses to get a very good idea of this.\n\n2. Are ``__dunder__`` methods provided by Python used whenever possible?\n3. Is either :class:`ModelReprMixin` subclassed or ``__repr__`` implemented?\n\nDescriptors\n-----------\n\n.. automodule:: pyflp._descriptors\n\nAdapters\n--------\n\n.. automodule:: pyflp._adapters\n"
  },
  {
    "path": "docs/architecture.rst",
    "content": "🏠 Architecture\n================\n\n.. toctree::\n\n   1️⃣ FLP Format & Events <architecture/flp-format>\n   2️⃣ How it works? <architecture/how-it-works>\n   3️⃣ Developer Reference <architecture/reference>\n"
  },
  {
    "path": "docs/changelog.rst",
    "content": ".. mdinclude:: ../CHANGELOG.md\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# type: ignore\n\n\"\"\"Sphinx configuration script.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nimport importlib.metadata\nimport inspect\nimport re\n\nimport m2r2\n\nfrom pyflp._descriptors import EventProp, FlagProp, NestedProp, StructProp\nfrom pyflp._events import EventEnum, RGBA\nfrom pyflp._models import ModelBase\nfrom pyflp.arrangement import _TrackColorProp\n\nBITLY_LINK = re.compile(r\"!\\[.*\\]\\((https://bit\\.ly/[A-z0-9]*)\\)\")\n\"\"\"Shortened URLs for links to in-docstring images and docs.\"\"\"\n\nNEW_IN_FL = re.compile(r\"\\*New in FL Studio v([^\\*]*)\\*[\\.:](.*)\")\n\"\"\"Matched in docstrings and replaced with an SVG by :meth:`badge_flstudio`.\"\"\"\n\nEVENT_ID_DOC = re.compile(r\"([0-9\\.]*)\\+\")\nFL_BADGE = \"https://img.shields.io/badge/FL%20Studio-{}+-5f686d?labelColor=ff7629&style=for-the-badge\"  # noqa\nGHUC_PREFIX = \"https://raw.githubusercontent.com/demberto/PyFLP/master/docs/\"\n\"\"\"Raw image URL root used for in-docstring images and docs.\"\"\"\n\nIGNORED_BITLY = [\"3RDM1yn\"]\n\nproject = \"PyFLP\"\nauthor = \"demberto\"\ncopyright = f\"2022, {author}\"\nrelease = importlib.metadata.version(\"pyflp\")  # Needs package installation!\nextensions = [\n    \"hoverxref.extension\",\n    \"m2r2\",  # Markdown to reStructuredText conversion\n    \"sphinx_copybutton\",  # Copy button for code blocks\n    \"sphinx_design\",  # Grids, cards, icons and tabs\n    \"sphinxcontrib.spelling\",  # Catch spelling mistakes\n    \"sphinxcontrib.svgbob\",  # ASCII diagrams -> SVG\n    \"sphinx.ext.autodoc\",  # Sphinx secret sauce\n    \"sphinx.ext.autosummary\",  # Summary of contents table\n    \"sphinx.ext.coverage\",  # Find what I missed to autodoc\n    \"sphinx.ext.duration\",  # Time required to build docs\n    \"sphinx.ext.intersphinx\",  # Automatic links to Python docs\n    \"sphinx.ext.napoleon\",  # Google-style docstrings\n    \"sphinx.ext.todo\",  # Items I need to document\n    \"sphinx.ext.viewcode\",  # \"Show source\" button next to autodoc output\n    \"sphinx_toolbox\",  # Badges and goodies\n    \"sphinx_toolbox.github\",  # Link to project issues / PRs easily\n    \"sphinx_toolbox.more_autodoc.autoprotocol\",  # Autodoc extension for typing.Protocol\n    \"sphinx_toolbox.more_autodoc.sourcelink\",  # Python docs-style source code link\n    \"sphinx_toolbox.sidebar_links\",  # Links to repo and PyPi project in the sidebar\n    \"sphinx_toolbox.wikipedia\",  # Diretive for wikipedia topics.\n]\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\nhtml_theme = \"furo\"  # Nice light/dark theme; has an auto-switch mode\nautodoc_inherit_docstrings = False\nautodoc_default_options = {\n    \"undoc-members\": True,  # Show undocumented members\n    \"exclude-members\": \"INTERNAL_NAME\",  # Exclude these members\n    \"no-value\": True,  # Don't show a default value (for descriptors mainly)\n}\nneeds_sphinx = \"5.0\"\nhoverxref_auto_ref = True  # Convert all :ref: roles to hoverxrefs\nnapoleon_preprocess_types = True\nnapoleon_attr_annotations = True\nhtml_permalinks_icon = \"<span>#</span>\"  # Get rid of the weird paragraph icon\ngithub_username = author  # sphinx_toolbox.github config\ngithub_repository = project  # sphinx_toolbox.github config\nautodoc_show_sourcelink = True  # sphinx_toolbox.more_autodoc.sourcelink\ntodo_include_todos = True  # Include .. todo:: directives in output\ntodo_emit_warnings = True  # Emit warnings about it as well, so I don't forget\nhtml_css_files = [\n    \"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css\"\n]  # https://sphinx-design.rtfd.io/en/furo-theme/badges_buttons.html#fontawesome-icons\nsd_fontawesome_latex = True  # Output FontAwesome icons in LaTeX\nintersphinx_mapping = {\n    \"python\": (\"https://docs.python.org/3\", None),\n    \"construct\": (\"https://construct.readthedocs.io/en/latest\", None),\n}  # Put hyperlinks to docs of other projects\n\nlinkcheck_allowed_redirects = {\n    r\"https://bit.ly/.*\": r\"https://raw.githubusercontent.com/demberto/PyFLP/master/docs/img/.*\",  # noqa\n    r\"https://pyflp.rtfd.io.*\": r\"https://pyflp.readthedocs.io/en/latest/.*\",\n    r\"https://www.python.org/dev/peps/.*\": r\"https://peps.python.org/.*\",\n    r\"https://github.com/demberto/PyFLP/files/.*\": r\"https://objects.githubusercontent.com/.*\",  # noqa\n    r\"https://stackoverflow.com/a/.*\": r\"https://stackoverflow.com/questions/.*\",\n}\n\n\ndef badge_flstudio(app, what, name, obj, options, lines):\n    \"\"\"Convert FL Studio version information in docstrings to nice badges.\"\"\"\n    for line in lines:\n        if name.split(\".\")[-2].endswith(\"ID\"):  # Event ID member\n            match = EVENT_ID_DOC.fullmatch(line)\n        else:\n            match = NEW_IN_FL.fullmatch(line)\n\n        if match is not None:\n            groups = tuple(\n                filter(\n                    lambda group: group != \"\",\n                    map(lambda group: group.strip(), match.groups()),\n                )\n            )\n\n            if len(groups) == 1:\n                lines.insert(0, f\".. image:: {FL_BADGE.format(groups[0])}\")\n                lines.insert(1, \"\")\n            elif len(groups) == 2:\n                grid = f\"\"\"\n                .. figure:: {FL_BADGE.format(groups[0])}\n                    :alt: New in FL Studio v{groups[0]}\n\n                    {groups[1].strip()}\n\n                \"\"\"\n                lines[:0] = grid.splitlines()  # https://stackoverflow.com/a/25855473\n            lines.remove(line)\n\n\ndef add_annotations(app, what, name, obj, options, signature, return_annotation):\n    \"\"\"Add type annotations for descriptors.\"\"\"\n    if what == \"class\" and issubclass(obj, ModelBase):\n        annotations = {}\n        for name_, type in vars(obj).items():\n            if isinstance(type, _TrackColorProp):\n                annotations[name_] = RGBA\n            elif isinstance(type, NestedProp):\n                annotations[name_] = type._type\n            elif isinstance(type, FlagProp):\n                annotations[name_] = bool | None\n            elif hasattr(type, \"__orig_class__\"):\n                annotations[name_] = type.__orig_class__.__args__[0]\n\n            if isinstance(type, (EventProp, StructProp)):\n                annotations[name_] |= None\n\n        if hasattr(obj, \"__annotations__\"):\n            obj.__annotations__.update(annotations)\n        else:\n            obj.__annotations__ = annotations\n\n\ndef autodoc_markdown(app, what, name, obj, options, lines):\n    \"\"\"Convert all markdown in docstrings to reStructuredText.\n\n    This includes images and tables. Docstrings are in markdown for VSCode\n    compatibility.\n    \"\"\"\n    filtered = [line for line in lines for link in IGNORED_BITLY if link not in line]\n    newlines = m2r2.convert(\"\\n\".join(filtered)).splitlines()\n    lines.clear()\n    lines.extend(newlines)\n\n\ndef remove_model_signature(app, what, name, obj, options, signature, return_annotation):\n    \"\"\"Removes the :func:`ModelBase.__init__` args from the docstrings.\n\n    It's an implementation detail, and only clutters the docs.\n    \"\"\"\n    if what == \"class\" and issubclass(obj, ModelBase):\n        return (\"\", return_annotation)\n\n\ndef remove_enum_signature(app, what, name, obj, options, signature, return_annotation):\n    \"\"\"Removes erroneous :attr:`signature` = '(value)' for `enum.Enum` subclasses.\"\"\"\n    if inspect.isclass(obj) and issubclass(obj, enum.Enum):  # Event ID class\n        return (\"\", return_annotation)\n\n\ndef include_obsolete_ids(app, what, name, obj, skip, options):\n    \"\"\"Includes obsolete / undocumented (prefixed with a `_`) event IDs.\"\"\"\n    if isinstance(obj, EventEnum):  # EventID member\n        return False\n\n\ndef show_model_dunders(app, what, name, obj, skip, options):\n    \"\"\"Subclasses of ``ModelBase`` show these dunders regardless of any settings.\"\"\"\n    if name in (\"__getitem__\", \"__setitem__\", \"__iter__\", \"__len__\"):\n        return False\n\n\ndef setup(app):\n    \"\"\"Connects all callbacks to their event handlers.\"\"\"\n    app.connect(\"autodoc-process-docstring\", badge_flstudio)\n    app.connect(\"autodoc-process-docstring\", autodoc_markdown)\n    app.connect(\"autodoc-process-signature\", add_annotations)\n    app.connect(\"autodoc-process-signature\", remove_model_signature)\n    app.connect(\"autodoc-process-signature\", remove_enum_signature)\n    app.connect(\"autodoc-skip-member\", include_obsolete_ids)\n    app.connect(\"autodoc-skip-member\", show_model_dunders)\n"
  },
  {
    "path": "docs/contributing.rst",
    "content": "\\ :fas:`user-gear` Contributor's Guide\n======================================\n\n🤝 All contributions are welcome.\n\n.. important::\n\n   PyFLP adheres to the `Contributor Covenant Code of Conduct\n   <https://github.com/demberto/PyFLP/blob/master/CODE_OF_CONDUCT.md>`_.\n   Please make sure you have read it and accept it before proceeding further.\n\n⬇ The sections below are ordered roughly in the order one would follow.\n\n\\ :fas:`code-pull-request;sd-text-primary` Making a PR\n------------------------------------------------------\n\n.. tip:: Format code with ``black``\n\n   PyFLP use the black code style, format your code with it.\n\n:fas:`clone` Clone the repo\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: console\n\n   git clone https://github.com/demberto/PyFLP\n\n:fas:`code-branch` Create a branch\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: console\n\n   git branch my_new_feature\n   git checkout my_new_feature\n\n🌱 Create a virtual environment\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nI prefer to use `venv <https://docs.python.org/3/library/venv.html>`_.\n\n.. code-block:: console\n\n   python -m venv venv\n\nThis will create a folder named ``venv`` in the current directory.\n\nNow, activate the venv:\n\n.. code-block:: console\n\n   ./venv/Scripts/activate\n\n📌 Install dependencies\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nInstall all dev, user and docs dependencies.\n\n.. code-block:: console\n\n   python -m pip install --upgrade pip\n   pip install -r requirements.txt -r docs/requirements.txt tox\n\n|vscode-icon| VS Code integration\n---------------------------------\n\nI use VS Code for development. I have made certain changes to the workspace to\nsuit my workflows and make life easier.\n\n.. todo Inspect whether venv creation can be automated through VSCode.\n\n:fab:`python` Python extension configuration\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTo ease linting, enforce strict type checking and improve code quality, I have\nmodified certain settings for the official Python / Pylance extension, so that\nyou don't need to manually configure it or encounter issues while committing.\nCheck `settings.json\n<https://github.com/demberto/PyFLP/blob/master/.vscode/settings.json>`_.\n\n:material-sharp:`extension;1.2em;sd-pb-1` Recommended extensions\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWhen you open the repo directory in VS Code, you will get recommendations for\nextensions. I use these extensions myself. You can check `extensions.json\n<https://github.com/demberto/PyFLP/blob/master/.vscode/extensions.json>`_ to\nknow why and where they are used.\n\n:fas:`list-check` Tasks\n^^^^^^^^^^^^^^^^^^^^^^^\n\nIf you use :fab:`windows;sd-text-secondary` Windows, I have made some shortcuts\nfor common tasks. Check `tasks.json\n<https://github.com/demberto/PyFLP/blob/master/.vscode/tasks.json>`_.\n\n.. |vscode-icon| image:: /img/contributing/vscode.svg\n   :width: 32\n\n|pytest-icon| Testing\n---------------------\n\nFL Studio comes with a handy feature 🚀 to export \"presets\" for various\n:doc:`models <./architecture>` like :class:`Channel`, :class:`Insert` and so\non. This is used for **isolating** test results. A look 👀 at `tests/assets\ndirectory <https://github.com/demberto/PyFLP/tree/master/tests/assets>`_ shows\nwhat possible models and properties could be tested from a preset file. I have\ndivided the tests such that they test a model or an individual property.\n\nThese presets have the same layout of a normal full FLP would use, but only the\nrequired events are kept. This *might* and **has** caused some problems while\ntesting properties dependant on data passed from its parent 😔. For instance, an\n:class:`Insert` gets version from :class:`Mixer` which gets it from\n:class:`Project` itself. This kind of dependency is not good in my opinion 😐,\nand I continue to look at ways to improve testing.\n\nThere also are models which cannot be exported into presets, notable being\n:class:`Pattern` (although scores can be exported) and the entire\n:mod:`pyflp.arrangement` module. Currently, I have kept the testing for these\nin a common FLP.\n\n✴️ Guidelines\n^^^^^^^^^^^^^^\n\n1. Follow the naming scheme of the test functions, it generally follows the\n   format of  ``test_{model_collection}`` *or* ``test_{model}_{descriptor}``.\n2. Create separate test assets, whenever possible.\n\n.. |pytest-icon| image:: /img/contributing/pytest.svg\n    :width: 32\n\n📖 Docs\n--------\n\nDon't forget to update the `docs <https://pyflp.rtfd.io/>`_ after you are done\nwith a feature or a bug fix that affects the documentation.\n\n✴️ Guidelines\n^^^^^^^^^^^^^^\n\n1. ↔ **80 columns** max, wherever possible. Don't consider this for inlined links\n   and tables.\n\n   .. tip::\n\n      Don't start a new sentence at the end of a line. Remember that it should\n      be easily readable to you, first of all.\n\n2. 🌐 **Inline links** if they aren't used twice in the same document.\n3. 📝 Should look **clean** enough in a simple text viewer as well.\n4. 💡 Use **emojis** if it conveys the meaning of the text next to it.\n5. ⚫⚪ Add images for both **light** and **dark** modes.\n\n🛠 Sphinx configuration\n^^^^^^^^^^^^^^^^^^^^^^^\n\nSphinx is the tool I use for generating PyFLP's docs. It comes with a handy\nplugin called ``sphinx-autodoc`` to automatically generate documentation for\nthe code from Python docstrings.\n\nOne thing about it, however is that its primarily reStructuredText driven,\nwhile my docstrings are all in Github-flavored markdown. Luckily, Sphinx being\npowerful and extensible provides APIs to modify the docstrings that are sent to\nthe ``sphinx-autodoc`` plugin.\n\nCurrently, the transformation is divided into these steps (in order):\n\n- ⤵ Replacing ``*New in FL Studio ...*`` with shields like these:\n\n  .. image:: https://img.shields.io/badge/FL%20Studio-20+-5f686d?labelColor=ff7629&style=for-the-badge\n\n- ➕ Adding the correct annotations for the :doc:`descriptors <./architecture>`.\n- ⤵ Converting GFM tables and images in the docstrings to reStructuredText.\n- ➖ Removing erroneous ``__init__`` method signatures from enums and models.\n- ➕ Include \"private\" (obsolete) :class:`pyflp._events.EventEnum` members.\n- ➕ Include model dunder methods like ``__len__``, ``__iter__`` etc.\n\nCheck `conf.py <https://github.com/demberto/PyFLP/blob/master/docs/conf.py>`_\nfor understanding how it is done.\n\n🚧 Still to be documented\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. todolist::\n"
  },
  {
    "path": "docs/faq.rst",
    "content": "❓ FAQ\n======\n\nNow I don't frequently get asked any questions, *(I would love to)* but these\nare some questions I think a newcomer or a contributor might ask?\n\n🗣 How do I get help?\n^^^^^^^^^^^^^^^^^^^^^\n\n- Check the [discussions](https://github.com/demberto/PyFLP/discussions), open\n  a new one.\n- Open an issue if you spot a bug 🐛 or want a new feature ✨.\n- Email me on demberto(at)proton.me.\n\nI will generally get back to you within a day ⏰.\n\n✨ Is \"X\" supported / implemented?\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nPretty much. I have organised PyFLP's code to be pretty self explanatory.\nIf you are completely new to the terminology used by PyFLP, you should also\nopen up FL Studio's documentation open besides the `reference <./reference>`_.\n\nIf you find something isn't yet implemented, open an issue or, a\n:doc:`PR <./contributing>` if you have implemented something.\n\n➕➖ Why is there no functionality to **add** / **remove** items from collections?\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n*Also answers alternative specific questions like \"Why can't I add a channel to\nthe channel rack?\", \"Why can't I add a new arrangement?\"*\n\n**Because of version compatibility**. The FLP format is a closed-source format\nwith no documentation. It evolves completely at the whims of Image-Line. I don't\nwork there, nor have a contributor who knows for sure that a particular thing I\nimplement will work for sure. *So*, instead of developing a feature which isn't\nguaranteed to work, I can better devote my time to support the **modification**\nof existing properties and items.\n\nHowever, some good news now. I am planning to add support for adding / removing\nnotes from a pattern, adding / deleting automation points. Basically stuff,\nwhich hasn't changed a lot since FL Studio first introduced it.\n\n🤝 I want to contribute\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nPlease check the :doc:`contributor's guide <./contributing>` if you are new to\nPyFLP. Check the :doc:`architecture` to understand the internals.\n\nAlso check out the :doc:`developer guides <./guides>`.\n\n🧵 Is PyFLP thread safe?\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**No.** PyFLP uses ``sortedcontainers``, an awesome library which unfortunately\n`isn't thread-safe <https://github.com/grantjenks/python-sortedcontainers/issues/105>`_.\n"
  },
  {
    "path": "docs/features.rst",
    "content": "✨ Features\n============\n\nNon-destructive editing\n-----------------------\n\nThe modifications you make will have a minimum effect on the internal structure\nof an FLP. Infact, I guarantee you that if you save a :class:`pyflp.Project`\nas-is, the new file will be exactly alike (compare hashes if you want).\n\n📝 Godlevel docstrings\n----------------------\n\nPyFLP has been carefully written to take advantage of the features provided\nby a modern editor, like VS Code. One area, I particularly devoted a lot of\ntime to are docstrings.\n\nSince PyFLP's entire documentation is only its reference, I thought it might\nbe challenging for a first time user to know where to find the data they need.\n\n:fas:`eye;sd-text-info` Visual hints\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. image:: /img/features/images-dark.png\n   :align: center\n   :class: only-dark\n\n.. image:: /img/features/images-light.png\n   :align: center\n   :class: only-light\n\nTo make it somewhat easier of a journey, I haved added links to images and GIFs\nfrom FL Studio's interface.\n\n:fas:`table;sd-text-info` Minimums, maximums and defaults\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. image:: /img/features/tables-dark.png\n   :align: center\n   :class: only-dark\n\n.. image:: /img/features/tables-light.png\n   :align: center\n   :class: only-light\n\nA lot of properties also have *suggested* minimum, maximum and default values.\nWhen I say suggested, I mean that FLP is a closed format owned by Image-Line.\nIts on their whims what they do with it. The values I suggest are only on a\n*last-I-checked-they-were-these* basis. However, my research till now has\nshown me that they rarely change.\n\n.. important:: For non-VS Code users\n\n   VS Code uses a rather unstandardised format for parsing docstrings. Unlike\n   PyCharm, it cannot parse rST docstrings. Hence PyCharm users will get a\n   rather bad result from the docstring previews where I have used tables and\n   images, *unfortunately*.\n\n   I haven't found a way to make docstrings look good while being equally\n   accessible in both PyCharm and VSCode.\n\n   .. seealso:: `#52 <https://github.com/demberto/PyFLP/issues/52>`_\n\n:octicon:`check-circle-fill;1em;sd-text-success` 100% type tested\n-----------------------------------------------------------------\n\nPyFLP is fully tested with `pyright <https://github.com/microsoft/pyright>`_,\na static type checker built right into VS Code as well as mypy.\n\n:fas:`umbrella;sd-text-primary` 85%+ code coverage\n--------------------------------------------------\n\nPyFLP boasts a total of more than 85+ combined code coverage across supported\nPython versions. Higher the coverage ⬆, lesser the amount of bugs 🐞\n"
  },
  {
    "path": "docs/guides/plugin.rst",
    "content": "🚶‍♂️ Walkthrough: Implementing a plugin data parser\n====================================================\n\nImplementing a native plugin data parser can be easy. Below is a walkthrough\nfor implementing a simple effect, :class:`Fruity Balance <pyflp.plugin.FruityBalance>`.\n\n.. note:: Prequisites\n\n   The steps ahead assume that you have an understanding of how binary data\n   types (C's integral types, in this context) work along with a basic\n   understanding of Python itself.\n\n1. Note the parameters exposed by the plugin\n\n.. image:: /img/guides/plugin/1-parameters.png\n   :align: center\n   :alt: Fruity Balance paramerers\n\nAlso take note of the **order** in which they occur. Here its **Balance**\nfollowed by **Volume**.\n\n2. Prepare a test FLP\n\nCreate an empty new FLP, add a **Fruity Balance** to one of the insert slots.\n\n.. image:: /img/guides/plugin/2-load-plugin.png\n   :align: center\n   :alt: Fruity Balance in an insert slot\n\nSave this FLP as ``fruity-balance.flp``.\n\n3. Getting the plugin data\n\nSince this is an **empty** FLP, with no other plugins loaded, you can simply\naccess the plugin data by,\n\n.. code-block:: python\n\n   import pyflp\n   from pyflp.plugin import PluginID\n\n   # Parse the FLP file into a project\n   project = pyflp.parse(\"fruity-balance.flp\")\n\n   # Collect all the events as a dict of ID to event\n   events = project.events_asdict()\n\n   # Get the first plugin data event - the Fruity Balance one itself\n   plugin_data_event = events[PluginID.Data][0]\n\n   # Get the raw data and convert it to a tuple of 8-bit unsigned integers\n   data = tuple(bytearray(plugin_data_event._struct))\n   print(data)\n\n4. Observe and analyze the output\n\n▶ You will get an output like this:\n\n.. code-block:: python\n\n   (0, 0, 0, 0, 256, 0, 0, 0)\n\nThat's a total of **8 bytes** worth of data for **2 knobs**.\n\nFL Studio *generally* uses 4 bytes for most type of data, so let's assume each\nknob takes **4 bytes**.\n\nNow compare it with the **positions** of the knobs in Fruity Balance.\n\n.. image:: /img/guides/plugin/3-observe-knob-positions.png\n   :align: center\n   :alt: Observe knob positions\n\n‼ Suddenly the data above makes sense.\n\nHow? Let me explain.\n\n- **Balance** knob is at 12 o' clock\n- **Volume** knob is somewhere at 80% of its maximum.\n\nNow convert the 8-bit integer tuple into a two 32-bit integer tuple. We get the\nvalues ``0`` and ``256`` respectively. So, now we know, that **Balance** is 0\n(because its centred) and **Volume** is at 256. Also, since we didn't modify\nthem at all, these are the **default** values.\n\n🥳 Success! We cracked it!\n\n5. Exercise: Find out the minimum and maximums (optional, but recommended)\n\nBy rotating the knobs to their extremes and following steps 3-4 again, you can\nfind out the minimum and maximums of each knob.\n\n.. hint::\n\n   One very important place for finding out the extremes is the hint panel.\n\n   .. image:: /img/guides/plugin/4-hint-panel.png\n      :align: center\n      :alt: FL Studio hint panel\n\n6. Writing the plugin code\n\nℹ All plugins are implemented in the :mod:`pyflp.plugin` module.\n\n.. note::\n\n   Take care to follow the naming conventions as shown below.\n\nBegin with writing the code for the plugin :ref:`event <architecture-event>`:\n\n.. code-block:: python\n\n   class FruityBalanceEvent(StructEventBase):\n       STRUCT = c.Struct(\"pan\" / c.Int32ul, \"volume\" / c.Int32ul).compile()\n\n.. note:: What is ``c.Struct``?\n\n   PyFLP uses the :mod:`construct` library to define and binary structures.\n   Its a fairly simple to understand declarative binary parser creator.\n\n   .. tip::\n\n      Call :meth:`construct.Struct.compile()` to get a faster version of the\n      \"Struct\". Check <https://construct.readthedocs.io/en/latest/compilation.html>\n      for more information.\n\nNow create a :ref:`model <architecture-model>` for the event we just created\nin the same module:\n\n.. code-block:: python\n\n   class FruityBalance(_PluginBase[FruityBalanceEvent]):\n       pan = _PluginDataProp[int]()\n       volume = _PluginDataProp[int]()\n\nYou don't need to worry about ``_PluginBase`` and ``_PluginDataProp``. They are\nimplementation-level details, you don't *generally* need to worry about.\n\nDerive our newly create ``FruityBalance`` from ``_IPlugin`` and implement it:\n\n.. important::\n\n   Don't forget to do this. Otherwise the event will not be parsed.\n\n.. code-block:: python\n   :emphasize-lines: 1, 2\n\n   class FruityBalance(_PluginBase[FruityBalanceEvent], _IPlugin):\n       INTERNAL_NAME = \"Fruity Balance\"\n       pan = _PluginDataProp[int]()\n       volume = _PluginDataProp[int]()\n\n.. note::\n\n   Use :doc:`FLPEdit <./reversing>` to find out ``INTERNAL_NAME`` of a plugin.\n\n🎉 And that's basically it. The implementation is complete! Now all we need to\ndo is glue ``FruityBalanceEvent`` and ``FruityBalance`` to the effect slot's\n:attr:`pyflp.mixer.Slot.plugin` attribute.\n\n7. Glue the implementation to :class:`pyflp.mixer.Slot`:\n\nImport our newly created classes in :mod:`pyflp.mixer` and add an entry to\n:attr:`pyflp.mixer.Slot.plugin` like so:\n\n.. code-block:: python\n   :emphasize-lines: 3\n\n   plugin = PluginProp(\n        {\n            FruityBalanceEvent: FruityBalance,\n            ...\n        }\n    )\n"
  },
  {
    "path": "docs/guides/reversing.rst",
    "content": "🤓 Reversing FLP format\n========================\n\n    You should first take a look at :doc:`what events are <../architecture>`.\n    A decent knowledge of the topics mentioned there as well as Python itself\n    is assumed.\n\nOne could use a hex editor, but its too tedious. I have a simpler solution:\n\n.. figure:: /img/guides/reversing/flpedit.png\n   :alt: A test FLP opened in FLPEdit\n\n   **FLPEdit**, an event view for FLP (and related formats) files.\n\n   Download it `here <https://github.com/demberto/PyFLP/files/9586342/FLPEdit.zip>`_.\n\n   This is an unmaintained software, written actually in C#. Event ID names are\n   different but the file attached above has source code as well. Check the\n   ``FLPFileFormat/FLP_File.cs`` file for a list of event IDs and compare them\n   to the ones from :class:`pyflp._events.EventEnum` in PyFLP.\n\nEvents\n------\n\nWhich event needs to be inspected can only be understood when you observe the\nordering of events, whether they occur for default values or not as well as\na general knowledge of new features and changes occuring inside FL Studio.\n\nCheck `this discussion <https://github.com/demberto/PyFLP/discussions/34>`_ for\na list of unknown / undiscovered events.\n\nStruct fields\n-------------\n\nStructs whose field names are prefixed with a ``_u`` are undiscovered fields.\nWherever possible, I have added helpful comments right next to them.\n\nAlso, throughout PyFLP's codebase, there are a number of ``# TODO`` comments.\nSome of these can have additional information about them.\n\nMy workflow\n-----------\n\n1. Create a new test FLP or a preset and save it.\n2. Parse the file with PyFLP and record the initial values.\n3. Turn knobs / faders all the way to their extremes, save and repeat (2)\n\n.. hint:: WhatsNew.rtf\n\n   FL Studio's changelog file ``WhatsNew.rtf`` exists in its install folder.\n   It is a very helpful source for understanding which features were added\n   when.\n"
  },
  {
    "path": "docs/guides.rst",
    "content": "📖 Developer guides\n====================\n\nWant to be a **contributor**? Interested in the internals of the FLP format?\nThis is the perfect place to begin. Reading :doc:`architecture` is also\nrecommended but not necessarily required.\n\n.. toctree::\n   :glob:\n\n   guides/*\n"
  },
  {
    "path": "docs/handbook.rst",
    "content": "📚 Handbook\n============\n\nThis page contains some ideas on how one can use PyFLP for automating\ntasks (*to a certain extent*) which can only be done via FL Studio.\n\nA basic-to-intermediate level of Python knowledge is assumed. No prior\nknowledge of PyFLP is required for any of the sections below.\n\n*These all are written from a dev's POV. I would ♥ to get more ideas and hear\nabout different use cases.*\n\n📦 Exporting to a ZIP\n----------------------\n\nImagine you had a folder structure like this:\n\n.. code-block::\n\n   📁 Samples\n      ├─── 🥁 kick.wav\n      ├─── 👏 clap.wav\n      └─── 🎵 toms.wav\n   📄 MyGreatSong.flp\n\n\n    For the purpose of simplicity, assume that ``📄 MyGreatSong.flp`` uses only\n    the samples from ``📁 Samples`` and all **sample file names are unique**.\n\nThe code below will create a ZIP containing all the samples used\n\n.. code-block:: python\n\n   from zipfile import ZipFile\n   import pyflp\n\n   project = pyflp.parse(\"MyGreatSong.flp\")\n\n   with ZipFile(\"MyGreatSong.zip\", \"x\") as zp:\n       zp.write(\"MyGreatSong.flp\")\n\n       for sampler in project.channels.samplers:\n           if sampler.sample_path is not None:\n               zp.write(sampler.sample_path)\n\n.. caution:: Missing samples\n\n   The above code assumes that all the samples exist at the paths the FLP has\n   stored. If any of the samples aren't found, there will be an error.\n\n   FL Studio doesn't give up this easily. It searches up a lot of paths,\n   including but not limited to the recursive scanning of folders which are:\n\n   - Current directory.\n   - Added to the sample browser.\n   - Containing previous samples / missing samples.\n\nThis will create a ZIP file of the structure:\n\n.. code-block::\n\n   📦 MyGreatSong.zip\n      ├─── 📄 MyGreatSong.flp\n      ├─── 🥁 kick.wav\n      ├─── 👏 clap.wav\n      └─── 🎵 toms.wav\n\n.. hint:: FL Studio stock samples\n\n   While this will work for 3rd party samples *unless there's 2 samples with\n   the same name*, FL Studio doesn't store the full path inside an FLP for\n   stock samples. See :attr:`pyflp.channel.Sampler.sample_path` for more info.\n\n🔓 Unlocking demo version FLPs\n-------------------------------\n\n.. caution::\n\n   This doesn't work for FL Studio 21 projects.\n   See `#146 <https://github.com/demberto/PyFLP/discussions/146>`\n\nFLPs saved with a trial version of FL Studio cannot be reopened again without\nsaving in a registered version. The state of demo versions of native plugins'\nis not retained either.\n\n.. hint::\n\n   This section **doesn't** explain how to make 3rd party plugin demos\n   recall their state. They have their own mechanisms for doing that.\n\nIt is possible to undo both of these:\n\n.. seealso::\n\n   :attr:`Project.licensed <pyflp.project.Project.licensed>` and\n   :attr:`_PluginBase.demo_mode <pyflp.plugin._PluginBase.demo_mode>`.\n\n.. code-block:: python\n\n   import pyflp\n\n   project = pyflp.parse(\"/path/to/myflp.flp\")\n\n   # Unlock the FLP itself\n   project.licensed = True\n\n   # Unlock trial version native plugins\n   for instument in project.channels.instruments:\n       instrument.plugin.demo_mode = False\n\n   for insert in project.mixer:\n       for slot in insert:\n           if slot.plugin is not None:\n              slot.plugin.demo_mode = False\n\n   pyflp.save(project, \"/path/to/myflp_unlocked.flp\")\n\n.. note::\n\n   An unregistered version of FL Studio will roll back these changes once you\n   save an FLP in it (even previously registered ones), so you need to repeat\n   this process everytime.\n"
  },
  {
    "path": "docs/helping.rst",
    "content": "🙌 Helping PyFLP\n=================\n\nPyFLP is completely free and open source (FOSS) software. It takes a lot of\ntime and efforts to maintain it and keep improving it. I try to help anyone\nhaving any issues or anyone who wants to contribute in any way possible to the\nbest of my efforts.\n\nI don't ask for donations or any sort of funding. If you like PyFLP and want it\nto grow and improve, you can do the following things:\n\n⭐ Star **PyFLP** on Github\n----------------------------\n\n.. image:: /img/helping/star-repo-dark.gif\n   :align: center\n   :class: only-dark\n   :target: https://github.com/demberto/PyFLP\n   :alt: ⭐ How to star PyFLP?\n\n.. image:: /img/helping/star-repo-light.gif\n   :align: center\n   :class: only-light\n   :target: https://github.com/demberto/PyFLP\n   :alt: ⭐ How to star PyFLP?\n\nYou can \"star\" the repo if you have a Github account. It is analogous to\n\"following\" on social media and helps :abbr:`SEO (Search engine optimization)`.\n\n👀 Watch **PyFLP** on Github\n-----------------------------\n\n.. image:: /img/helping/watch-repo-dark.gif\n   :align: center\n   :class: only-dark\n   :target: https://github.com/demberto/PyFLP\n   :alt: 👀 How to watch PyFLP?\n\n.. image:: /img/helping/watch-repo-light.gif\n   :align: center\n   :class: only-light\n   :target: https://github.com/demberto/PyFLP\n   :alt: 👀 How to watch PyFLP?\n\nYou can \"watch\" the repo if you have a Github account. It will notify you about\nall changes taking places in PyFLP right in your 📨 email.\n\n🐞 Reporting bugs\n------------------\n\n.. image:: /img/helping/open-issue-dark.png\n   :align: center\n   :class: only-dark\n   :target: https://github.com/demberto/PyFLP\n   :alt: 🐞 How to open an issue?\n\n.. image:: /img/helping/open-issue-light.png\n   :align: center\n   :class: only-light\n   :target: https://github.com/demberto/PyFLP\n   :alt: 🐞 How to open an issue?\n\nIf you find out that something isn't quite working as its supposed to, please\nopen an issue `here <https://github.com/demberto/PyFLP/issues>`_ and follow\nthe instructions provided in the template to fill out a bug report.\n\n🔎 Check out the **Discussions**\n---------------------------------\n\n🗣 `Discussions <https://github.com/demberto/PyFLP/discussions>`_ is the place\nwhere I announce what's coming new, when its coming and a few other topics\nrelated to the FLP format.\n\n❗ If you have a taste in reverse engineering or binary formats, you must most\ndefinitely check it out.\n\n🙌 You can also open a new discussion to tell me what you made with PyFLP and\nhow it helped you. I am more than glad to find how PyFLP is getting used.\n\nHelp the tools that power **PyFLP**\n-----------------------------------\n\n- `construct <https://github.com/construct/construct>`_\n- `f-enum <https://github.com/Bobronium/fastenum>`_\n- `sortedcontainers <https://github.com/grantjenks/python-sortedcontainers>`_\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. mdinclude:: ../README.md\n\nNavigation\n----------\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n\n   handbook\n   reference\n   features\n   architecture\n   contributing\n   guides\n   faq\n   limitations\n   helping\n   ⏰ Changelog <changelog>\n\n.. sidebar-links::\n   :github:\n   :pypi: PyFLP\n\nIndices and tables\n------------------\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/limitations.rst",
    "content": "🚫 Limitations\n===============\n\nBefore you begin reading, I would like to **emphasize** that FLP is a closed\nand undocumented format. The knowledge needed for understanding the internals\nis published nowhere, except for a few notes lying around here and there and\nsome existing implementations which I deeply thank for saving my time.\n\nWhatever PyFLP does, is on a best-effort level. Things can go wrong so its\nalways wise to have **backups** and *avoid* **overwriting**.\n\nMost properties are discovered; their representations aren't\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nYou will find almost all the properties you could imagine. However FLP being\na binary format stores any and all kinds of stuff as integers. Its actually\nharder to calculate the formula used for representing stuff like frequency,\nvolume and other such non-linear stuff.\n\nAnother thing is musical timings, check :github:issue:`75` for a more info.\n\nItems cannot be added or removed, only modified\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nI am working on making it possible to add or remove items (like channels,\npatterns, MIDI notes, arrangements etc.) currently.\n\nCertain things like MIDI notes are simpler to add, but things like patterns\nand channels will be harder to get correct at. For those things, a **clone**\nlike operation is easier to implement.\n\n.. note::\n\n   It is possible, however (as of PyFLP 2.0.0a4) to add or remove events.\n   If you don't know what they are, you probably shouldn't be handling them.\n   If you are confident about working with events directly, you can very\n   much add new events to effectively do things like adding your own items.\n\nWhy is it *slow*?\n^^^^^^^^^^^^^^^^^\n\nSlow is a relative term - to some it might not be noticeable at all.\nAlthough in my opinion, PyFLP has become way slower since I migrated to\n``construct``, which provides a lot more benefits than what I did earlier.\n\n``construct`` has an opt-in compilation feature which although isn't usable\nfor all kinds of structs, is available for most of them, which gived quite a\nspeed boost for structs that occur a lot (MIDI notes, playlist items to name\na few.)\n\n* Due to PyFLP's lazily evaluated nature, most delays don't occur upfront i.e\n  during :meth:`pyflp.parse`.\n* Python enums are quite slow, to the point that adding the ``f-enum`` library\n  patch, reduced parse time by 50%.\n* :class:`pyflp._events.EventTree` class' need for ``sortedcontainers.SortedList``\n  which is implemented in pure Python.\n\nDifficult to make ports\n^^^^^^^^^^^^^^^^^^^^^^^\n\nThe current working of PyFLP is non-replicable in most other languages.\nDescriptors are a Python specific feature I have yet to find anywhere else.\nTherefore, the possibility of a port that's as clean (and featured) as PyFLP is\nless. Most languages however have some sort of 3rd party Python interop library\navailable, so its not like PyFLP is completely unuseable from other languages.\n\nA quick search on Github will return some FLP parsers available for other\nlanguages, but almost all of them are pretty much unmaintained or archived.\n\nUnit-testing is paramount\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\nDue to the lazy nature of models and their descriptors, each of them should be\ntested so as to ensure that no changes in the event handling affect or break.\n\nFor a long time, I used only a single FLP to test all of PyFLP's API. Things\nhave changed now and I use presets exported from FL Studio itself for the\ntesting of a huge chunk of API to ensure isolation of test results.\n\nThe problem is that all the test data comes from FL Studio itself and can\nbe only really validated in the same. That's the reason I usually don't\nraise any errors event if I know quite surely that, for example a value out of\nrange is set for some property.\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=.\nset BUILDDIR=_build\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.https://www.sphinx-doc.org/\n\texit /b 1\n)\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\n\n:end\npopd\n"
  },
  {
    "path": "docs/reference/arrangement/arrangement.rst",
    "content": "\\ :fas:`trowel-bricks` Arrangement\n==================================\n\n.. currentmodule:: pyflp.arrangement\n\n.. autoclass:: Arrangement\n   :members:\n\n.. autoclass:: TimeSignature\n   :members:\n\n.. autoclass:: ArrangementID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/arrangement/index.rst",
    "content": "Arrangements\n============\n\n.. module:: pyflp.arrangement\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n   :caption: Contents:\n   :glob:\n\n   *\n\n.. autoclass:: Arrangements\n   :members:\n\n.. autoclass:: ArrangementsID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/arrangement/playlist.rst",
    "content": "\\ :material-sharp:`playlist_play;1.2em;sd-pb-1` Playlist\n========================================================\n\n.. currentmodule:: pyflp.arrangement\n\n.. autoclass:: PLItemBase\n   :members:\n\n.. autoclass:: ChannelPLItem\n   :members:\n   :show-inheritance: PLItemBase\n\n.. autoclass:: PatternPLItem\n   :members:\n   :show-inheritance: PLItemBase\n"
  },
  {
    "path": "docs/reference/arrangement/track.rst",
    "content": "Track\n=====\n\n.. currentmodule:: pyflp.arrangement\n\n.. autoclass:: Track\n   :members:\n\n.. grid::\n\n   .. grid-item::\n\n      .. autoclass:: TrackMotion\n         :members:\n\n   .. grid-item::\n      :child-align: center\n      :columns: auto\n\n      .. image:: /img/arrangement/track/motion.png\n\n.. grid::\n\n   .. grid-item::\n\n      .. autoclass:: TrackPress\n         :members:\n\n   .. grid-item::\n      :child-align: center\n      :columns: auto\n\n      .. image:: /img/arrangement/track/press.png\n\n.. grid::\n\n   .. grid-item::\n\n      .. autoclass:: TrackSync\n         :members:\n\n   .. grid-item::\n      :child-align: center\n      :columns: auto\n\n      .. image:: /img/arrangement/track/sync.png\n\n.. autoclass:: TrackID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/channel/automation.rst",
    "content": "\\ :fas:`bezier-curve` Automation\n================================\n\n.. currentmodule:: pyflp.channel\n\n.. autoclass:: Automation\n   :show-inheritance:\n   :members:\n\n.. autoclass:: AutomationLFO\n   :members:\n\n.. autoclass:: AutomationPoint\n   :members:\n"
  },
  {
    "path": "docs/reference/channel/channel.rst",
    "content": "Channel\n=======\n\n.. currentmodule:: pyflp.channel\n\n.. autoclass:: Channel\n   :members:\n\nEnums\n-----\n\n.. autoclass:: ChannelType\n   :members:\n\n.. autoclass:: ChannelID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/channel/display-group.rst",
    "content": "DisplayGroup\n============\n\n.. currentmodule:: pyflp.channel\n\n.. autoclass:: DisplayGroup\n   :members:\n\n.. autoclass:: DisplayGroupID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/channel/index.rst",
    "content": "\\ :material-sharp:`dns;1.2em;sd-pb-1` Channel Rack\n==================================================\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n   :caption: Contents:\n   :glob:\n\n   *\n\n.. module:: pyflp.channel\n.. autoclass:: ChannelRack\n   :members:\n\n.. autoclass:: RackID\n   :members:\n   :member-order: bysource\n\n.. autoexception:: ChannelNotFound\n"
  },
  {
    "path": "docs/reference/channel/instrument.rst",
    "content": "Instrument\n==========\n\n.. currentmodule:: pyflp.channel\n\n.. autoclass:: Instrument\n   :show-inheritance:\n   :members:\n   :inherited-members: Channel\n"
  },
  {
    "path": "docs/reference/channel/layer.rst",
    "content": "\\ :fas:`layer-group` Layer\n==========================\n\n.. currentmodule:: pyflp.channel\n\n.. autoclass:: Layer\n   :show-inheritance:\n   :members:\n"
  },
  {
    "path": "docs/reference/channel/sampler.rst",
    "content": "\\ :material-sharp:`audio_file;1.2em;sd-pb-1` Sampler\n====================================================\n\n.. currentmodule:: pyflp.channel\n\n.. autoclass:: Sampler\n   :show-inheritance:\n   :members:\n   :inherited-members: Channel\n\n.. autoclass:: Content\n   :members:\n\n.. autoclass:: Envelope\n   :members:\n\n.. autoclass:: Filter\n   :members:\n\n.. autoclass:: FX\n   :members:\n\n.. autoclass:: Playback\n   :members:\n\n.. autoclass:: Reverb\n   :members:\n\n.. autoclass:: SamplerLFO\n   :members:\n\n.. autoclass:: TimeStretching\n   :members:\n\nEnums\n-----\n\n.. autoclass:: DeclickMode\n   :members:\n\n.. autoclass:: LFOShape\n   :members:\n\n.. autoclass:: ReverbType\n   :members:\n\n.. grid::\n\n   .. grid-item::\n\n      .. autoclass:: StretchMode\n         :members:\n\n   .. grid-item::\n      :child-align: center\n      :columns: auto\n\n      .. image:: /img/channel/stretch-mode.png\n"
  },
  {
    "path": "docs/reference/channel/shared.rst",
    "content": "Shared\n======\n\n.. currentmodule:: pyflp.channel\n\nThese implement functionality used by :class:`Channel` or its subclasses.\n\n.. autoclass:: Arp\n   :members:\n\n.. autoclass:: Delay\n   :members:\n\n.. autoclass:: Keyboard\n   :members:\n\n.. autoclass:: LevelAdjusts\n   :members:\n\n.. autoclass:: Polyphony\n   :members:\n\n.. autoclass:: Time\n   :members:\n\n.. autoclass:: Tracking\n   :members:\n\nEnums\n-----\n\n.. autoclass:: ArpDirection\n   :members:\n"
  },
  {
    "path": "docs/reference/controllers.rst",
    "content": "🎛 Controllers\n=============\n\n.. module:: pyflp.controller\n.. autoclass:: RemoteController\n   :members:\n\nEnums\n-----\n\n.. autoclass:: ControllerID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/events.rst",
    "content": "\\ :fas:`ellipsis` Events\n========================\n\n    This section is intended for those who want to delve into PyFLP's low-level\n    API or understand how internally events are ordered. A good understanding\n    of FL Studio's GUI is assumed.\n\nWhen to use the low level API?\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIf PyFLP fails to parse a particular model or you want to dive deep into the\ntrue / raw / real (whatever you want to call it) representation of an FLP.\n\n.. seealso:: :ref:`Binary layout <architecture-event>` of an event\n\nStructure\n---------\n\n    Very early versions of FL Studio were literally a **dump** of the changes\n    taking place in FL's GUI. Say for example, you create a channel and *then*\n    add notes to some pattern; the events for those would be dumped in the same\n    order.\n\n    Hopefully its not the same now, but some of those characteristics are still\n    visible.\n\n.. caution::\n\n    DO NOT use the following section as a definitive / complete source of\n    information for adding / removing your own events. While *most likely* your\n    events will get parsed correctly, there's always a chance of corrupting\n    your FLPs.\n\nThis is *roughly* the order of the events (as of latest FL Studio):\n\n1. Project-wide / metadata\n2. Display groups / channel filters\n3. Initialised controls\n4. Pattern notes / controllers\n5. MIDI remote controllers\n6. Internal remote controllers / automations\n7. 1st channel\n8. Pattern metadata\n9. Remaining channels\n10. Arrangements:\n\n    a. Index, name\n    b. Playlist items\n    c. Time markers\n    d. Tracks:\n\n        I. All data except name\n        II. Name\n\n11. Mixer:\n\n    a. Inserts:\n\n        A list of events in the order:\n\n        I. Color, name, icon and flags\n        II. Effect slots\n        III. Post EQ, input/output, routing\n\n    b. Remaining insert data\n12. Channel rack height\n\nChannel\n^^^^^^^\n\nThere are currently 5 types of channels (specified in :class:`ChannelType`).\nAlthough some of them don't use certain events, FL Studio dumps the same\nevent tree for any type of channel. For e.g. a :class:`Layer` channel will have\nall the events a :class:`Sampler` channel has, irrespective of whether the\nevents have any meaning in that context. Certain channels have extra events.\n\n=== =================================== ================================================\n#   Event ID                            Model / property\n=== =================================== ================================================\n1   :attr:`ChannelID.New`               :attr:`Channel.iid`\n2   :attr:`ChannelID.Type`              :class:`Channel` subclasses\n3   :attr:`PluginID.InternalName`       :attr:`Channel.internal_name`\n4   :attr:`PluginID.Wrapper`            :attr:`Instrument.plugin`\n5   :attr:`PluginID.Name`               :attr:`Channel.name`\n6   :attr:`PluginID.Icon`               :attr:`Channel.icon`\n7   :attr:`PluginID.Color`              :attr:`Channel.color`\n8   :attr:`PluginID.Data`               :attr:`Instrument.plugin`\n9   :attr:`ChannelID.IsEnabled`         :attr:`Channel.enabled`\n10  :attr:`ChannelID.Delay`             :attr:`_SamplerInstrument.delay` [#1]_\n11  :attr:`ChannelID.DelayModXY`        :attr:`_SamplerInstrument.delay` [#1]_\n12  :attr:`ChannelID.Reverb`            :attr:`Sampler.fx.reverb`\n13  :attr:`ChannelID.TimeShift`         :attr:`_SamplerInstrument.time.shift` [#1]\n14  :attr:`ChannelID.Swing`             :attr:`_SamplerInstrument.time.swing` [#1]_\n15  :attr:`ChannelID.FreqTilt`          :attr:`Sampler.fx.freq_tilt`\n16  :attr:`ChannelID.Pogo`              :attr:`Sampler.fx.pogo`\n17  :attr:`ChannelID.Cutoff`            :attr:`Sampler.fx.cutoff`\n18  :attr:`ChannelID.Resonance`         :attr:`Sampler.fx.reso`\n19  :attr:`ChannelID.Preamp`            :attr:`Sampler.fx.boost`\n20  :attr:`ChannelID.FadeOut`           :attr:`Sampler.fx.fade_out`\n21  :attr:`ChannelID.FadeIn`            :attr:`Sampler.fx.fade_in`\n22  :attr:`ChannelID.StereoDelay`       :attr:`Sampler.fx.stereo_delay`\n23  :attr:`ChannelID.RingMod`           :attr:`Sampler.fx.ringmod`\n24  :attr:`ChannelID.FXFlags`           Quite a few, refer code.\n25  :attr:`ChannelID.RoutedTo`          :attr:`_SamplerInstrument.insert` [#1]_\n26  :attr:`ChannelID.Levels`            :attr:`Sampler.filter` + few more\n27  :attr:`ChannelID.LevelAdjusts`      :attr:`_SamplerInstrument.level_adjusts` [#1]_\n28  :attr:`ChannelID.Polyphony`         :attr:`_SamplerInstrument.polyphony` [#1]_\n29  :attr:`ChannelID.Parameters`        A lot; spread across many models.\n30  :attr:`ChannelID.CutGroup`          :attr:`_SamplerInstrument.cut_group` [#1]_\n31  :attr:`ChannelID.LayerFlags`        :attr:`Layer.random`, :attr:`Layer.crossfade`\n32  :attr:`ChannelID.GroupNum`          :attr:`Channel.group`\n33* :attr:`ChannelID.Automation`        :class:`Automation`\n34  :attr:`ChannelID.IsLocked`          :attr:`Channel.locked`\n35  :attr:`ChannelID.Tracking` * 2      :attr:`_SamplerInstrument.tracking` [#1]_\n37  :attr:`ChannelID.EnvelopeLFO` * 5   :attr:`Sampler.envelopes`, :attr:`Sampler.lfos`\n42  :attr:`ChannelID.SamplerFlags`      Certain :class:`Sampler` properties.\n43  :attr:`ChannelID.PingPongLoop`      :attr:`Sampler.playback.ping_pong_loop`\n44* :attr:`ChannelID.SamplePath`        :attr:`Sampler.sample_path` [#2]_\n=== =================================== ================================================\n\n.. [#1] :class:`Sampler` & :class:`Instrument` base off of :class:`_SamplerInstrument`.\n.. [#2] Optional event for :class:`Sampler` only.\n\nPattern\n^^^^^^^\n\n:class:`Pattern` events are serialised at 2 different places inside an FLP.\nThe first section contains the notes and controllers held by a pattern if any.\n\n= ============================= ===========================\n# Event ID                      Property\n= ============================= ===========================\n1 :attr:`PatternID.New`         :attr:`Pattern.iid`\n2 :attr:`PatternID.Controllers` :attr:`Pattern.controllers`\n3 :attr:`PatternID.Notes`       :attr:`Pattern.notes`\n= ============================= ===========================\n\nThe next section contains colour, icon, timemarkers and any new events get\nadded here. Some events aren't listed because their order is not confirmed yet.\n\n= ============================= ======================\n# Event ID                      Property\n= ============================= ======================\n1 :attr:`PatternID.New` [#3]_   :attr:`Pattern.iid`\n2 :attr:`PatternID.Name`        :attr:`Pattern.name`\n3 :attr:`PatternID.Color`       :attr:`Pattern.color`\n4 157 [#3]_                     N.A.\n5 158 [#3]_                     N.A\n6 164 [#3]_                     N.A.\n= ============================= ======================\n\n.. [#3] Acts as an identifier here.\n.. [#4] Unknown events; complete list `here <https://github.com/demberto/PyFLP/discussions/34>`_.\n\nVST plugin parsing\n^^^^^^^^^^^^^^^^^^\n\nImplemented in :class:`VSTPluginEvent`, this is arguably the hardest event to\nparse *cleanly*. If you are familiar with PyFLP's internals, you might be\nsurprised to know that this event has events *inside events*. Why a struct\nwasn't usable is beyond me.\n"
  },
  {
    "path": "docs/reference/exceptions.rst",
    "content": "🛑 Exceptions\n==============\n\n.. automodule:: pyflp.exceptions\n   :members:\n   :show-inheritance:\n   :undoc-members:\n"
  },
  {
    "path": "docs/reference/mixer/index.rst",
    "content": "\\ :material-sharp:`settings_input_component;1.2em;sd-pb-1` Mixer\n================================================================\n\n.. module:: pyflp.mixer\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n   :caption: Contents:\n   :glob:\n\n   *\n\n.. autoclass:: Mixer\n   :members:\n\nEnums\n-----\n\n.. autoclass:: MixerID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/mixer/insert.rst",
    "content": "\\ :fas:`sliders` Insert\n=======================\n\n.. currentmodule:: pyflp.mixer\n\n.. autoclass:: Insert\n   :members:\n\n.. autoclass:: InsertEQ\n   :members:\n\n.. autoclass:: InsertEQBand\n   :members:\n\nEnums\n-----\n\n.. grid:: auto\n\n   .. grid-item::\n\n      .. autoclass:: InsertDock\n         :members:\n\n   .. grid-item::\n      :child-align: center\n\n      .. image:: /img/mixer/insert/dock.png\n\n.. autoclass:: InsertID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/mixer/slot.rst",
    "content": "\\ :fas:`folder-tree` Slot\n=========================\n\n.. currentmodule:: pyflp.mixer\n\n.. autoclass:: Slot\n   :members:\n\nEnums\n-----\n\n.. autoclass:: SlotID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/patterns/index.rst",
    "content": "🎹 Patterns\n============\n\n.. module:: pyflp.pattern\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n   :caption: Contents:\n   :glob:\n\n   *\n\n.. autoclass:: Patterns\n   :members:\n\nEnums\n-----\n\n.. autoclass:: PatternsID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/patterns/pattern.rst",
    "content": "Pattern\n=======\n\n.. currentmodule:: pyflp.pattern\n\n.. autoclass:: Pattern\n   :members:\n\n.. autoclass:: Controller\n   :members:\n\n.. autoclass:: Note\n   :members:\n\nEnums\n-----\n\n.. autoclass:: PatternID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/plugins/effects.rst",
    "content": "Effects\n=======\n\n.. currentmodule:: pyflp.plugin\n\n.. autoclass:: FruityBalance\n   :members:\n\n.. autoclass:: FruityBloodOverdrive\n   :members:\n\n.. autoclass:: FruityCenter\n   :members:\n\n.. autoclass:: FruityFastDist\n   :members:\n\n.. autoclass:: FruityNotebook2\n   :members:\n\n.. autoclass:: FruitySend\n   :members:\n\n.. autoclass:: FruitySoftClipper\n   :members:\n\n.. autoclass:: FruityStereoEnhancer\n   :members:\n\n.. autoclass:: Soundgoodizer\n   :members:\n"
  },
  {
    "path": "docs/reference/plugins/generators.rst",
    "content": "Generators\n==========\n\n.. currentmodule:: pyflp.plugin\n\n.. autoclass:: BooBass\n   :members:\n"
  },
  {
    "path": "docs/reference/plugins/index.rst",
    "content": "\\ :material-sharp:`extension;1.2em;sd-pb-1` Plugins\n===================================================\n\n.. module:: pyflp.plugin\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n   :caption: Contents:\n   :glob:\n\n   *\n\n.. autoclass:: _PluginBase\n   :members:\n\n.. autoclass:: PluginIOInfo\n   :members:\n\nEnums\n-----\n\n.. autoclass:: WrapperPage\n   :members:\n\n.. autoclass:: PluginID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/plugins/vst.rst",
    "content": "VST\n===\n\n.. currentmodule:: pyflp.plugin\n\n.. autoclass:: VSTPlugin\n   :members:\n\n   .. tab-set::\n\n      .. tab-item:: Settings\n\n         .. image:: /img/plugin/wrapper/settings.png\n\n         .. autoclass:: pyflp.plugin::VSTPlugin._AutomationOptions\n            :members:\n         .. autoclass:: pyflp.plugin::VSTPlugin._MIDIOptions\n            :members:\n         .. autoclass:: pyflp.plugin::VSTPlugin._UIOptions\n            :members:\n\n      .. tab-item:: Processing\n\n         .. image:: /img/plugin/wrapper/processing.png\n\n         .. autoclass:: pyflp.plugin::VSTPlugin._ProcessingOptions\n            :members:\n\n      .. tab-item:: Troubleshooting\n\n         .. image:: /img/plugin/wrapper/troubleshooting.png\n\n         .. autoclass:: pyflp.plugin::VSTPlugin._CompatibilityOptions\n            :members:\n"
  },
  {
    "path": "docs/reference/project.rst",
    "content": "\\ :fas:`file-waveform` Project\n==============================\n\n.. module:: pyflp.project\n\n.. autoclass:: Project\n   :members:\n\n   .. dropdown:: Information page\n      :open:\n\n      .. grid::\n\n         .. grid-item::\n            :columns: auto\n\n            * :attr:`Project.artists`\n            * :attr:`Project.created_on`\n            * :attr:`Project.comments`\n            * :attr:`Project.genre`\n            * :attr:`Project.show_info`\n            * :attr:`Project.url`\n            * :attr:`Project.time_spent`\n\n         .. grid-item::\n            :columns: 12 8 8 8\n            :margin: auto\n\n            .. image:: /img/project/info.png\n\n   .. dropdown:: Settings page\n      :open:\n\n      .. grid::\n\n         .. grid-item::\n\n            * :attr:`Project.data_path`\n            * :attr:`Project.pan_law`\n            * :attr:`Project.ppq`\n            * :attr:`Arrangements.time_signature <pyflp.arrangement.Arrangements.time_signature>`\n            * :attr:`Patterns.play_cut_notes <pyflp.pattern.Patterns.play_cut_notes>`\n\n         .. grid-item::\n\n            .. image:: /img/project/settings.png\n               :align: right\n\nEnums\n-----\n\n.. autoclass:: FileFormat\n   :members:\n   :member-order: bysource\n\n.. autoclass:: PanLaw\n   :members:\n   :member-order: bysource\n\n.. autoclass:: ProjectID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference/timemarkers.rst",
    "content": "\\ :fas:`timeline` Timemarkers\n=============================\n\n.. module:: pyflp.timemarker\n\n.. autoclass:: TimeMarker\n   :members:\n\nEnums\n-----\n\n.. grid::\n\n   .. grid-item::\n\n      .. autoclass:: TimeMarkerType\n         :members:\n\n   .. grid-item::\n      :child-align: center\n      :columns: auto\n\n      .. image:: /img/arrangement/timemarker/action.png\n\n.. autoclass:: TimeMarkerID\n   :members:\n   :member-order: bysource\n"
  },
  {
    "path": "docs/reference.rst",
    "content": "🧾 Reference\n=============\n\n.. toctree::\n   :maxdepth: 2\n   :titlesonly:\n   :caption: Contents:\n   :glob:\n\n   reference/*/index\n   reference/*\n\n:material-outlined:`api` API\n----------------------------\n\nPyFLP provides a low-level events-based API and a high-level API. Generally,\nyou should only need the high level API though.\n\n.. module:: pyflp\n.. autofunction:: parse\n.. autofunction:: save\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "furo==2023.5.20\nm2r2==0.3.2  # https://github.com/CrossNox/m2r2/issues/55\nsphinx==6.1.3\nsphinx-copybutton==0.5.2\nsphinx-design==0.4.1\nsphinx-hoverxref\nsphinx-toolbox==3.4.0\nsphinxcontrib-spelling==8.0.0\nsphinxcontrib-svgbob==0.2.1\n"
  },
  {
    "path": "pyflp/__init__.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"\nPyFLP - FL Studio project file parser\n=====================================\n\nLoad a project file:\n\n    >>> import pyflp\n    >>> project = pyflp.parse(\"/path/to/parse.flp\")\n\nSave the project:\n\n    >>> pyflp.save(project, \"/path/to/save.flp\")\n\nFull docs are available at https://pyflp.rtfd.io.\n\"\"\"  # noqa\n\nfrom __future__ import annotations\n\nimport io\nimport os\nimport pathlib\nimport struct\nimport sys\n\nimport construct as c\n\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    NEW_TEXT_IDS,\n    TEXT,\n    WORD,\n    AnyEvent,\n    AsciiEvent,\n    EventEnum,\n    EventTree,\n    IndexedEvent,\n    U8Event,\n    U16Event,\n    U32Event,\n    UnicodeEvent,\n    UnknownDataEvent,\n)\nfrom pyflp.exceptions import HeaderCorrupted, VersionNotDetected\nfrom pyflp.plugin import PluginID, get_event_by_internal_name\nfrom pyflp.project import VALID_PPQS, FileFormat, Project, ProjectID\n\n__all__ = [\"parse\", \"save\"]\n\nFLP_HEADER = struct.Struct(\"4sIh2H\")\n\nif sys.version_info < (3, 11):  # https://github.com/Bobronium/fastenum/issues/2\n    import fastenum\n\n    fastenum.enable()  # 33% faster parse()\n\n\ndef parse(file: pathlib.Path | str) -> Project:\n    \"\"\"Parses an FL Studio project file and returns a parsed :class:`Project`.\n\n    Args:\n        file: Path to the FLP.\n\n    Raises:\n        HeaderCorrupted: When an invalid value is found in the file header.\n        VersionNotDetected: A correct string type couldn't be determined.\n    \"\"\"\n    with open(file, \"rb\") as flp:\n        stream = io.BytesIO(flp.read())\n\n    events: list[AnyEvent] = []\n    header = stream.read(FLP_HEADER.size)\n\n    try:\n        hdr_magic, hdr_size, fmt, channel_count, ppq = FLP_HEADER.unpack(header)\n    except struct.error as exc:\n        raise HeaderCorrupted(\"Couldn't read the header entirely\") from exc\n\n    if hdr_magic != b\"FLhd\":\n        raise HeaderCorrupted(\"Unexpected header chunk magic; expected 'FLhd'\")\n\n    if hdr_size != 6:\n        raise HeaderCorrupted(\"Unexpected header chunk size; expected 6\")\n\n    try:\n        file_format = FileFormat(fmt)\n    except ValueError as exc:\n        raise HeaderCorrupted(\"Unsupported project file format\") from exc\n\n    if ppq not in VALID_PPQS:\n        raise HeaderCorrupted(\"Invalid PPQ\")\n\n    if stream.read(4) != b\"FLdt\":\n        raise HeaderCorrupted(\"Unexpected data chunk magic; expected 'FLdt'\")\n\n    events_size = int.from_bytes(stream.read(4), \"little\")\n    if not events_size:  # pragma: no cover\n        raise HeaderCorrupted(\"Data chunk size couldn't be read\")\n\n    stream.seek(0, os.SEEK_END)\n    file_size = stream.tell()\n    if file_size != events_size + 22:\n        raise HeaderCorrupted(\"Data chunk size corrupted\")\n\n    plug_name = None\n    str_type: type[AsciiEvent] | type[UnicodeEvent] | None = None\n    stream.seek(22)  # Back to start of events\n    while stream.tell() < file_size:\n        event_type: type[AnyEvent] | None = None\n        id = EventEnum(int.from_bytes(stream.read(1), \"little\"))\n\n        if id < WORD:\n            value = stream.read(1)\n        elif id < DWORD:\n            value = stream.read(2)\n        elif id < TEXT:\n            value = stream.read(4)\n        else:\n            size = c.VarInt.parse_stream(stream)\n            value = stream.read(size)\n\n        if id == ProjectID.FLVersion:\n            parts = value.decode(\"ascii\").rstrip(\"\\0\").split(\".\")\n            if [int(part) for part in parts][0:2] >= [11, 5]:\n                str_type = UnicodeEvent\n            else:\n                str_type = AsciiEvent\n\n        for enum_ in EventEnum.__subclasses__():\n            if id in enum_:\n                event_type = getattr(enum_(id), \"type\")\n                break\n\n        if event_type is None:\n            if id < WORD:\n                event_type = U8Event\n            elif id < DWORD:\n                event_type = U16Event\n            elif id < TEXT:\n                event_type = U32Event\n            elif id < DATA or id.value in NEW_TEXT_IDS:\n                if str_type is None:  # pragma: no cover\n                    raise VersionNotDetected  # ! This should never happen\n                event_type = str_type\n\n                if id == PluginID.InternalName:\n                    plug_name = event_type(id, value).value\n            elif id == PluginID.Data and plug_name is not None:\n                event_type = get_event_by_internal_name(plug_name)\n            else:\n                event_type = UnknownDataEvent\n\n        events.append(event_type(id, value))\n\n    return Project(\n        EventTree(init=(IndexedEvent(r, e) for r, e in enumerate(events))),\n        channel_count=channel_count,\n        format=file_format,\n        ppq=ppq,\n    )\n\n\ndef save(project: Project, file: pathlib.Path | str) -> None:\n    \"\"\"Save a parsed project back into a file.\n\n    Caution:\n        Always have a backup ready, just in case 😉\n\n    Args:\n        project: The object returned by :meth:`parse`.\n        file: The file in which the contents of :attr:`project` are serialised back.\n    \"\"\"\n    buf = bytearray()\n    num_channels = len(project.channels)\n    header = FLP_HEADER.pack(b\"FLhd\", 6, project.format, num_channels, project.ppq)\n    buf.extend(header)\n    buf.extend(b\"FLdt\" + (b\"\\0\" * 4))\n    total_size = 0\n    for event in project.events:\n        raw = bytes(event)\n        total_size += len(raw)\n        buf.extend(raw)\n    buf[18:22] = total_size.to_bytes(4, \"little\")\n\n    with open(file, \"wb\") as fp:\n        fp.write(buf)\n"
  },
  {
    "path": "pyflp/_adapters.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\nfrom __future__ import annotations\n\nimport math\nimport warnings\nfrom typing import Any, List, Tuple\n\nimport construct as c\nimport construct_typed as ct\nfrom typing_extensions import TypeAlias\n\nfrom pyflp.types import ET, MusicalTime, T, U\n\nSimpleAdapter: TypeAlias = ct.Adapter[T, T, U, U]\n\"\"\"Duplicates type parameters for `construct.Adapter`.\"\"\"\n\nFourByteBool: c.ExprAdapter[int, int, bool, int] = c.ExprAdapter(\n    c.Int32ul, lambda obj_, *_: bool(obj_), lambda obj_, *_: int(obj_)  # type: ignore\n)\n\n\nclass List2Tuple(SimpleAdapter[Any, Tuple[int, int]]):\n    def _decode(self, obj: c.ListContainer[int], *_: Any) -> tuple[int, int]:\n        _1, _2 = tuple(obj)\n        return _1, _2\n\n    def _encode(self, obj: tuple[int, int], *_: Any) -> c.ListContainer[int]:\n        return c.ListContainer([*obj])\n\n\nclass LinearMusical(SimpleAdapter[int, MusicalTime]):\n    def _encode(self, obj: MusicalTime, *_: Any) -> int:\n        if obj.ticks % 5:\n            warnings.warn(\"Ticks must be a multiple of 5\", UserWarning)\n\n        return (obj.bars * 768) + (obj.beats * 48) + int(obj.ticks * 0.2)\n\n    def _decode(self, obj: int, *_: Any) -> MusicalTime:\n        bars, remainder = divmod(obj, 768)\n        beats, remainder = divmod(remainder, 48)\n        return MusicalTime(bars, beats, ticks=remainder * 5)\n\n\nclass Log2(SimpleAdapter[int, float]):\n    def __init__(self, subcon: Any, factor: int) -> None:\n        super().__init__(subcon)  # type: ignore[call-arg]\n        self.factor = factor\n\n    def _encode(self, obj: float, *_: Any) -> int:\n        return int(self.factor * math.log2(obj))\n\n    def _decode(self, obj: int, *_: Any) -> float:\n        return 2 ** (obj / self.factor)\n\n\n# Thanks to @algmyr from Python Discord server for finding out the formulae used\n# ! See https://github.com/construct/construct/issues/999\nclass LogNormal(SimpleAdapter[List[int], float]):\n    def __init__(self, subcon: Any, bound: tuple[int, int]) -> None:\n        super().__init__(subcon)  # type: ignore[call-arg]\n        self.lo, self.hi = bound\n\n    def _encode(self, obj: float, *_: Any) -> list[int]:\n        \"\"\"Clamps the integer representation of ``obj`` and returns it.\"\"\"\n        if not 0.0 <= obj <= 1.0:\n            raise ValueError(f\"Expected a value between 0.0 to 1.0; got {obj}\")\n\n        if not obj:  # log2(0.0) --> -inf ==> 0\n            return [0, 0]\n\n        return [min(max(self.lo, int(2**12 * (math.log2(obj) + 15))), self.hi), 63]\n\n    def _decode(self, obj: list[int], *_: Any) -> float:\n        \"\"\"Returns a float representation of ``obj[0]`` between 0.0 to 1.0.\"\"\"\n        if not obj[0]:\n            return 0.0\n\n        if obj[1] != 63:\n            raise ValueError(f\"Not a LogNormal, 2nd int must be 63; not {obj[1]}\")\n\n        return max(min(1.0, 2 ** (obj[0] / 2**12) / 2**15), 0.0)\n\n\nclass StdEnum(SimpleAdapter[int, ET]):\n    def _encode(self, obj: ET, *_: Any) -> int:\n        return obj.value\n\n    def _decode(self, obj: int, *_: Any) -> ET:\n        return self.__orig_class__.__args__[0](obj)  # type: ignore\n"
  },
  {
    "path": "pyflp/_descriptors.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the descriptor and adaptor classes used by models and events.\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport enum\nfrom typing import Any, Protocol, overload, runtime_checkable\n\nfrom typing_extensions import Self, final\n\nfrom pyflp._events import AnyEvent, EventEnum, StructEventBase\nfrom pyflp._models import VE, EMT_co, EventModel, ItemModel, ModelBase\nfrom pyflp.exceptions import PropertyCannotBeSet\nfrom pyflp.types import T, T_co\n\n\n@runtime_checkable\nclass ROProperty(Protocol[T_co]):\n    \"\"\"Protocol for a read-only descriptor.\"\"\"\n\n    def __get__(self, ins: Any, owner: Any = None) -> T_co | Self | None:\n        ...\n\n\n@runtime_checkable\nclass RWProperty(ROProperty[T], Protocol):\n    \"\"\"Protocol for a read-write descriptor.\"\"\"\n\n    def __set__(self, ins: Any, value: T) -> None:\n        ...\n\n\nclass NamedPropMixin:\n    def __init__(self, prop: str | None = None) -> None:\n        self._prop = prop or \"\"\n\n    def __set_name__(self, _: Any, name: str) -> None:\n        if not self._prop:\n            self._prop = name\n\n\nclass PropBase(abc.ABC, RWProperty[T]):\n    def __init__(self, *ids: EventEnum, default: T | None = None, readonly: bool = False):\n        self._ids = ids\n        self._default = default\n        self._readonly = readonly\n\n    @overload\n    def _get_event(self, ins: ItemModel[VE]) -> ItemModel[VE]:\n        ...\n\n    @overload\n    def _get_event(self, ins: EventModel) -> AnyEvent | None:\n        ...\n\n    def _get_event(self, ins: ItemModel[VE] | EventModel):\n        if isinstance(ins, ItemModel):\n            return ins\n\n        if not self._ids:\n            if len(ins.events) > 1:  # Prevent ambiguous situations\n                raise LookupError(\"Event ID not specified\")\n\n            return tuple(ins.events)[0]\n\n        for id in self._ids:\n            if id in ins.events:\n                return ins.events.first(id)\n\n    @property\n    def default(self) -> T | None:  # Configure version based defaults here\n        return self._default\n\n    @abc.abstractmethod\n    def _get(self, ev_or_ins: Any) -> T | None:\n        ...\n\n    @abc.abstractmethod\n    def _set(self, ev_or_ins: Any, value: T) -> None:\n        ...\n\n    @final\n    def __get__(self, ins: Any, owner: Any = None) -> T | Self | None:\n        if ins is None:\n            return self\n\n        if owner is None:\n            return NotImplemented\n\n        event: Any = self._get_event(ins)\n        if event is not None:\n            return self._get(event)\n\n        return self.default\n\n    @final\n    def __set__(self, ins: Any, value: T) -> None:\n        if self._readonly:\n            raise PropertyCannotBeSet(*self._ids)\n\n        event: Any = self._get_event(ins)\n        if event is not None:\n            self._set(event, value)\n        else:\n            raise PropertyCannotBeSet(*self._ids)\n\n\nclass FlagProp(PropBase[bool]):\n    \"\"\"Properties derived from enum flags.\"\"\"\n\n    def __init__(\n        self,\n        flag: enum.IntFlag,\n        *ids: EventEnum,\n        prop: str = \"flags\",\n        inverted: bool = False,\n        default: bool | None = None,\n    ) -> None:\n        \"\"\"\n        Args:\n            flag: The flag which is to be checked for.\n            id: Event ID (required for MultiEventModel).\n            prop: The dict key which contains the flags in a `Struct`.\n            inverted: If this is true, property getter and setters\n                      invert the value to be set / returned.\n        \"\"\"\n        self._flag = flag\n        self._flag_type = type(flag)\n        self._prop = prop\n        self._inverted = inverted\n        super().__init__(*ids, default=default)\n\n    def _get(self, ev_or_ins: Any) -> bool | None:\n        if isinstance(ev_or_ins, (ItemModel, StructEventBase)):\n            flags = ev_or_ins[self._prop]\n        else:\n            flags = ev_or_ins.value  # type: ignore\n\n        if flags is not None:\n            retbool = self._flag in self._flag_type(flags)\n            return not retbool if self._inverted else retbool\n\n    def _set(self, ev_or_ins: Any, value: bool) -> None:\n        if self._inverted:\n            value = not value\n\n        if isinstance(ev_or_ins, (ItemModel, StructEventBase)):\n            if value:\n                ev_or_ins[self._prop] |= self._flag\n            else:\n                ev_or_ins[self._prop] &= ~self._flag\n        else:\n            if value:\n                ev_or_ins.value |= self._flag  # type: ignore\n            else:\n                ev_or_ins.value &= ~self._flag  # type: ignore\n\n\nclass KWProp(NamedPropMixin, RWProperty[T]):\n    \"\"\"Properties derived from non-local event values.\n\n    These values are passed to the class constructor as keyword arguments.\n    \"\"\"\n\n    def __get__(self, ins: ModelBase | None, owner: Any = None) -> T | Self:\n        if ins is None:\n            return self\n\n        if owner is None:\n            return NotImplemented\n        return ins._kw[self._prop]\n\n    def __set__(self, ins: ModelBase, value: T) -> None:\n        if self._prop not in ins._kw:\n            raise KeyError(self._prop)\n        ins._kw[self._prop] = value\n\n\nclass EventProp(PropBase[T]):\n    \"\"\"Properties bound directly to one of fixed size or string events.\"\"\"\n\n    def _get(self, ev_or_ins: AnyEvent) -> T | None:\n        return ev_or_ins.value\n\n    def _set(self, ev_or_ins: AnyEvent, value: T) -> None:\n        ev_or_ins.value = value\n\n\nclass NestedProp(ROProperty[EMT_co]):\n    def __init__(self, type: type[EMT_co], *ids: EventEnum) -> None:\n        self._ids = ids\n        self._type = type\n\n    def __get__(self, ins: EventModel, owner: Any = None) -> EMT_co:\n        if owner is None:\n            return NotImplemented\n\n        return self._type(ins.events.subtree(lambda e: e.id in self._ids))\n\n\nclass StructProp(PropBase[T], NamedPropMixin):\n    \"\"\"Properties obtained from a :class:`construct.Struct`.\"\"\"\n\n    def __init__(self, *ids: EventEnum, prop: str | None = None, **kwds: Any) -> None:\n        super().__init__(*ids, **kwds)\n        NamedPropMixin.__init__(self, prop)\n\n    def _get(self, ev_or_ins: ItemModel[Any]) -> T | None:\n        return ev_or_ins[self._prop]\n\n    def _set(self, ev_or_ins: ItemModel[Any], value: T) -> None:\n        ev_or_ins[self._prop] = value\n"
  },
  {
    "path": "pyflp/_events.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains implementations for various types of event data and its container.\n\nThese types serve as the backbone for model creation and simplify marshalling\nand unmarshalling.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport enum\nimport warnings\nfrom collections.abc import Callable, Iterable, Iterator, Sequence\nfrom dataclasses import dataclass, field\nfrom itertools import zip_longest\nfrom typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Tuple, cast\n\nimport construct as c\nfrom sortedcontainers import SortedList\nfrom typing_extensions import Concatenate, TypeAlias\n\nfrom pyflp.exceptions import (\n    EventIDOutOfRange,\n    InvalidEventChunkSize,\n    PropertyCannotBeSet,\n)\nfrom pyflp.types import RGBA, P, T, AnyContainer, AnyListContainer, AnyList, AnyDict\n\nBYTE: Final = 0\nWORD: Final = 64\nDWORD: Final = 128\nTEXT: Final = 192\nDATA: Final = 208\nNEW_TEXT_IDS: Final = (\n    TEXT + 49,  # ArrangementID.Name\n    TEXT + 39,  # DisplayGroupID.Name\n    TEXT + 47,  # TrackID.Name\n)\n\n\nclass _EventEnumMeta(enum.EnumMeta):\n    def __contains__(self, obj: object) -> bool:\n        \"\"\"Whether ``obj`` is one of the integer values of enum members.\n\n        Args:\n            obj: Can be an ``int`` or an ``EventEnum``.\n        \"\"\"\n        return obj in tuple(self)\n\n\nclass EventEnum(int, enum.Enum, metaclass=_EventEnumMeta):\n    \"\"\"IDs used by events.\n\n    Event values are stored as a tuple of event ID and its designated type.\n    The types are used to serialise/deserialise events by the parser.\n\n    All event names prefixed with an underscore (_) are deprecated w.r.t to\n    the latest version of FL Studio, *to the best of my knowledge*.\n    \"\"\"\n\n    def __new__(cls, id: int, type: type[AnyEvent] | None = None):\n        obj = int.__new__(cls, id)\n        obj._value_ = id\n        setattr(obj, \"type\", type)\n        return obj\n\n    # This allows EventBase.id to actually use EventEnum for representation and\n    # not just equality checks. It will be much simpler to debug problematic\n    # events, if the name of the ID is directly visible.\n    @classmethod\n    def _missing_(cls, value: object) -> EventEnum | None:\n        \"\"\"Allows unknown IDs in the range of 0-255.\"\"\"\n        if isinstance(value, int) and 0 <= value <= 255:\n            # First check in existing subclasses\n            for sc in cls.__subclasses__():\n                if value in sc:\n                    return sc(value)\n\n            # Else create a new pseudo member\n            pseudo_member = cls._value2member_map_.get(value, None)\n            if pseudo_member is None:\n                new_member = int.__new__(cls, value)\n                new_member._name_ = str(value)\n                new_member._value_ = value\n                pseudo_member = cls._value2member_map_.setdefault(value, new_member)\n            return cast(EventEnum, pseudo_member)\n        # Raises ValueError in Enum.__new__\n\n\nclass EventBase(Generic[T]):\n    \"\"\"Generic ABC representing an event.\"\"\"\n\n    STRUCT: c.Construct[T, T]\n    ALLOWED_IDS: ClassVar[Sequence[int]] = []\n\n    def __init__(self, id: EventEnum, data: bytes, **kwds: Any) -> None:\n        if self.ALLOWED_IDS and id not in self.ALLOWED_IDS:\n            raise EventIDOutOfRange(id, *self.ALLOWED_IDS)\n\n        if id < TEXT:\n            if id < WORD:\n                expected_size = 1\n            elif id < DWORD:\n                expected_size = 2\n            else:\n                expected_size = 4\n\n            if len(data) != expected_size:\n                raise InvalidEventChunkSize(expected_size, len(data))\n\n        self.id = EventEnum(id)\n        self._kwds = kwds\n        self.value = self.STRUCT.parse(data, **self._kwds)\n\n    def __eq__(self, o: object) -> bool:\n        if not isinstance(o, EventBase):\n            raise TypeError(f\"Cannot find equality of an {type(o)} and {type(self)!r}\")\n        return self.id == o.id and self.value == cast(EventBase[T], o).value\n\n    def __ne__(self, o: object) -> bool:\n        if not isinstance(o, EventBase):\n            raise TypeError(f\"Cannot find inequality of a {type(o)} and {type(self)!r}\")\n        return self.id != o.id or self.value != cast(EventBase[T], o).value\n\n    def __bytes__(self) -> bytes:\n        id = c.Byte.build(self.id)\n        data = self.STRUCT.build(self.value, **self._kwds)\n\n        if self.id < TEXT:\n            return id + data\n\n        length = c.VarInt.build(len(data))\n        return id + length + data\n\n    def __repr__(self) -> str:\n        return f\"<{type(self)!r}(id={self.id!r}, value={self.value!r})>\"\n\n    @property\n    def size(self) -> int:\n        \"\"\"Serialised event size (in bytes).\"\"\"\n\n        if self.id >= TEXT:\n            return len(bytes(self))\n        elif self.id >= DWORD:\n            return 5\n        elif self.id >= WORD:\n            return 3\n        else:\n            return 2\n\n\nAnyEvent: TypeAlias = EventBase[Any]\n\n\nclass ByteEventBase(EventBase[T]):\n    \"\"\"Base class of events used for storing 1 byte data.\"\"\"\n\n    ALLOWED_IDS = range(BYTE, WORD)\n\n    def __init__(self, id: EventEnum, data: bytes) -> None:\n        \"\"\"\n        Args:\n            id: **0** to **63**.\n            data: Event data of size 1.\n\n        Raises:\n            EventIDOutOfRangeError: When ``id`` is not in range of 0-63.\n            InvalidEventChunkSizeError: When size of `data` is not 1.\n        \"\"\"\n        super().__init__(id, data)\n\n\nclass BoolEvent(ByteEventBase[bool]):\n    \"\"\"An event used for storing a boolean.\"\"\"\n\n    STRUCT = c.Flag\n\n\nclass I8Event(ByteEventBase[int]):\n    \"\"\"An event used for storing a 1 byte signed integer.\"\"\"\n\n    STRUCT = c.Int8sl\n\n\nclass U8Event(ByteEventBase[int]):\n    \"\"\"An event used for storing a 1 byte unsigned integer.\"\"\"\n\n    STRUCT = c.Int8ul\n\n\nclass WordEventBase(EventBase[int], abc.ABC):\n    \"\"\"Base class of events used for storing 2 byte data.\"\"\"\n\n    ALLOWED_IDS = range(WORD, DWORD)\n\n    def __init__(self, id: EventEnum, data: bytes) -> None:\n        \"\"\"\n        Args:\n            id: **64** to **127**.\n            data: Event data of size 2.\n\n        Raises:\n            EventIDOutOfRangeError: When ``id`` is not in range of 64-127.\n            InvalidEventChunkSizeError: When size of `data` is not 2.\n        \"\"\"\n        super().__init__(id, data)\n\n\nclass I16Event(WordEventBase):\n    \"\"\"An event used for storing a 2 byte signed integer.\"\"\"\n\n    STRUCT = c.Int16sl\n\n\nclass U16Event(WordEventBase):\n    \"\"\"An event used for storing a 2 byte unsigned integer.\"\"\"\n\n    STRUCT = c.Int16ul\n\n\nclass DWordEventBase(EventBase[T], abc.ABC):\n    \"\"\"Base class of events used for storing 4 byte data.\"\"\"\n\n    ALLOWED_IDS = range(DWORD, TEXT)\n\n    def __init__(self, id: EventEnum, data: bytes) -> None:\n        \"\"\"\n        Args:\n            id: **128** to **191**.\n            data: Event data of size 4.\n\n        Raises:\n            EventIDOutOfRangeError: When ``id`` is not in range of 128-191.\n            InvalidEventChunkSizeError: When size of `data` is not 4.\n        \"\"\"\n        super().__init__(id, data)\n\n\nclass F32Event(DWordEventBase[float]):\n    \"\"\"An event used for storing 4 byte floats.\"\"\"\n\n    STRUCT = c.Float32l\n\n\nclass I32Event(DWordEventBase[int]):\n    \"\"\"An event used for storing a 4 byte signed integer.\"\"\"\n\n    STRUCT = c.Int32sl\n\n\nclass U32Event(DWordEventBase[int]):\n    \"\"\"An event used for storing a 4 byte unsigned integer.\"\"\"\n\n    STRUCT = c.Int32ul\n\n\nclass U16TupleEvent(DWordEventBase[Tuple[int, int]]):\n    \"\"\"An event used for storing a two-tuple of 2 byte unsigned integers.\"\"\"\n\n    STRUCT = c.ExprAdapter(\n        c.Int16ul[2],\n        lambda obj_, *_: tuple(obj_),  # type: ignore\n        lambda obj_, *_: list(obj_),  # type: ignore\n    )\n\n\nclass ColorEvent(DWordEventBase[RGBA]):\n    \"\"\"A 4 byte event which stores a color.\"\"\"\n\n    STRUCT = c.ExprAdapter(\n        c.Bytes(4),\n        lambda obj, *_: RGBA.from_bytes(obj),  # type: ignore\n        lambda obj, *_: bytes(obj),  # type: ignore\n    )\n\n\nclass StrEventBase(EventBase[str]):\n    \"\"\"Base class of events used for storing strings.\"\"\"\n\n    ALLOWED_IDS = (*range(TEXT, DATA), *NEW_TEXT_IDS)\n\n    def __init__(self, id: EventEnum, data: bytes) -> None:\n        \"\"\"\n        Args:\n            id: **192** to **207** or in :attr:`NEW_TEXT_IDS`.\n            data: ASCII or UTF16 encoded string data.\n\n        Raises:\n            ValueError: When ``id`` is not in 192-207 or in :attr:`NEW_TEXT_IDS`.\n        \"\"\"\n        super().__init__(id, data)\n\n\nclass AsciiEvent(StrEventBase):\n    if TYPE_CHECKING:\n        STRUCT: c.ExprAdapter[str, str, str, str]\n    else:\n        STRUCT = c.ExprAdapter(\n            c.GreedyString(\"ascii\"),\n            lambda obj, *_: obj.rstrip(\"\\0\"),\n            lambda obj, *_: obj + \"\\0\",\n        )\n\n\nclass UnicodeEvent(StrEventBase):\n    if TYPE_CHECKING:\n        STRUCT: c.ExprAdapter[str, str, str, str]\n    else:\n        STRUCT = c.ExprAdapter(\n            c.GreedyString(\"utf-16-le\"),\n            lambda obj, *_: obj.rstrip(\"\\0\"),\n            lambda obj, *_: obj + \"\\0\",\n        )\n\n\nclass StructEventBase(EventBase[AnyContainer], AnyDict):\n    \"\"\"Base class for events used for storing fixed size structured data.\n\n    Consists of a collection of POD types like int, bool, float, but not strings.\n    Its size is determined by the event as well as FL version.\n    \"\"\"\n\n    def __init__(self, id: EventEnum, data: bytes) -> None:\n        super().__init__(id, data, len=len(data))\n        self.data = self.value  # Akin to UserDict.__init__\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        if key not in self:\n            raise KeyError\n\n        if self[key] is None:\n            raise PropertyCannotBeSet\n\n        self.data[key] = value\n\n\nclass ListEventBase(EventBase[AnyListContainer], AnyList):\n    \"\"\"Base class for events storing an array of structured data.\n\n    Attributes:\n        kwds: Keyword args passed to :meth:`STRUCT.parse` & :meth:`STRUCT.build`.\n    \"\"\"\n\n    STRUCT: c.Subconstruct[Any, Any, Any, Any]\n    SIZES: ClassVar[list[int]] = []\n    \"\"\"Manual :meth:`STRUCT.sizeof` override(s).\"\"\"\n\n    def __init__(self, id: EventEnum, data: bytes, **kwds: Any) -> None:\n        super().__init__(id, data, **kwds)\n        self._struct_size: int | None = None\n\n        if not self.SIZES:\n            self._struct_size = self.STRUCT.subcon.sizeof()\n\n        for size in self.SIZES:\n            if not len(data) % size:\n                self._struct_size = size\n                break\n\n        if self._struct_size is None:  # pragma: no cover\n            warnings.warn(\n                f\"Cannot parse event {id} as event size {len(data)} \"\n                f\"is not a multiple of struct size(s) {self.SIZES}\"\n            )\n        else:\n            self.data = self.value  # Akin to UserList.__init__\n\n\nclass UnknownDataEvent(EventBase[bytes]):\n    \"\"\"Used for events whose structure is unknown as of yet.\"\"\"\n\n    STRUCT = c.GreedyBytes\n\n\n@dataclass(order=True)\nclass IndexedEvent:\n    r: int\n    \"\"\"Root index of occurence of :attr:`e`.\"\"\"\n\n    e: AnyEvent = field(compare=False)\n    \"\"\"The indexed event.\"\"\"\n\n\ndef yields_child(func: Callable[Concatenate[EventTree, P], Iterator[EventTree]]):\n    \"\"\"Adds an :class:`EventTree` to its parent's list of children and yields it.\"\"\"\n\n    def wrapper(self: EventTree, *args: P.args, **kwds: P.kwargs):\n        for child in func(self, *args, **kwds):\n            self.children.append(child)\n            yield child\n\n    return wrapper\n\n\nclass EventTree:\n    \"\"\"Provides mutable \"views\" which propagate changes back to parents.\n\n    This tree is analogous to the hierarchy used by models.\n\n    Attributes:\n        parent: Immediate ancestor / parent. Defaults to self.\n        root: Parent of all parent trees.\n        children: List of children.\n    \"\"\"\n\n    def __init__(\n        self,\n        parent: EventTree | None = None,\n        init: Iterable[IndexedEvent] | None = None,\n    ) -> None:\n        \"\"\"Create a new dictionary with an optional :attr:`parent`.\"\"\"\n        self.children: list[EventTree] = []\n        self.lst: list[IndexedEvent] = SortedList(init or [])  # type: ignore\n\n        self.parent = parent\n        if parent is not None:\n            parent.children.append(self)\n\n        while parent is not None and parent.parent is not None:\n            parent = parent.parent\n        self.root = parent or self\n\n    def __contains__(self, id: EventEnum) -> bool:\n        \"\"\"Whether the key :attr:`id` exists in the list.\"\"\"\n        return any(ie.e.id == id for ie in self.lst)\n\n    def __eq__(self, o: object) -> bool:\n        \"\"\"Compares equality of internal lists.\"\"\"\n        if not isinstance(o, EventTree):\n            return NotImplemented\n\n        return self.lst == o.lst\n\n    def __iadd__(self, *events: AnyEvent) -> None:\n        \"\"\"Analogous to :meth:`list.extend`.\"\"\"\n        for event in events:\n            self.append(event)\n\n    def __iter__(self) -> Iterator[AnyEvent]:\n        return (ie.e for ie in self.lst)\n\n    def __len__(self) -> int:\n        return len(self.lst)\n\n    def __repr__(self) -> str:\n        return f\"EventTree({len(self.ids)} IDs, {len(self)} events)\"\n\n    def _get_ie(self, *ids: EventEnum) -> Iterator[IndexedEvent]:\n        return (ie for ie in self.lst if ie.e.id in ids)\n\n    def _recursive(self, action: Callable[[EventTree], None]) -> None:\n        \"\"\"Recursively performs :attr:`action` on self and all parents.\"\"\"\n        action(self)\n        ancestor = self.parent\n        while ancestor is not None:\n            action(ancestor)\n            ancestor = ancestor.parent\n\n    def append(self, event: AnyEvent) -> None:\n        \"\"\"Appends an event at its corresponding key's list's end.\"\"\"\n        self.insert(len(self), event)\n\n    def count(self, id: EventEnum) -> int:\n        \"\"\"Returns the count of the events with :attr:`id`.\"\"\"\n        return len(list(self._get_ie(id)))\n\n    @yields_child\n    def divide(self, separator: EventEnum, *ids: EventEnum) -> Iterator[EventTree]:\n        \"\"\"Yields subtrees containing events separated by ``separator`` infinitely.\"\"\"\n        el: list[IndexedEvent] = []\n        first = True\n        for ie in self.lst:\n            if ie.e.id == separator:\n                if not first:\n                    yield EventTree(self, el)\n                    el = []\n                else:\n                    first = False\n\n            if ie.e.id in ids:\n                el.append(ie)\n        yield EventTree(self, el)  # Yield the last one\n\n    def first(self, id: EventEnum) -> AnyEvent:\n        \"\"\"Returns the first event with :attr:`id`.\n\n        Raises:\n            KeyError: An event with :attr:`id` isn't found.\n        \"\"\"\n        try:\n            return next(self.get(id))\n        except StopIteration as exc:\n            raise KeyError(id) from exc\n\n    def get(self, *ids: EventEnum) -> Iterator[AnyEvent]:\n        \"\"\"Yields events whose ID is one of :attr:`ids`.\"\"\"\n        return (e for e in self if e.id in ids)\n\n    @yields_child\n    def group(self, *ids: EventEnum) -> Iterator[EventTree]:\n        \"\"\"Yields EventTrees of zip objects of events with matching :attr:`ids`.\"\"\"\n        for iet in zip_longest(*(self._get_ie(id) for id in ids)):  # unpack magic\n            yield EventTree(self, [ie for ie in iet if ie])  # filter out None values\n\n    def insert(self, pos: int, e: AnyEvent) -> None:\n        \"\"\"Inserts :attr:`ev` at :attr:`pos` in this and all parent trees.\"\"\"\n        rootidx = sorted(self.indexes)[pos] if len(self) else 0\n\n        # Shift all root indexes after rootidx by +1 to prevent collisions\n        # while sorting the entire list by root indexes before serialising.\n        for ie in self.root.lst:\n            if ie.r >= rootidx:\n                ie.r += 1\n\n        self._recursive(lambda et: et.lst.add(IndexedEvent(rootidx, e)))  # type: ignore\n\n    def pop(self, id: EventEnum, pos: int = 0) -> AnyEvent:\n        \"\"\"Pops the event with ``id`` at ``pos`` in ``self`` and all parents.\"\"\"\n        if id not in self.ids:\n            raise KeyError(id)\n\n        ie = [ie for ie in self.lst if ie.e.id == id][pos]\n        self._recursive(lambda et: et.lst.remove(ie))\n\n        # Shift all root indexes of events after rootidx by -1.\n        for root_ie in self.root.lst:\n            if root_ie.r >= ie.r:\n                root_ie.r -= 1\n\n        return ie.e\n\n    def remove(self, id: EventEnum, pos: int = 0) -> None:\n        \"\"\"Removes the event with ``id`` at ``pos`` in ``self`` and all parents.\"\"\"\n        self.pop(id, pos)\n\n    @yields_child\n    def separate(self, id: EventEnum) -> Iterator[EventTree]:\n        \"\"\"Yields a separate ``EventTree`` for every event with matching ``id``.\"\"\"\n        yield from (EventTree(self, [ie]) for ie in self._get_ie(id))\n\n    def subtree(self, select: Callable[[AnyEvent], bool | None]) -> EventTree:\n        \"\"\"Returns a mutable view containing events for which ``select`` was True.\n\n        Caution:\n            Always use this function to create a mutable view. Maintaining\n            chilren and passing parent to a child are best done here.\n        \"\"\"\n        el: list[IndexedEvent] = []\n        for ie in self.lst:\n            if select(ie.e):\n                el.append(ie)\n        obj = EventTree(self, el)\n        self.children.append(obj)\n        return obj\n\n    @yields_child\n    def subtrees(\n        self, select: Callable[[AnyEvent], bool | None], repeat: int\n    ) -> Iterator[EventTree]:\n        \"\"\"Yields mutable views till ``select`` and ``repeat`` are satisfied.\n\n        Args:\n            select: Called for every event in this dictionary by iterating over\n                a chained, sorted list. Returns True if event must be included.\n                Once it returns False, rest of them are ignored and resulting\n                EventTree is returned. Return None to skip an event.\n            repeat: Use -1 for infinite iterations.\n        \"\"\"\n        el: list[IndexedEvent] = []\n        for ie in self.lst:\n            if not repeat:\n                return\n\n            result = select(ie.e)\n            if result is False:\n                yield EventTree(self, el)\n                el = [ie]  # Don't skip current event\n                repeat -= 1\n            elif result is not None:\n                el.append(ie)\n\n    @property\n    def ids(self) -> frozenset[EventEnum]:\n        return frozenset(ie.e.id for ie in self.lst)\n\n    @property\n    def indexes(self) -> frozenset[int]:\n        \"\"\"Returns root indexes for all events in ``self``.\"\"\"\n        return frozenset(ie.r for ie in self.lst)\n"
  },
  {
    "path": "pyflp/_models.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the ABCs used by model classes and some shared classes.\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport functools\nfrom typing import (\n    Any,\n    Callable,\n    Generic,\n    Iterable,\n    Protocol,\n    Sequence,\n    TypeVar,\n    Union,\n    overload,\n    runtime_checkable,\n)\n\nimport construct as c\n\nfrom pyflp._events import EventTree, ListEventBase, StructEventBase\n\nVE = TypeVar(\"VE\", bound=Union[StructEventBase, ListEventBase])\n\n\nclass ModelBase(abc.ABC):\n    def __init__(self, *args: Any, **kw: Any) -> None:\n        self._kw = kw\n\n\nclass ItemModel(ModelBase, Generic[VE]):\n    \"\"\"Base class for event-less models.\"\"\"\n\n    def __init__(self, item: c.Container[Any], index: int, parent: VE, **kw: Any) -> None:\n        \"\"\"Create a new item model.\n\n        Args:\n            item: Parsed :class:`construct.Struct` instance from :attr:`parent`.\n            index: 0-based index used to propagate changes back to :attr:`parent`.\n            parent: A :class:`StructEventBase` or :class:`ListEventBase` instance.\n        \"\"\"\n        self._item = item\n        self._index = index\n        self._parent = parent\n        super().__init__(**kw)\n\n    def __getitem__(self, prop: str):\n        return self._item[prop]\n\n    def __setitem__(self, prop: str, value: Any) -> None:\n        self._item[prop] = value\n\n        if not isinstance(self._parent, ListEventBase):\n            raise NotImplementedError\n\n        self._parent[self._index] = self._item\n\n\nclass EventModel(ModelBase):\n    def __init__(self, events: EventTree, **kw: Any) -> None:\n        super().__init__(**kw)\n        self.events = events\n\n    def __eq__(self, o: object) -> bool:\n        if not isinstance(o, type(self)):\n            raise TypeError(f\"Cannot compare {type(o)!r} with {type(self)!r}\")\n\n        return o.events == self.events\n\n\nMT_co = TypeVar(\"MT_co\", bound=ModelBase, covariant=True)\nEMT_co = TypeVar(\"EMT_co\", bound=EventModel, covariant=True)\n\n\n@runtime_checkable\nclass ModelCollection(Iterable[MT_co], Protocol[MT_co]):\n    @overload\n    def __getitem__(self, i: int | str) -> MT_co:\n        ...\n\n    @overload\n    def __getitem__(self, i: slice) -> Sequence[MT_co]:\n        ...\n\n\ndef supports_slice(func: Callable[[ModelCollection[MT_co], str | int | slice], MT_co]):\n    \"\"\"Wraps a :meth:`ModelCollection.__getitem__` to return a sequence if required.\"\"\"\n\n    @overload\n    def wrapper(self: ModelCollection[MT_co], i: int | str) -> MT_co:\n        ...\n\n    @overload\n    def wrapper(self: ModelCollection[MT_co], i: slice) -> Sequence[MT_co]:\n        ...\n\n    @functools.wraps(func)\n    def wrapper(self: Any, i: Any) -> MT_co | Sequence[MT_co]:\n        if isinstance(i, slice):\n            return [model for idx, model in enumerate(self) if idx in range(i.start, i.stop)]\n        return func(self, i)\n\n    return wrapper\n\n\nclass ModelReprMixin:\n    \"\"\"I am too lazy to make one `__repr__()` for every model.\"\"\"\n\n    def __repr__(self) -> str:\n        mapping: dict[str, Any] = {}\n        for var in [var for var in vars(type(self)) if not var.startswith(\"_\")]:\n            mapping[var] = getattr(self, var, None)\n\n        params = \", \".join([f\"{k}={v!r}\" for k, v in mapping.items()])\n        return f\"{type(self).__name__}({params})\"\n"
  },
  {
    "path": "pyflp/arrangement.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types used by tracks and arrangements.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nfrom typing import Any, Iterator, Literal, Optional, cast\n\nimport construct as c\nimport construct_typed as ct\nfrom typing_extensions import TypedDict, Unpack\n\nfrom pyflp._adapters import FourByteBool, StdEnum\nfrom pyflp._descriptors import EventProp, NestedProp, StructProp\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    TEXT,\n    WORD,\n    AnyEvent,\n    EventEnum,\n    EventTree,\n    ListEventBase,\n    StructEventBase,\n    U8Event,\n    U16Event,\n    U16TupleEvent,\n)\nfrom pyflp._models import (\n    EventModel,\n    ItemModel,\n    ModelCollection,\n    ModelReprMixin,\n    supports_slice,\n)\nfrom pyflp.channel import Channel, ChannelRack\nfrom pyflp.exceptions import ModelNotFound, NoModelsFound, PropertyCannotBeSet\nfrom pyflp.pattern import Pattern, Patterns\nfrom pyflp.timemarker import TimeMarker, TimeMarkerID\nfrom pyflp.types import RGBA, FLVersion\n\n__all__ = [\n    \"Arrangements\",\n    \"Arrangement\",\n    \"Track\",\n    \"TrackMotion\",\n    \"TrackPress\",\n    \"TrackSync\",\n    \"ChannelPLItem\",\n    \"PatternPLItem\",\n]\n\n\nclass PLSelectionEvent(StructEventBase):\n    STRUCT = c.Struct(\"start\" / c.Optional(c.Int32ul), \"end\" / c.Optional(c.Int32ul)).compile()\n\n\nclass PlaylistEvent(ListEventBase):\n    STRUCT = c.GreedyRange(\n        c.Struct(\n            \"position\" / c.Int32ul,  # 4\n            \"pattern_base\" / c.Int16ul * \"Always 20480\",  # 6\n            \"item_index\" / c.Int16ul,  # 8\n            \"length\" / c.Int32ul,  # 12\n            \"track_rvidx\" / c.Int16ul * \"Stored reversed i.e. Track 1 would be 499\",  # 14\n            \"group\" / c.Int16ul,  # 16\n            \"_u1\" / c.Bytes(2) * \"Always (120, 0)\",  # 18\n            \"item_flags\" / c.Int16ul * \"Always (64, 0)\",  # 20\n            \"_u2\" / c.Bytes(4) * \"Always (64, 100, 128, 128)\",  # 24\n            \"start_offset\" / c.Float32l,  # 28\n            \"end_offset\" / c.Float32l,  # 32\n            \"_u3\" / c.If(c.this._params[\"new\"], c.Bytes(28)) * \"New in FL 21\",  # 60\n        )\n    )\n    SIZES = [32, 60]\n\n    def __init__(self, id: EventEnum, data: bytes) -> None:\n        super().__init__(id, data, new=not len(data) % 60)\n\n\n@enum.unique\nclass TrackMotion(ct.EnumBase):\n    Stay = 0\n    OneShot = 1\n    MarchWrap = 2\n    MarchStay = 3\n    MarchStop = 4\n    Random = 5\n    ExclusiveRandom = 6\n\n\n@enum.unique\nclass TrackPress(ct.EnumBase):\n    Retrigger = 0\n    HoldStop = 1\n    HoldMotion = 2\n    Latch = 3\n\n\n@enum.unique\nclass TrackSync(ct.EnumBase):\n    Off = 0\n    QuarterBeat = 1\n    HalfBeat = 2\n    Beat = 3\n    TwoBeats = 4\n    FourBeats = 5\n    Auto = 6\n\n\nclass HeightAdapter(ct.Adapter[float, float, str, str]):\n    def _decode(self, obj: float, *_: Any) -> str:\n        return str(int(obj * 100)) + \"%\"\n\n    def _encode(self, obj: str, *_: Any) -> float:\n        return int(obj[:-1]) / 100\n\n\nclass TrackEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"iid\" / c.Optional(c.Int32ul),  # 4\n        \"color\" / c.Optional(c.Int32ul),  # 8\n        \"icon\" / c.Optional(c.Int32ul),  # 12\n        \"enabled\" / c.Optional(c.Flag),  # 13\n        \"height\" / c.Optional(HeightAdapter(c.Float32l)),  # 17\n        \"locked_height\" / c.Optional(c.Int32sl),  # 21\n        \"content_locked\" / c.Optional(c.Flag),  # 22\n        \"motion\" / c.Optional(StdEnum[TrackMotion](c.Int32ul)),  # 26\n        \"press\" / c.Optional(StdEnum[TrackPress](c.Int32ul)),  # 30\n        \"trigger_sync\" / c.Optional(StdEnum[TrackSync](c.Int32ul)),  # 34\n        \"queued\" / c.Optional(FourByteBool),  # 38\n        \"tolerant\" / c.Optional(FourByteBool),  # 42\n        \"position_sync\" / c.Optional(StdEnum[TrackSync](c.Int32ul)),  # 46\n        \"grouped\" / c.Optional(c.Flag),  # 47\n        \"locked\" / c.Optional(c.Flag),  # 48\n        \"_u1\" / c.Optional(c.GreedyBytes),  # * 66 as of 20.9.1\n    ).compile()\n\n\n@enum.unique\nclass ArrangementsID(EventEnum):\n    TimeSigNum = (17, U8Event)\n    TimeSigBeat = (18, U8Event)\n    Current = (WORD + 36, U16Event)\n    _LoopPos = (DWORD + 24, U16TupleEvent)  #: 1.3.8+\n    PLSelection = (DATA + 9, PLSelectionEvent)\n    \"\"\".. versionadded:: v2.1.0\"\"\"\n\n\n@enum.unique\nclass ArrangementID(EventEnum):\n    New = (WORD + 35, U16Event)\n    # _PlaylistItem = DWORD + 1\n    Name = TEXT + 49\n    Playlist = (DATA + 25, PlaylistEvent)\n\n\n@enum.unique\nclass TrackID(EventEnum):\n    Name = TEXT + 47\n    Data = (DATA + 30, TrackEvent)\n\n\nclass PLItemBase(ItemModel[PlaylistEvent], ModelReprMixin):\n    group = StructProp[int]()\n    \"\"\"Returns 0 for no group, else a group number for clips in the same group.\"\"\"\n\n    length = StructProp[int]()\n    \"\"\"PPQ-dependant quantity.\"\"\"\n\n    muted = StructProp[bool]()\n    \"\"\"Whether muted / disabled in the playlist. *New in FL Studio v9.0.0*.\"\"\"\n\n    @property\n    def offsets(self) -> tuple[float, float]:\n        \"\"\"Returns a ``(start, end)`` offset tuple.\n\n        An offset is the distance from the item's actual start or end.\n        \"\"\"\n        return (self[\"start_offset\"], self[\"end_offset\"])\n\n    @offsets.setter\n    def offsets(self, value: tuple[float, float]) -> None:\n        self[\"start_offset\"], self[\"end_offset\"] = value\n\n    position = StructProp[int]()\n    \"\"\"PPQ-dependant quantity.\"\"\"\n\n\nclass ChannelPLItem(PLItemBase, ModelReprMixin):\n    \"\"\"An audio clip or automation on the playlist of an arrangement.\n\n    *New in FL Studio v2.0.1*.\n    \"\"\"\n\n    @property\n    def channel(self) -> Channel:\n        return self._kw[\"channel\"]\n\n    @channel.setter\n    def channel(self, channel: Channel) -> None:\n        self._kw[\"channel\"] = channel\n        self[\"item_index\"] = channel.iid\n\n\nclass PatternPLItem(PLItemBase, ModelReprMixin):\n    \"\"\"A pattern block or clip on the playlist of an arrangement.\n\n    *New in FL Studio v7.0.0*.\n    \"\"\"\n\n    @property\n    def pattern(self) -> Pattern:\n        return self._kw[\"pattern\"]\n\n    @pattern.setter\n    def pattern(self, pattern: Pattern) -> None:\n        self._kw[\"pattern\"] = pattern\n        self[\"item_index\"] = pattern.iid + self[\"pattern_base\"]\n\n\nclass _TrackColorProp(StructProp[RGBA]):\n    def _get(self, ev_or_ins: Any) -> RGBA | None:\n        value = cast(Optional[int], super()._get(ev_or_ins))\n        if value is not None:\n            return RGBA.from_bytes(value.to_bytes(4, \"little\"))\n\n    def _set(self, ev_or_ins: Any, value: RGBA) -> None:\n        super()._set(ev_or_ins, int.from_bytes(bytes(value), \"little\"))  # type: ignore\n\n\nclass _TrackKW(TypedDict):\n    items: list[PLItemBase]\n\n\nclass Track(EventModel, ModelCollection[PLItemBase]):\n    \"\"\"Represents a track in an arrangement on which playlist items are arranged.\n\n    ![](https://bit.ly/3de6R8y)\n    \"\"\"\n\n    def __init__(self, events: EventTree, **kw: Unpack[_TrackKW]) -> None:\n        super().__init__(events, **kw)\n\n    def __getitem__(self, index: int | slice | str):\n        if isinstance(index, str):\n            return NotImplemented\n        return self._kw[\"items\"][index]\n\n    def __iter__(self) -> Iterator[PLItemBase]:\n        \"\"\"An iterator over :attr:`items`.\"\"\"\n        yield from self._kw[\"items\"]\n\n    def __len__(self) -> int:\n        return len(self._kw[\"items\"])\n\n    def __repr__(self) -> str:\n        return f\"Track(name={self.name}, iid={self.iid}, {len(self)} items)\"\n\n    color = _TrackColorProp(TrackID.Data)\n    \"\"\"Defaults to #485156 (dark slate gray).\n\n    ![](https://bit.ly/3yVGGuW)\n\n    Note:\n        Unlike :attr:`Channel.color` and :attr:`Insert.color`, values below ``20`` for\n        any color component (i.e red, green or blue) are NOT ignored by FL Studio.\n    \"\"\"\n\n    content_locked = StructProp[bool](TrackID.Data)\n    \"\"\":guilabel:`Lock to content`, defaults to ``False``.\"\"\"\n\n    enabled = StructProp[bool](TrackID.Data)\n    \"\"\"![](https://bit.ly/3eGd91O)\"\"\"\n\n    grouped = StructProp[bool](TrackID.Data)\n    \"\"\"Whether grouped with the track above (index - 1) or not.\n\n    ![](https://bit.ly/3yXO5tM)\n\n    :guilabel:`&Group with above track`\n    \"\"\"\n\n    height = StructProp[str](TrackID.Data)\n    \"\"\"Track height in FL's interface. Linear. :guilabel:`&Size`.\"\"\"\n\n    icon = StructProp[int](TrackID.Data)\n    \"\"\"Returns ``0`` if not set, else an internal icon ID.\n\n    ![](https://bit.ly/3gln8Kc)\n\n    :guilabel:`Change icon`\n    \"\"\"\n\n    iid = StructProp[int](TrackID.Data)\n    \"\"\"An integer in the range of 1 to :attr:`Arrangements.max_tracks`.\"\"\"\n\n    locked = StructProp[bool](TrackID.Data)\n    \"\"\"Whether the tracked is in a locked state.\n\n    ![](https://bit.ly/3VFG6eP)\n    \"\"\"\n\n    motion = StructProp[TrackMotion](TrackID.Data)\n    \"\"\":guilabel:`&Performance settings`, defaults to :attr:`TrackMotion.Stay`.\"\"\"\n\n    name = EventProp[str](TrackID.Name)\n    \"\"\"Returns a string or ``None`` if not set.\"\"\"\n\n    position_sync = StructProp[TrackSync](TrackID.Data)\n    \"\"\":guilabel:`&Performance settings`, defaults to :attr:`TrackSync.Off`.\"\"\"\n\n    press = StructProp[TrackPress](TrackID.Data)\n    \"\"\":guilabel:`&Performance settings`, defaults to :attr:`TrackPress.Retrigger`.\"\"\"\n\n    tolerant = StructProp[bool](TrackID.Data)\n    \"\"\":guilabel:`&Performance settings`, defaults to ``True``.\"\"\"\n\n    trigger_sync = StructProp[TrackSync](TrackID.Data)\n    \"\"\":guilabel:`&Performance settings`, defaults to :attr:`TrackSync.FourBeats`.\"\"\"\n\n    queued = StructProp[bool](TrackID.Data)\n    \"\"\":guilabel:`&Performance settings`, defaults to ``False``.\"\"\"\n\n\nclass _ArrangementKW(TypedDict):\n    channels: ChannelRack\n    patterns: Patterns\n    version: FLVersion\n\n\nclass Arrangement(EventModel):\n    \"\"\"Contains the timemarkers and tracks in an arrangement.\n\n    ![](https://bit.ly/3B6is1z)\n\n    *New in FL Studio v12.9.1*: Support for multiple arrangements.\n    \"\"\"\n\n    def __init__(self, events: EventTree, **kw: Unpack[_ArrangementKW]) -> None:\n        super().__init__(events, **kw)\n\n    def __repr__(self) -> str:\n        return \"Arrangement(iid={}, name={}, {} timemarkers, {} tracks)\".format(\n            self.iid,\n            repr(self.name),\n            len(tuple(self.timemarkers)),\n            len(tuple(self.tracks)),\n        )\n\n    iid = EventProp[int](ArrangementID.New)\n    \"\"\"A 1-based internal index.\"\"\"\n\n    name = EventProp[str](ArrangementID.Name)\n    \"\"\"Name of the arrangement; defaults to **Arrangement**.\"\"\"\n\n    @property\n    def timemarkers(self) -> Iterator[TimeMarker]:\n        yield from (TimeMarker(ed) for ed in self.events.group(*TimeMarkerID))\n\n    @property\n    def tracks(self) -> Iterator[Track]:\n        pl_evt = None\n        max_idx = 499 if self._kw[\"version\"] >= FLVersion(12, 9, 1) else 198\n        channels = {channel.iid: channel for channel in self._kw[\"channels\"]}\n        patterns = {pattern.iid: pattern for pattern in self._kw[\"patterns\"]}\n\n        if ArrangementID.Playlist in self.events.ids:\n            pl_evt = cast(PlaylistEvent, self.events.first(ArrangementID.Playlist))\n\n        for track_idx, ed in enumerate(self.events.divide(TrackID.Data, *TrackID)):\n            if pl_evt is None:\n                yield Track(ed, items=[])\n                continue\n\n            items: list[PLItemBase] = []\n            for i, item in enumerate(pl_evt):\n                if max_idx - item[\"track_rvidx\"] != track_idx:\n                    continue\n\n                if item[\"item_index\"] <= item[\"pattern_base\"]:\n                    iid = item[\"item_index\"]\n                    items.append(ChannelPLItem(item, i, pl_evt, channel=channels[iid]))\n                else:\n                    num = item[\"item_index\"] - item[\"pattern_base\"]\n                    items.append(PatternPLItem(item, i, pl_evt, pattern=patterns[num]))\n            yield Track(ed, items=items)\n\n\n# TODO Find whether time is set to signature or division mode.\nclass TimeSignature(EventModel, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3EYiMmy)\"\"\"\n\n    def __str__(self) -> str:\n        return f\"Global time signature: {self.num}/{self.beat}\"\n\n    num = EventProp[int](ArrangementsID.TimeSigNum)\n    \"\"\"Beats per bar in time division & numerator in time signature mode.\n\n    | Min | Max | Default |\n    |-----|-----|---------|\n    | 1   | 16  | 4       |\n    \"\"\"\n\n    beat = EventProp[int](ArrangementsID.TimeSigBeat)\n    \"\"\"Steps per beat in time division & denominator in time signature mode.\n\n    In time signature mode it can be 2, 4, 8 or 16 but in time division mode:\n\n    | Min | Max | Default |\n    |-----|-----|---------|\n    | 1   | 16  | 4       |\n    \"\"\"\n\n\nclass Arrangements(EventModel, ModelCollection[Arrangement]):\n    \"\"\"Iterator over arrangements in the project and some related properties.\"\"\"\n\n    def __init__(self, events: EventTree, **kw: Unpack[_ArrangementKW]) -> None:\n        super().__init__(events, **kw)\n\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: int | str | slice) -> Arrangement:\n        \"\"\"Returns an arrangement based either on its index or name.\n\n        Args:\n            i: The index of the arrangement in which they occur or\n               :attr:`Arrangement.name` of the arrangement to lookup for or a\n               slice of indexes.\n\n        Raises:\n            ModelNotFound: An :class:`Arrangement` with the specifed name or\n                index isn't found.\n        \"\"\"\n        for idx, arr in enumerate(self):\n            if (isinstance(i, str) and i == arr.name) or idx == i:\n                return arr\n        raise ModelNotFound(i)\n\n    # TODO Verify ArrangementsID.Current is the end\n    # FL changed event ordering a lot, the latest being the most easiest to\n    # parse; it contains ArrangementID.New event followed by TimeMarker events\n    # followed by 500 TrackID events. TimeMarkers occured before new arrangement\n    # event in initial versions of FL20, making them harder to group.\n    # TODO This logic might not work on older versions of FL.\n    def __iter__(self) -> Iterator[Arrangement]:\n        \"\"\"Yields :class:`Arrangement` found in the project.\n\n        Raises:\n            NoModelsFound: When no arrangements are found.\n        \"\"\"\n        arrnew_occured = False\n\n        def select(e: AnyEvent) -> bool | None:\n            nonlocal arrnew_occured\n            if e.id == ArrangementID.New:\n                if arrnew_occured:\n                    return False\n                arrnew_occured = True\n\n            if e.id in (*ArrangementID, *TimeMarkerID, *TrackID):\n                return True\n\n            if e.id == ArrangementsID.Current:\n                return False  # Yield out last arrangement\n\n        yield from (Arrangement(ed, **self._kw) for ed in self.events.subtrees(select, len(self)))\n\n    def __len__(self) -> int:\n        \"\"\"The number of arrangements present in the project.\n\n        Raises:\n            NoModelsFound: When no arrangements are found.\n        \"\"\"\n        if ArrangementID.New not in self.events.ids:\n            raise NoModelsFound\n        return self.events.count(ArrangementID.New)\n\n    def __repr__(self) -> str:\n        return f\"{len(self)} arrangements\"\n\n    @property\n    def current(self) -> Arrangement | None:\n        \"\"\"Currently selected arrangement (via FL's interface).\n\n        Raises:\n            ModelNotFound: When the underlying event value points to an\n                invalid arrangement index.\n        \"\"\"\n        if ArrangementsID.Current in self.events.ids:\n            event = self.events.first(ArrangementsID.Current)\n            index: int = event.value\n            try:\n                return list(self)[index]\n            except IndexError as exc:\n                raise ModelNotFound(index) from exc\n\n    @property\n    def loop_pos(self) -> tuple[int, int] | None:\n        \"\"\"Playlist loop start and end points. PPQ dependant.\n\n        .. versionchanged:: v2.1.0\n\n           :attr:`ArrangementsID.PLSelection` is used by default\n           while :attr:`ArrangementsID._LoopPos` is a fallback.\n\n        *New in FL Studio v1.3.8*.\n        \"\"\"\n        if ArrangementsID.PLSelection in self.events:\n            event = cast(PLSelectionEvent, self.events.first(ArrangementsID.PLSelection))\n            return event[\"start\"], event[\"end\"]\n\n        if ArrangementsID._LoopPos in self.events:\n            return self.events.first(ArrangementsID._LoopPos).value\n\n    @loop_pos.setter\n    def loop_pos(self, value: tuple[int, int]) -> None:\n        if ArrangementsID.PLSelection in self.events:\n            event = cast(PLSelectionEvent, self.events.first(ArrangementsID.PLSelection))\n            event[\"start\"], event[\"end\"] = value\n        elif ArrangementsID._LoopPos in self.events:\n            self.events.first(ArrangementsID._LoopPos).value = value\n        else:\n            raise PropertyCannotBeSet(ArrangementsID.PLSelection, ArrangementsID._LoopPos)\n\n    @property\n    def max_tracks(self) -> Literal[500, 199]:\n        return 500 if self._kw[\"version\"] >= FLVersion(12, 9, 1) else 199\n\n    time_signature = NestedProp(\n        TimeSignature, ArrangementsID.TimeSigNum, ArrangementsID.TimeSigBeat\n    )\n    \"\"\"Project time signature (also used by playlist).\n\n    :menuselection:`Options --> &Project general settings --> Time settings`\n    \"\"\"\n"
  },
  {
    "path": "pyflp/channel.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types used by channels and the channel rack.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nimport pathlib\nfrom typing import Any, Iterator, Literal, Tuple, cast\n\nimport construct as c\nimport construct_typed as ct\n\nfrom pyflp._adapters import LinearMusical, List2Tuple, Log2, LogNormal, StdEnum\nfrom pyflp._descriptors import EventProp, FlagProp, NestedProp, StructProp\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    TEXT,\n    WORD,\n    BoolEvent,\n    EventEnum,\n    F32Event,\n    I8Event,\n    I32Event,\n    StructEventBase,\n    U8Event,\n    U16Event,\n    U16TupleEvent,\n    U32Event,\n)\nfrom pyflp._models import EventModel, ItemModel, ModelCollection, ModelReprMixin, supports_slice\nfrom pyflp.exceptions import ModelNotFound, NoModelsFound, PropertyCannotBeSet\nfrom pyflp.plugin import BooBass, FruitKick, Plucked, PluginID, PluginProp, VSTPlugin\nfrom pyflp.types import RGBA, MusicalTime\n\n__all__ = [\n    \"ArpDirection\",\n    \"Automation\",\n    \"AutomationPoint\",\n    \"Channel\",\n    \"Instrument\",\n    \"Layer\",\n    \"ChannelRack\",\n    \"ChannelNotFound\",\n    \"DeclickMode\",\n    \"LFOShape\",\n    \"ReverbType\",\n    \"FX\",\n    \"Reverb\",\n    \"Delay\",\n    \"Envelope\",\n    \"SamplerLFO\",\n    \"Tracking\",\n    \"Keyboard\",\n    \"LevelAdjusts\",\n    \"StretchMode\",\n    \"Time\",\n    \"TimeStretching\",\n    \"Polyphony\",\n    \"Playback\",\n    \"ChannelType\",\n]\n\nEnvelopeName = Literal[\"Panning\", \"Volume\", \"Mod X\", \"Mod Y\", \"Pitch\"]\nLFOName = EnvelopeName\n\n\nclass ChannelNotFound(ModelNotFound, KeyError):\n    pass\n\n\nclass AutomationEvent(StructEventBase):\n    @staticmethod\n    def _get_position(stream: c.StreamType, index: int) -> float:\n        cur = stream.tell()\n        position = 0.0\n        for i in range(index + 1):\n            stream.seek(21 + (i * 24))\n            position += c.Float64l.parse_stream(stream)\n        stream.seek(cur)\n        return position\n\n    STRUCT = c.Struct(\n        \"_u1\" / c.Bytes(4),  # 4  # ? Always 1\n        \"lfo.amount\" / c.Int32sl,\n        \"_u2\" / c.Bytes(1),  # 9\n        \"_u3\" / c.Bytes(2),  # 11\n        \"_u4\" / c.Bytes(2),  # 13  # ? Always 0\n        \"_u5\" / c.Bytes(4),  # 17\n        \"points\"\n        / c.PrefixedArray(\n            c.Int32ul,  # 21\n            c.Struct(\n                \"_offset\" / c.Float64l * \"Change in X-axis w.r.t last point\",\n                \"position\"  # TODO Implement a setter\n                / c.IfThenElse(\n                    lambda ctx: ctx._index > 0,\n                    c.Computed(lambda ctx: AutomationEvent._get_position(ctx._io, ctx._index)),\n                    c.Computed(lambda ctx: ctx[\"_offset\"]),\n                ),\n                \"value\" / c.Float64l,\n                \"tension\" / c.Float32l,\n                \"_u1\" / c.Bytes(4),  # Linked to tension\n            ),  # 24 per struct\n        ),\n        \"_u6\" / c.GreedyBytes,  # TODO Upto a whooping 112 bytes\n    )\n\n\nclass DelayEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"feedback\" / c.Optional(c.Int32ul),\n        \"pan\" / c.Optional(c.Int32sl),\n        \"pitch_shift\" / c.Optional(c.Int32sl),\n        \"echoes\" / c.Optional(c.Int32ul),\n        \"time\" / c.Optional(c.Int32ul),\n    ).compile()\n\n\n@enum.unique\nclass _EnvLFOFlags(enum.IntFlag):\n    EnvelopeTempoSync = 1 << 0\n    Unknown = 1 << 2  # Occurs for volume envlope only. Likely a bug in FL's serialiser\n    LFOTempoSync = 1 << 1\n    LFOPhaseRetrig = 1 << 5\n\n\n@enum.unique\nclass LFOShape(ct.EnumBase):\n    \"\"\"Used by :attr:`LFO.shape`.\"\"\"\n\n    Sine = 0\n    Triangle = 1\n    Pulse = 2\n\n\n# FL Studio 2.5.0+\nclass EnvelopeLFOEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"flags\" / c.Optional(StdEnum[_EnvLFOFlags](c.Int32sl)),  # 4\n        \"envelope.enabled\" / c.Optional(c.Int32sl),  # 8\n        \"envelope.predelay\" / c.Optional(c.Int32sl),  # 12\n        \"envelope.attack\" / c.Optional(c.Int32sl),  # 16\n        \"envelope.hold\" / c.Optional(c.Int32sl),  # 20\n        \"envelope.decay\" / c.Optional(c.Int32sl),  # 24\n        \"envelope.sustain\" / c.Optional(c.Int32sl),  # 28\n        \"envelope.release\" / c.Optional(c.Int32sl),  # 32\n        \"envelope.amount\" / c.Optional(c.Int32sl),  # 36\n        \"lfo.predelay\" / c.Optional(c.Int32ul),  # 40\n        \"lfo.attack\" / c.Optional(c.Int32ul),  # 44\n        \"lfo.amount\" / c.Optional(c.Int32sl),  # 48\n        \"lfo.speed\" / c.Optional(c.Int32ul),  # 52\n        \"lfo.shape\" / c.Optional(StdEnum[LFOShape](c.Int32sl)),  # 56\n        \"envelope.attack_tension\" / c.Optional(c.Int32sl),  # 60\n        \"envelope.decay_tension\" / c.Optional(c.Int32sl),  # 64\n        \"envelope.release_tension\" / c.Optional(c.Int32sl),  # 68\n    ).compile()\n\n\nclass LevelAdjustsEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"pan\" / c.Optional(c.Int32sl),  # 4\n        \"volume\" / c.Optional(c.Int32ul),  # 8\n        \"_u1\" / c.Optional(c.Int32ul),  # 12\n        \"mod_x\" / c.Optional(c.Int32sl),  # 16\n        \"mod_y\" / c.Optional(c.Int32sl),  # 20\n    ).compile()\n\n\nclass FilterType(ct.EnumBase):\n    FastLP = 0\n    LP = 1\n    BP = 2\n    HP = 3\n    BS = 4\n    LPx2 = 5\n    SVFLP = 6\n    SVFLPx2 = 7\n\n\nclass LevelsEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"pan\" / c.Optional(c.Int32sl),  # 4\n        \"volume\" / c.Optional(c.Int32ul),  # 8\n        \"pitch_shift\" / c.Optional(c.Int32sl),  # 12\n        \"filter.mod_x\" / c.Optional(c.Int32ul),  # 16\n        \"filter.mod_y\" / c.Optional(c.Int32ul),  # 20\n        \"filter.type\" / c.Optional(StdEnum[FilterType](c.Int32ul)),  # 24\n    ).compile()\n\n\n@enum.unique\nclass ArpDirection(ct.EnumBase):\n    \"\"\"Used by :attr:`Arp.direction`.\"\"\"\n\n    Off = 0\n    Up = 1\n    Down = 2\n    UpDownBounce = 3\n    UpDownSticky = 4\n    Random = 5\n\n\n@enum.unique\nclass DeclickMode(ct.EnumBase):\n    OutOnly = 0\n    TransientNoBleeding = 1\n    Transient = 2\n    Generic = 3\n    Smooth = 4\n    Crossfade = 5\n\n\n@enum.unique\nclass _DelayFlags(enum.IntFlag):\n    PingPong = 1 << 1\n    FatMode = 1 << 2\n\n\n@enum.unique\nclass StretchMode(ct.EnumBase):\n    Stretch = -1\n    Resample = 0\n    E3Generic = 1\n    E3Mono = 2\n    SliceStretch = 3\n    SliceMap = 4\n    Auto = 5\n    E2Generic = 6\n    E2Transient = 7\n    E2Mono = 8\n    E2Speech = 9\n\n\nclass ParametersEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.Optional(c.Bytes(9)),  # 9\n        \"fx.remove_dc\" / c.Optional(c.Flag),  # 10\n        \"delay.flags\" / c.Optional(StdEnum[_DelayFlags](c.Int8ul)),  # 11\n        \"keyboard.main_pitch\" / c.Optional(c.Flag),  # 12\n        \"_u2\" / c.Optional(c.Bytes(28)),  # 40\n        \"arp.direction\" / c.Optional(StdEnum[ArpDirection](c.Int32ul)),  # 44\n        \"arp.range\" / c.Optional(c.Int32ul),  # 48\n        \"arp.chord\" / c.Optional(c.Int32ul),  # 52\n        \"arp.time\" / c.Optional(c.Float32l),  # 56\n        \"arp.gate\" / c.Optional(c.Float32l),  # 60\n        \"arp.slide\" / c.Optional(c.Flag),  # 61\n        \"_u3\" / c.Optional(c.Bytes(1)),  # 62\n        \"time.full_porta\" / c.Optional(c.Flag),  # 63\n        \"keyboard.add_root\" / c.Optional(c.Flag),  # 64\n        \"time.gate\" / c.Optional(c.Int16ul),  # 66\n        \"_u4\" / c.Optional(c.Bytes(2)),  # 68\n        \"keyboard.key_region\" / c.Optional(List2Tuple(c.Int32ul[2])),  # 76\n        \"_u5\" / c.Optional(c.Bytes(4)),  # 80\n        \"fx.normalize\" / c.Optional(c.Flag),  # 81\n        \"fx.inverted\" / c.Optional(c.Flag),  # 82\n        \"_u6\" / c.Optional(c.Bytes(1)),  # 83\n        \"content.declick_mode\" / c.Optional(StdEnum[DeclickMode](c.Int8ul)),  # 84\n        \"fx.crossfade\" / c.Optional(c.Int32ul),  # 88\n        \"fx.trim\" / c.Optional(c.Int32ul),  # 92\n        \"arp.repeat\" / c.Optional(c.Int32ul),  # 96; FL 4.5.2+\n        \"stretching.time\" / c.Optional(LinearMusical(c.Int32ul)),  # 100\n        \"stretching.pitch\" / c.Optional(c.Int32sl),  # 104\n        \"stretching.multiplier\" / c.Optional(Log2(c.Int32sl, 10000)),  # 108\n        \"stretching.mode\" / c.Optional(StdEnum[StretchMode](c.Int32sl)),  # 112\n        \"_u7\" / c.Optional(c.Bytes(21)),  # 133\n        \"fx.start\" / c.Optional(LogNormal(c.Int16ul[2], (0, 61440))),  # 137\n        \"_u8\" / c.Optional(c.Bytes(4)),  # 141\n        \"fx.length\" / c.Optional(LogNormal(c.Int16ul[2], (0, 61440))),  # 145\n        \"_u9\" / c.Optional(c.Bytes(3)),  # 148\n        \"playback.start_offset\" / c.Optional(c.Int32ul),  # 152\n        \"_u10\" / c.Optional(c.Bytes(5)),  # 157\n        \"fx.fix_trim\" / c.Optional(c.Flag),  # 158 (FL 20.8.4 max)\n        \"_extra\" / c.GreedyBytes,  # * 168 as of 20.9.1\n    )\n\n\n@enum.unique\nclass _PolyphonyFlags(enum.IntFlag):\n    None_ = 0\n    Mono = 1 << 0\n    Porta = 1 << 1\n\n\nclass PolyphonyEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"max\" / c.Optional(c.Int32ul),  # 4\n        \"slide\" / c.Optional(c.Int32ul),  # 8\n        \"flags\" / c.Optional(StdEnum[_PolyphonyFlags](c.Byte)),  # 9\n    ).compile()\n\n\nclass TrackingEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"middle_value\" / c.Optional(c.Int32ul),  # 4\n        \"pan\" / c.Optional(c.Int32sl),  # 8\n        \"mod_x\" / c.Optional(c.Int32sl),  # 12\n        \"mod_y\" / c.Optional(c.Int32sl),  # 16\n    ).compile()\n\n\n@enum.unique\nclass ChannelID(EventEnum):\n    IsEnabled = (0, BoolEvent)\n    _VolByte = (2, U8Event)\n    _PanByte = (3, U8Event)\n    Zipped = (15, BoolEvent)\n    # _19 = (19, BoolEvent)\n    PingPongLoop = (20, BoolEvent)\n    Type = (21, U8Event)\n    RoutedTo = (22, I8Event)\n    # FXProperties = 27\n    IsLocked = (32, BoolEvent)  #: 12.3+\n    New = (WORD, U16Event)\n    FreqTilt = (WORD + 5, U16Event)\n    FXFlags = (WORD + 6, U16Event)\n    Cutoff = (WORD + 7, U16Event)\n    _VolWord = (WORD + 8, U16Event)\n    _PanWord = (WORD + 9, U16Event)\n    Preamp = (WORD + 10, U16Event)  #: 1.2.12+\n    FadeOut = (WORD + 11, U16Event)  #: 1.7.6+\n    FadeIn = (WORD + 12, U16Event)\n    # _DotNote = WORD + 13\n    # _DotPitch = WORD + 14\n    # _DotMix = WORD + 15\n    Resonance = (WORD + 19, U16Event)\n    # _LoopBar = WORD + 20\n    StereoDelay = (WORD + 21, U16Event)  #: 1.3.56+\n    Pogo = (WORD + 22, U16Event)\n    # _DotReso = WORD + 23\n    # _DotCutOff = WORD + 24\n    TimeShift = (WORD + 25, U16Event)\n    # _Dot = WORD + 27\n    # _DotRel = WORD + 32\n    # _DotShift = WORD + 28\n    Children = (WORD + 30, U16Event)  #: 3.4.0+\n    Swing = (WORD + 33, U16Event)\n    # Echo = DWORD + 2\n    RingMod = (DWORD + 3, U16TupleEvent)\n    CutGroup = (DWORD + 4, U16TupleEvent)\n    RootNote = (DWORD + 7, U32Event)\n    # _MainResoCutOff = DWORD + 9\n    DelayModXY = (DWORD + 10, U16TupleEvent)\n    Reverb = (DWORD + 11, U32Event)  #: 1.4.0+\n    _StretchTime = (DWORD + 12, F32Event)  #: 5.0+\n    FineTune = (DWORD + 14, I32Event)\n    SamplerFlags = (DWORD + 15, U32Event)\n    LayerFlags = (DWORD + 16, U32Event)\n    GroupNum = (DWORD + 17, I32Event)\n    AUSampleRate = (DWORD + 25, U32Event)\n    _Name = TEXT\n    SamplePath = TEXT + 4\n    Delay = (DATA + 1, DelayEvent)\n    Parameters = (DATA + 7, ParametersEvent)\n    EnvelopeLFO = (DATA + 10, EnvelopeLFOEvent)\n    Levels = (DATA + 11, LevelsEvent)\n    # _Filter = DATA + 12\n    Polyphony = (DATA + 13, PolyphonyEvent)\n    # _LegacyAutomation = DATA + 15\n    Tracking = (DATA + 20, TrackingEvent)\n    LevelAdjusts = (DATA + 21, LevelAdjustsEvent)\n    Automation = (DATA + 26, AutomationEvent)\n\n\n@enum.unique\nclass DisplayGroupID(EventEnum):\n    Name = TEXT + 39  #: 3.4.0+\n\n\n@enum.unique\nclass RackID(EventEnum):\n    Swing = (11, U8Event)\n    _FitToSteps = (13, U8Event)\n    WindowHeight = (DWORD + 5, U32Event)\n\n\n@enum.unique\nclass ReverbType(enum.IntEnum):\n    \"\"\"Used by :attr:`Reverb.type`.\"\"\"\n\n    A = 0\n    B = 65536\n\n\n# The type of a channel may decide how a certain event is interpreted. An\n# example of this is `ChannelID.Levels` event, which is used for storing\n# volume, pan and pich bend range of any channel other than automations. In\n# automations it is used for **Min** and **Max** knobs.\n@enum.unique\nclass ChannelType(ct.EnumBase):  # cuz Type would be a super generic name\n    \"\"\"An internal marker used to indicate the type of a channel.\"\"\"\n\n    Sampler = 0\n    \"\"\"Used exclusively for the inbuilt Sampler.\"\"\"\n\n    Native = 2\n    \"\"\"Used by audio clips and other native FL Studio synths.\"\"\"\n\n    Layer = 3  # 3.4.0+\n    Instrument = 4\n    Automation = 5  # 5.0+\n\n\nclass _FXFlags(enum.IntFlag):\n    FadeStereo = 1 << 0\n    Reverse = 1 << 1\n    Clip = 1 << 2\n    SwapStereo = 1 << 8\n\n\nclass _LayerFlags(enum.IntFlag):\n    Random = 1 << 0\n    Crossfade = 1 << 1\n\n\nclass _SamplerFlags(enum.IntFlag):\n    Resample = 1 << 0\n    LoadRegions = 1 << 1\n    LoadSliceMarkers = 1 << 2\n    UsesLoopPoints = 1 << 3\n    KeepOnDisk = 1 << 8\n\n\nclass DisplayGroup(EventModel, ModelReprMixin):\n    def __str__(self) -> str:\n        if self.name is None:\n            return \"Unnamed display group\"\n        return f\"Display group {self.name}\"\n\n    name = EventProp[str](DisplayGroupID.Name)\n\n\nclass Arp(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler`: and :class:`Instrument`.\n\n    ![](https://bit.ly/3Lbk7Yi)\n    \"\"\"\n\n    chord = StructProp[int]()\n    \"\"\"Index of the selected arpeggio chord.\"\"\"\n\n    direction = StructProp[ArpDirection]()\n    gate = StructProp[float]()\n    \"\"\"Delay between two successive notes played.\"\"\"\n\n    range = StructProp[int]()\n    \"\"\"Range (in octaves).\"\"\"\n\n    repeat = StructProp[int]()\n    \"\"\"Number of times a note is repeated.\n\n    *New in FL Studio v4.5.2*.\n    \"\"\"\n\n    slide = StructProp[bool]()\n    \"\"\"Whether arpeggio will slide between notes.\"\"\"\n\n    time = StructProp[float]()\n    \"\"\"Delay between two successive notes played.\"\"\"\n\n\nclass Delay(EventModel, ModelReprMixin):\n    \"\"\"Echo delay / fat mode section.\n\n    Used by :class:`Sampler` and :class:`Instrument`.\n\n    ![](https://bit.ly/3RyzbBD)\n    \"\"\"\n\n    echoes = StructProp[int](ChannelID.Delay)\n    \"\"\"Number of echoes generated for each note. Min = 1. Max = 10.\"\"\"\n\n    fat_mode = FlagProp(_DelayFlags.FatMode, ChannelID.Parameters, prop=\"delay.flags\")\n    \"\"\"*New in FL Studio v3.4.0*.\"\"\"\n\n    feedback = StructProp[int](ChannelID.Delay)\n    \"\"\"Factor with which the volume of every next echo is multiplied.\n\n    Defaults to minimum value.\n\n    | Type | Value | Representation |\n    |------|-------|----------------|\n    | Min  | 0     | 0%             |\n    | Max  | 25600 | 200%           |\n    \"\"\"\n\n    @property\n    def mod_x(self) -> int:\n        \"\"\"Min = 0. Max = 256. Default = 128.\"\"\"\n        return self.events.first(ChannelID.DelayModXY).value[0]\n\n    @mod_x.setter\n    def mod_x(self, value: int) -> None:\n        event = self.events.first(ChannelID.DelayModXY)\n        event.value = (value, event.value[1])\n\n    @property\n    def mod_y(self) -> int:\n        \"\"\"Min = 0. Max = 256. Default = 128.\"\"\"\n        return self.events.first(ChannelID.DelayModXY).value[1]\n\n    @mod_y.setter\n    def mod_y(self, value: int) -> None:\n        event = self.events.first(ChannelID.DelayModXY)\n        event.value = (event.value[0], value)\n\n    pan = StructProp[int](ChannelID.Delay)\n    \"\"\"\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -6400 | 100% left      |\n    | Max     | 6400  | 100% right     |\n    | Default | 0     | Centred        |\n    \"\"\"\n\n    ping_pong = FlagProp(\n        _DelayFlags.PingPong,\n        ChannelID.Parameters,\n        prop=\"delay.flags\",\n    )\n    \"\"\"*New in FL Studio v1.7.6*.\"\"\"\n\n    pitch_shift = StructProp[int](ChannelID.Delay)\n    \"\"\"Pitch shift (in cents).\n\n    | Min   | Max   | Default |\n    |-------|-------|---------|\n    | -1200 | 1200  | 0       |\n    \"\"\"\n\n    time = StructProp[int](ChannelID.Delay)\n    \"\"\"Tempo-synced delay time. PPQ dependant.\n\n    | Type    | Value     | Representation |\n    |---------|-----------|----------------|\n    | Min     | 0         | 0:00           |\n    | Max     | PPQ * 4   | 8:00           |\n    | Default | PPQ * 3/2 | 3:00           |\n    \"\"\"\n\n\nclass Filter(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler`.\n\n    ![](https://bit.ly/3zT5tAH)\n    \"\"\"\n\n    mod_x = StructProp[int](ChannelID.Levels, prop=\"filter.mod_x\")\n    \"\"\"Filter cutoff. Min = 0. Max = 256. Defaults to maximum.\"\"\"\n\n    mod_y = StructProp[int](ChannelID.Levels, prop=\"filter.mod_y\")\n    \"\"\"Filter resonance. Min = 0. Max = 256. Defaults to minimum.\"\"\"\n\n    type = StructProp[FilterType](ChannelID.Levels, prop=\"filter.type\")\n    \"\"\"Defaults to :attr:`FilterType.FastLP`.\"\"\"\n\n\nclass LevelAdjusts(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Layer`, :class:`Instrument` and :class:`Sampler`.\n\n    ![](https://bit.ly/3xkKeGn)\n\n    *New in FL Studio v3.3.0*.\n    \"\"\"\n\n    mod_x = StructProp[int]()\n    mod_y = StructProp[int]()\n    pan = StructProp[int]()\n    volume = StructProp[int]()\n\n\nclass Time(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler` and :class:`Instrument`.\n\n    ![](https://bit.ly/3xjxUGG)\n    \"\"\"\n\n    swing = EventProp[int](ChannelID.Swing)\n    \"\"\"Percentage of the ``ChannelRack.swing`` that affects this channel.\n\n    Linear. Min = 0. Max = 128. Defaults to maximum.\n    \"\"\"\n\n    gate = StructProp[int](ChannelID.Parameters, prop=\"time.gate\")\n    \"\"\"Logarithmic. Defaults to disabled state.\n\n    | Type     | Value | Representation |\n    |----------|-------|----------------|\n    | Min      | 450   | 0:03           |\n    | Max      | 1446  | 4:00           |\n    | Disabled | 1447  | Off            |\n    \"\"\"\n\n    shift = EventProp[int](ChannelID.TimeShift)\n    \"\"\"Fine time shift. Nonlinear. Defaults to minimum.\n\n    | Type | Value | Representation |\n    |------|-------|----------------|\n    | Min  | 0     | 0:00           |\n    | Max  | 1024  | 1:00           |\n    \"\"\"\n\n    full_porta = StructProp[bool](ChannelID.Parameters, prop=\"time.full_porta\")\n    \"\"\"Whether :attr:`gate` is bypassed when :attr:`Polyphony.porta` is on.\"\"\"\n\n\nclass Reverb(EventModel, ModelReprMixin):\n    \"\"\"Precalculated reverb used by :class:`Sampler`.\n\n    *New in FL Studio v1.4.0*.\n    \"\"\"\n\n    @property\n    def type(self) -> ReverbType | None:\n        if ChannelID.Reverb in self.events.ids:\n            event = self.events.first(ChannelID.Reverb)\n            return ReverbType.B if event.value >= ReverbType.B else ReverbType.A\n\n    @type.setter\n    def type(self, value: ReverbType) -> None:\n        if self.mix is None:\n            raise PropertyCannotBeSet(ChannelID.Reverb)\n\n        self.events.first(ChannelID.Reverb).value = value.value + self.mix\n\n    @property\n    def mix(self) -> int | None:\n        \"\"\"Mix % (wet). Defaults to minimum value.\n\n        | Min | Max |\n        |-----|-----|\n        | 0   | 256 |\n        \"\"\"\n        if ChannelID.Reverb in self.events.ids:\n            return self.events.first(ChannelID.Reverb).value - self.type\n\n    @mix.setter\n    def mix(self, value: int) -> None:\n        if ChannelID.Reverb not in self.events.ids:\n            raise PropertyCannotBeSet(ChannelID.Reverb)\n\n        self.events.first(ChannelID.Reverb).value += value\n\n\nclass FX(EventModel, ModelReprMixin):\n    \"\"\"Pre-computed effects used by :class:`Sampler`.\n\n    ![](https://bit.ly/3U3Ys8l)\n    ![](https://bit.ly/3qvdBSN)\n\n    See Also:\n        :attr:`Sampler.fx`, :attr:`Reverb`\n    \"\"\"\n\n    boost = EventProp[int](ChannelID.Preamp)\n    \"\"\"Pre-amp gain. Defaults to minimum value.\n\n    | Min | Max |\n    |-----|-----|\n    | 0   | 256 |\n\n    *New in FL Studio v1.2.12*.\n    \"\"\"\n\n    clip = FlagProp(_FXFlags.Clip, ChannelID.FXFlags)\n    \"\"\"Whether output is clipped at 0dB for :attr:`boost`.\"\"\"\n\n    crossfade = StructProp[int](ChannelID.Parameters, prop=\"fx.crossfade\")\n    \"\"\"Linear. Defaults to minimum value\n\n    | Type | Value | Representation |\n    |------|-------|----------------|\n    | Min  | 0     | 0%             |\n    | Max  | 256   | 100%           |\n    \"\"\"\n\n    cutoff = EventProp[int](ChannelID.Cutoff)\n    \"\"\"Filter Mod X. Defaults to maximum value. Min = 16. Max = 1024.\"\"\"\n\n    fade_in = EventProp[int](ChannelID.FadeIn)\n    \"\"\"Quick fade-in. Defaults to minimum value. Min = 0. Max = 1024.\"\"\"\n\n    fade_out = EventProp[int](ChannelID.FadeOut)\n    \"\"\"Quick fade-out. Defaults to minimum value. Min = 0. Max = 1024.\n\n    *New in FL Studio v1.7.6*.\n    \"\"\"\n\n    fade_stereo = FlagProp(_FXFlags.FadeStereo, ChannelID.FXFlags)\n    fix_trim = StructProp[bool](ChannelID.Parameters, prop=\"fx.fix_trim\")\n    \"\"\":menuselection:`Trim --> Fix legacy precomputed length`.\n\n    Has no effect on the value of :attr:`trim`.\n    \"\"\"\n\n    freq_tilt = EventProp[int](ChannelID.FreqTilt)\n    \"\"\"Shifts the frequency balance. Bipolar.\n\n    | Min | Max | Default |\n    |-----|-----|---------|\n    | 0   | 256 | 128     |\n    \"\"\"\n\n    inverted = StructProp[bool](ChannelID.Parameters, prop=\"fx.inverted\")\n    \"\"\"Named :guilabel:`Reverse polarity` in FL's interface.\"\"\"\n\n    length = StructProp[float](ChannelID.Parameters, prop=\"fx.length\")\n    \"\"\"Min = 0.0, Max = 1.0. Defaults to minimum value.\n\n    Named :guilabel:`SMP START` in FL's interface.\n    \"\"\"\n\n    normalize = StructProp[bool](ChannelID.Parameters, prop=\"fx.normalize\")\n    \"\"\"Maximizes volume without clipping by normalizing peaks to 0dB.\"\"\"\n\n    pogo = EventProp[int](ChannelID.Pogo)\n    \"\"\"Pitch bend effect. Bipolar.\n\n    | Min | Max | Default |\n    |-----|-----|---------|\n    | 0   | 512 | 256     |\n    \"\"\"\n\n    remove_dc = StructProp[bool](ChannelID.Parameters, prop=\"fx.remove_dc\")\n    \"\"\"Whether DC offset (if present) is removed.\n\n    *New in FL Studio v2.5.0*.\n    \"\"\"\n\n    resonance = EventProp[int](ChannelID.Resonance)\n    \"\"\"Filter Mod Y. Min = 0. Max = 640. Defaults to minimum value.\"\"\"\n\n    reverb = NestedProp[Reverb](Reverb, ChannelID.Reverb)\n    reverse = FlagProp(_FXFlags.Reverse, ChannelID.FXFlags)\n    \"\"\"Whether sample is reversed or not.\"\"\"\n\n    ringmod = EventProp[Tuple[int, int]](ChannelID.RingMod)\n    \"\"\"Ring modulation returned as a tuple of ``(mix, frequency)``.\n\n    Limits for both:\n\n    | Min | Max | Default |\n    |-----|-----|---------|\n    | 0   | 256 | 128     |\n    \"\"\"\n\n    start = StructProp[float](ChannelID.Parameters, prop=\"fx.start\")\n    \"\"\"Min = 0.0, Max = 1.0. Defaults to minimum value.\n\n    Always set to 0.0 irrespective of the knob position unless a sample is loaded.\n    \"\"\"\n\n    stereo_delay = EventProp[int](ChannelID.StereoDelay)\n    \"\"\"Linear. Bipolar.\n\n    | Min | Max  | Default |\n    |-----|------|---------|\n    | 0   | 4096 | 2048    |\n\n    *New in FL Studio v1.3.56*.\n    \"\"\"\n\n    swap_stereo = FlagProp(_FXFlags.SwapStereo, ChannelID.FXFlags)\n    \"\"\"Whether left and right channels are swapped or not.\"\"\"\n\n    trim = StructProp[int](ChannelID.Parameters, prop=\"fx.trim\")\n    \"\"\"Silence trimming threshold. Defaults to minimum. Linear.\n\n    | Type | Value | Representation |\n    |------|-------|----------------|\n    | Min  | 0     | 0%             |\n    | Max  | 256   | 100%           |\n    \"\"\"\n\n\nclass Envelope(EventModel, ModelReprMixin):\n    \"\"\"A PAHDSR envelope for various :class:`Sampler` paramters.\n\n    ![](https://bit.ly/3d9WCCh)\n\n    See Also:\n        :attr:`Sampler.envelopes`\n\n    *New in FL Studio v2.5.0*.\n    \"\"\"\n\n    enabled = StructProp[bool](prop=\"envelope.enabled\")\n    \"\"\"Whether envelope section is enabled.\"\"\"\n\n    predelay = StructProp[int](prop=\"envelope.predelay\")\n    \"\"\"Linear. Defaults to minimum value.\n\n    | Type | Value | Representation |\n    |------|-------|----------------|\n    | Min  | 100   | 0%             |\n    | Max  | 65536 | 100%           |\n    \"\"\"\n\n    amount = StructProp[int](prop=\"envelope.amount\")\n    \"\"\"Linear. Bipolar.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -128  | -100%          |\n    | Max     | 128   | 100%           |\n    | Default | 0     | 0%             |\n    \"\"\"\n\n    attack = StructProp[int](prop=\"envelope.attack\")\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 100   | 0%             |\n    | Max     | 65536 | 100%           |\n    | Default | 20000 | 31%            |\n    \"\"\"\n\n    hold = StructProp[int](prop=\"envelope.hold\")\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 100   | 0%             |\n    | Max     | 65536 | 100%           |\n    | Default | 20000 | 31%            |\n    \"\"\"\n\n    decay = StructProp[int](prop=\"envelope.decay\")\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 100   | 0%             |\n    | Max     | 65536 | 100%           |\n    | Default | 30000 | 46%            |\n    \"\"\"\n\n    sustain = StructProp[int](prop=\"envelope.sustain\")\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0%             |\n    | Max     | 128   | 100%           |\n    | Default | 50    | 39%            |\n    \"\"\"\n\n    release = StructProp[int](prop=\"envelope.release\")\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 100   | 0%             |\n    | Max     | 65536 | 100%           |\n    | Default | 20000 | 31%            |\n    \"\"\"\n\n    synced = FlagProp(_EnvLFOFlags.EnvelopeTempoSync)\n    \"\"\"Whether envelope is synced to tempo or not.\"\"\"\n\n    attack_tension = StructProp[int](prop=\"envelope.attack_tension\")\n    \"\"\"Linear. Bipolar.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -128  | -100%          |\n    | Max     | 128   | 100%           |\n    | Default | 0     | 0%             |\n\n    *New in FL Studio v3.5.4*.\n    \"\"\"\n\n    decay_tension = StructProp[int](prop=\"envelope.decay_tension\")\n    \"\"\"Linear. Bipolar.\n\n    | Type    | Value | Mix (wet) |\n    |---------|-------|-----------|\n    | Min     | -128  | -100%     |\n    | Max     | 128   | 100%      |\n    | Default | 0     | 0%        |\n\n    *New in FL Studio v3.5.4*.\n    \"\"\"\n\n    release_tension = StructProp[int](prop=\"envelope.release_tension\")\n    \"\"\"Linear. Bipolar.\n\n    | Type    | Value | Mix (wet) |\n    |---------|-------|-----------|\n    | Min     | -128  | -100%     |\n    | Max     | 128   | 100%      |\n    | Default | -101  | -79%      |\n\n    *New in FL Studio v3.5.4*.\n    \"\"\"\n\n\nclass SamplerLFO(EventModel, ModelReprMixin):\n    \"\"\"A basic LFO for certain :class:`Sampler` parameters.\n\n    ![](https://bit.ly/3RG5Jtw)\n\n    See Also:\n        :attr:`Sampler.lfos`\n\n    *New in FL Studio v2.5.0*.\n    \"\"\"\n\n    amount = StructProp[int](prop=\"lfo.amount\")\n    \"\"\"Linear. Bipolar.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -128  | -100%          |\n    | Max     | 128   | 100%           |\n    | Default | 0     | 0%             |\n    \"\"\"\n\n    attack = StructProp[int](prop=\"lfo.attack\")\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 100   | 0%             |\n    | Max     | 65536 | 100%           |\n    | Default | 20000 | 31%            |\n    \"\"\"\n\n    predelay = StructProp[int](prop=\"lfo.predelay\")\n    \"\"\"Linear. Defaults to minimum value.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 100   | 0%             |\n    | Max     | 65536 | 100%           |\n    \"\"\"\n\n    speed = StructProp[int](prop=\"lfo.speed\")\n    \"\"\"Logarithmic. Provides tempo synced options.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 200   | 0%             |\n    | Max     | 65536 | 100%           |\n    | Default | 32950 | 50% (16 steps) |\n    \"\"\"\n\n    synced = FlagProp(_EnvLFOFlags.LFOTempoSync)\n    \"\"\"Whether LFO is synced with tempo.\"\"\"\n\n    retrig = FlagProp(_EnvLFOFlags.LFOPhaseRetrig)\n    \"\"\"Whether LFO phase is in global / retriggered mode.\"\"\"\n\n    shape = StructProp[LFOShape](prop=\"lfo.shape\")\n    \"\"\"Sine, triangle or pulse. Default: Sine.\"\"\"\n\n\nclass Polyphony(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler` and :class:`Instrument`.\n\n    ![](https://bit.ly/3DlvWcl)\n    \"\"\"\n\n    mono = FlagProp(_PolyphonyFlags.Mono)\n    \"\"\"Whether monophonic mode is enabled or not.\"\"\"\n\n    porta = FlagProp(_PolyphonyFlags.Porta)\n    \"\"\"*New in FL Studio v3.3.0*.\"\"\"\n\n    max = StructProp[int]()\n    \"\"\"Max number of voices.\"\"\"\n\n    slide = StructProp[int]()\n    \"\"\"Portamento time. Nonlinear.\n\n    | Type    | Value | Representation  |\n    |---------|-------|-----------------|\n    | Min     | 0     | 0:00            |\n    | Max     | 1660  | 8:00 (8 steps)  |\n    | Default | 820   | 0:12 (1/2 step) |\n\n    *New in FL Studio v3.3.0*.\n    \"\"\"\n\n\nclass Tracking(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler` and :class:`Instrument`.\n\n    ![](https://bit.ly/3DmveM8)\n\n    *New in FL Studio v3.3.0*.\n    \"\"\"\n\n    middle_value = StructProp[int]()\n    \"\"\"Note index. Min: C0 (0), Max: B10 (131).\"\"\"\n\n    mod_x = StructProp[int]()\n    \"\"\"Bipolar.\n\n    | Min  | Max | Default |\n    |------|-----|---------|\n    | -256 | 256 | 0       |\n    \"\"\"\n\n    mod_y = StructProp[int]()\n    \"\"\"Bipolar.\n\n    | Min  | Max | Default |\n    |------|-----|---------|\n    | -256 | 256 | 0       |\n    \"\"\"\n\n    pan = StructProp[int]()\n    \"\"\"Linear. Bipolar.\n\n    | Min  | Max | Default |\n    |------|-----|---------|\n    | -256 | 256 | 0       |\n    \"\"\"\n\n\nclass Keyboard(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler` and :class:`Instrument`.\n\n    ![](https://bit.ly/3qwIK8r)\n\n    *New in FL Studio v1.3.56*.\n    \"\"\"\n\n    fine_tune = EventProp[int](ChannelID.FineTune)\n    \"\"\"-100 to +100 cents.\"\"\"\n\n    # TODO Return this as a note name, like `Note.key`\n    root_note = EventProp[int](ChannelID.RootNote, default=60)\n    \"\"\"Min - 0 (C0), Max - 131 (B10).\"\"\"\n\n    main_pitch = StructProp[bool](ChannelID.Parameters, prop=\"keyboard.main_pitch\")\n    \"\"\"Whether triggered note is affected by changes to :attr:`Project.main_pitch`.\"\"\"\n\n    add_root = StructProp[bool](ChannelID.Parameters, prop=\"keyboard.add_root\")\n    \"\"\"Whether to add root note (instead of pitch) to triggered note.\n\n    Named as :guilabel:`Add to key`. Defaults to ``False``.\n\n    *New in FL Studio v3.4.0*.\n    \"\"\"\n\n    key_region = StructProp[Tuple[int, int]](ChannelID.Parameters, prop=\"keyboard.key_region\")\n    \"\"\"A `(start_note, end_note)` tuple representing the playable range.\"\"\"\n\n\nclass Playback(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler`.\n\n    ![](https://bit.ly/3xjSypY)\n    \"\"\"\n\n    ping_pong_loop = EventProp[bool](ChannelID.PingPongLoop)\n    start_offset = StructProp[int](ChannelID.Parameters, prop=\"playback.start_offset\")\n    \"\"\"Linear. Defaults to minimum value.\n\n    | Type | Value      | Representation |\n    |------|------------|----------------|\n    | Min  | 0          | 0%             |\n    | Max  | 1072693248 | 100%           |\n    \"\"\"\n\n    use_loop_points = FlagProp(_SamplerFlags.UsesLoopPoints, ChannelID.SamplerFlags)\n\n\nclass TimeStretching(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler`.\n\n    ![](https://bit.ly/3eIAjnG)\n\n    *New in FL Studio v5.0*.\n    \"\"\"\n\n    mode = StructProp[StretchMode](ChannelID.Parameters, prop=\"stretching.mode\")\n    multiplier = StructProp[float](ChannelID.Parameters, prop=\"stretching.multiplier\")\n    \"\"\"Logarithmic. Bipolar.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0.25  | 25%            |\n    | Max     | 4.0   | 400%           |\n    | Default | 0     | 100%           |\n    \"\"\"\n\n    pitch = StructProp[int](ChannelID.Parameters, prop=\"stretching.pitch\")\n    \"\"\"Pitch shift (in cents). Min = -1200. Max = 1200. Defaults to 0.\"\"\"\n\n    time = StructProp[MusicalTime](ChannelID.Parameters, prop=\"stretching.time\")\n    \"\"\"Returns a tuple of ``(bars, beats, ticks)``.\"\"\"\n\n\nclass Content(EventModel, ModelReprMixin):\n    \"\"\"Used by :class:`Sampler`.\n\n    ![](https://bit.ly/3TCXFKI)\n    \"\"\"\n\n    declick_mode = StructProp[DeclickMode](ChannelID.Parameters, prop=\"content.declick_mode\")\n    \"\"\"Defaults to ``DeclickMode.OutOnly``.\n\n    *New in FL Studio v9.0.0*.\n    \"\"\"\n\n    keep_on_disk = FlagProp(_SamplerFlags.KeepOnDisk, ChannelID.SamplerFlags)\n    \"\"\"Whether a sample is streamed from disk or kept in RAM, defaults to ``False``.\n\n    *New in FL Studio v2.5.0*.\n    \"\"\"\n\n    load_regions = FlagProp(_SamplerFlags.LoadRegions, ChannelID.SamplerFlags)\n    \"\"\"Load regions found in the sample, if any, defaults to ``True``.\"\"\"\n\n    load_slices = FlagProp(_SamplerFlags.LoadSliceMarkers, ChannelID.SamplerFlags)\n    \"\"\"Defaults to ``False``.\"\"\"\n\n    resample = FlagProp(_SamplerFlags.Resample, ChannelID.SamplerFlags)\n    \"\"\"Defaults to ``False``.\n\n    *New in FL Studio v2.5.0*.\n    \"\"\"\n\n\nclass AutomationLFO(EventModel, ModelReprMixin):\n    amount = StructProp[int](ChannelID.Automation, prop=\"lfo.amount\")\n    \"\"\"Linear. Bipolar.\n\n    | Type    | Value      | Representation |\n    |---------|------------|----------------|\n    | Min     | -128       | -100%          |\n    | Max     | 128        | 100%           |\n    | Default | 64 or 0    | 50% or 0%      |\n    \"\"\"\n\n\nclass AutomationPoint(ItemModel[AutomationEvent], ModelReprMixin):\n    def __setitem__(self, prop: str, value: Any) -> None:\n        self._item[prop] = value\n        self._parent[\"points\"][self._index] = self._item\n\n    position = StructProp[int](readonly=True)\n    \"\"\"PPQ dependant. Position on X-axis.\n\n    This property cannot be set as of yet.\n    \"\"\"\n\n    tension = StructProp[float]()\n    \"\"\"A value in the range of 0 to 1.0.\"\"\"\n\n    value = StructProp[float]()\n    \"\"\"Position on Y-axis in the range of 0 to 1.0.\"\"\"\n\n\nclass Channel(EventModel):\n    \"\"\"Represents a channel in the channel rack.\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"{type(self).__name__} (name={self.display_name!r}, iid={self.iid})\"\n\n    color = EventProp[RGBA](PluginID.Color)\n    \"\"\"Defaults to #5C656A (granite gray).\n\n    ![](https://bit.ly/3SllDsG)\n\n    Values below 20 for any color component (R, G or B) are ignored by FL.\n    \"\"\"\n\n    # TODO controllers = KWProp[List[RemoteController]]()\n    internal_name = EventProp[str](PluginID.InternalName)\n    \"\"\"Internal name of the channel.\n\n    The value of this depends on the type of `plugin`:\n\n    * Native (stock) plugin: Empty *afaik*.\n    * VST instruments: \"Fruity Wrapper\".\n\n    See Also:\n        :attr:`name`\n    \"\"\"\n\n    enabled = EventProp[bool](ChannelID.IsEnabled)\n    \"\"\"![](https://bit.ly/3sbN8KU)\"\"\"\n\n    @property\n    def group(self) -> DisplayGroup:  # TODO Setter\n        \"\"\"Display group / filter under which this channel is grouped.\"\"\"\n        return self._kw[\"group\"]\n\n    icon = EventProp[int](PluginID.Icon)\n    \"\"\"Internal ID of the icon shown beside the ``display_name``.\n\n    ![](https://bit.ly/3zjK2sf)\n    \"\"\"\n\n    iid = EventProp[int](ChannelID.New)\n    keyboard = NestedProp(Keyboard, ChannelID.FineTune, ChannelID.RootNote, ChannelID.Parameters)\n    \"\"\"Located at the bottom of :menuselection:`Miscellaneous functions (page)`.\"\"\"\n\n    locked = EventProp[bool](ChannelID.IsLocked)\n    \"\"\"Whether in a locked state or not; mute / solo acts differently when ``True``.\n\n    ![](https://bit.ly/3BOBc7j)\n    \"\"\"\n\n    name = EventProp[str](PluginID.Name, ChannelID._Name)\n    \"\"\"The name associated with a channel.\n\n    It's value depends on the type of plugin:\n\n    * Native (stock): User-given name, None if not given one.\n    * VST instrument: The name obtained from the VST or the user-given name.\n\n    See Also:\n        :attr:`internal_name` and :attr:`display_name`.\n    \"\"\"\n\n    @property\n    def pan(self) -> int | None:\n        \"\"\"Linear. Bipolar.\n\n        | Min | Max   | Default |\n        |-----|-------|---------|\n        | 0   | 12800 | 6400    |\n        \"\"\"\n        if ChannelID.Levels in self.events.ids:\n            return cast(LevelsEvent, self.events.first(ChannelID.Levels))[\"pan\"]\n\n        for id in (ChannelID._PanWord, ChannelID._PanByte):\n            if id in self.events.ids:\n                return self.events.first(id).value\n\n    @pan.setter\n    def pan(self, value: int) -> None:\n        if self.pan is None:\n            raise PropertyCannotBeSet\n\n        if ChannelID.Levels in self.events.ids:\n            cast(LevelsEvent, self.events.first(ChannelID.Levels))[\"pan\"] = value\n            return\n\n        for id in (ChannelID._PanWord, ChannelID._PanByte):\n            if id in self.events.ids:\n                self.events.first(id).value = value\n\n    @property\n    def volume(self) -> int | None:\n        \"\"\"Nonlinear.\n\n        | Min | Max   | Default |\n        |-----|-------|---------|\n        | 0   | 12800 | 10000   |\n        \"\"\"\n        if ChannelID.Levels in self.events.ids:\n            return cast(LevelsEvent, self.events.first(ChannelID.Levels))[\"volume\"]\n\n        for id in (ChannelID._VolWord, ChannelID._VolByte):\n            if id in self.events.ids:\n                return self.events.first(id).value\n\n    @volume.setter\n    def volume(self, value: int) -> None:\n        if self.volume is None:\n            raise PropertyCannotBeSet\n\n        if ChannelID.Levels in self.events.ids:\n            cast(LevelsEvent, self.events.first(ChannelID.Levels))[\"volume\"] = value\n            return\n\n        for id in (ChannelID._VolWord, ChannelID._VolByte):\n            if id in self.events.ids:\n                self.events.first(id).value = value\n\n    # If the channel is not zipped, underlying event is not stored.\n    @property\n    def zipped(self) -> bool:\n        \"\"\"Whether the channel is zipped / minimized.\n\n        ![](https://bit.ly/3S2imib)\n        \"\"\"\n        if ChannelID.Zipped in self.events.ids:\n            return self.events.first(ChannelID.Zipped).value\n        return False\n\n    @property\n    def display_name(self) -> str | None:\n        \"\"\"The name of the channel that will be displayed in FL Studio.\"\"\"\n        return self.name or self.internal_name  # type: ignore\n\n\nclass Automation(Channel, ModelCollection[AutomationPoint]):\n    \"\"\"Represents an automation clip present in the channel rack.\n\n    Iterate to get the :attr:`points` inside the clip.\n\n        >>> repr([point for point in automation])\n        AutomationPoint(position=0.0, value=1.0, tension=0.5), ...\n\n    ![](https://bit.ly/3RXQhIN)\n    \"\"\"\n\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: int | slice) -> AutomationPoint:\n        for idx, p in enumerate(self):\n            if idx == i:\n                return p\n        raise ModelNotFound(i)\n\n    def __iter__(self) -> Iterator[AutomationPoint]:\n        \"\"\"Iterator over the automation points inside the automation clip.\"\"\"\n        if ChannelID.Automation in self.events.ids:\n            event = cast(AutomationEvent, self.events.first(ChannelID.Automation))\n            for i, point in enumerate(event[\"points\"]):\n                yield AutomationPoint(point, i, event)\n\n    lfo = NestedProp(AutomationLFO, ChannelID.Automation)  # TODO Add image\n\n\nclass Layer(Channel, ModelCollection[Channel]):\n    \"\"\"Represents a layer channel present in the channel rack.\n\n    ![](https://bit.ly/3S2MLgf)\n\n    *New in FL Studio v3.4.0*.\n    \"\"\"\n\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: int | str | slice) -> Channel:\n        \"\"\"Returns a child :class:`Channel` with an IID of :attr:`Channel.iid`.\n\n        Args:\n            i: IID or 0-based index of the child(ren).\n\n        Raises:\n            ChannelNotFound: Child(ren) with the specific index or IID couldn't\n                be found. This exception derives from ``KeyError`` as well.\n        \"\"\"\n        for child in self:\n            if i == child.iid:\n                return child\n        raise ChannelNotFound(i)\n\n    def __iter__(self) -> Iterator[Channel]:\n        if ChannelID.Children in self.events.ids:\n            for event in self.events.get(ChannelID.Children):\n                yield self._kw[\"channels\"][event.value]\n\n    def __len__(self) -> int:\n        \"\"\"Returns the number of channels whose parent this layer is.\"\"\"\n        try:\n            return self.events.count(ChannelID.Children)\n        except KeyError:\n            return 0\n\n    def __repr__(self) -> str:\n        return f\"{super().__repr__()[:-1]}, {len(self)} children)\"\n\n    crossfade = FlagProp(_LayerFlags.Crossfade, ChannelID.LayerFlags)\n    \"\"\":menuselection:`Miscellaneous functions --> Layering`\"\"\"\n\n    random = FlagProp(_LayerFlags.Random, ChannelID.LayerFlags)\n    \"\"\":menuselection:`Miscellaneous functions --> Layering`\"\"\"\n\n\nclass _SamplerInstrument(Channel):\n    arp = NestedProp(Arp, ChannelID.Parameters)\n    \"\"\":menuselection:`Miscellaneous functions -> Arpeggiator`\"\"\"\n\n    cut_group = EventProp[Tuple[int, int]](ChannelID.CutGroup)\n    \"\"\"Cut group in the form of (Cut self, cut by).\n\n    :menuselection:`Miscellaneous functions --> Group`\n\n    Hint:\n        To cut itself when retriggered, set the same value for both.\n    \"\"\"\n\n    delay = NestedProp(Delay, ChannelID.Delay, ChannelID.DelayModXY, ChannelID.Parameters)\n    \"\"\":menuselection:`Miscellaneous functions -> Echo delay / fat mode`\"\"\"\n\n    insert = EventProp[int](ChannelID.RoutedTo)\n    \"\"\"The index of the :class:`Insert` the channel is routed to according to FL.\n\n    \"Current\" insert = -1, Master = 0 and so on... till :attr:`Mixer.max_inserts`.\n    \"\"\"\n\n    level_adjusts = NestedProp(LevelAdjusts, ChannelID.LevelAdjusts)\n    \"\"\":menuselection:`Miscellaneous functions -> Level adjustments`\"\"\"\n\n    @property\n    def pitch_shift(self) -> int | None:\n        \"\"\"-4800 to +4800 (cents).\n\n        Raises:\n            PropertyCannotBeSet: When a `ChannelID.Levels` event is not found.\n        \"\"\"\n        if ChannelID.Levels in self.events.ids:\n            return cast(LevelsEvent, self.events.first(ChannelID.Levels))[\"pitch_shift\"]\n\n    @pitch_shift.setter\n    def pitch_shift(self, value: int) -> None:\n        try:\n            event = self.events.first(ChannelID.Levels)\n        except KeyError as exc:\n            raise PropertyCannotBeSet(ChannelID.Levels) from exc\n        else:\n            cast(LevelsEvent, event)[\"pitch_shift\"] = value\n\n    polyphony = NestedProp(Polyphony, ChannelID.Polyphony)\n    \"\"\":menuselection:`Miscellaneous functions -> Polyphony`\"\"\"\n\n    time = NestedProp(Time, ChannelID.Swing, ChannelID.TimeShift, ChannelID.Parameters)\n    \"\"\":menuselection:`Miscellaneous functions -> Time`\"\"\"\n\n    @property\n    def tracking(self) -> dict[str, Tracking] | None:\n        \"\"\"A :class:`Tracking` each for Volume & Keyboard.\n\n        :menuselection:`Miscellaneous functions -> Tracking`\n        \"\"\"\n        if ChannelID.Tracking in self.events.ids:\n            tracking = [Tracking(e) for e in self.events.separate(ChannelID.Tracking)]\n            return dict(zip((\"volume\", \"keyboard\"), tracking))\n\n\nclass Instrument(_SamplerInstrument):\n    \"\"\"Represents a native or a 3rd party plugin loaded in a channel.\"\"\"\n\n    plugin = PluginProp(VSTPlugin, BooBass, FruitKick, Plucked)\n    \"\"\"The plugin loaded into the channel.\"\"\"\n\n\n# TODO New in FL Studio v1.4.0 & v1.5.23: Sampler spectrum views\nclass Sampler(_SamplerInstrument):\n    \"\"\"Represents the native Sampler, either as a clip or a channel.\n\n    ![](https://bit.ly/3DlHPiI)\n    \"\"\"\n\n    def __repr__(self) -> str:\n        return f\"{super().__repr__()[:-1]}, sample_path={self.sample_path!r})\"\n\n    au_sample_rate = EventProp[int](ChannelID.AUSampleRate)\n    \"\"\"AU-format sample specific.\"\"\"\n\n    content = NestedProp(Content, ChannelID.SamplerFlags, ChannelID.Parameters)\n    \"\"\":menuselection:`Sample settings --> Content`\"\"\"\n\n    # FL's interface doesn't have an envelope for panning, but still stores\n    # the default values in event data.\n    @property\n    def envelopes(self) -> dict[EnvelopeName, Envelope] | None:\n        \"\"\"An :class:`Envelope` each for Volume, Panning, Mod X, Mod Y and Pitch.\n\n        :menuselection:`Envelope / instruement settings`\n        \"\"\"\n        if ChannelID.EnvelopeLFO in self.events.ids:\n            envs = [Envelope(e) for e in self.events.separate(ChannelID.EnvelopeLFO)]\n            return dict(zip(EnvelopeName.__args__, envs))  # type: ignore\n\n    filter = NestedProp(Filter, ChannelID.Levels)\n\n    fx = NestedProp(\n        FX,\n        ChannelID.Cutoff,\n        ChannelID.FadeIn,\n        ChannelID.FadeOut,\n        ChannelID.FreqTilt,\n        ChannelID.Parameters,\n        ChannelID.Pogo,\n        ChannelID.Preamp,\n        ChannelID.Resonance,\n        ChannelID.Reverb,\n        ChannelID.RingMod,\n        ChannelID.StereoDelay,\n        ChannelID.FXFlags,\n    )\n    \"\"\":menuselection:`Sample settings (page) --> Precomputed effects`\"\"\"\n\n    @property\n    def lfos(self) -> dict[LFOName, SamplerLFO] | None:\n        \"\"\"An :class:`LFO` each for Volume, Panning, Mod X, Mod Y and Pitch.\n\n        :menuselection:`Envelope / instruement settings (page)`\n        \"\"\"\n        if ChannelID.EnvelopeLFO in self.events.ids:\n            lfos = [SamplerLFO(e) for e in self.events.separate(ChannelID.EnvelopeLFO)]\n            return dict(zip(LFOName.__args__, lfos))  # type: ignore\n\n    playback = NestedProp(\n        Playback, ChannelID.SamplerFlags, ChannelID.PingPongLoop, ChannelID.Parameters\n    )\n    \"\"\":menuselection:`Sample settings (page) --> Playback`\"\"\"\n\n    @property\n    def sample_path(self) -> pathlib.Path | None:\n        \"\"\"Absolute path of a sample file on the disk.\n\n        :menuselection:`Sample settings (page) --> File`\n\n        Contains the string ``%FLStudioFactoryData%`` for stock samples.\n        \"\"\"\n        if ChannelID.SamplePath in self.events.ids:\n            return pathlib.Path(self.events.first(ChannelID.SamplePath).value)\n\n    @sample_path.setter\n    def sample_path(self, value: pathlib.Path) -> None:\n        if self.sample_path is None:\n            raise PropertyCannotBeSet(ChannelID.SamplePath)\n\n        path = \"\" if str(value) == \".\" else str(value)\n        self.events.first(ChannelID.SamplePath).value = path\n\n    # TODO Find whether ChannelID._StretchTime was really used for attr ``time``.\n    stretching = NestedProp(TimeStretching, ChannelID.Parameters)\n    \"\"\":menuselection:`Sample settings (page) --> Time stretching`\"\"\"\n\n\nclass ChannelRack(EventModel, ModelCollection[Channel]):\n    \"\"\"Represents the channel rack, contains all :class:`Channel` instances.\n\n    ![](https://bit.ly/3RXR50h)\n    \"\"\"\n\n    def __repr__(self) -> str:\n        return f\"ChannelRack - {len(self)} channels\"\n\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: str | int | slice) -> Channel:\n        \"\"\"Gets a channel from the rack based on its IID or name.\n\n        Args:\n            i: Compared with :attr:`Channel.iid` if an int or\n               slice or with the :attr:`Channel.display_name`.\n\n        Raises:\n            ChannelNotFound: A channel with the specified IID or name isn't found.\n        \"\"\"\n        for ch in self:\n            if (isinstance(i, int) and i == ch.iid) or (i == ch.display_name):\n                return ch\n        raise ChannelNotFound(i)\n\n    def __iter__(self) -> Iterator[Channel]:\n        \"\"\"Yields all the channels found in the project.\"\"\"\n        ch_dict: dict[int, Channel] = {}\n        groups = [DisplayGroup(et) for et in self.events.separate(DisplayGroupID.Name)]\n\n        for et in self.events.divide(ChannelID.New, *ChannelID, *PluginID):\n            iid = et.first(ChannelID.New).value\n            typ = et.first(ChannelID.Type).value\n            groupnum = et.first(ChannelID.GroupNum).value\n\n            ct = Channel  # prevent type error and logic failure below\n            if typ == ChannelType.Automation:\n                ct = Automation\n            elif typ == ChannelType.Layer:\n                ct = Layer\n            elif typ == ChannelType.Sampler:\n                ct = Sampler\n            elif typ in (ChannelType.Instrument, ChannelType.Native):\n                ct = Instrument\n\n            # Audio clips are stored as Instrument until a sample is loaded in them\n            if all(id in et for id in (ChannelID.SamplePath, PluginID.InternalName)):\n                if not et.first(PluginID.InternalName).value and ct == Instrument:\n                    ct = Sampler\n\n            if iid is not None:\n                cur_ch = ch_dict[iid] = ct(et, channels=ch_dict, group=groups[groupnum])\n                yield cur_ch\n\n    def __len__(self) -> int:\n        \"\"\"Returns the number of channels found in the project.\n\n        Raises:\n            NoModelsFound: No channels could be found in the project.\n        \"\"\"\n        if ChannelID.New not in self.events.ids:\n            raise NoModelsFound\n        return self.events.count(ChannelID.New)\n\n    @property\n    def automations(self) -> Iterator[Automation]:\n        \"\"\"Yields automation clips in the project.\"\"\"\n        yield from (ch for ch in self if isinstance(ch, Automation))\n\n    # TODO Find out what this meant\n    fit_to_steps = EventProp[int](RackID._FitToSteps)\n\n    @property\n    def groups(self) -> Iterator[DisplayGroup]:\n        for ed in self.events.separate(DisplayGroupID.Name):\n            yield DisplayGroup(ed)\n\n    height = EventProp[int](RackID.WindowHeight)\n    \"\"\"Window height of the channel rack in the interface (in pixels).\"\"\"\n\n    @property\n    def instruments(self) -> Iterator[Instrument]:\n        \"\"\"Yields native and 3rd-party synth channels in the project.\"\"\"\n        yield from (ch for ch in self if isinstance(ch, Instrument))\n\n    @property\n    def layers(self) -> Iterator[Layer]:\n        \"\"\"Yields ``Layer`` channels in the project.\"\"\"\n        yield from (ch for ch in self if isinstance(ch, Layer))\n\n    @property\n    def samplers(self) -> Iterator[Sampler]:\n        \"\"\"Yields samplers and audio clips in the project.\"\"\"\n        yield from (ch for ch in self if isinstance(ch, Sampler))\n\n    swing = EventProp[int](RackID.Swing)\n    \"\"\"Global channel swing mix. Linear. Defaults to minimum value.\n\n    | Type | Value | Mix (wet) |\n    |------|-------|-----------|\n    | Min  | 0     | 0%        |\n    | Max  | 128   | 100%      |\n    \"\"\"\n"
  },
  {
    "path": "pyflp/controller.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types used by MIDI and remote (\"internal\") controllers.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nfrom typing import cast\n\nimport construct as c\n\nfrom pyflp._events import DATA, EventEnum, StructEventBase\nfrom pyflp._models import EventModel, ModelReprMixin\n\n__all__ = [\"RemoteController\"]\n\n\nclass MIDIControllerEvent(StructEventBase):\n    STRUCT = c.Struct(\"_u1\" / c.GreedyBytes)\n\n\nclass RemoteControllerEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.Optional(c.Bytes(2)),  # 2\n        \"_u2\" / c.Optional(c.Byte),  # 3\n        \"_u3\" / c.Optional(c.Byte),  # 4\n        \"parameter_data\" / c.Optional(c.Int16ul),  # 6\n        \"destination_data\" / c.Optional(c.Int16sl),  # 8\n        \"_u4\" / c.Optional(c.Bytes(8)),  # 16\n        \"_u5\" / c.Optional(c.Bytes(4)),  # 20\n    ).compile()\n\n\n@enum.unique\nclass ControllerID(EventEnum):\n    MIDI = (DATA + 18, MIDIControllerEvent)\n    Remote = (DATA + 19, RemoteControllerEvent)\n\n\nclass RemoteController(EventModel, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3S0i4Zf)\n\n    *New in FL Studio v3.3.0*.\n    \"\"\"\n\n    @property\n    def parameter(self) -> int | None:\n        \"\"\"The ID of the plugin parameter to which controller is linked to.\"\"\"\n        if (\n            value := cast(StructEventBase, self.events.first(ControllerID.Remote))[\"parameter_data\"]\n            is not None\n        ):\n            return value & 0x7FFF\n\n    @property\n    def controls_vst(self) -> bool | None:\n        \"\"\"Whether `parameter` is linked to a VST plugin.\n\n        None when linked to a plugin parameter on an insert slot.\n        \"\"\"\n        if (\n            value := cast(StructEventBase, self.events.first(ControllerID.Remote))[\"parameter_data\"]\n            is not None\n        ):\n            return (value & 0x8000) > 0\n"
  },
  {
    "path": "pyflp/exceptions.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the exceptions used by and shared across PyFLP.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\n\n__all__ = [\n    \"Error\",\n    \"NoModelsFound\",\n    \"EventIDOutOfRange\",\n    \"InvalidEventChunkSize\",\n    \"PropertyCannotBeSet\",\n    \"HeaderCorrupted\",\n    \"VersionNotDetected\",\n    \"ModelNotFound\",\n    \"DataCorrupted\",\n]\n\n\nclass Error(Exception):\n    \"\"\"Base class for PyFLP exceptions.\n\n    It is not guaranteed that exceptions raised from PyFLP always subclass Error.\n    This is done to prevent duplication of exceptions. All exceptions raised by\n    a function (in its body) explicitly are documented.\n\n    Some exceptions derive from standard Python exceptions to ease handling.\n    \"\"\"\n\n\nclass EventIDOutOfRange(Error, ValueError):\n    \"\"\"An event is created with an ID out of its allowed range.\"\"\"\n\n    def __init__(self, id: int, *expected: int) -> None:\n        super().__init__(f\"Expected ID in {expected!r}; got {id!r} instead\")\n\n\nclass InvalidEventChunkSize(Error, BufferError):\n    \"\"\"A fixed size event is created with a wrong amount of bytes.\"\"\"\n\n    def __init__(self, expected: int, got: int) -> None:\n        super().__init__(f\"Expected a bytes object of length {expected}; got {got}\")\n\n\nclass PropertyCannotBeSet(Error, AttributeError):\n    def __init__(self, *ids: enum.Enum | int) -> None:\n        super().__init__(f\"Event(s) {ids!r} was / were not found\")\n\n\nclass DataCorrupted(Error):\n    \"\"\"Base class for parsing exceptions.\"\"\"\n\n\nclass HeaderCorrupted(DataCorrupted, ValueError):\n    \"\"\"Header chunk contains an unexpected / invalid value.\n\n    Args:\n        desc: A string containing details about what is corrupted.\n    \"\"\"\n\n    def __init__(self, desc: str) -> None:\n        super().__init__(f\"Error parsing header: {desc}\")\n\n\nclass NoModelsFound(DataCorrupted, LookupError):\n    \"\"\"Model's `__iter__` method fails to generate any model.\"\"\"\n\n\nclass ModelNotFound(DataCorrupted, IndexError):\n    \"\"\"An invalid index is passed to model's `__getitem__` method.\"\"\"\n\n\nclass VersionNotDetected(DataCorrupted):\n    \"\"\"String decoder couldn't be decided due to absence of project version.\"\"\"\n"
  },
  {
    "path": "pyflp/mixer.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types used by the mixer, inserts and effect slots.\"\"\"\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport enum\nfrom collections import defaultdict\nfrom typing import Any, DefaultDict, Iterator, NamedTuple, cast\n\nimport construct as c\nimport construct_typed as ct\nfrom typing_extensions import NotRequired, TypedDict, Unpack\n\nfrom pyflp._adapters import StdEnum\nfrom pyflp._descriptors import EventProp, FlagProp, NamedPropMixin, ROProperty, RWProperty\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    TEXT,\n    WORD,\n    AnyEvent,\n    ColorEvent,\n    EventEnum,\n    EventTree,\n    I16Event,\n    I32Event,\n    ListEventBase,\n    StructEventBase,\n    U16Event,\n)\nfrom pyflp._models import EventModel, ModelBase, ModelCollection, ModelReprMixin, supports_slice\nfrom pyflp.exceptions import ModelNotFound, NoModelsFound, PropertyCannotBeSet\nfrom pyflp.plugin import (\n    FruityBalance,\n    FruityBloodOverdrive,\n    FruityCenter,\n    FruityFastDist,\n    FruityNotebook2,\n    FruitySend,\n    FruitySoftClipper,\n    FruityStereoEnhancer,\n    PluginID,\n    PluginProp,\n    Soundgoodizer,\n    VSTPlugin,\n)\nfrom pyflp.types import RGBA, FLVersion, T\n\n__all__ = [\"Insert\", \"InsertDock\", \"InsertEQ\", \"InsertEQBand\", \"Mixer\", \"Slot\"]\n\n\n@enum.unique\nclass _InsertFlags(enum.IntFlag):\n    None_ = 0\n    PolarityReversed = 1 << 0\n    SwapLeftRight = 1 << 1\n    EnableEffects = 1 << 2\n    Enabled = 1 << 3\n    DisableThreadedProcessing = 1 << 4\n    U5 = 1 << 5\n    DockMiddle = 1 << 6\n    DockRight = 1 << 7\n    U8 = 1 << 8\n    U9 = 1 << 9\n    SeparatorShown = 1 << 10\n    Locked = 1 << 11\n    Solo = 1 << 12\n    U13 = 1 << 13\n    U14 = 1 << 14\n    AudioTrack = 1 << 15  # Whether insert is linked to an audio track\n\n\n@enum.unique\nclass _MixerParamsID(ct.EnumBase):\n    SlotEnabled = 0\n    SlotMix = 1\n    RouteVolStart = 64  # 64 - 191 are send level events\n    Volume = 192\n    Pan = 193\n    StereoSeparation = 194\n    LowGain = 208\n    MidGain = 209\n    HighGain = 210\n    LowFreq = 216\n    MidFreq = 217\n    HighFreq = 218\n    LowQ = 224\n    MidQ = 225\n    HighQ = 226\n\n\nclass InsertFlagsEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.Optional(c.Bytes(4)),  # 4\n        \"flags\" / c.Optional(StdEnum[_InsertFlags](c.Int32ul)),  # 8\n        \"_u2\" / c.Optional(c.Bytes(4)),  # 12\n    ).compile()\n\n\nclass InsertRoutingEvent(ListEventBase):\n    STRUCT = c.GreedyRange(c.Flag)\n\n\n@dataclasses.dataclass\nclass _InsertItems:\n    slots: DefaultDict[int, dict[int, dict[str, Any]]] = dataclasses.field(\n        default_factory=lambda: defaultdict(dict)\n    )\n    own: dict[int, dict[str, Any]] = dataclasses.field(default_factory=dict)\n\n\nclass MixerParamsEvent(ListEventBase):\n    STRUCT = c.GreedyRange(\n        c.Struct(\n            \"_u4\" / c.Bytes(4),  # 4\n            \"id\" / StdEnum[_MixerParamsID](c.Byte),  # 5\n            \"_u1\" / c.Byte,  # 6\n            \"channel_data\" / c.Int16ul,  # 8\n            \"msg\" / c.Int32sl,  # 12\n        )\n    )\n\n    def __init__(self, id: Any, data: bytearray) -> None:\n        super().__init__(id, data)\n        self.items_: DefaultDict[int, _InsertItems] = defaultdict(_InsertItems)\n\n        for item in self.data:\n            insert_idx = (item[\"channel_data\"] >> 6) & 0x7F\n            slot_idx = item[\"channel_data\"] & 0x3F\n            insert = self.items_[insert_idx]\n            id = item[\"id\"]\n\n            if id in (_MixerParamsID.SlotEnabled, _MixerParamsID.SlotMix):\n                insert.slots[slot_idx][id] = item\n            else:\n                insert.own[id] = item\n\n\n@enum.unique\nclass InsertID(EventEnum):\n    Icon = (WORD + 31, I16Event)\n    Output = (DWORD + 19, I32Event)\n    Color = (DWORD + 21, ColorEvent)  #: 4.0+\n    Input = (DWORD + 26, I32Event)\n    Name = TEXT + 12  #: 3.5.4+\n    Routing = (DATA + 27, InsertRoutingEvent)\n    Flags = (DATA + 28, InsertFlagsEvent)\n\n\n@enum.unique\nclass MixerID(EventEnum):\n    APDC = 29\n    Params = (DATA + 17, MixerParamsEvent)\n\n\n@enum.unique\nclass SlotID(EventEnum):\n    Index = (WORD + 34, U16Event)\n\n\n# ? Maybe added in FL Studio v6.0.1\nclass InsertDock(enum.Enum):\n    \"\"\"![](https://bit.ly/3eLum9D)\n\n    See Also:\n        :attr:`Insert.dock`\n    \"\"\"  # noqa\n\n    Left = enum.auto()\n    Middle = enum.auto()\n    Right = enum.auto()\n\n\nclass _InsertEQBandKW(TypedDict, total=False):\n    gain: dict[str, Any]\n    freq: dict[str, Any]\n    reso: dict[str, Any]\n\n\nclass _InsertEQBandProp(NamedPropMixin, RWProperty[int]):\n    def __get__(self, ins: InsertEQBand, owner: Any = None) -> int | None:\n        if owner is None:\n            return NotImplemented\n        return ins._kw[self._prop][\"msg\"]\n\n    def __set__(self, ins: InsertEQBand, value: int) -> None:\n        ins._kw[self._prop][\"msg\"] = value\n\n\nclass InsertEQBand(ModelBase, ModelReprMixin):\n    def __init__(self, **kw: Unpack[_InsertEQBandKW]) -> None:\n        super().__init__(**kw)\n\n    @property\n    def size(self) -> int:\n        return 12 * len(self._kw)  # ! TODO\n\n    gain = _InsertEQBandProp()\n    \"\"\"\n    | Min   | Max  | Default |\n    |-------|------|---------|\n    | -1800 | 1800 | 0       |\n    \"\"\"\n\n    freq = _InsertEQBandProp()\n    \"\"\"Nonlinear. Default depends on band e.g. ``InsertEQ.low``.\n\n    | Type | Value | Representation |\n    |------|-------|----------------|\n    | Min  | 0     | 10 Hz          |\n    | Max  | 65536 | 16 kHz         |\n    \"\"\"\n\n    reso = _InsertEQBandProp()\n    \"\"\"\n    | Min | Max   | Default |\n    |-----|-------|---------|\n    | 0   | 65536 | 17500   |\n    \"\"\"\n\n\nclass _InsertEQPropArgs(NamedTuple):\n    freq: int\n    gain: int\n    reso: int\n\n\nclass _InsertEQProp(NamedPropMixin, ROProperty[InsertEQBand]):\n    def __init__(self, ids: _InsertEQPropArgs) -> None:\n        super().__init__()\n        self._ids = ids\n\n    def __get__(self, ins: InsertEQ, owner: Any = None) -> InsertEQBand:\n        if owner is None:\n            return NotImplemented\n\n        items: _InsertEQBandKW = {}\n        for id, param in cast(_InsertItems, ins._kw[\"params\"]).own.items():\n            if id == self._ids.freq:\n                items[\"freq\"] = param\n            elif id == self._ids.gain:\n                items[\"gain\"] = param\n            elif id == self._ids.reso:\n                items[\"reso\"] = param\n        return InsertEQBand(**items)\n\n\n# Stored in MixerID.Params event.\nclass InsertEQ(ModelBase, ModelReprMixin):\n    \"\"\"Post-effect :class:`Insert` EQ with 3 adjustable bands.\n\n    ![](https://bit.ly/3RUCQt6)\n\n    See Also:\n        :attr:`Insert.eq`\n    \"\"\"\n\n    def __init__(self, params: _InsertItems) -> None:\n        super().__init__(params=params)\n\n    @property\n    def size(self) -> int:\n        return 12 * self._kw[\"param\"]  # ! TODO\n\n    low = _InsertEQProp(\n        _InsertEQPropArgs(_MixerParamsID.LowFreq, _MixerParamsID.LowGain, _MixerParamsID.LowQ)\n    )\n    \"\"\"Low shelf band. Default frequency - 5777 (90 Hz).\"\"\"\n\n    mid = _InsertEQProp(\n        _InsertEQPropArgs(_MixerParamsID.MidFreq, _MixerParamsID.MidGain, _MixerParamsID.MidQ)\n    )\n    \"\"\"Middle band. Default frequency - 33145 (1500 Hz).\"\"\"\n\n    high = _InsertEQProp(\n        _InsertEQPropArgs(_MixerParamsID.HighFreq, _MixerParamsID.HighGain, _MixerParamsID.HighQ)\n    )\n    \"\"\"High shelf band. Default frequency - 55825 (8000 Hz).\"\"\"\n\n\nclass _MixerParamProp(RWProperty[T]):\n    def __init__(self, id: int) -> None:\n        self._id = id\n\n    def __get__(self, ins: Insert, owner: object = None) -> T | None:\n        if owner is None:\n            return NotImplemented\n\n        for id, item in cast(_InsertItems, ins._kw[\"params\"]).own.items():\n            if id == self._id:\n                return item[\"msg\"]\n\n    def __set__(self, ins: Insert, value: T) -> None:\n        for id, item in cast(_InsertItems, ins._kw[\"params\"]).own.items():\n            if id == self._id:\n                item[\"msg\"] = value\n                return\n        raise PropertyCannotBeSet(self._id)\n\n\nclass Slot(EventModel):\n    \"\"\"Represents an effect slot in an `Insert` / mixer channel.\n\n    ![](https://bit.ly/3RUDtTu)\n    \"\"\"\n\n    def __init__(self, events: EventTree, params: list[dict[str, Any]] | None = None) -> None:\n        super().__init__(events, params=params or [])\n\n    def __repr__(self) -> str:\n        return f\"Slot (name={self.name}, iid={self.index}, plugin={self.plugin!r})\"\n\n    color = EventProp[RGBA](PluginID.Color)\n    # TODO controllers = KWProp[List[RemoteController]]()\n    iid = EventProp[int](SlotID.Index)\n    \"\"\"A 0-based internal index.\"\"\"\n\n    internal_name = EventProp[str](PluginID.InternalName)\n    \"\"\"'Fruity Wrapper' for VST/AU plugins or factory name for native plugins.\"\"\"\n\n    enabled = _MixerParamProp[bool](_MixerParamsID.SlotEnabled)\n    \"\"\"![](https://bit.ly/3eN4Ile)\"\"\"\n\n    icon = EventProp[int](PluginID.Icon)\n    index = EventProp[int](SlotID.Index)\n    mix = _MixerParamProp[int](_MixerParamsID.SlotMix)\n    \"\"\"Dry/Wet mix. Defaults to maximum value.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -6400 | 100% left      |\n    | Max     | 6400  | 100% right     |\n    | Default | 0     | Centred        |\n    \"\"\"\n\n    name = EventProp[str](PluginID.Name)\n    plugin = PluginProp(\n        VSTPlugin,\n        FruityBalance,\n        FruityBloodOverdrive,\n        FruityCenter,\n        FruityFastDist,\n        FruityNotebook2,\n        FruitySend,\n        FruitySoftClipper,\n        FruityStereoEnhancer,\n        Soundgoodizer,\n    )\n    \"\"\"The effect loaded into the slot.\"\"\"\n\n\nclass _InsertKW(TypedDict):\n    iid: int\n    max_slots: int\n    params: NotRequired[_InsertItems]\n\n\n# TODO Need to make a `load()` method which will be able to parse preset files\n# (by looking at Project.format) and use `MixerParameterEvent.items` to get\n# remaining data. Normally, the `Mixer` passes this information to the Inserts\n# (and Inserts to the `Slot`s directly).\nclass Insert(EventModel, ModelCollection[Slot]):\n    \"\"\"Represents a mixer track to which channel from the rack are routed to.\n\n    ![](https://bit.ly/3LeGKuN)\n    \"\"\"\n\n    def __init__(self, events: EventTree, **kw: Unpack[_InsertKW]) -> None:\n        super().__init__(events, **kw)\n\n    # TODO Add number of used slots\n    def __repr__(self) -> str:\n        return f\"Insert(name={self.name!r}, iid={self.iid})\"\n\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: int | str) -> Slot:\n        \"\"\"Returns an effect slot of the specified index or name.\n\n        Args:\n            i: An index in the range of 0 to :attr:`Mixer.max_slots`\n               or the name of the :class:`Slot`.\n\n        Raises:\n            ModelNotFound: An effect :class:`Slot` with the specified index\n                or name isn't found.\n        \"\"\"\n        for idx, slot in enumerate(self):\n            if (isinstance(i, int) and idx == i) or i == slot.name:\n                return slot\n        raise ModelNotFound(i)\n\n    @property\n    def iid(self) -> int:\n        \"\"\"-1 for \"current\" insert, 0 for master and upto :attr:`Mixer.max_inserts`.\"\"\"\n        return self._kw[\"iid\"]\n\n    def __iter__(self) -> Iterator[Slot]:\n        \"\"\"Iterator over the effect empty and used slots.\"\"\"\n        for idx, ed in enumerate(self.events.divide(SlotID.Index, *SlotID, *PluginID)):\n            yield Slot(ed, params=self._kw[\"params\"].slots[idx])\n\n    def __len__(self) -> int:\n        try:\n            return self.events.count(SlotID.Index)\n        except KeyError:\n            return len(list(self))\n\n    bypassed = FlagProp(_InsertFlags.EnableEffects, InsertID.Flags, inverted=True)\n    \"\"\"Whether all slots are bypassed.\"\"\"\n\n    channels_swapped = FlagProp(_InsertFlags.SwapLeftRight, InsertID.Flags)\n    \"\"\"Whether the left and right channels are swapped.\"\"\"\n\n    color = EventProp[RGBA](InsertID.Color)\n    \"\"\"Defaults to #636C71 (granite gray) in FL Studio.\n\n    ![](https://bit.ly/3yVKXPc)\n\n    Values below 20 for any color component (R, G, B) are ignored by FL.\n\n    *New in FL Studio v4.0*.\n    \"\"\"\n\n    @property\n    def dock(self) -> InsertDock | None:\n        \"\"\"The position (left, middle or right) where insert is docked in mixer.\n\n        :menuselection:`Insert --> Layout --> Dock to`\n\n        ![](https://bit.ly/3eLum9D)\n        \"\"\"\n        try:\n            event = cast(InsertFlagsEvent, self.events.first(InsertID.Flags))\n        except KeyError:\n            return None\n\n        flags = _InsertFlags(event[\"flags\"])\n        if _InsertFlags.DockMiddle in flags:\n            return InsertDock.Middle\n        if _InsertFlags.DockRight in flags:\n            return InsertDock.Right\n        return InsertDock.Left\n\n    enabled = FlagProp(_InsertFlags.Enabled, InsertID.Flags)\n    \"\"\"Whether an insert in the mixer is enabled or disabled.\n\n    ![](https://bit.ly/3BoRBOj)\n    \"\"\"\n\n    @property\n    def eq(self) -> InsertEQ:\n        \"\"\"3-band post EQ.\n\n        ![](https://bit.ly/3RUCQt6)\n        \"\"\"\n        return InsertEQ(self._kw[\"params\"])\n\n    icon = EventProp[int](InsertID.Icon)\n    \"\"\"Internal ID of the icon shown beside ``name``.\n\n    ![](https://bit.ly/3Slr6jc)\n    \"\"\"\n\n    input = EventProp[int](InsertID.Input)\n    \"\"\"![](https://bit.ly/3RO0ckC)\"\"\"\n\n    is_solo = FlagProp(_InsertFlags.Solo, InsertID.Flags)\n    \"\"\"Whether the insert is solo'd.\"\"\"\n\n    locked = FlagProp(_InsertFlags.Locked, InsertID.Flags)\n    \"\"\"Whether an insert in the mixer is in locked state.\n\n    ![](https://bit.ly/3SdPbc2)\n    \"\"\"\n\n    name = EventProp[str](InsertID.Name)\n    \"\"\"*New in FL Studio v3.5.4*.\"\"\"\n\n    output = EventProp[int](InsertID.Output)\n    \"\"\"![](https://bit.ly/3LjWjBD)\"\"\"\n\n    pan = _MixerParamProp[int](_MixerParamsID.Pan)\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -6400 | 100% left      |\n    | Max     | 6400  | 100% right     |\n    | Default | 0     | Centred        |\n\n    ![](https://bit.ly/3DsZRj4)\n    \"\"\"\n\n    polarity_reversed = FlagProp(_InsertFlags.PolarityReversed, InsertID.Flags)\n    \"\"\"Whether phase / polarity is reversed / inverted.\"\"\"\n\n    @property\n    def routes(self) -> Iterator[int]:\n        \"\"\"Send volumes to routed inserts.\n\n        *New in FL Studio v4.0*.\n        \"\"\"\n        items = iter(cast(InsertRoutingEvent, self.events.first(InsertID.Routing)))\n        for id, item in cast(_InsertItems, self._kw[\"params\"]).own.items():\n            if id >= _MixerParamsID.RouteVolStart:\n                try:\n                    cond = next(items)\n                except StopIteration:\n                    continue\n                else:\n                    if cond:\n                        yield item[\"msg\"]\n\n    separator_shown = FlagProp(_InsertFlags.SeparatorShown, InsertID.Flags)\n    \"\"\"Whether separator is shown before the insert.\n\n    :menuselection:`Insert --> Group --> Separator`\n    \"\"\"\n\n    stereo_separation = _MixerParamProp[int](_MixerParamsID.StereoSeparation)\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 64    | 100% merged    |\n    | Max     | -64   | 100% separated |\n    | Default | 0     | No effect      |\n    \"\"\"\n\n    volume = _MixerParamProp[int](_MixerParamsID.Volume)\n    \"\"\"Post volume fader. Logarithmic.\n\n    | Type    | Value | Representation      |\n    |---------|-------|---------------------|\n    | Min     | 0     | 0% / -INFdB / 0.00  |\n    | Max     | 16000 | 125% / 5.6dB / 1.90 |\n    | Default | 12800 | 100% / 0.0dB / 1.00 |\n    \"\"\"\n\n\nclass _MixerKW(TypedDict):\n    version: FLVersion\n\n\n# TODO FL Studio version in which slots were increased to 10\n# TODO A move() method to change the placement of Inserts; it's difficult!\nclass Mixer(EventModel, ModelCollection[Insert]):\n    \"\"\"Represents the mixer which contains :class:`Insert` instances.\n\n    ![](https://bit.ly/3eOsblF)\n    \"\"\"\n\n    _MAX_INSERTS = {\n        (1, 6, 5): 5,\n        (2, 0, 1): 8,\n        (3, 0, 0): 18,\n        (3, 3, 0): 20,\n        (4, 0, 0): 64,\n        (9, 0, 0): 105,\n        (12, 9, 0): 127,\n    }\n\n    _MAX_SLOTS = {(1, 6, 5): 4, (3, 0, 0): 8}\n\n    def __init__(self, events: EventTree, **kw: Unpack[_MixerKW]) -> None:\n        super().__init__(events, **kw)\n\n    # Inserts don't store their index internally.\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: int | str | slice) -> Insert:\n        \"\"\"Returns an insert with the specified index or name.\n\n        Args:\n            i: An index between 0 to :attr:`Mixer.max_inserts` resembling the\n                one shown by FL Studio or the name of the insert. Use 0 for\n                master and -1 for \"current\" insert.\n\n        Raises:\n            ModelNotFound: An :class:`Insert` with the specifcied name or index\n                isn't found.\n        \"\"\"\n        for idx, insert in enumerate(self):\n            if (isinstance(i, int) and idx == i + 1) or i == insert.name:\n                return insert\n        raise ModelNotFound(i)\n\n    def __iter__(self) -> Iterator[Insert]:\n        def select(e: AnyEvent) -> bool | None:\n            if e.id == InsertID.Output:\n                return False\n\n            if e.id in (*InsertID, *PluginID, *SlotID):\n                return True\n\n        params: dict[int, _InsertItems] = {}\n        if MixerID.Params in self.events.ids:\n            params = cast(MixerParamsEvent, self.events.first(MixerID.Params)).items_\n\n        for i, ed in enumerate(self.events.subtrees(select, self.max_inserts)):\n            if i in params:\n                yield Insert(ed, iid=i - 1, max_slots=self.max_slots, params=params[i])\n            else:\n                yield Insert(ed, iid=i - 1, max_slots=self.max_slots)\n\n    def __len__(self) -> int:\n        \"\"\"Returns the number of inserts present in the project.\n\n        Raises:\n            NoModelsFound: No inserts could be found.\n        \"\"\"\n        if InsertID.Flags not in self.events.ids:\n            raise NoModelsFound\n        return self.events.count(InsertID.Flags)\n\n    def __str__(self) -> str:\n        return f\"Mixer: {len(self)} inserts\"\n\n    apdc = EventProp[bool](MixerID.APDC)\n    \"\"\"Whether automatic plugin delay compensation is enabled for the inserts.\"\"\"\n\n    @property\n    def max_inserts(self) -> int:\n        \"\"\"Estimated max number of inserts including sends, master and current.\n\n        Maximum number of slots w.r.t. FL Studio:\n\n        * 1.6.5: 4 inserts + master, 5 in total\n        * 2.0.1: 8\n        * 3.0.0: 16 inserts, 2 sends.\n        * 3.3.0: +2 sends.\n        * 4.0.0: 64\n        * 9.0.0: 99 inserts, 105 in total.\n        * 12.9.0: 125 + master + current.\n        \"\"\"\n        version = dataclasses.astuple(self._kw[\"version\"])\n        for k, v in self._MAX_INSERTS.items():\n            if version <= k:\n                return v\n        return 127\n\n    @property\n    def max_slots(self) -> int:\n        \"\"\"Estimated max number of effect slots per insert.\n\n        Maximum number of slots w.r.t. FL Studio:\n\n        * 1.6.5: 4\n        * 3.3.0: 8\n        \"\"\"\n        version = dataclasses.astuple(self._kw[\"version\"])\n        for k, v in self._MAX_SLOTS.items():\n            if version <= k:\n                return v\n        return 10\n"
  },
  {
    "path": "pyflp/pattern.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types used by patterns, MIDI notes and their automation data.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nfrom collections import defaultdict\nfrom typing import DefaultDict, Iterator, cast\n\nimport construct as c\n\nfrom pyflp._adapters import StdEnum\nfrom pyflp._descriptors import EventProp, FlagProp, StructProp\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    TEXT,\n    WORD,\n    BoolEvent,\n    ColorEvent,\n    EventEnum,\n    EventTree,\n    I32Event,\n    IndexedEvent,\n    ListEventBase,\n    U16Event,\n    U32Event,\n)\nfrom pyflp._models import EventModel, ItemModel, ModelCollection, ModelReprMixin, supports_slice\nfrom pyflp.exceptions import ModelNotFound, NoModelsFound\nfrom pyflp.timemarker import TimeMarker, TimeMarkerID\nfrom pyflp.types import RGBA\n\n__all__ = [\"Note\", \"Controller\", \"Pattern\", \"Patterns\"]\n\n\nclass ControllerEvent(ListEventBase):\n    STRUCT = c.GreedyRange(\n        c.Struct(\n            \"position\" / c.Int32ul,  # 4, can be delta as well!\n            \"_u1\" / c.Byte,  # 5\n            \"_u2\" / c.Byte,  # 6\n            \"channel\" / c.Int8ul,  # 7\n            \"_flags\" / c.Int8ul,  # 8\n            \"value\" / c.Float32l,  # 12\n        )\n    )\n\n\n@enum.unique\nclass _NoteFlags(enum.IntFlag):\n    Slide = 1 << 3\n\n\nclass NotesEvent(ListEventBase):\n    STRUCT = c.GreedyRange(\n        c.Struct(\n            \"position\" / c.Int32ul,  # 4\n            \"flags\" / StdEnum[_NoteFlags](c.Int16ul),  # 6\n            \"rack_channel\" / c.Int16ul,  # 8\n            \"length\" / c.Int32ul,  # 12\n            \"key\" / c.Int16ul,  # 14\n            \"group\" / c.Int16ul,  # 16\n            \"fine_pitch\" / c.Int8ul,  # 17\n            \"_u1\" / c.Byte,  # 18\n            \"release\" / c.Int8ul,  # 19\n            \"midi_channel\" / c.Int8ul,  # 20\n            \"pan\" / c.Int8ul,  # 21\n            \"velocity\" / c.Int8ul,  # 22\n            \"mod_x\" / c.Int8ul,  # 23\n            \"mod_y\" / c.Int8ul,  # 24\n        )\n    )\n\n\nclass PatternsID(EventEnum):\n    PlayTruncatedNotes = (30, BoolEvent)\n    CurrentlySelected = (WORD + 3, U16Event)\n\n\n# ChannelIID, _161, _162, Looped, Length occur when pattern is looped.\n# ChannelIID and _161 occur for every channel in order.\nclass PatternID(EventEnum):\n    Looped = (26, BoolEvent)\n    New = (WORD + 1, U16Event)  # Marks the beginning of a new pattern, twice.\n    Color = (DWORD + 22, ColorEvent)\n    Name = TEXT + 1\n    # _157 = DWORD + 29  #: 12.5+\n    # _158 = DWORD + 30  # default: -1\n    ChannelIID = (DWORD + 32, U32Event)  # TODO (FL v20.1b1+)\n    _161 = (DWORD + 33, I32Event)  # TODO -3 if channel is looped else 0 (FL v20.1b1+)\n    _162 = (DWORD + 34, U32Event)  # TODO Appears when pattern is looped, default: 2\n    Length = (DWORD + 36, U32Event)\n    Controllers = (DATA + 15, ControllerEvent)\n    Notes = (DATA + 16, NotesEvent)\n\n\nclass Note(ItemModel[NotesEvent]):\n    _NOTE_NAMES = (\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\")\n\n    def __repr__(self) -> str:\n        return \"Note(key={}, position={}, length={}, channel={})\".format(\n            self.key, self.position, self.length, self.rack_channel\n        )\n\n    def __str__(self) -> str:\n        return f\"{self.key} note @ {self.position} of {self.length}\"\n\n    fine_pitch = StructProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | -1200 cents    |\n    | Max     | 240   | +1200 cents    |\n    | Default | 120   | No fine tuning |\n\n    *New in FL Studio v3.3.0*.\n    \"\"\"\n\n    group = StructProp[int]()\n    \"\"\"A number shared by notes in the same group or ``0`` if ungrouped.\n\n    ![](https://bit.ly/3TgjFva)\n    \"\"\"\n\n    @property\n    def key(self) -> str:\n        \"\"\"Note name with octave, for e.g. 'C5' or 'A#3' ranging from C0 to B10.\n\n        Only sharp key names (C#, D#, etc.) are used, flats aren't.\n\n        Raises:\n            ValueError: A value not in between 0-131 is tried to be set.\n            ValueError: Invalid note name (not in the format {note-name}{octave}).\n        \"\"\"\n        return self._NOTE_NAMES[self[\"key\"] % 12] + str(self[\"key\"] // 12)  # pyright: ignore\n\n    @key.setter\n    def key(self, value: int | str) -> None:\n        if isinstance(value, int):\n            if value not in range(132):\n                raise ValueError(\"Expected a value between 0-131.\")\n            self[\"key\"] = value\n        else:\n            for i, name in enumerate(self._NOTE_NAMES):\n                if value.startswith(name):\n                    octave = int(value.replace(name, \"\", 1))\n                    self[\"key\"] = octave * 12 + i\n            raise ValueError(f\"Invalid key name: {value}\")\n\n    length = StructProp[int]()\n    \"\"\"Returns 0 for notes punched in through step sequencer.\"\"\"\n\n    midi_channel = StructProp[int]()\n    \"\"\"Used for a variety of purposes.\n\n    For note colors, min: 0, max: 15.\n    +128 for MIDI dragged into the piano roll.\n\n    *Changed in FL Studio v6.0.1*: Used for both, MIDI channels and colors.\n    \"\"\"\n\n    mod_x = StructProp[int]()\n    \"\"\"Plugin configurable parameter.\n\n    | Min | Max | Default |\n    | --- | --- | ------- |\n    | 0   | 255 | 128     |\n    \"\"\"\n\n    mod_y = StructProp[int]()\n    \"\"\"Plugin configurable parameter.\n\n    | Min | Max | Default |\n    | --- | --- | ------- |\n    | 0   | 255 | 128     |\n    \"\"\"\n\n    pan = StructProp[int]()\n    \"\"\"\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 100% left      |\n    | Max     | 128   | 100% right     |\n    | Default | 64    | Centered       |\n    \"\"\"\n\n    position = StructProp[int]()\n    rack_channel = StructProp[int]()\n    \"\"\"Containing channel's :attr:`Channel.IID`.\"\"\"\n\n    release = StructProp[int]()\n    \"\"\"\n    | Min | Max | Default |\n    | --- | --- | ------- |\n    | 0   | 128 | 64      |\n    \"\"\"\n\n    slide = FlagProp(_NoteFlags.Slide)\n    \"\"\"Whether note is a sliding note.\"\"\"\n\n    velocity = StructProp[int]()\n    \"\"\"\n    | Min | Max | Default |\n    | --- | --- | ------- |\n    | 0   | 128 | 100     |\n    \"\"\"\n\n\nclass Controller(ItemModel[ControllerEvent], ModelReprMixin):\n    def __str__(self) -> str:\n        return f\"Controller @ {self.position} of channel #{self.channel}\"\n\n    channel = StructProp[int]()\n    \"\"\"Corresponds to the containing channel's :attr:`Channel.iid`.\"\"\"\n\n    position = StructProp[int]()\n    value = StructProp[float]()\n\n\nclass Pattern(EventModel):\n    \"\"\"Represents a pattern which can contain notes, controllers and time markers.\"\"\"\n\n    def __repr__(self) -> str:\n        try:\n            num_notes = len(self.events.first(PatternID.Notes))  # type: ignore\n        except KeyError:\n            num_notes = 0\n\n        try:\n            num_ctrls = len(self.events.first(PatternID.Controllers))  # type: ignore\n        except KeyError:\n            num_ctrls = 0\n\n        return (\n            f\"Pattern(iid={self.iid}, name={self.name!r},\"\n            f\"{num_notes} notes, {num_ctrls} controllers)\"\n        )\n\n    color = EventProp[RGBA](PatternID.Color)\n    \"\"\"Returns a colour if one is set while saving the project file, else ``None``.\n\n    ![](https://bit.ly/3eNeSSW)\n\n    Defaults to #485156 in FL Studio.\n    \"\"\"\n\n    @property\n    def controllers(self) -> Iterator[Controller]:\n        \"\"\"Parameter automations associated with this pattern (if any).\"\"\"\n        if PatternID.Controllers in self.events.ids:\n            event = cast(ControllerEvent, self.events.first(PatternID.Controllers))\n            yield from (Controller(item, i, event) for i, item in enumerate(event))\n\n    @property\n    def iid(self) -> int:\n        \"\"\"Internal index of the pattern starting from 1.\n\n        Caution:\n            Changing this will not solve any collisions thay may occur due to\n            2 patterns that might end up having the same index.\n        \"\"\"\n        return self.events.first(PatternID.New).value\n\n    @iid.setter\n    def iid(self, value: int) -> None:\n        for event in self.events.get(PatternID.New):\n            event.value = value\n\n    length = EventProp[int](PatternID.Length)\n    \"\"\"The number of steps multiplied by the :attr:`pyflp.project.Project.ppq`.\n\n    Returns `None` if pattern is in Auto mode (i.e. :attr:`looped` is `False`).\n    \"\"\"\n\n    looped = EventProp[bool](PatternID.Looped, default=False)\n    \"\"\"Whether a pattern is in live loop mode.\n\n    *New in FL Studio v2.5.0*.\n    \"\"\"\n\n    name = EventProp[str](PatternID.Name)\n    \"\"\"User given name of the pattern; None if not set.\"\"\"\n\n    @property\n    def notes(self) -> Iterator[Note]:\n        \"\"\"MIDI notes contained inside the pattern.\n\n        Note:\n            FL Studio uses its own custom format to represent notes internally.\n            However by using the :class:`Note` properties with a MIDI parsing\n            library for example, you can export them to MIDI.\n        \"\"\"\n        if PatternID.Notes in self.events.ids:\n            event = cast(NotesEvent, self.events.first(PatternID.Notes))\n            yield from (Note(item, i, event) for i, item in enumerate(event))\n\n    @property\n    def timemarkers(self) -> Iterator[TimeMarker]:\n        \"\"\"Yields timemarkers inside this pattern.\"\"\"\n        yield from (TimeMarker(et) for et in self.events.group(*TimeMarkerID))\n\n\nclass Patterns(EventModel, ModelCollection[Pattern]):\n    def __str__(self) -> str:\n        iids = [pattern.iid for pattern in self]\n        return f\"{len(iids)} Patterns {iids!r}\"\n\n    @supports_slice  # type: ignore\n    def __getitem__(self, i: int | str | slice) -> Pattern:\n        \"\"\"Returns the pattern with the specified index or :attr:`Pattern.name`.\n\n        Args:\n            i: A zero-based index, its name or a slice of indexes.\n\n        Raises:\n            ModelNotFound: A :class:`Pattern` with the specified name or index\n                isn't found.\n        \"\"\"\n        for idx, pattern in enumerate(self):\n            if (isinstance(i, int) and idx == i) or i == pattern.name:\n                return pattern\n        raise ModelNotFound(i)\n\n    # Doesn't use EventTree delegates since PatternID.New occurs twice.\n    # Once for note and controller events and again for the rest of them.\n    def __iter__(self) -> Iterator[Pattern]:\n        \"\"\"An iterator over the patterns found in the project.\"\"\"\n        cur_pat_id = 0\n        tmp_dict: DefaultDict[int, list[IndexedEvent]] = defaultdict(list)\n\n        for ie in self.events.lst:\n            if ie.e.id == PatternID.New:\n                cur_pat_id = ie.e.value\n\n            if ie.e.id in (*PatternID, *TimeMarkerID):\n                tmp_dict[cur_pat_id].append(ie)\n\n        for events in tmp_dict.values():\n            et = EventTree(self.events, events)\n            self.events.children.append(et)\n            yield Pattern(et)\n\n    def __len__(self) -> int:\n        \"\"\"Returns the number of patterns found in the project.\n\n        Raises:\n            NoModelsFound: No patterns were found.\n        \"\"\"\n        if PatternID.New not in self.events.ids:\n            raise NoModelsFound\n        return len({e.value for e in self.events.get(PatternID.New)})\n\n    play_cut_notes = EventProp[bool](PatternsID.PlayTruncatedNotes)\n    \"\"\"Whether truncated notes of patterns placed in the playlist should be played.\n\n    Located at :menuselection:`Options -> &Project general settings --> Advanced`\n    under the name :guilabel:`Play truncated notes in clips`.\n\n    *Changed in FL Studio v12.3 beta 3*: Enabled by default.\n    \"\"\"\n\n    @property\n    def current(self) -> Pattern | None:\n        \"\"\"Returns the currently selected pattern.\"\"\"\n        if PatternsID.CurrentlySelected in self.events.ids:\n            index = self.events.first(PatternsID.CurrentlySelected).value\n            for pattern in self:\n                if pattern.iid == index:\n                    return pattern\n"
  },
  {
    "path": "pyflp/plugin.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types used by native and VST plugins to store their preset data.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nimport warnings\nfrom typing import Any, ClassVar, Dict, Generic, Literal, Protocol, TypeVar, cast, runtime_checkable\n\nimport construct as c\nimport construct_typed as ct\n\nfrom pyflp._adapters import FourByteBool, StdEnum\nfrom pyflp._descriptors import FlagProp, NamedPropMixin, RWProperty, StructProp\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    TEXT,\n    AnyEvent,\n    ColorEvent,\n    EventEnum,\n    EventTree,\n    StructEventBase,\n    U32Event,\n    UnknownDataEvent,\n)\nfrom pyflp._models import EventModel, ModelReprMixin\nfrom pyflp.types import T\n\n__all__ = [\n    \"BooBass\",\n    \"FruitKick\",\n    \"FruityBalance\",\n    \"FruityBloodOverdrive\",\n    \"FruityFastDist\",\n    \"FruityNotebook2\",\n    \"FruitySend\",\n    \"FruitySoftClipper\",\n    \"FruityStereoEnhancer\",\n    \"Plucked\",\n    \"PluginID\",\n    \"PluginIOInfo\",\n    \"Soundgoodizer\",\n    \"VSTPlugin\",\n]\n\n\n@enum.unique\nclass _WrapperFlags(enum.IntFlag):\n    Visible = 1 << 0\n    _Disabled = 1 << 1\n    Detached = 1 << 2\n    # _U3 = 1 << 3\n    Generator = 1 << 4\n    SmartDisable = 1 << 5\n    ThreadedProcessing = 1 << 6\n    DemoMode = 1 << 7  # saved with a demo version\n    HideSettings = 1 << 8\n    Minimized = 1 << 9\n    _DirectX = 1 << 16  # indicates the plugin is a DirectX plugin\n    _EditorSize = 2 << 16\n\n\nclass BooBassEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.If(c.this._.len == 16, c.Bytes(4)),\n        \"bass\" / c.Int32ul,\n        \"mid\" / c.Int32ul,\n        \"high\" / c.Int32ul,\n    ).compile()\n\n\nclass FruitKickEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.Bytes(4),\n        \"max_freq\" / c.Int32sl,\n        \"min_freq\" / c.Int32sl,\n        \"freq_decay\" / c.Int32ul,\n        \"amp_decay\" / c.Int32ul,\n        \"click\" / c.Int32ul,\n        \"distortion\" / c.Int32ul,\n        \"_u2\" / c.Bytes(4),\n    ).compile()\n\n\nclass FruityBalanceEvent(StructEventBase):\n    STRUCT = c.Struct(\"pan\" / c.Int32ul, \"volume\" / c.Int32ul).compile()\n\n\nclass FruityBloodOverdriveEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"plugin_marker\" / c.If(c.this._.len == 36, c.Bytes(4)),  # redesigned native plugin marker\n        \"pre_band\" / c.Int32ul,\n        \"color\" / c.Int32ul,\n        \"pre_amp\" / c.Int32ul,\n        \"x100\" / FourByteBool,\n        \"post_filter\" / c.Int32ul,\n        \"post_gain\" / c.Int32ul,\n        \"_u1\" / c.Bytes(4),\n        \"_u2\" / c.Bytes(4),\n    ).compile()\n\n\nclass FruityCenterEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.If(c.this._.len == 8, c.Bytes(4)), \"enabled\" / FourByteBool\n    ).compile()\n\n\nclass FruityFastDistEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"pre\" / c.Int32ul,\n        \"threshold\" / c.Int32ul,\n        \"kind\" / c.Enum(c.Int32ul, A=0, B=1),\n        \"mix\" / c.Int32ul,\n        \"post\" / c.Int32ul,\n    ).compile()\n\n\nclass FruityNotebook2Event(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.Bytes(4),\n        \"active_page\" / c.Int32ul,\n        \"pages\"\n        / c.GreedyRange(\n            c.Struct(\n                \"index\" / c.Int32sl,\n                c.StopIf(lambda ctx: ctx[\"index\"] == -1),\n                \"length\" / c.VarInt,\n                \"value\" / c.PaddedString(lambda ctx: ctx[\"length\"] * 2, \"utf-16-le\"),\n            ),\n        ),\n        \"editable\" / c.Flag,\n    )\n\n\nclass FruitySendEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"pan\" / c.Int32sl,\n        \"dry\" / c.Int32ul,\n        \"volume\" / c.Int32ul,\n        \"send_to\" / c.Int32sl,\n    ).compile()\n\n\nclass FruitySoftClipperEvent(StructEventBase):\n    STRUCT = c.Struct(\"threshold\" / c.Int32ul, \"post\" / c.Int32ul).compile()\n\n\nclass FruityStereoEnhancerEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"pan\" / c.Int32sl,\n        \"volume\" / c.Int32ul,\n        \"stereo_separation\" / c.Int32ul,\n        \"phase_offset\" / c.Int32ul,\n        \"effect_position\" / c.Enum(c.Int32ul, pre=0, post=1),\n        \"phase_inversion\" / c.Enum(c.Int32ul, none=0, left=1, right=2),\n    ).compile()\n\n\nclass PluckedEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"decay\" / c.Int32ul,\n        \"color\" / c.Int32ul,\n        \"normalize\" / FourByteBool,\n        \"gate\" / FourByteBool,\n        \"widen\" / FourByteBool,\n    ).compile()\n\n\nclass SoundgoodizerEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.If(c.this._.len == 12, c.Bytes(4)),\n        \"mode\" / c.Enum(c.Int32ul, A=0, B=1, C=2, D=3),\n        \"amount\" / c.Int32ul,\n    ).compile()\n\n\nNativePluginEvent = UnknownDataEvent\n\"\"\"Placeholder event type for unimplemented native :attr:`PluginID.Data` events.\"\"\"\n\n\nclass WrapperPage(ct.EnumBase):\n    Editor = 0\n    \"\"\":guilabel:`Plugin editor`.\"\"\"\n\n    Settings = 1\n    \"\"\":guilabel:`VST wrapper settings`.\"\"\"\n\n    Sample = 3\n    \"\"\":guilabel:`Sample settings`.\"\"\"\n\n    Envelope = 4\n    \"\"\":guilabel:`Envelope / instrument settings`.\"\"\"\n\n    Miscellaneous = 5\n    \"\"\":guilabel:`Miscallenous functions`.\"\"\"\n\n\nclass WrapperEvent(StructEventBase):\n    STRUCT = c.Struct(\n        \"_u1\" / c.Optional(c.Bytes(16)),  # 16\n        \"flags\" / c.Optional(StdEnum[_WrapperFlags](c.Int16ul)),  # 18\n        \"_u2\" / c.Optional(c.Bytes(2)),  # 20\n        \"page\" / c.Optional(StdEnum[WrapperPage](c.Int8ul)),  # 21\n        \"_u3\" / c.Optional(c.Bytes(23)),  # 44\n        \"width\" / c.Optional(c.Int32ul),  # 48\n        \"height\" / c.Optional(c.Int32ul),  # 52\n        \"_extra\" / c.GreedyBytes,  # None as of 20.9.2\n    ).compile()\n\n\n@enum.unique\nclass _VSTPluginEventID(ct.EnumBase):\n    MIDI = 1\n    Flags = 2\n    IO = 30\n    Inputs = 31\n    Outputs = 32\n    PluginInfo = 50\n    FourCC = 51  # Not present for Waveshells & VST3\n    GUID = 52\n    State = 53\n    Name = 54\n    PluginPath = 55\n    Vendor = 56\n    _57 = 57  # TODO, not present for Waveshells\n\n\nclass _VSTFlags(enum.IntFlag):\n    SendPBRange = 1 << 0\n    FixedSizeBuffers = 1 << 1\n    NotifyRender = 1 << 2\n    ProcessInactive = 1 << 3\n    DontSendRelVelo = 1 << 5\n    DontNotifyChanges = 1 << 6\n    SendLoopPos = 1 << 11\n    AllowThreaded = 1 << 12\n    KeepFocus = 1 << 15\n    DontKeepCPUState = 1 << 16\n    SendModX = 1 << 17\n    LoadBridged = 1 << 18\n    ExternalWindow = 1 << 21\n    UpdateWhenHidden = 1 << 23\n    DontResetOnTransport = 1 << 25\n    DPIAwareBridged = 1 << 26\n    AcceptFileDrop = 1 << 28\n    AllowSmartDisable = 1 << 29\n    ScaleEditor = 1 << 30\n    DontUseTimeOffset = 1 << 31\n\n\nclass _VSTFlags2(enum.IntFlag):\n    ProcessMaxSize = 1 << 0\n    UseMaxFromHost = 1 << 1\n\n\nclass VSTPluginEvent(StructEventBase):\n    _MIDIStruct = c.Struct(\n        \"input\" / c.Optional(c.Int32sl),  # 4\n        \"output\" / c.Optional(c.Int32sl),  # 8\n        \"pb_range\" / c.Optional(c.Int32ul),  # 12\n        \"_extra\" / c.GreedyBytes,  # upto 20\n    ).compile()\n\n    _FlagsStruct = c.Struct(\n        \"_u1\" / c.Optional(c.Bytes(9)),  # 9\n        \"flags\" / c.Optional(StdEnum[_VSTFlags](c.Int32ul)),  # 13\n        \"flags2\" / c.Optional(StdEnum[_VSTFlags2](c.Int32ul)),  # 17\n        \"_u2\" / c.Optional(c.Bytes(5)),  # 22\n        \"fast_idle\" / c.Optional(c.Flag),  # 23\n        \"_extra\" / c.GreedyBytes,\n    ).compile()\n\n    STRUCT = c.Struct(\n        \"type\" / c.Int32ul,  # * 8 or 10 for VSTs, but I am not forcing it\n        \"events\"\n        / c.GreedyRange(\n            c.Struct(\n                \"id\" / StdEnum[_VSTPluginEventID](c.Int32ul),\n                # ! Using a c.Select or c.IfThenElse doesn't work here\n                # Check https://github.com/construct/construct/issues/993\n                \"data\"  # pyright: ignore\n                / c.Prefixed(\n                    c.Int64ul,\n                    c.Switch(\n                        c.this[\"id\"],\n                        {\n                            _VSTPluginEventID.MIDI: _MIDIStruct,\n                            _VSTPluginEventID.Flags: _FlagsStruct,\n                            _VSTPluginEventID.FourCC: c.GreedyString(\"utf8\"),\n                            _VSTPluginEventID.Name: c.GreedyString(\"utf8\"),  # See #150\n                            _VSTPluginEventID.Vendor: c.GreedyString(\"utf8\"),\n                            _VSTPluginEventID.PluginPath: c.GreedyString(\"utf8\"),\n                        },\n                        default=c.GreedyBytes,\n                    ),\n                ),\n            ),\n        ),\n    ).compile()\n\n    def __init__(self, id: Any, data: bytearray) -> None:\n        if data[0] not in (8, 10):\n            warnings.warn(\n                f\"VSTPluginEvent: Unknown marker {data[0]} detected. \"\n                \"Open an issue at https://github.com/demberto/PyFLP/issues \"\n                \"if you are seeing this!\",\n                RuntimeWarning,\n                stacklevel=3,\n            )\n        super().__init__(id, data)\n\n\n@enum.unique\nclass PluginID(EventEnum):\n    \"\"\"IDs shared by :class:`pyflp.channel.Channel` and :class:`pyflp.mixer.Slot`.\"\"\"\n\n    Color = (DWORD, ColorEvent)\n    Icon = (DWORD + 27, U32Event)\n    InternalName = TEXT + 9\n    Name = TEXT + 11  #: 3.3.0+ for :class:`pyflp.mixer.Slot`.\n    # TODO Additional possible fields: Plugin wrapper data, window\n    # positions of plugin, currently selected plugin wrapper page, etc.\n    Wrapper = (DATA + 4, WrapperEvent)\n    # * The type of this event is decided during event collection\n    Data = DATA + 5  #: 1.6.5+\n\n\n@runtime_checkable\nclass _IPlugin(Protocol):\n    INTERNAL_NAME: ClassVar[str]\n    \"\"\"The name used internally by FL to decide the type of plugin data.\"\"\"\n\n\n_PE_co = TypeVar(\"_PE_co\", bound=AnyEvent, covariant=True)\n\n\nclass _WrapperProp(FlagProp):\n    def __init__(self, flag: _WrapperFlags, **kw: Any) -> None:\n        super().__init__(flag, PluginID.Wrapper, **kw)\n\n\nclass _PluginBase(EventModel, Generic[_PE_co]):\n    def __init__(self, events: EventTree, **kw: Any) -> None:\n        super().__init__(events, **kw)\n\n    compact = _WrapperProp(_WrapperFlags.HideSettings)\n    \"\"\"Whether plugin page toolbar (:guilabel:`Detailed settings`) is hidden.\n\n    ![](https://bit.ly/3qzOMoO)\n    \"\"\"\n\n    demo_mode = _WrapperProp(_WrapperFlags.DemoMode)  # TODO Verify if this works\n    \"\"\"Whether the plugin state was saved in a demo / trial version.\"\"\"\n\n    detached = _WrapperProp(_WrapperFlags.Detached)\n    \"\"\"Plugin editor can be moved between different monitors when detached.\"\"\"\n\n    disabled = _WrapperProp(_WrapperFlags._Disabled)\n    \"\"\"This is a legacy property; DON'T use it.\n\n    Check :attr:`Channel.enabled` or :attr:`Slot.enabled` instead.\n    \"\"\"\n\n    directx = _WrapperProp(_WrapperFlags._DirectX)\n    \"\"\"Whether the plugin is a DirectX plugin or not.\"\"\"\n\n    generator = _WrapperProp(_WrapperFlags.Generator)\n    \"\"\"Whether the plugin is a generator or an effect.\"\"\"\n\n    height = StructProp[int](PluginID.Wrapper)\n    \"\"\"Height of the plugin editor (in pixels).\"\"\"\n\n    minimized = _WrapperProp(_WrapperFlags.Minimized)\n    \"\"\"Whether the plugin editor is maximized or minimized.\n\n    ![](https://bit.ly/3QDMWO3)\n    \"\"\"\n\n    multithreaded = _WrapperProp(_WrapperFlags.ThreadedProcessing)\n    \"\"\"Whether threaded processing is enabled or not.\"\"\"\n\n    page = StructProp[WrapperPage](PluginID.Wrapper)\n    \"\"\"Active / selected / current page.\n\n    ![](https://bit.ly/3ffJKM3)\n    \"\"\"\n\n    smart_disable = _WrapperProp(_WrapperFlags.SmartDisable)\n    \"\"\"Whether smart disable is enabled or not.\"\"\"\n\n    visible = _WrapperProp(_WrapperFlags.Visible)\n    \"\"\"Whether the editor of the plugin is visible or closed.\"\"\"\n\n    width = StructProp[int](PluginID.Wrapper)\n    \"\"\"Width of the plugin editor (in pixels).\"\"\"\n\n\nAnyPlugin = _PluginBase[AnyEvent]  # TODO alias to _IPlugin + _PluginBase (both)\n\n\nclass PluginProp(RWProperty[AnyPlugin]):\n    def __init__(self, *types: type[AnyPlugin]) -> None:\n        self._types = types\n\n    @staticmethod\n    def _get_plugin_events(ins: EventModel) -> EventTree:\n        return ins.events.subtree(lambda e: e.id in (PluginID.Wrapper, PluginID.Data))\n\n    def __get__(self, ins: EventModel, owner: Any = None) -> AnyPlugin | None:\n        if owner is None:\n            return NotImplemented\n\n        try:\n            data_event = ins.events.first(PluginID.Data)\n        except KeyError:\n            return None\n\n        if isinstance(data_event, UnknownDataEvent):\n            return _PluginBase(self._get_plugin_events(ins))\n\n        for ptype in self._types:\n            event_type = ptype.__orig_bases__[0].__args__[0]  # type: ignore\n            if isinstance(data_event, event_type):\n                return ptype(self._get_plugin_events(ins))\n\n    def __set__(self, ins: EventModel, value: AnyPlugin) -> None:\n        if isinstance(value, _IPlugin):\n            setattr(ins, \"internal_name\", value.INTERNAL_NAME)\n\n        for id in (PluginID.Data, PluginID.Wrapper):\n            for ie in ins.events.lst:\n                if ie.e.id == id:\n                    ie.e = value.events.first(id)\n\n\nclass _NativePluginProp(StructProp[T]):\n    def __init__(self, prop: str | None = None, **kwds: Any) -> None:\n        super().__init__(PluginID.Data, prop=prop, **kwds)\n\n\nclass _VSTPluginProp(RWProperty[T], NamedPropMixin):\n    def __init__(self, id: _VSTPluginEventID, prop: str | None = None) -> None:\n        self._id = id\n        NamedPropMixin.__init__(self, prop)\n\n    def __get__(self, ins: EventModel, _=None) -> T:\n        event = cast(VSTPluginEvent, ins.events.first(PluginID.Data))\n        for e in event[\"events\"]:\n            if e[\"id\"] == self._id:\n                return self._get(e[\"data\"])\n        raise AttributeError(self._id)\n\n    def _get(self, value: Any) -> T:\n        return cast(T, value if isinstance(value, (str, bytes)) else value[self._prop])\n\n    def __set__(self, ins: EventModel, value: T) -> None:\n        self._set(cast(VSTPluginEvent, ins.events.first(PluginID.Data)), value)\n\n    def _set(self, event: VSTPluginEvent, value: T) -> None:\n        for e in event[\"events\"]:\n            if e[\"id\"] == self._id:\n                e[\"data\"] = value\n                break\n\n\nclass _VSTFlagProp(_VSTPluginProp[bool]):\n    def __init__(\n        self, flag: _VSTFlags | _VSTFlags2, prop: str = \"flags\", inverted: bool = False\n    ) -> None:\n        super().__init__(_VSTPluginEventID.Flags, prop)\n        self._flag = flag\n        self._inverted = inverted\n\n    def _get(self, value: Any) -> bool:\n        retbool = self._flag in value[self._prop]\n        return retbool if not self._inverted else not retbool\n\n    def _set(self, event: VSTPluginEvent, value: bool) -> None:\n        if self._inverted:\n            value = not value\n\n        for e in event[\"events\"]:\n            if e[\"id\"] == self._id:\n                if value:\n                    e[\"data\"][self._prop] |= value\n                else:\n                    e[\"data\"][self._prop] &= ~value\n                break\n\n\nclass PluginIOInfo(EventModel):\n    mixer_offset = StructProp[int]()\n    flags = StructProp[int]()\n\n\nclass VSTPlugin(_PluginBase[VSTPluginEvent], _IPlugin):\n    \"\"\"Represents a VST2 or a VST3 generator or effect.\n\n    *New in FL Studio v1.5.23*: VST2 support (beta).\n    *New in FL Studio v9.0.3*: VST3 support.\n    \"\"\"\n\n    INTERNAL_NAME = \"Fruity Wrapper\"\n\n    def __repr__(self) -> str:\n        return f\"VSTPlugin(name={self.name!r}, vendor={self.vendor!r})\"\n\n    class _AutomationOptions(EventModel):\n        \"\"\"See :attr:`VSTPlugin.automation`.\"\"\"\n\n        notify_changes = _VSTFlagProp(_VSTFlags.DontNotifyChanges, inverted=True)\n        \"\"\"Record parameter changes as automation.\n\n        :guilabel:`Notify about parameter changes`. Defaults to ``True``.\n        \"\"\"\n\n    class _CompatibilityOptions(EventModel):\n        \"\"\"See :attr:`VSTPlugin.compatibility`.\"\"\"\n\n        buffers_maxsize = _VSTFlagProp(_VSTFlags2.UseMaxFromHost, prop=\"flags2\")\n        \"\"\":guilabel:`Use maximum buffer size from host`. Defaults to ``False``.\"\"\"\n\n        fast_idle = _VSTPluginProp[bool](_VSTPluginEventID.Flags)\n        \"\"\"Increases idle rate - can make plugin GUI feel more responsive if its slow.\n\n        May increase CPU usage. Defaults to ``False``.\n        \"\"\"\n\n        fixed_buffers = _VSTFlagProp(_VSTFlags.FixedSizeBuffers)\n        \"\"\":guilabel:`Use fixed size buffers`. Defaults to ``False``.\n\n        Makes FL Studio send fixed size buffers instead of variable ones when ``True``.\n        Can fix rendering errors caused by plugins. Increases latency by 2ms.\n        \"\"\"\n\n        process_maximum = _VSTFlagProp(_VSTFlags2.ProcessMaxSize, prop=\"flags2\")\n        \"\"\":guilabel:`Process maximum size buffers`. Defaults to ``False``.\"\"\"\n\n        reset_on_transport = _VSTFlagProp(_VSTFlags.DontResetOnTransport, inverted=True)\n        \"\"\":guilabel:`Reset plugin when FL Studio resets`. Defaults to ``True``.\"\"\"\n\n        send_loop = _VSTFlagProp(_VSTFlags.SendLoopPos)\n        \"\"\"Lets the plugin know about :attr:`Arrangemnt.loop_pos`.\n\n        :guilabel:`Send loop position`. Defaults to ``True``.\n        \"\"\"\n\n        use_time_offset = _VSTFlagProp(_VSTFlags.DontUseTimeOffset, inverted=True)\n        \"\"\"Adjust time information reported by plugin.\n\n        Can fix timing issues caused by plugins in FL Studio <20.7 project.\n        :guilabel:`Use time offset`. Defaults to ``False``.\n        \"\"\"\n\n    class _MIDIOptions(EventModel):\n        \"\"\"See :attr:`VSTPlugin.midi`.\n\n        ![](https://bit.ly/3NbGr4U)\n        \"\"\"\n\n        input = _VSTPluginProp[int](_VSTPluginEventID.MIDI)\n        \"\"\"MIDI Input Port. Min = 0, Max = 255. Not selected = -1 (default).\"\"\"\n\n        output = _VSTPluginProp[int](_VSTPluginEventID.MIDI)\n        \"\"\"MIDI Output Port. Min = 0, Max = 255. Not selected = -1 (default).\"\"\"\n\n        pb_range = _VSTPluginProp[int](_VSTPluginEventID.MIDI)\n        \"\"\"Pitch bend range MIDI RPN sent to the plugin (in semitones).\n\n        Min = 1. Max = 48. Defaults to 12.\n        \"\"\"\n\n        send_modx = _VSTFlagProp(_VSTFlags.SendModX)\n        \"\"\":guilabel:`Send MOD X as polyphonic aftertouch`. Defaults to ``False``.\"\"\"\n\n        send_pb = _VSTFlagProp(_VSTFlags.SendPBRange)\n        \"\"\":guilabel:`Send pitch bend range (semitones)`. Defaults to ``False``.\n\n        See also:\n            :attr:`pb_range` - Sent to plugin as a MIDI RPN if this is ``True``.\n        \"\"\"\n\n        send_release = _VSTFlagProp(_VSTFlags.DontSendRelVelo, inverted=True)\n        \"\"\"Whether release velocity should be sent in note off messages.\n\n        :guilabel:`Send note release velocity`. Defaults to ``True``.\n        \"\"\"\n\n    class _ProcessingOptions(EventModel):\n        \"\"\"See :attr:`VSTPlugin.processing`.\"\"\"\n\n        allow_sd = _VSTFlagProp(_VSTFlags.AllowSmartDisable)\n        \"\"\":guilabel:`Allow smart disable`. Defaults to ``True``.\n\n        Disables the :attr:`VSTPlugin.smart_disable` feature if ``False``.\n        \"\"\"\n\n        bridged = _VSTFlagProp(_VSTFlags.LoadBridged)\n        \"\"\"Load a plugin in separate process.\n\n        :guilabel:`Make bridged`. Defaults to ``False``.\n        \"\"\"\n\n        external = _VSTFlagProp(_VSTFlags.ExternalWindow)\n        \"\"\"Keep plugin editor in bridge process.\n\n        :guilabel:`External window`. Defaults to ``False``.\n        \"\"\"\n\n        keep_state = _VSTFlagProp(_VSTFlags.DontKeepCPUState, inverted=True)\n        \"\"\"Don't touch unless you have issues like DC offsets, spikes and crashes.\n\n        :guilabel:`Ensure processor state in callbacks`. Defaults to ``True``.\n        \"\"\"\n\n        multithreaded = _VSTFlagProp(_VSTFlags.AllowThreaded)\n        \"\"\"Allow plugin to be multi-threaded by FL Studio.\n\n        Disables the :attr:`VSTPlugin.multithreaded` feature if ``False``.\n\n        :guilabel:`Allow threaded processing`. Defaults to ``True``.\n        \"\"\"\n\n        notify_render = _VSTFlagProp(_VSTFlags.NotifyRender)\n        \"\"\"Lets the plugin know when rendering to audio file.\n\n        This can be used by the plugin to switch to HQ processing or disable\n        output entirely if it is in demo mode (depends on the plugin logic).\n\n        :guilabel:`Notify about rendering mode`. Defaults to ``True``.\n        \"\"\"\n\n        process_inactive = _VSTFlagProp(_VSTFlags.ProcessInactive)\n        \"\"\"Make FL Studio also process inputs / outputs marked as inactive by plugin.\n\n        :guilabel:`Process inactive inputs and outputs`. Defaults to ``True``.\n        \"\"\"\n\n    class _UIOptions(EventModel):\n        \"\"\"See :attr:`VSTPlugin.ui`.\n\n        ![](https://bit.ly/3Nb3dtP)\n        \"\"\"\n\n        accept_drop = _VSTFlagProp(_VSTFlags.AcceptFileDrop)\n        \"\"\"Host is bypassed when a file is dropped on the plugin editor.\n\n        :guilabel:`Accept dropped files`. Defaults to ``False``.\n        \"\"\"\n\n        always_update = _VSTFlagProp(_VSTFlags.UpdateWhenHidden)\n        \"\"\"Whether plugin UI should be updated when hidden; default to ``False``.\"\"\"\n\n        dpi_aware = _VSTFlagProp(_VSTFlags.DPIAwareBridged)\n        \"\"\"Enable if plugin editors look too big or small.\n\n        :guilabel:`DPI aware when bridged`. Defaults to ``True``.\n        \"\"\"\n\n        scale_editor = _VSTFlagProp(_VSTFlags.ScaleEditor)\n        \"\"\"Scale dimensions of editor that appear cut-off on high-res screens.\n\n        :guilabel:`Scale editor dimensions`. Defaults to ``False``.\n        \"\"\"\n\n    def __init__(self, events: EventTree, **kw: Any) -> None:\n        super().__init__(events, **kw)\n\n        # This doesn't break lazy evaluation in any way\n        self.automation = self._AutomationOptions(events)\n        self.compatibility = self._CompatibilityOptions(events)\n        self.midi = self._MIDIOptions(events)\n        self.processing = self._ProcessingOptions(events)\n        self.ui = self._UIOptions(events)\n\n    fourcc = _VSTPluginProp[str](_VSTPluginEventID.FourCC)\n    \"\"\"A unique four character code identifying the plugin.\n\n    A database can be found on Steinberg's developer portal.\n    \"\"\"\n\n    guid = _VSTPluginProp[bytes](_VSTPluginEventID.GUID)  # See issue #8\n    name = _VSTPluginProp[str](_VSTPluginEventID.Name)\n    \"\"\"Factory name of the plugin.\"\"\"\n\n    # num_inputs = _VSTPluginProp[int]()\n    # \"\"\"Number of inputs the plugin supports.\"\"\"\n\n    # num_outputs = _VSTPluginProp[int]()\n    # \"\"\"Number of outputs the plugin supports.\"\"\"\n\n    plugin_path = _VSTPluginProp[str](_VSTPluginEventID.PluginPath)\n    \"\"\"The absolute path to the plugin binary.\"\"\"\n\n    state = _VSTPluginProp[bytes](_VSTPluginEventID.State)\n    \"\"\"Plugin specific preset data blob.\"\"\"\n\n    vendor = _VSTPluginProp[str](_VSTPluginEventID.Vendor)\n    \"\"\"Plugin developer (vendor) name.\"\"\"\n\n    # vst_number = _VSTPluginProp[int]()  # TODO\n\n\nclass BooBass(_PluginBase[BooBassEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3Bk3aGK)\"\"\"\n\n    INTERNAL_NAME = \"BooBass\"\n    bass = _NativePluginProp[int]()\n    \"\"\"Volume of the bass region.\n\n    | Min | Max   | Default |\n    |-----|-------|---------|\n    | 0   | 65535 | 32767   |\n    \"\"\"\n\n    high = _NativePluginProp[int]()\n    \"\"\"Volume of the high region.\n\n    | Min | Max   | Default |\n    |-----|-------|---------|\n    | 0   | 65535 | 32767   |\n    \"\"\"\n\n    mid = _NativePluginProp[int]()\n    \"\"\"Volume of the mid region.\n\n    | Min | Max   | Default |\n    |-----|-------|---------|\n    | 0   | 65535 | 32767   |\n    \"\"\"\n\n\nclass FruitKick(_PluginBase[FruitKickEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/41fIPxE)\"\"\"\n\n    INTERNAL_NAME = \"Fruit Kick\"\n    amp_decay = _NativePluginProp[int]()\n    \"\"\"Amplitude (volume) decay length. Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0%             |\n    | Max     | 256   | 100%           |\n    | Default | 128   | 50%            |\n    \"\"\"\n\n    click = _NativePluginProp[int]()\n    \"\"\"Amount of phase offset added to produce a click. Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0%             |\n    | Max     | 64    | 100%           |\n    | Default | 32    | 50%            |\n    \"\"\"\n\n    distortion = _NativePluginProp[int]()\n    \"\"\"Linear. Defaults to minimum.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0%             |\n    | Max     | 128   | 100%           |\n    \"\"\"\n\n    freq_decay = _NativePluginProp[int]()\n    \"\"\"Pitch sweep time / pitch decay. Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0%             |\n    | Max     | 256   | 100%           |\n    | Default | 64    | 25%            |\n    \"\"\"\n\n    max_freq = _NativePluginProp[int]()\n    \"\"\"Start frequency. Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -900  | -67%           |\n    | Max     | 3600  | 100%           |\n    | Default | 0     | 0%             |\n    \"\"\"\n\n    min_freq = _NativePluginProp[int]()\n    \"\"\"Sweep to / end frequency. Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -1200 | -100%          |\n    | Max     | 1200  | 100%           |\n    | Default | -600  | -50%           |\n    \"\"\"\n\n\nclass FruityBalance(_PluginBase[FruityBalanceEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3RWItqU)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Balance\"\n    pan = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -128  | 100% left      |\n    | Max     | 127   | 100% right     |\n    | Default | 0     | Centred        |\n    \"\"\"\n\n    volume = _NativePluginProp[int]()\n    \"\"\"Logarithmic.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | -INFdB / 0.00  |\n    | Max     | 320   | 5.6dB / 1.90   |\n    | Default | 256   | 0.0dB / 1.00   |\n    \"\"\"\n\n\nclass FruityBloodOverdrive(_PluginBase[FruityBloodOverdriveEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3LnS1LE)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Blood Overdrive\"\n\n    pre_band = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0.0000         |\n    | Max     | 10000 | 1.0000         |\n    | Default | 0     | 0.0000         |\n    \"\"\"\n\n    color = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0.0000         |\n    | Max     | 10000 | 1.0000         |\n    | Default | 5000  | 0.5000         |\n    \"\"\"\n\n    pre_amp = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0.0000         |\n    | Max     | 10000 | 1.0000         |\n    | Default | 0     | 0.0000         |\n    \"\"\"\n\n    x100 = _NativePluginProp[bool]()\n    \"\"\"Boolean.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Off     | 0     | Off            |\n    | On      | 1     | On             |\n    | Default | 0     | Off            |\n    \"\"\"\n\n    post_filter = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | 0.0000         |\n    | Max     | 10000 | 1.0000         |\n    | Default | 0     | 0.0000         |\n    \"\"\"\n\n    post_gain = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | -1.0000        |\n    | Max     | 10000 |  0.0000        |\n    | Default | 10000 |  0.0000        |\n    \"\"\"\n\n\nclass FruityCenter(_PluginBase[FruityCenterEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3TA9IIv)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Center\"\n    enabled = _NativePluginProp[bool]()\n    \"\"\"Removes DC offset if True; effectively behaving like a bypass button.\n\n    Labelled as **Status** for some reason in the UI.\n    \"\"\"\n\n\nclass FruityFastDist(_PluginBase[FruityFastDistEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3qT6Jil)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Fast Dist\"\n    kind = _NativePluginProp[Literal[\"A\", \"B\"]]()\n    mix = _NativePluginProp[int]()\n    \"\"\"Linear. Defaults to maximum value.\n\n    | Type | Value | Mix (wet) |\n    |------|-------|-----------|\n    | Min  | 0     | 0%        |\n    | Max  | 128   | 100%      |\n    \"\"\"\n\n    post = _NativePluginProp[int]()\n    \"\"\"Linear. Defaults to maximum value.\n\n    | Type | Value | Mix (wet) |\n    |------|-------|-----------|\n    | Min  | 0     | 0%        |\n    | Max  | 128   | 100%      |\n    \"\"\"\n\n    pre = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Percentage |\n    |---------|-------|------------|\n    | Min     | 64    | 33%        |\n    | Max     | 192   | 100%       |\n    | Default | 128   | 67%        |\n    \"\"\"\n\n    threshold = _NativePluginProp[int]()\n    \"\"\"Linear, Stepped. Defaults to maximum value.\n\n    | Type | Value | Percentage |\n    |------|-------|------------|\n    | Min  | 1     | 10%        |\n    | Max  | 10    | 100%       |\n    \"\"\"\n\n\nclass FruityNotebook2(_PluginBase[FruityNotebook2Event], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3RHa4g5)\"\"\"\n\n    INTERNAL_NAME = \"Fruity NoteBook 2\"\n    active_page = _NativePluginProp[int]()\n    \"\"\"Active page number of the notebook. Min: 0, Max: 100.\"\"\"\n\n    editable = _NativePluginProp[bool]()\n    \"\"\"Whether the notebook is marked as editable or read-only.\n\n    This attribute is just a visual marker used by FL Studio.\n    \"\"\"\n\n    pages = _NativePluginProp[Dict[int, str]]()\n    \"\"\"A dict of page numbers to their contents.\"\"\"\n\n\nclass FruitySend(_PluginBase[FruitySendEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3DqjvMu)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Send\"\n    dry = _NativePluginProp[int]()\n    \"\"\"Linear. Defaults to maximum value.\n\n    | Type | Value | Mix (wet) |\n    |------|-------|-----------|\n    | Min  | 0     | 0%        |\n    | Max  | 256   | 100%      |\n    \"\"\"\n\n    pan = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -128  | 100% left      |\n    | Max     | 127   | 100% right     |\n    | Default | 0     | Centred        |\n    \"\"\"\n\n    send_to = _NativePluginProp[int]()\n    \"\"\"Target insert index; depends on insert routing. Defaults to -1 (Master).\"\"\"\n\n    volume = _NativePluginProp[int]()\n    \"\"\"Logarithmic.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | -INFdB / 0.00  |\n    | Max     | 320   | 5.6dB / 1.90   |\n    | Default | 256   | 0.0dB / 1.00   |\n    \"\"\"\n\n\nclass FruitySoftClipper(_PluginBase[FruitySoftClipperEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3BCWfJX)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Soft Clipper\"\n    post = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Mix (wet) |\n    |---------|-------|-----------|\n    | Min     | 0     | 0%        |\n    | Max     | 160   | 100%      |\n    | Default | 128   | 80%       |\n    \"\"\"\n\n    threshold = _NativePluginProp[int]()\n    \"\"\"Logarithmic.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 1     | -INFdB / 0.00  |\n    | Max     | 127   | 0.0dB / 1.00   |\n    | Default | 100   | -4.4dB / 0.60  |\n    \"\"\"\n\n\nclass FruityStereoEnhancer(_PluginBase[FruityStereoEnhancerEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3DoHvji)\"\"\"\n\n    INTERNAL_NAME = \"Fruity Stereo Enhancer\"\n    effect_position = _NativePluginProp[Literal[\"pre\", \"post\"]]()\n    \"\"\"Defaults to ``post``.\"\"\"\n\n    pan = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -128  | 100% left      |\n    | Max     | 127   | 100% right     |\n    | Default | 0     | Centred        |\n    \"\"\"\n\n    phase_inversion = _NativePluginProp[Literal[\"none\", \"left\", \"right\"]]()\n    \"\"\"Default to ``None``.\"\"\"\n\n    phase_offset = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -512  | 500ms L        |\n    | Max     | 512   | 500ms R        |\n    | Default | 0     | No offset      |\n    \"\"\"\n\n    stereo_separation = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | -96   | 100% separated |\n    | Max     | 96    | 100% merged    |\n    | Default | 0     | No effect      |\n    \"\"\"\n\n    volume = _NativePluginProp[int]()\n    \"\"\"Logarithmic.\n\n    | Type    | Value | Representation |\n    |---------|-------|----------------|\n    | Min     | 0     | -INFdB / 0.00  |\n    | Max     | 320   | 5.6dB / 1.90   |\n    | Default | 256   | 0.0dB / 1.00   |\n    \"\"\"\n\n\nclass Plucked(_PluginBase[PluckedEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3GuFz9k)\"\"\"\n\n    INTERNAL_NAME = \"Plucked!\"\n    color = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Min | Max  | Default |\n    |-----|------|---------|\n    | 0   | 128  | 64      |\n    \"\"\"\n\n    decay = _NativePluginProp[int]()\n    \"\"\"Linear.\n\n    | Min | Max  | Default |\n    |-----|------|---------|\n    | 0   | 256  | 128     |\n    \"\"\"\n\n    gate = _NativePluginProp[bool]()\n    \"\"\"Stops the voices abruptly when released, otherwise the decay keeps going.\"\"\"\n\n    normalize = _NativePluginProp[bool]()\n    \"\"\"Same :attr:`decay` is tried to be used for all semitones.\n\n    If not, higher notes have a shorter decay.\n    \"\"\"\n\n    widen = _NativePluginProp[bool]()\n    \"\"\"Enriches the stereo panorama of the sound.\"\"\"\n\n\nclass Soundgoodizer(_PluginBase[SoundgoodizerEvent], _IPlugin, ModelReprMixin):\n    \"\"\"![](https://bit.ly/3dip70y)\"\"\"\n\n    INTERNAL_NAME = \"Soundgoodizer\"\n    amount = _NativePluginProp[int]()\n    \"\"\"Logarithmic.\n\n    | Min | Max  | Default |\n    |-----|------|---------|\n    | 0   | 1000 | 600     |\n    \"\"\"\n\n    mode = _NativePluginProp[Literal[\"A\", \"B\", \"C\", \"D\"]]()\n    \"\"\"4 preset modes (A, B, C and D). Defaults to ``A``.\"\"\"\n\n\ndef get_event_by_internal_name(name: str) -> type[AnyEvent]:\n    for cls in _PluginBase.__subclasses__():\n        if getattr(cls, \"INTERNAL_NAME\", None) == name:\n            return cls.__orig_bases__[0].__args__[0]  # type: ignore\n    return NativePluginEvent\n"
  },
  {
    "path": "pyflp/project.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the class (and types it uses) used by the parser and serializer.\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime\nimport enum\nimport math\nimport pathlib\nfrom typing import Final, Literal, cast\n\nimport construct as c\nimport construct_typed as ct\nfrom typing_extensions import TypedDict, Unpack\n\nfrom pyflp._descriptors import EventProp, KWProp\nfrom pyflp._events import (\n    DATA,\n    DWORD,\n    TEXT,\n    WORD,\n    AnyEvent,\n    AsciiEvent,\n    BoolEvent,\n    EventEnum,\n    EventTree,\n    I16Event,\n    I32Event,\n    StructEventBase,\n    U8Event,\n    U32Event,\n)\nfrom pyflp._models import EventModel\nfrom pyflp.arrangement import ArrangementID, Arrangements, ArrangementsID, TrackID\nfrom pyflp.channel import ChannelID, ChannelRack, DisplayGroupID, RackID\nfrom pyflp.exceptions import PropertyCannotBeSet\nfrom pyflp.mixer import InsertID, Mixer, MixerID, SlotID\nfrom pyflp.pattern import PatternID, Patterns, PatternsID\nfrom pyflp.plugin import PluginID\nfrom pyflp.timemarker import TimeMarkerID\nfrom pyflp.types import FLVersion\n\n__all__ = [\"PanLaw\", \"Project\", \"FileFormat\", \"VALID_PPQS\"]\n\n_DELPHI_EPOCH: Final = datetime.datetime(1899, 12, 30)\nMIN_TEMPO: Final = 10.000\n\"\"\"Minimum tempo (in BPM) FL Studio supports.\"\"\"\n\nVALID_PPQS: Final = (24, 48, 72, 96, 120, 144, 168, 192, 384, 768, 960)\n\"\"\"PPQs / timebase supported by FL Studio as of its latest version.\"\"\"\n\n\nclass TimestampEvent(StructEventBase):\n    STRUCT = c.Struct(\"created_on\" / c.Float64l, \"time_spent\" / c.Float64l).compile()\n\n\n@enum.unique\nclass PanLaw(ct.EnumBase):\n    \"\"\"Used by :attr:`Project.pan_law`.\"\"\"\n\n    Circular = 0\n    Triangular = 2\n\n\n@enum.unique\nclass FileFormat(enum.IntEnum):\n    \"\"\"File formats used by FL Studio.\n\n    *New in FL Studio v2.5.0*: FST (FL Studio state) file format.\n    \"\"\"\n\n    None_ = -1\n    \"\"\"Temporary file.\"\"\"\n\n    Project = 0\n    \"\"\"FL Studio project (.flp).\"\"\"\n\n    Score = 0x10\n    \"\"\"FL Studio score (.fsc). Stores pattern notes and controller events.\"\"\"\n\n    Automation = 24\n    \"\"\"Stores controller events and automation channels as FST.\"\"\"\n\n    ChannelState = 0x20\n    \"\"\"Entire channel (including plugin events). Stored as FST.\"\"\"\n\n    PluginState = 0x30\n    \"\"\"Events of a native plugin on a channel or insert slot. Stored as FST.\"\"\"\n\n    GeneratorState = 0x31\n    \"\"\"Plugins events of a VST instrument. Stored as FST.\"\"\"\n\n    FXState = 0x32\n    \"\"\"Plugin events of a VST effect. Stored as FST.\"\"\"\n\n    InsertState = 0x40\n    \"\"\"Insert and all its slots. Stored as FST.\"\"\"\n\n    _ProbablyPatcher = 0x50  # * Patcher presets are stored as `PluginState`.\n\n\nclass ProjectID(EventEnum):\n    LoopActive = (9, BoolEvent)\n    ShowInfo = (10, BoolEvent)\n    _Volume = (12, U8Event)\n    PanLaw = (23, U8Event)\n    Licensed = (28, BoolEvent)\n    _TempoCoarse = WORD + 2\n    Pitch = (WORD + 16, I16Event)\n    _TempoFine = WORD + 29  #: 3.4.0+\n    CurGroupId = (DWORD + 18, I32Event)\n    Tempo = (DWORD + 28, U32Event)\n    FLBuild = (DWORD + 31, U32Event)\n    Title = TEXT + 2\n    Comments = TEXT + 3\n    Url = TEXT + 5\n    _RTFComments = TEXT + 6  #: 1.2.10+\n    FLVersion = (TEXT + 7, AsciiEvent)\n    Licensee = TEXT + 8  #: 1.3.9+\n    DataPath = TEXT + 10  #: 9.0+\n    Genre = TEXT + 14  #: 5.0+\n    Artists = TEXT + 15  #: 5.0+\n    Timestamp = (DATA + 29, TimestampEvent)\n\n\nclass _ProjectKW(TypedDict):\n    channel_count: int\n    ppq: int\n    format: FileFormat\n\n\nclass Project(EventModel):\n    \"\"\"Represents an FL Studio project.\"\"\"\n\n    def __init__(self, events: EventTree, **kw: Unpack[_ProjectKW]) -> None:\n        super().__init__(events, **kw)\n\n    def __repr__(self) -> str:\n        return f\"<Project(format={self.format!r}, version={self.version!r}>\"\n\n    def __str__(self) -> str:\n        return f\"FL Studio v{self.version!s} {self.format.name}\"  # type: ignore\n\n    @property\n    def arrangements(self) -> Arrangements:\n        \"\"\"Provides an iterator over arrangements and other related properties.\"\"\"\n        arrnew_occured = False\n\n        def select(e: AnyEvent) -> Literal[True] | None:\n            nonlocal arrnew_occured\n\n            if e.id == ArrangementID.New:\n                arrnew_occured = True\n\n            # * Prevents accidentally passing on Pattern's timemarkers\n            # TODO This logic will still be incorrect if arrangement's\n            # timemarkers occur before ArrangementID.New event.\n            if e.id in TimeMarkerID and arrnew_occured:\n                return True\n\n            if e.id in (*ArrangementID, *ArrangementsID, *TrackID):\n                return True\n\n        return Arrangements(\n            self.events.subtree(select),\n            channels=self.channels,\n            patterns=self.patterns,\n            version=self.version,\n        )\n\n    artists = EventProp[str](ProjectID.Artists)\n    \"\"\"Authors / artists info. to be embedded in exported WAV & MP3.\n\n    :menuselection:`Options --> &Project info --> Author`\n\n    *New in FL Studio v5.0.*\n    \"\"\"\n\n    @property\n    def channel_count(self) -> int:\n        \"\"\"Number of channels in the rack.\n\n        For Patcher presets, the total number of plugins used inside it.\n\n        Raises:\n            ValueError: When a value less than zero is tried to be set.\n        \"\"\"\n        return self._kw[\"channel_count\"]\n\n    @channel_count.setter\n    def channel_count(self, value: int) -> None:\n        if value < 0:\n            raise ValueError(\"Channel count cannot be less than zero\")\n        self._kw[\"channel_count\"] = value\n\n    @property\n    def channels(self) -> ChannelRack:\n        \"\"\"Provides an iterator over channels and channel rack properties.\"\"\"\n\n        def select(e: AnyEvent) -> bool | None:\n            if e.id == InsertID.Flags:\n                return False\n\n            if e.id in (*ChannelID, *DisplayGroupID, *PluginID, *RackID):\n                return True\n\n        return ChannelRack(\n            self.events.subtree(select),\n            channel_count=self.channel_count,\n        )\n\n    comments = EventProp[str](ProjectID.Comments, ProjectID._RTFComments)\n    \"\"\"Comments / project description / summary.\n\n    :menuselection:`Options --> &Project info --> Comments`\n\n    Caution:\n        Very old versions of FL used to store comments in RTF (Rich Text Format).\n        PyFLP makes no efforts to parse that and stores it like a normal string\n        as it is. It is upto you to extract the text out of it.\n    \"\"\"\n\n    # Stored as a duration in days since the Delphi epoch (30 Dec, 1899).\n    @property\n    def created_on(self) -> datetime.datetime | None:\n        \"\"\"The local date and time on which this project was created.\n\n        Located at the bottom of :menuselection:`Options --> &Project info` page.\n        \"\"\"\n        if ProjectID.Timestamp in self.events.ids:\n            event = cast(TimestampEvent, self.events.first(ProjectID.Timestamp))\n            return _DELPHI_EPOCH + datetime.timedelta(days=event[\"created_on\"])\n\n    format = KWProp[FileFormat]()\n    \"\"\"Internal format marker used by FL Studio to distinguish between types.\"\"\"\n\n    @property\n    def data_path(self) -> pathlib.Path | None:\n        \"\"\"The absolute path used by FL to store all your renders.\n\n        :menuselection:`Options --> &Project general settings --> Data folder`\n\n        *New in FL Studio v9.0.0.*\n        \"\"\"\n        if ProjectID.DataPath in self.events.ids:\n            return pathlib.Path(self.events.first(ProjectID.DataPath).value)\n\n    @data_path.setter\n    def data_path(self, value: str | pathlib.Path) -> None:\n        if ProjectID.DataPath not in self.events.ids:\n            raise PropertyCannotBeSet(ProjectID.DataPath)\n\n        if isinstance(value, pathlib.Path):\n            value = str(value)\n\n        path = \"\" if value == \".\" else value\n        self.events.first(ProjectID.DataPath).value = path\n\n    genre = EventProp[str](ProjectID.Genre)\n    \"\"\"Genre of the song to be embedded in exported WAV & MP3.\n\n    :menuselection:`Options --> &Project info --> Genre`\n\n    *New in FL Studio v5.0*.\n    \"\"\"\n\n    licensed = EventProp[bool](ProjectID.Licensed)\n    \"\"\"Whether the project was last saved with a licensed copy of FL Studio.\n\n    Tip:\n        Setting this to `True` and saving back the FLP will make it load the\n        next time in a trial version of FL if it wouldn't open before.\n    \"\"\"\n\n    # Internally, this is jumbled up. Thanks to @codecat/libflp for decode algo.\n    @property\n    def licensee(self) -> str | None:\n        \"\"\"The license holder's username who last saved the project file.\n\n        If saved with a trial version this is empty.\n\n        Tip:\n            As of the latest version, FL doesn't check for the contents of\n            this for deciding whether to open or not when in trial version.\n\n        *New in FL Studio v1.3.9*.\n        \"\"\"\n        if ProjectID.Licensee in self.events.ids:\n            event = self.events.first(ProjectID.Licensee)\n            licensee = bytearray()\n            for idx, char in enumerate(event.value):\n                c1 = ord(char) - 26 + idx\n                c2 = ord(char) + 49 + idx\n                for num in c1, c2:\n                    if chr(num).isalnum():\n                        licensee.append(num)\n                        break\n\n            return licensee.decode(\"ascii\")\n\n    @licensee.setter\n    def licensee(self, value: str) -> None:\n        if self.version < FLVersion(1, 3, 9):\n            pass\n\n        if ProjectID.Licensee not in self.events.ids:\n            raise PropertyCannotBeSet(ProjectID.Licensee)\n\n        event = self.events.first(ProjectID.Licensee)\n        licensee = bytearray()\n        for idx, char in enumerate(value):\n            c1 = ord(char) + 26 - idx\n            c2 = ord(char) - 49 - idx\n            for cp in c1, c2:\n                if 0 < cp <= 127:\n                    licensee.append(cp)\n                    break\n        event.value = licensee.decode(\"ascii\")\n\n    looped = EventProp[bool](ProjectID.LoopActive)\n    \"\"\"Whether a portion of the playlist is selected.\"\"\"\n\n    main_pitch = EventProp[int](ProjectID.Pitch)\n    \"\"\":guilabel:`Master pitch` (in cents). Min = -1200. Max = +1200. Defaults to 0.\"\"\"\n\n    main_volume = EventProp[int](ProjectID._Volume)\n    \"\"\"*Changed in FL Studio v1.7.6*: Can be upto 125% (+5.6dB) now.\"\"\"\n\n    @property\n    def mixer(self) -> Mixer:\n        \"\"\"Provides an iterator over inserts and other mixer related properties.\"\"\"\n        inserts_began = False\n\n        def select(e: AnyEvent) -> Literal[True] | None:\n            nonlocal inserts_began\n            if e.id in (*MixerID, *InsertID, *SlotID):\n                # TODO Find a more reliable to detect when inserts start.\n                inserts_began = True\n                return True\n\n            if inserts_began and e.id in PluginID:\n                return True\n\n        return Mixer(self.events.subtree(select), version=self.version)\n\n    @property\n    def patterns(self) -> Patterns:\n        \"\"\"Returns a collection of patterns and other related properties.\"\"\"\n        arrnew_occured = False\n\n        def select(e: AnyEvent) -> Literal[True] | None:\n            nonlocal arrnew_occured\n\n            if e.id == ArrangementID.New:\n                arrnew_occured = True\n\n            # * Prevents accidentally passing on Arrangement's timemarkers\n            elif e.id in TimeMarkerID and not arrnew_occured:\n                return True\n\n            elif e.id in (*PatternID, *PatternsID):\n                return True\n\n        return Patterns(self.events.subtree(select))\n\n    pan_law = EventProp[PanLaw](ProjectID.PanLaw)\n    \"\"\"Whether a circular or a triangular pan law is used for the project.\n\n    :menuselection:`Options -> &Project general settings -> Advanced -> Panning law`\n    \"\"\"\n\n    @property\n    def ppq(self) -> int:\n        \"\"\"Pulses per quarter.\n\n        ![](https://bit.ly/3F0UrMT)\n\n        :menuselection:`Options --> &Project general settings --> Timebase (PPQ)`.\n\n        Note:\n            All types of lengths, positions and offsets internally use the PPQ\n            as a multiplying factor.\n\n        Danger:\n            Don't try to set this property, it affects all the length, position\n            and offset calculations used for deciding the placement of playlist,\n            automations, timemarkers and patterns.\n\n            When you change this in FL, it recalculates all the above. It is\n            beyond PyFLP's scope to properly recalculate the timings.\n\n        Raises:\n            ValueError: When a value not in ``VALID_PPQS`` is tried to be set.\n\n        *Changed in FL Studio v2.1.1*: Defaults to ``96``.\n        \"\"\"\n        return self._kw[\"ppq\"]\n\n    @ppq.setter\n    def ppq(self, value: int) -> None:\n        if value not in VALID_PPQS:\n            raise ValueError(f\"Expected one of {VALID_PPQS}; got {value} instead\")\n        self._kw[\"ppq\"] = value\n\n    show_info = EventProp[bool](ProjectID.ShowInfo)\n    \"\"\"Whether to show a banner while the project is loading inside FL Studio.\n\n    :menuselection:`Options --> &Project info --> Show info on opening`\n\n    The banner shows the :attr:`title`, :attr:`artists`, :attr:`genre`,\n    :attr:`comments` and :attr:`url`.\n    \"\"\"\n\n    title = EventProp[str](ProjectID.Title)\n    \"\"\"Name of the song / project.\n\n    :menuselection:`Options --> &Project info --> Title`\n    \"\"\"\n\n    # Stored internally as the actual BPM * 1000 as an integer.\n    @property\n    def tempo(self) -> int | float | None:\n        \"\"\"Tempo at the current position of the playhead (in BPM).\n\n        ![](https://bit.ly/3MKdAEO)\n\n        Raises:\n            TypeError: When a fine-tuned tempo (``float``) isn't\n                supported. Use an ``int`` (coarse tempo) value.\n            PropertyCannotBeSet: If underlying event isn't found.\n            ValueError: When a tempo outside the allowed range is set.\n\n        * *Changed in FL Studio v1.4.2*: Max tempo increased to ``999`` (int).\n        * *New in FL Studio v3.4.0*: Fine tuned tempo (a float).\n        * *Changed in FL Studio v11*: Max tempo limited to ``522.000``.\n            Probably when tempo automations\n        \"\"\"\n        if ProjectID.Tempo in self.events.ids:\n            return self.events.first(ProjectID.Tempo).value / 1000\n\n        tempo = None\n        if ProjectID._TempoCoarse in self.events.ids:\n            tempo = self.events.first(ProjectID._TempoCoarse).value\n        if ProjectID._TempoFine in self.events.ids:\n            tempo += self.events.first(ProjectID._TempoFine).value / 1000\n        return tempo\n\n    @tempo.setter\n    def tempo(self, value: int | float) -> None:\n        if self.tempo is None:\n            raise PropertyCannotBeSet(ProjectID.Tempo, ProjectID._TempoCoarse, ProjectID._TempoFine)\n\n        max_tempo = 999.0 if FLVersion(1, 4, 2) <= self.version < FLVersion(11) else 522.0\n\n        if isinstance(value, float) and self.version < FLVersion(3, 4, 0):\n            raise TypeError(\"Expected an 'int' object got a 'float' instead\")\n\n        if float(value) > max_tempo or float(value) < MIN_TEMPO:\n            raise ValueError(f\"Invalid tempo {value}; expected {MIN_TEMPO}-{max_tempo}\")\n\n        if ProjectID.Tempo in self.events.ids:\n            self.events.first(ProjectID.Tempo).value = int(value * 1000)\n\n        if ProjectID._TempoFine in self.events.ids:\n            tempo_fine = int((value - math.floor(value)) * 1000)\n            self.events.first(ProjectID._TempoFine).value = tempo_fine\n\n        if ProjectID._TempoCoarse in self.events.ids:\n            self.events.first(ProjectID._TempoCoarse).value = math.floor(value)\n\n    @property\n    def time_spent(self) -> datetime.timedelta | None:\n        \"\"\"Time spent on the project since its creation.\n\n        ![](https://bit.ly/3TsBzdM)\n\n        Located at the bottom of :menuselection:`Options --> &Project info` page.\n        \"\"\"\n        if ProjectID.Timestamp in self.events.ids:\n            event = cast(TimestampEvent, self.events.first(ProjectID.Timestamp))\n            return datetime.timedelta(days=event[\"time_spent\"])\n\n    url = EventProp[str](ProjectID.Url)\n    \"\"\":menuselection:`Options --> &Project info --> Web link`.\"\"\"\n\n    # Internally represented as a string with a format of\n    # `major.minor.patch.build?` *where `build` is optional, since older\n    # versions of FL didn't follow the same versioning scheme*.\n    #\n    # To maintain backward compatibility with FL Studio prior to v11.5 which\n    # stored strings in ASCII, this event is always stored with ASCII data,\n    # even if the rest of the strings use Windows Unicode (UTF16).\n    @property\n    def version(self) -> FLVersion:\n        \"\"\"The version of FL Studio which was used to save the file.\n\n        ![](https://bit.ly/3TD3BU0)\n\n        Located at the top of :menuselection:`Help --> &About` page.\n\n        Caution:\n            Changing this to a lower version will not make a file load magically\n            inside FL Studio, as newer events and/or plugins might have been used.\n\n        Raises:\n            PropertyCannotBeSet: This error should NEVER occur; if it does,\n                it indicates possible corruption.\n            ValueError: When a string with an invalid format is tried to be set.\n        \"\"\"\n        event = cast(AsciiEvent, self.events.first(ProjectID.FLVersion))\n        return FLVersion(*tuple(int(part) for part in event.value.split(\".\")))\n\n    @version.setter\n    def version(self, value: FLVersion | str | tuple[int, ...]) -> None:\n        if ProjectID.FLVersion not in self.events.ids:\n            raise PropertyCannotBeSet(ProjectID.FLVersion)\n\n        if isinstance(value, FLVersion):\n            parts = [value.major, value.minor, value.patch]\n            if value.build is not None:\n                parts.append(value.build)\n        elif isinstance(value, str):\n            parts = [int(part) for part in value.split(\".\")]\n        else:\n            parts = list(value)\n\n        if len(parts) < 3 or len(parts) > 4:\n            raise ValueError(\"Expected format: major.minor.build.patch?\")\n\n        version = \".\".join(str(part) for part in parts)\n        self.events.first(ProjectID.FLVersion).value = version\n        if len(parts) == 4 and ProjectID.FLBuild in self.events.ids:\n            self.events.first(ProjectID.FLBuild).value = parts[3]\n"
  },
  {
    "path": "pyflp/py.typed",
    "content": ""
  },
  {
    "path": "pyflp/timemarker.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2022 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\n\"\"\"Contains the types required for pattern and playlist timemarkers.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\n\nfrom pyflp._descriptors import EventProp\nfrom pyflp._events import DWORD, TEXT, EventEnum, U8Event, U32Event\nfrom pyflp._models import EventModel, ModelReprMixin\n\n__all__ = [\"TimeMarkerID\", \"TimeMarkerType\", \"TimeMarker\"]\n\n\n@enum.unique\nclass TimeMarkerID(EventEnum):\n    Numerator = (33, U8Event)\n    Denominator = (34, U8Event)\n    Position = (DWORD + 20, U32Event)\n    Name = TEXT + 13\n\n\nclass TimeMarkerType(enum.IntEnum):\n    Marker = 0\n    \"\"\"Normal text marker.\"\"\"\n\n    Signature = 134217728\n    \"\"\"Used for time signature markers.\"\"\"\n\n\nclass TimeMarker(EventModel, ModelReprMixin):\n    \"\"\"A marker in the timeline of an :class:`Arrangement`.\n\n    ![](https://bit.ly/3gltKbt)\n    \"\"\"\n\n    def __str__(self) -> str:\n        if self.type == TimeMarkerType.Marker:\n            if self.name:\n                return f\"Marker {self.name!r} @ {self.position!r}\"\n            return f\"Unnamed marker @ {self.position!r}\"\n\n        time_sig = f\"{self.numerator}/{self.denominator}\"\n        if self.name:\n            return f\"Signature {self.name!r} ({time_sig}) @ {self.position!r}\"\n        return f\"Unnamed {time_sig} signature @ {self.position!r}\"\n\n    denominator: EventProp[int] = EventProp[int](TimeMarkerID.Denominator)\n    name = EventProp[str](TimeMarkerID.Name)\n    numerator = EventProp[int](TimeMarkerID.Numerator)\n\n    @property\n    def position(self) -> int | None:\n        if TimeMarkerID.Position in self.events.ids:\n            event = self.events.first(TimeMarkerID.Position)\n            if event.value < TimeMarkerType.Signature:\n                return event.value\n            return event.value - TimeMarkerType.Signature\n\n    @property\n    def type(self) -> TimeMarkerType | None:\n        \"\"\"The action with which a time marker is associated.\n\n        [![](https://bit.ly/3RDM1yn)]()\n        \"\"\"\n        if TimeMarkerID.Position in self.events.ids:\n            event = self.events.first(TimeMarkerID.Position)\n            if event.value >= TimeMarkerType.Signature:\n                return TimeMarkerType.Signature\n            return TimeMarkerType.Marker\n"
  },
  {
    "path": "pyflp/types.py",
    "content": "# PyFLP - An FL Studio project file (.flp) parser\n# Copyright (C) 2023 demberto\n#\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the Free\n# Software Foundation, either version 3 of the License, or (at your option)\n# any later version. This program is distributed in the hope that it will be\n# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General\n# Public License for more details. You should have received a copy of the\n# GNU General Public License along with this program. If not, see\n# <https://www.gnu.org/licenses/>.\n\nfrom __future__ import annotations\n\nimport enum\nfrom collections import UserDict, UserList\nfrom dataclasses import dataclass\nfrom typing import Any, NamedTuple, TypeVar, Union, TYPE_CHECKING\n\nimport construct\nimport construct_typed as ct\nfrom typing_extensions import ParamSpec, TypeAlias\n\nP = ParamSpec(\"P\")\nT = TypeVar(\"T\")\nU = TypeVar(\"U\")\nET = TypeVar(\"ET\", bound=Union[ct.EnumBase, enum.IntFlag])\nT_co = TypeVar(\"T_co\", covariant=True)\n\n\n@dataclass(frozen=True, order=True)\nclass FLVersion:\n    major: int\n    minor: int = 0\n    patch: int = 0\n    build: int | None = None\n\n    def __str__(self) -> str:\n        version = f\"{self.major}.{self.minor}.{self.patch}\"\n        if self.build is not None:\n            return f\"{version}.{self.build}\"\n        return version\n\n\nclass MusicalTime(NamedTuple):\n    bars: int\n    \"\"\"1 bar == 16 beats == 768 (internal representation).\"\"\"\n\n    beats: int\n    \"\"\"1 beat == 240 ticks == 48 (internal representation).\"\"\"\n\n    ticks: int\n    \"\"\"5 ticks == 1 (internal representation).\"\"\"\n\n\nclass RGBA(NamedTuple):\n    red: float\n    green: float\n    blue: float\n    alpha: float\n\n    @staticmethod\n    def from_bytes(buf: bytes) -> RGBA:\n        return RGBA(*(c / 255 for c in buf))\n\n    def __bytes__(self) -> bytes:\n        return bytes(round(c * 255) for c in self)\n\n\nif TYPE_CHECKING:\n    AnyContainer: TypeAlias = construct.Container[Any]\n    AnyListContainer: TypeAlias = construct.ListContainer[Any]\n    AnyDict: TypeAlias = UserDict[str, Any]\n    AnyList: TypeAlias = UserList[AnyContainer]\nelse:\n    AnyContainer = construct.Container\n    AnyListContainer = construct.ListContainer\n    AnyDict = UserDict\n    AnyList = UserList\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=61.0.0\", \"setuptools_scm[toml]>=6.2\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"pyflp\"\nauthors = [{ name = \"demberto\", email = \"demberto@protonmail.com\" }]\ndescription = \"FL Studio project file parser\"\nreadme = \"README.md\"\nrequires-python = \">=3.8\"\nclassifiers = [\n  \"Development Status :: 3 - Alpha\",\n  \"Intended Audience :: Developers\",\n  \"License :: OSI Approved :: GNU General Public License v3 (GPLv3)\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3.8\",\n  \"Programming Language :: Python :: 3.9\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: Implementation :: CPython\",\n  \"Programming Language :: Python :: Implementation :: PyPy\",\n  \"Topic :: Multimedia\",\n  \"Topic :: Software Development :: Libraries :: Python Modules\",\n  \"Typing :: Typed\",\n]\nlicense = { text = \"GPL-3.0\" }\ndependencies = [\n  \"f-enum>=0.2.0;python_version<='3.10'\",\n  \"construct-typing>=0.5.6\",\n  \"sortedcontainers>=2.4.0\",\n  \"typing_extensions>=4.6.1\",\n]\ndynamic = [\"version\"]\n\n[project.optional-dependencies]\ndev = [\n  \"coverage >=7.2.6\",\n  \"pre-commit >= 3.3.2\",\n  \"pytest >=7.3.1\",\n  \"tox >=4.5.1\",\n]\n# for docs dependencies see docs/requirements.txt\n\n[project.urls]\nSource = \"https://github.com/demberto/PyFLP\"\nChangelog = \"https://github.com/demberto/PyFLP/blob/master/CHANGELOG.md\"\nDocumentation = \"https://pyflp.rtfd.io\"\n\"Bug Tracker\" = \"https://github.com/demberto/PyFLP/issues\"\n\n[tool.black]\nline-length = 100\n\n[tool.coverage.run]\nbranch = true\nparallel = true\nomit = [\"main.py\"]\n\n[tool.coverage.report]\nexclude_lines = [\n  \"pragma: no cover\",         # Have to re-enable the standard pragma\n  \"def __repr__\",\n  \"\\\\.\\\\.\\\\.\",                # Ellipsis operator used in protocols\n  \"if owner is None:\",        # Descriptor __get__() checks\n  \"@(abc\\\\.)?abstractmethod\", # \"@abc.abstractmethod\" or \"@abstractmethod\"\n]\nignore_errors = true\n\n[tool.isort]\nprofile = \"black\"\nline-length = 100\n\n[tool.mypy]\npython_version = \"3.8\"\ncheck_untyped_defs = true\nenable_incomplete_feature = [\"Unpack\"]\nignore_missing_imports = true\nwarn_no_return = false\n\n[tool.pyright]\nreportPrivateUsage = false\nreportMissingTypeStubs = false\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\naddopts = \"-ra -q\"\ntestpaths = \"tests\"\n\n[tool.ruff]\ntarget-version = \"py38\"\nline-length = 100\n\n[tool.setuptools]\npackages = [\"pyflp\"]\n\n[tool.setuptools_scm]\nwrite_to = \"pyflp/_version.py\"\n"
  },
  {
    "path": "requirements.txt",
    "content": "construct-typing==0.5.6\nf-enum==0.2.0;python_version<=\"3.10\"\nsortedcontainers==2.4.0\ntyping_extensions==4.7.1\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "from __future__ import annotations\n\nimport pathlib\nfrom typing import TypeVar\n\nimport pytest\n\nimport pyflp\nfrom pyflp import Project\nfrom pyflp._events import EventEnum\nfrom pyflp._models import ModelBase\nfrom pyflp.mixer import Mixer\n\nMT = TypeVar(\"MT\", bound=ModelBase)\n\n\n@pytest.fixture(scope=\"session\")\ndef project():\n    return pyflp.parse(pathlib.Path(__file__).parent / \"assets\" / \"FL 20.8.4.flp\")\n\n\n@pytest.fixture(scope=\"session\")\ndef arrangements(project: Project):\n    return project.arrangements\n\n\n@pytest.fixture(scope=\"session\")\ndef rack(project: Project):\n    return project.channels\n\n\n@pytest.fixture(scope=\"session\")\ndef mixer(project: Project):\n    return project.mixer\n\n\n@pytest.fixture(scope=\"session\")\ndef inserts(mixer: Mixer):\n    return tuple(mixer)[:25]\n\n\n@pytest.fixture(scope=\"session\")\ndef patterns(project: Project):\n    return project.patterns\n\n\ndef get_model(suffix: str, type: type[MT], *only: EventEnum) -> MT:\n    parsed = pyflp.parse(pathlib.Path(__file__).parent / \"assets\" / suffix)\n    if only:\n        return type(parsed.events.subtree(lambda e: e.id in only))\n    return type(parsed.events)\n"
  },
  {
    "path": "tests/test_arrangement.py",
    "content": "from __future__ import annotations\n\nfrom typing import Callable\n\nimport pytest\n\nfrom pyflp._events import RGBA\nfrom pyflp.arrangement import (\n    Arrangement,\n    Arrangements,\n    ChannelPLItem,\n    PatternPLItem,\n    Track,\n    TrackMotion,\n    TrackPress,\n    TrackSync,\n)\n\n\ndef test_arrangements(arrangements: Arrangements):\n    assert len(arrangements) == 2\n    assert arrangements.current == arrangements[0]\n    assert arrangements.loop_pos == (3840, 5376)\n    assert arrangements.max_tracks == 500\n    assert arrangements.time_signature.num == 4\n    assert arrangements.time_signature.beat == 4\n\n\n@pytest.fixture(scope=\"session\")\ndef arrangement(arrangements: Arrangements):\n    def wrapper(index: int):\n        return arrangements[index]\n\n    return wrapper\n\n\n@pytest.fixture(scope=\"session\")\ndef tracks(arrangement: Callable[[int], Arrangement]):\n    return tuple(arrangement(0).tracks)[:22]\n\n\ndef test_track_color(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert (\n            track.color == RGBA(1.0, 0.0, 0.0, 0.0)\n            if track.name == \"Red\"\n            else track.color == RGBA.from_bytes(bytes((72, 81, 86, 0)))\n        )\n\n\ndef test_track_content_locked(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert (\n            track.content_locked if track.name == \"Locked to content\" else not track.content_locked\n        )\n\n\ndef test_track_enabled(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert not track.enabled if track.name == \"Disabled\" else track.enabled\n\n\ndef test_track_grouped(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert track.grouped if track.name == \"Grouped\" else not track.grouped\n\n\ndef test_track_height(tracks: tuple[Track, ...]):\n    for track in tracks:\n        if track.name == \"Min Size\":\n            assert track.height == \"0%\"\n        elif track.name == \"Max Size\":\n            assert track.height == \"1000%\"\n        else:\n            assert track.height == \"100%\"\n\n\ndef test_track_icon(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert track.icon == 70 if track.name == \"Iconified\" else not track.icon\n\n\ndef test_track_items(tracks: tuple[Track, ...]):\n    for track in tracks:\n        num_items = 0\n        if track.name == \"Audio track\":\n            num_items = 16\n            assert {type(i) for i in track} == {ChannelPLItem}\n            assert {i.channel.iid for i in track} == {11}  # type: ignore\n        elif track.name == \"MIDI\":\n            num_items = 4\n            assert {type(i) for i in track} == {PatternPLItem}\n            assert {i.pattern.iid for i in track} == {3}  # type: ignore\n            assert [i.position for i in track] == [p * 384 for p in range(num_items)]\n        elif track.name in (\"Cut pattern\", \"Automation\"):\n            num_items = 1\n\n        assert len(track) == num_items\n        assert [i.group for i in track] == [0] * num_items\n\n\ndef test_track_locked(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert track.locked if track.name == \"Locked\" else not track.locked\n\n\ndef test_track_motion(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert (\n            track.motion == TrackMotion.Random\n            if track.name == \"Random Motion\"\n            else track.motion == TrackMotion.Stay\n        )\n\n\ndef test_track_name(tracks: tuple[Track, ...]):\n    assert [track.name for track in tracks] == [\n        None,\n        \"Enabled\",\n        \"Disabled\",\n        \"Locked\",\n        \"Red\",\n        \"Iconified\",\n        \"Grouped\",\n        \"Audio track\",\n        \"Instrument track\",\n        \"MIDI\",\n        \"Cut pattern\",\n        \"Automation\",\n        \"Locked to content\",\n        \"Locked to size\",\n        \"Min Size\",\n        \"Max Size\",\n        \"Latched\",\n        \"Random Motion\",\n        \"Trigger Sync OFF\",\n        \"Position Sync AUTO\",\n        \"Queued\",\n        \"Intolerant\",\n    ]\n\n\ndef test_track_position_sync(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert (\n            track.position_sync == TrackSync.Auto\n            if track.name == \"Position Sync AUTO\"\n            else track.position_sync == TrackSync.Off\n        )\n\n\ndef test_track_press(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert (\n            track.press == TrackPress.Latch\n            if track.name == \"Latched\"\n            else track.press == TrackPress.Retrigger\n        )\n\n\ndef test_track_tolerant(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert not track.tolerant if track.name == \"Intolerant\" else track.tolerant\n\n\ndef test_track_queued(tracks: tuple[Track, ...]):\n    for track in tracks:\n        assert track.queued if track.name == \"Queued\" else not track.queued\n\n\ndef test_first_arrangement(arrangement: Callable[[int], Arrangement]):\n    arr = arrangement(0)\n    assert arr.name == \"Just tracks\"\n    assert not tuple(arr.timemarkers)\n    assert len(tuple(arr.tracks)) == 500\n\n\ndef test_second_arrangement(arrangement: Callable[[int], Arrangement]):\n    arr = arrangement(1)\n    assert arr.name == \"Just timemarkers\"\n    assert len(tuple(arr.timemarkers)) == 11\n    assert len(tuple(arr.tracks)) == 500\n"
  },
  {
    "path": "tests/test_channel.py",
    "content": "from __future__ import annotations\n\nimport pathlib\nfrom typing import TypeVar\n\nfrom pyflp._events import RGBA\nfrom pyflp.channel import (\n    Automation,\n    Channel,\n    ChannelRack,\n    DeclickMode,\n    FilterType,\n    Instrument,\n    Layer,\n    LFOShape,\n    ReverbType,\n    Sampler,\n    StretchMode,\n)\nfrom pyflp.project import Project\n\nfrom .conftest import get_model\n\nCT = TypeVar(\"CT\", bound=Channel)\n\n\ndef _load_channel(preset: str, type: type[CT]):\n    return get_model(f\"channels/{preset}\", type)\n\n\n# This is separated only to pass type checks\n# (preset: str, type: type[CT] = Channel) -> CT messes inferred return type\ndef load_channel(preset: str):\n    return _load_channel(preset, Channel)\n\n\ndef load_automation(preset: str):\n    return _load_channel(preset, Automation)\n\n\ndef load_instrument(preset: str):\n    return _load_channel(preset, Instrument)\n\n\ndef load_layer(preset: str):\n    return _load_channel(preset, Layer)\n\n\ndef load_sampler(preset: str):\n    return _load_channel(preset, Sampler)\n\n\ndef test_channels(project: Project, rack: ChannelRack):\n    assert len(rack) == project.channel_count\n    assert rack.fit_to_steps is None\n    assert rack.height == 646\n    assert [group.name for group in rack.groups] == [\"Audio\", \"Generators\", \"Unsorted\"]\n    assert not rack.swing\n\n\ndef test_automation_lfo():\n    lfo = load_automation(\"automation-lfo.fst\").lfo\n    assert lfo.amount == 64\n\n\ndef test_automation_points():\n    points = [point for point in load_automation(\"automation-points.fst\")]\n    assert [int(p.position or 0) for p in points] == [0, 8, 8, 16, 24, 32]\n\n\ndef test_channel_color():\n    assert load_channel(\"colored.fst\").color == RGBA.from_bytes(bytes((20, 20, 255, 0)))\n\n\ndef test_channel_enabled():\n    assert not load_channel(\"disabled.fst\").enabled\n\n\ndef test_channel_group(rack: ChannelRack):\n    for channel in rack:\n        if channel.name == \"22in Kick\":\n            assert channel.group.name == \"Audio\"\n        elif channel.display_name in (\"BooBass\", \"Fruit Kick\", \"Plucked!\"):\n            assert channel.group.name == \"Generators\"\n        else:\n            assert channel.group.name == \"Unsorted\"\n\n\ndef test_channel_icon():\n    assert load_channel(\"iconified.fst\").icon == 116\n\n\ndef test_channel_pan():\n    assert load_channel(r\"100%-left.fst\").pan == 0\n    assert load_channel(r\"100%-right.fst\").pan == 12800\n\n\ndef test_channel_volume():\n    assert load_channel(\"full-volume.fst\").volume == 12800\n    assert not load_channel(\"zero-volume.fst\").volume\n\n\ndef test_channel_zipped(rack: ChannelRack):\n    for channel in rack:\n        if channel.name == \"Zipped\":\n            assert channel.zipped\n        else:\n            assert not channel.zipped\n\n\ndef test_instrument_delay():\n    delay = load_instrument(\"delay.fst\").delay\n    assert delay.feedback == 12800\n    assert delay.echoes == 10\n    assert delay.fat_mode\n    assert delay.mod_x == 0\n    assert delay.mod_y == 256\n    assert delay.pan == -6400\n    assert delay.ping_pong\n    assert delay.time == 144\n\n\ndef test_instrument_keyboard():\n    keyboard = load_instrument(\"keyboard.fst\").keyboard\n    assert keyboard.add_root\n    assert keyboard.fine_tune == 100\n    assert keyboard.key_region == (48, 72)\n    assert keyboard.main_pitch\n    assert keyboard.root_note == 60\n\n\ndef test_instrument_polyphony():\n    polyphony = load_instrument(\"polyphony.fst\").polyphony\n    assert polyphony.mono\n    assert polyphony.porta\n    assert polyphony.max == 4\n    assert polyphony.slide == 820\n\n\ndef test_instrument_routing():\n    assert load_instrument(\"routed.fst\").insert == 125\n\n\ndef test_instrument_time():\n    time = load_instrument(\"time.fst\").time\n    assert time.full_porta\n    assert time.gate == 450\n    assert time.shift == 1024\n    assert time.swing == 64\n\n\ndef test_instrument_tracking():\n    tracking = load_instrument(\"tracking.fst\").tracking\n    assert tracking and len(tracking) == 2\n\n    key_tracking = tracking[\"keyboard\"]\n    assert key_tracking.middle_value == 84\n    assert key_tracking.mod_x == -256\n    assert key_tracking.mod_y == 256\n    assert key_tracking.pan == 256\n\n\n# ! Apparently, layer children events aren't stored in presets\n# def test_layer_children(): pass\n\n\ndef test_layer_crossfade():\n    assert load_layer(\"layer-crossfade.fst\").crossfade\n\n\ndef test_layer_random():\n    assert load_layer(\"layer-random.fst\").random\n\n\ndef test_sampler_content():\n    content = load_sampler(\"sampler-content.fst\").content\n    assert content.keep_on_disk\n    assert content.resample\n    assert not content.load_regions\n    assert not content.load_slices\n    assert content.declick_mode == DeclickMode.Generic\n\n\ndef test_sampler_cut_group():\n    assert load_sampler(\"cut-groups.fst\").cut_group == (1, 2)\n\n\ndef test_sampler_envelopes():\n    envelopes = load_sampler(\"envelope.fst\").envelopes\n    assert envelopes and len(envelopes) == 5\n\n    volume = envelopes[\"Volume\"]\n    assert volume.enabled\n    assert volume.predelay == 100\n    assert volume.attack == 100\n    assert volume.hold == 100\n    assert volume.decay == 100\n    assert volume.sustain == 0\n    assert volume.release == 100\n    assert volume.synced\n    assert volume.attack_tension == volume.release_tension == volume.decay_tension == 0\n\n    mod_x = envelopes[\"Mod X\"]\n    assert mod_x.enabled\n    assert mod_x.predelay == 65536\n    assert mod_x.attack == 65536\n    assert mod_x.hold == 65536\n    assert mod_x.decay == 65536\n    assert mod_x.sustain == 128\n    assert mod_x.release == 65536\n    assert mod_x.amount == 128\n    assert not mod_x.synced\n    assert mod_x.attack_tension == mod_x.release_tension == mod_x.decay_tension == 128\n\n\ndef test_sampler_filter():\n    filter = load_sampler(\"sampler-filter.fst\").filter\n    assert filter.mod_x == 0\n    assert filter.mod_y == 256\n    assert filter.type == FilterType.SVFLPx2\n\n\ndef test_sampler_fx():\n    fx = load_sampler(\"sampler-fx.fst\").fx\n    assert fx.boost == 128\n    assert fx.clip\n    assert fx.cutoff == 16\n    assert fx.crossfade == 0\n    assert fx.fade_in == 1024\n    assert fx.fade_out == 0\n    assert fx.fade_stereo\n    assert fx.fix_trim\n    assert fx.freq_tilt == 0\n    assert fx.length == 1.0\n    assert not fx.normalize\n    assert fx.pogo == 256\n    assert fx.inverted\n    assert not fx.remove_dc\n    assert fx.resonance == 640\n    assert fx.reverb.type == ReverbType.A\n    assert fx.reverb.mix == 128\n    assert not fx.reverse\n    assert fx.ringmod == (64, 192)\n    assert fx.start == 0.0\n    assert fx.stereo_delay == 4096\n    assert fx.swap_stereo\n    assert fx.trim == 256\n\n\ndef test_sampler_lfo():\n    lfos = load_sampler(\"lfo.fst\").lfos\n    assert lfos and len(lfos) == 5\n\n    volume = lfos[\"Volume\"]\n    assert volume.amount == 128\n    assert volume.attack == 65536\n    assert volume.predelay == 100\n    assert volume.shape == LFOShape.Pulse\n    assert volume.speed == 65536\n    assert volume.retrig\n    assert not volume.synced\n\n    mod_x = lfos[\"Mod X\"]\n    assert mod_x.amount == -128\n    assert mod_x.attack == 100\n    assert mod_x.predelay == 65536\n    assert mod_x.shape == LFOShape.Sine\n    assert mod_x.speed == 200\n    assert not mod_x.retrig\n    assert mod_x.synced\n\n\ndef test_sampler_path():\n    assert load_sampler(\"sampler-path.fst\").sample_path == pathlib.Path(\n        r\"%FLStudioFactoryData%\\Data\\Patches\\Packs\\Drums\\Kicks\\22in Kick.wav\"\n    )\n\n\ndef test_sampler_pitch_shift():\n    assert load_sampler(\"+4800-cents.fst\").pitch_shift == 4800\n    assert load_sampler(\"-4800-cents.fst\").pitch_shift == -4800\n\n\ndef test_sampler_playback():\n    playback = load_sampler(\"sampler-playback.fst\").playback\n    assert playback.use_loop_points\n    assert playback.ping_pong_loop\n    assert playback.start_offset == 1072693248\n\n\ndef test_sampler_stretching():\n    stretching = load_sampler(\"sampler-stretching.fst\").stretching\n    assert stretching.mode == StretchMode.E3Generic\n    assert stretching.multiplier == 0.25\n    assert stretching.pitch == 1200\n    assert stretching.time == (4, 0, 0)\n"
  },
  {
    "path": "tests/test_corrupted.py",
    "content": "from __future__ import annotations\n\nimport pathlib\n\nimport pytest\n\nimport pyflp\nfrom pyflp.exceptions import HeaderCorrupted\n\nCORRUPTED = pathlib.Path(__file__).parent / \"assets\" / \"corrupted\"\n\n\ndef test_invalid_header_magic():\n    with pytest.raises(HeaderCorrupted, match=\"FLhd\"):\n        pyflp.parse(CORRUPTED / \"invalid-header-magic.flp\")\n\n\ndef test_invalid_header_size():\n    with pytest.raises(HeaderCorrupted, match=\"6\"):\n        pyflp.parse(CORRUPTED / \"invalid-header-size.flp\")\n\n\ndef test_invalid_format():\n    with pytest.raises(HeaderCorrupted, match=\"Unsupported project file format\"):\n        pyflp.parse(CORRUPTED / \"invalid-format.flp\")\n\n\ndef test_invalid_ppq():\n    with pytest.raises(HeaderCorrupted, match=\"Invalid PPQ\"):\n        # ! Opening this FLP in FL will crash it with a division by zero error\n        pyflp.parse(CORRUPTED / \"invalid-ppq.flp\")\n\n\ndef test_invalid_data_magic():\n    with pytest.raises(HeaderCorrupted, match=\"FLdt\"):\n        pyflp.parse(CORRUPTED / \"invalid-data-magic.flp\")\n\n\ndef test_invalid_data_size():\n    with pytest.raises(HeaderCorrupted, match=\"Data chunk size corrupted\"):\n        pyflp.parse(CORRUPTED / \"invalid-event-size.flp\")\n"
  },
  {
    "path": "tests/test_events.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom pyflp._events import AsciiEvent, EventEnum, EventTree, U8Event, WORD\nfrom pyflp.exceptions import EventIDOutOfRange, InvalidEventChunkSize\n\n\ndef test_id_out_of_range():\n    with pytest.raises(EventIDOutOfRange, match=str(tuple(range(0, WORD)))):\n        U8Event(EventEnum(128), b\"\\x00\")\n\n    with pytest.raises(ValueError):\n        AsciiEvent(EventEnum(0), b\"1234-decode-me-baby\")\n\n\ndef test_invalid_chunk_size():\n    with pytest.raises(InvalidEventChunkSize, match=\"1\"):\n        U8Event(EventEnum(0), b\"12\")\n\n\ndef test_event_tree():\n    root = EventTree()\n    child = EventTree(root)\n    assert child in root.children\n    event = U8Event(EventEnum(0), b\"\\x01\")\n    child.append(event)\n    assert root.first(EventEnum(0)) == event\n    child.remove(EventEnum(0))\n    assert not root\n"
  },
  {
    "path": "tests/test_mixer.py",
    "content": "from __future__ import annotations\n\nfrom typing import cast\n\nfrom pyflp._events import RGBA\nfrom pyflp.mixer import Insert, InsertDock, Mixer, MixerID, MixerParamsEvent\n\nfrom .conftest import get_model\n\n\ndef get_insert(preset: str):\n    # Parse as Mixer to get events, because an Insert cannot parse\n    # MixerID.Params which holds most of its information.\n    mixer = get_model(f\"inserts/{preset}\", Mixer)\n\n    # A preset stores items only for a single insert, currently thats 32 per\n    # insert. Pass these to Insert's constructor. This mimics Mixer's normal\n    # behaviour, however that depends on InsertID.Output as a marker to indicate\n    # the end of an Insert, which surprisingly isn't a part of presets.\n    params = cast(MixerParamsEvent, mixer.events.first(MixerID.Params))\n    items = tuple(params.items_.values())[0]\n    return Insert(mixer.events, iid=0, max_slots=10, params=items)\n\n\ndef test_insert_bypassed():\n    assert get_insert(\"effects-bypassed.fst\").bypassed\n\n\ndef test_insert_channels_swapped():\n    assert get_insert(\"channels-swapped.fst\").channels_swapped\n\n\ndef test_insert_color():\n    assert get_insert(\"colored.fst\").color == RGBA.from_bytes(bytes((255, 20, 20, 0)))\n\n\ndef test_insert_dock(inserts: tuple[Insert, ...]):\n    sends = (101, 102, 103, 104)\n    for insert in inserts:\n        if insert.name in (\"Docked left\", \"Master\"):\n            assert insert.dock == InsertDock.Left\n        elif insert.name == \"Docked right\" or insert.iid in sends:\n            assert insert.dock == InsertDock.Right\n        else:\n            assert insert.dock == InsertDock.Middle\n\n\ndef test_insert_enabled():\n    assert not get_insert(\"disabled.fst\").enabled\n\n\ndef test_insert_locked():\n    assert get_insert(\"locked.fst\").locked\n\n\ndef test_insert_pan():\n    assert get_insert(r\"100%-left.fst\").pan == -6400\n    assert get_insert(r\"100%-right.fst\").pan == 6400\n\n\ndef test_insert_polarity_reversed():\n    assert get_insert(\"polarity-reversed.fst\").polarity_reversed\n\n\ndef test_insert_routes(inserts: tuple[Insert, ...]):\n    assert not tuple(inserts[5].routes)\n\n\ndef test_insert_stereo_separation():\n    assert get_insert(r\"100%-merged.fst\").stereo_separation == 64\n    assert get_insert(r\"100%-separated.fst\").stereo_separation == -64\n\n\ndef test_insert_eq():\n    eq = get_insert(\"post-eq.fst\").eq\n    assert eq.low.freq == 0\n    assert eq.low.gain == 1800\n    assert eq.low.reso == 0\n    assert eq.mid.freq == 33145\n    assert eq.mid.gain == 0\n    assert eq.mid.reso == 17500\n    assert eq.high.freq == 65536\n    assert eq.high.gain == -1800\n    assert eq.high.reso == 65536\n\n\ndef test_mixer(mixer: Mixer):\n    assert mixer.apdc\n    assert len(mixer) == mixer.max_inserts == 127\n    assert mixer.max_slots == 10\n"
  },
  {
    "path": "tests/test_models.py",
    "content": "from __future__ import annotations\n\nfrom pyflp.types import FLVersion\n\n\ndef test_flversion():\n    assert str(FLVersion(20, 8, 4)) == \"20.8.4\"\n    assert str(FLVersion(20, 8, 4, 2576)) == \"20.8.4.2576\"\n"
  },
  {
    "path": "tests/test_pattern.py",
    "content": "from __future__ import annotations\n\nfrom pyflp._events import RGBA\nfrom pyflp.pattern import Pattern, PatternID, Patterns\n\nfrom .conftest import get_model\n\n\ndef get_notes(score: str):\n    return tuple(get_model(f\"patterns/{score}\", Pattern, *PatternID).notes)\n\n\ndef test_patterns(patterns: Patterns):\n    assert len(patterns) == 5\n    assert patterns.current == patterns[4]\n    assert patterns.play_cut_notes\n\n\ndef test_pattern_color(patterns: Patterns):\n    assert patterns[2].color == RGBA(0.0, 1.0, 0.0, 0.0)\n\n\ndef test_pattern_names(patterns: Patterns):\n    assert {pattern.name for pattern in patterns} == {\n        \"Default\",\n        \"Colored\",\n        \"MIDI\",\n        \"Timemarkers\",\n        \"Selected\",\n    }\n\n\ndef test_pattern_timemarkers(patterns: Patterns):\n    assert len(tuple(patterns[\"Timemarkers\"].timemarkers)) == 5\n\n\ndef test_empty_pattern():\n    assert not len(get_notes(\"empty.fsc\"))\n\n\ndef test_note_color():\n    assert get_notes(\"color-9.fsc\")[0].midi_channel == 8\n\n\ndef test_note_fine_pitch():\n    assert [n.fine_pitch for n in get_notes(\"fine-pitch-min-max.fsc\")] == [0, 240]\n\n\ndef test_note_group():\n    assert [n.group for n in get_notes(\"common-group.fsc\")] == [1, 1]\n\n\ndef test_note_length():\n    assert get_notes(\"c5-1bar.fsc\")[0].length == 384\n\n\ndef test_note_mod_x():\n    assert [n.mod_x for n in get_notes(\"modx-min-max.fsc\")] == [255, 0]\n\n\ndef test_note_mod_y():\n    assert [n.mod_y for n in get_notes(\"mody-min-max.fsc\")] == [0, 255]\n\n\ndef test_note_key():\n    c_major = [\"C5\", \"D5\", \"E5\", \"F5\", \"G5\", \"A5\", \"B5\", \"C6\"]\n    assert [n.key for n in get_notes(\"c-major-scale.fsc\")] == c_major\n\n\ndef test_note_pan():\n    assert [n.pan for n in get_notes(\"pan-min-max.fsc\")] == [128, 0]\n\n\ndef test_note_position():\n    notes = get_notes(\"c-major-scale.fsc\")\n    assert [n.position for n in notes] == [x * 384 for x in range(8)]\n\n\ndef test_note_rack_channel():\n    assert {n.rack_channel for n in get_notes(\"multi-channel.flp\")} == {0, 1}\n\n\ndef test_note_release():\n    assert [n.release for n in get_notes(\"release-min-max.fsc\")] == [0, 128]\n\n\ndef test_note_slide():\n    assert get_notes(\"slide-note.fsc\")[0].slide\n\n\ndef test_note_velocity():\n    assert [n.velocity for n in get_notes(\"velocity-min-max.fsc\")] == [0, 128]\n"
  },
  {
    "path": "tests/test_plugin.py",
    "content": "from __future__ import annotations\n\nfrom typing import TypeVar\n\nfrom pyflp.plugin import (\n    AnyPlugin,\n    BooBass,\n    FruitKick,\n    FruityBalance,\n    FruityBloodOverdrive,\n    FruityCenter,\n    FruityFastDist,\n    FruitySend,\n    FruitySoftClipper,\n    FruityStereoEnhancer,\n    Plucked,\n    PluginID,\n    Soundgoodizer,\n    VSTPlugin,\n    WrapperPage,\n)\n\nfrom .conftest import get_model\n\nT = TypeVar(\"T\", bound=AnyPlugin)\n\n\ndef get_plugin(preset_file: str, type: type[T]):\n    return get_model(f\"plugins/{preset_file}\", type, PluginID.Data, PluginID.Wrapper)\n\n\ndef test_boobass():\n    boobass = get_plugin(\"boobass.fst\", BooBass)\n    assert boobass.bass == boobass.mid == boobass.high == 32767\n\n\ndef test_fruit_kick():\n    fruit_kick = get_plugin(\"fruit-kick.fst\", FruitKick)\n    assert fruit_kick.max_freq == -876\n    assert fruit_kick.min_freq == 75\n    assert fruit_kick.freq_decay == 163\n    assert fruit_kick.amp_decay == 208\n    assert fruit_kick.click == 39\n    assert fruit_kick.distortion == 62\n\n\ndef test_fruity_balance():\n    fruity_balance = get_plugin(\"fruity-balance.fst\", FruityBalance)\n    assert fruity_balance.volume == 256\n    assert fruity_balance.pan == 0\n\n\ndef test_fruity_blood_overdrive():\n    fruity_blood_overdrive = get_plugin(\"fruity-blood-overdrive.fst\", FruityBloodOverdrive)\n    assert fruity_blood_overdrive.pre_band == 0\n    assert fruity_blood_overdrive.color == 5000\n    assert fruity_blood_overdrive.pre_amp == 0\n    assert fruity_blood_overdrive.x100 == 0\n    assert fruity_blood_overdrive.post_filter == 0\n\n\ndef test_fruity_center():\n    fruity_center = get_plugin(\"fruity-center.fst\", FruityCenter)\n    assert not fruity_center.enabled\n\n\ndef test_fruity_fast_dist():\n    fruity_fast_dist = get_plugin(\"fruity-fast-dist.fst\", FruityFastDist)\n    assert fruity_fast_dist.pre == 128\n    assert fruity_fast_dist.threshold == 10\n    assert fruity_fast_dist.kind == \"A\"\n    assert fruity_fast_dist.mix == 128\n    assert fruity_fast_dist.post == 128\n\n\ndef test_fruity_send():\n    fruity_send = get_plugin(\"fruity-send.fst\", FruitySend)\n    assert fruity_send.dry == 256\n    assert fruity_send.send_to == -1\n    assert fruity_send.pan == 0\n    assert fruity_send.volume == 256\n\n\ndef test_fruity_soft_clipper():\n    fruity_soft_clipper = get_plugin(\"fruity-soft-clipper.fst\", FruitySoftClipper)\n    assert fruity_soft_clipper.threshold == 100\n    assert fruity_soft_clipper.post == 128\n\n\ndef test_fruity_stereo_enhancer():\n    fruity_stereo_enhancer = get_plugin(\"fruity-stereo-enhancer.fst\", FruityStereoEnhancer)\n    assert fruity_stereo_enhancer.stereo_separation == 0\n    assert fruity_stereo_enhancer.effect_position == \"post\"\n    assert fruity_stereo_enhancer.phase_offset == 0\n    assert fruity_stereo_enhancer.phase_inversion == \"none\"\n    assert fruity_stereo_enhancer.pan == 0\n    assert fruity_stereo_enhancer.volume == 256\n\n\ndef test_plucked():\n    plucked = get_plugin(\"plucked.fst\", Plucked)\n    assert plucked.decay == 176\n    assert plucked.color == 56\n    assert plucked.normalize\n    assert plucked.gate\n    assert not plucked.widen\n\n\ndef test_soundgoodizer():\n    soundgoodizer = get_plugin(\"soundgoodizer.fst\", Soundgoodizer)\n    assert soundgoodizer.amount == 600\n    assert soundgoodizer.mode == \"A\"\n\n\ndef test_vst_plugin():\n    djmfilter = get_plugin(\"xfer-djmfilter.fst\", VSTPlugin)\n    assert djmfilter.name == \"DJMFilter\"\n    assert djmfilter.vendor == \"Xfer Records\"\n    assert (\n        djmfilter.plugin_path\n        == r\"C:\\Program Files\\Common Files\\VST2\\Xfer Records\\DJMFilter_x64.dll\"\n    )\n\n\ndef test_fruity_wrapper():\n    wrapper = get_plugin(\"fruity-wrapper.fst\", VSTPlugin)\n\n    # WrapperEvent properties\n    assert not wrapper.compact\n    assert not wrapper.demo_mode\n    assert not wrapper.detached\n    assert not wrapper.directx\n    assert not wrapper.disabled\n    assert wrapper.generator\n    assert wrapper.height == 410\n    assert not wrapper.minimized\n    assert wrapper.multithreaded\n    assert wrapper.page == WrapperPage.Settings\n    assert not wrapper.smart_disable\n    assert wrapper.visible\n    assert wrapper.width == 561\n\n    # VSTPluginEvent properties\n    assert wrapper.automation.notify_changes\n    assert wrapper.compatibility.buffers_maxsize\n    assert wrapper.compatibility.fast_idle\n    assert not wrapper.compatibility.fixed_buffers\n    assert wrapper.compatibility.process_maximum\n    assert wrapper.compatibility.reset_on_transport\n    assert wrapper.compatibility.send_loop\n    assert not wrapper.compatibility.use_time_offset\n    assert wrapper.midi.input == 6\n    assert wrapper.midi.output == 9\n    assert wrapper.midi.pb_range == 36\n    assert not wrapper.midi.send_modx\n    assert not wrapper.midi.send_pb\n    assert wrapper.midi.send_release\n    assert wrapper.processing.allow_sd\n    assert not wrapper.processing.bridged\n    assert wrapper.processing.keep_state\n    assert wrapper.processing.multithreaded\n    assert wrapper.processing.notify_render\n    assert wrapper.ui.accept_drop\n    assert not wrapper.ui.always_update\n    assert wrapper.ui.dpi_aware\n    assert not wrapper.ui.scale_editor\n"
  },
  {
    "path": "tests/test_project.py",
    "content": "from __future__ import annotations\n\nimport datetime\nimport pathlib\nimport textwrap\n\nimport pytest\n\nimport pyflp\nfrom pyflp.project import VALID_PPQS, FileFormat, FLVersion, PanLaw, Project\n\n\ndef test_project(project: Project):\n    assert project.artists == \"demberto\"\n    assert project.channel_count == 19\n    assert (\n        project.comments\n        == textwrap.dedent(\n            \"\"\"\\\n    This is a testing FLP used by PyFLP - An FL Studio project file parser.\n\n    Notes for contributors:\n    1. Make a separate item for every testable property (and its inverse if its a bool).\n    2. Give item names related to the property they will be tested for.\n\n    Terms:\n    \"item(s)\": Refers to a channel, insert, slot, track, pattern, timemarker, etc.\n    \"\"\"\n        ).replace(\"\\n\", \"\\r\")\n    )  # Who the hell uses \\r?\n    assert project.created_on == datetime.datetime(2022, 9, 16, 20, 47, 12, 746000)\n    assert project.data_path == pathlib.Path(\"\")\n    assert project.format == FileFormat.Project\n    assert project.genre == \"Testing...\"\n    assert project.licensed\n    assert project.licensee == \"VIKTORKHLEBNIKOV38394416\"\n    assert project.looped\n    assert project.main_pitch == 0\n    assert project.main_volume is None\n    assert project.pan_law == PanLaw.Circular\n    assert project.ppq == 96\n    assert project.show_info\n    assert project.tempo == 69.420\n    # ! assert project.time_spent == datetime.timedelta(hours=2, minutes=35, seconds=53)\n    assert project.title == \"PyFLP Test FLP\"\n    assert project.url == \"https://github.com/demberto/PyFLP\"\n    assert project.version == FLVersion(20, 8, 4, 2576)\n\n    with pytest.raises(ValueError, match=\"cannot be less than zero\"):\n        project.channel_count = -1\n\n    with pytest.raises(ValueError, match=f\"{VALID_PPQS}\"):\n        project.ppq = 0\n\n    with pytest.raises(ValueError, match=\"10.0-522.0\"):\n        project.tempo = 999.0\n\n    with pytest.raises(ValueError, match=\"major.minor.build.patch?\"):\n        project.version = \"2.2\"  # type: ignore\n\n\ndef test_null_check(project: Project, tmp_path: pathlib.Path):\n    pyflp.save(project, tmp_path / \"null_check.flp\")\n    b1 = open(pathlib.Path(__file__).parent / \"assets\" / \"FL 20.8.4.flp\", \"rb\").read()\n    b2 = open(tmp_path / \"null_check.flp\", \"rb\").read()\n    # result = b1 == b2  # ! Don't compare 2 big bytes objects in pytest EVER\n    assert b1 == b2\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = precommit,py{38,39,310,311},pypy{38,39},docs\nminversion = 4.0\nparallel = auto\n\n[testenv]\ndeps =\n  -rrequirements.txt\n  coverage\n  mypy\n  pytest\ncommands =\n  coverage run -m pytest\n  mypy pyflp\n\n[testenv:precommit]\nskip_install = True\ndeps = pre-commit\ncommands = pre-commit run --all-files\n\n[testenv:docs]\n# Exclude GH Actions Mac OS runners due to PyEnchant (needed by sphinxcontrib.spelling)\n# issue on Apple silicon, see https://github.com/pyenchant/pyenchant/issues/265\nplatform = ^((?!darwin).)*$\nbase_python = py310\ndeps =\n  -rdocs/requirements.txt\n  -rrequirements.txt\ncommands =\n  sphinx-build -b linkcheck docs docs/_build/linkcheck\n\n[gh]\npython =\n  3.8: py38\n  3.9: py39\n  3.10: py310, docs\n  3.11: py311, precommit\n  pypy-3.8: pypy38\n  pypy-3.9: pypy39\n"
  }
]