├── .gitignore
├── LICENSE
├── README.md
├── examples
├── 808basic.pti
├── bbb.pti
├── moodypad.pti
└── original_files
│ ├── LEGAL LICENSE INFO READ ME.html
│ ├── 080_bella-bonga-beat.wav
│ ├── 808_basic
│ ├── cl_hihat.wav
│ ├── claves.wav
│ ├── conga1.wav
│ ├── cowbell.wav
│ ├── crashcym.wav
│ ├── handclap.wav
│ ├── hi_conga.wav
│ ├── hightom.wav
│ ├── kick1.wav
│ ├── kick2.wav
│ ├── maracas.wav
│ ├── open_hh.wav
│ ├── rimshot.wav
│ ├── snare.wav
│ └── tom1.wav
│ ├── bbb_slices
│ ├── bbb 001.wav
│ ├── bbb 002.wav
│ ├── bbb 003.wav
│ ├── bbb 004.wav
│ ├── bbb 005.wav
│ ├── bbb 006.wav
│ ├── bbb 007.wav
│ ├── bbb 008.wav
│ └── bbb 009.wav
│ ├── moody-pad.wav
│ └── moody_pad_left.wav
├── polyend_tracker_pti_creator
├── __init__.py
├── __pycache__
│ └── __init__.cpython-38.pyc
├── creator.py
└── utils
│ ├── __init__.py
│ ├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── exceptions.cpython-38.pyc
│ └── settings.cpython-38.pyc
│ ├── args.py
│ ├── audio
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── audio.cpython-38.pyc
│ │ └── wave_chunk_parser_extended.cpython-38.pyc
│ ├── audio.py
│ └── wave_chunk_parser_extended.py
│ ├── exceptions.py
│ ├── pti
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── constants.cpython-38.pyc
│ │ ├── header.cpython-38.pyc
│ │ └── pti.cpython-38.pyc
│ ├── constants.py
│ ├── header.py
│ └── pti.py
│ └── settings.py
├── requirements.txt
├── setup.py
├── standard.rc
├── test_requirements.txt
└── tests
├── __init__.py
├── __pycache__
└── __init__.cpython-38.pyc
├── integ
├── __init__.py
└── test_integ.py
└── utils
├── __init__.py
├── __pycache__
├── __init__.cpython-38.pyc
└── test_settings.cpython-38.pyc
├── audio
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── test_audio.cpython-38.pyc
│ └── test_wave_chunk_parser_extended.cpython-38.pyc
├── test_audio.py
└── test_wave_chunk_parser_extended.py
├── files
├── default_header.pti
├── test_tone.wav
├── tone.pti
├── tone.wav
├── tone2.wav
├── tone_os_1.pti
├── tone_os_2.pti
└── tone_slice.pti
├── pti
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── test_header.cpython-38.pyc
│ └── test_pti.cpython-38.pyc
├── test_header.py
└── test_pti.py
└── test_settings.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 | .coverage
4 | polyend_tracker_pti_creator.egg-info/
5 | build/
6 | *.pyc
7 | env/
8 | venv/
9 | test/integ
10 | tests/utils/files/integ/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 adriananders
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # polyend-tracker-pti-creator
2 | Python 3 [Polyend Tracker](https://polyend.com/tracker/) .pti instrument creator.
3 |
4 | Initial purpose of the utility python CLI app is threefold.
5 | - Create .pti forward-looping instruments with clean loop points that are encoded in a wave file sample. Currently Polyend Tracker doesn't read these loop points and thus makes clean looping of instrument samples rather challenging.
6 | - Create .pti beat-slicer/slicer instruments from a series of individual wave-files to create drum-kits and perfect conversions of previously sliced loops (REX for example) into .pti loops.
7 | - Batch convert wave files into individual pti instruments. For individuals making sample packs or libraries for distribution, this may save time over individually creating the .pti files in Polyend Tracker directly.
8 |
9 | Special thanks to [@jaap3](https://github.com/jaap3) for his excellent .pti file format [documentation](https://github.com/jaap3/pti-file-format).
10 |
11 |
12 | ## Installation
13 | 1. Install dependencies
14 |
15 | ### Python
16 | #### MacOS
17 | Follow Instructions to install [Homebrew](https://brew.sh/).
18 | ```brew update && brew install python```
19 | #### Windows
20 | 1. Download Python installer from [python.org](https://www.python.org/).
21 | 2. Install. Be sure to select "Add Python to path" during installation as well as disable path length limit.
22 |
23 | ### ffmpeg
24 | #### MacOS
25 | ```brew install ffmpeg```
26 | #### Windows
27 | 1. Download the latest Windows binary from [ffmpeg.org](http://ffmpeg.org/).
28 | 2. Unzip and move contents of bin to "C:\Program Files\ffmpeg".
29 | 3. Add ffmpeg to path.
30 | 1. Open Start Menu.
31 | 2. Type "Edit Environment Variables".
32 | 3. Click on "Edit Environment Variables For Your Account".
33 | 4. In Edit Environment Variables, you will see two boxes. In system variables box find and select "path" variables.
34 | 5. Click "Edit".
35 | 6. In window pop up, click "New".
36 | 7. Type "C:\Program Files\ffmpeg", then click OK twice to save your changes.
37 |
38 |
39 | 2. Install pet-pti-creator command
40 | 1. Clone or download polyend-tracker-pti-creator package from GitHub. Unzip if downloaded.
41 | 2. In Terminal (MacOS) or Command Prompt (Windows) navigate to the polyend-tracker-pti-creator package folder.
42 | 3. Run ```python setup.py install```. This will install pet-pti-creator to your path and be accessible as a terminal command.
43 | 4. Restart Terminal or Command Prompt before first usage.
44 |
45 | ## Usage
46 | ### Basic Usage
47 | ```pet-pti-creator --source path/of/wave/file```
48 |
49 | ### Specify Destination
50 | ```pet-pti-creator --source path/of/wave/file --destination path/of/destination/directory```
51 |
52 | ### Batch processing
53 | ```pet-pti-creator --source path/of/wave/file/directory```
54 |
55 | ### Merge multiple files to single beat slice instrument
56 | ```pet-pti-creator --source path/of/wave/file/directory --mode merge```
57 |
58 | ### Help
59 | ```pet-pti-creator --help```
60 |
61 | ## Notes
62 |
63 | - Currently, only .WAV is supported as a source file. This is because of robust [documentation](https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#fmt) of the format for extracting loop points out of the file. AIFF and CAF are feasible to support, but not in initial version.
64 |
65 | - Currently, loop points for loop-playback modes are pulled by parsing the wave file itself. If you're unsure if your wave file has loop-points, download the free [Endless WAV](https://www.bjoernbojahr.de/endlesswav.html) editor. In addition to being able to view file loop points, it can also algorithmically create loop points for files without them originally.
66 |
67 | - To convert a REX file into a sliced .pti instrument, use an application that can export REX loops as individual wave file slices then use 'merge' mode to keep those slice points exactly the same in the resulting .pti file. Easiest way to convert REX/RX2 files to individual wave slices is use [Recycle](https://www.reasonstudios.com/recycle).
68 |
69 | - Another use for the merge mode is to collect up to 48 individual drum hits into a drumkit.
70 |
71 | - As mentioned in the MIT license, this software is provided as-is without warranty. Although care is taken to prevent damage to the original audio files, please back up originals to a separate location prior to use. I'm not aware of an straight-forward freeware solution at this time.
--------------------------------------------------------------------------------
/examples/808basic.pti:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/808basic.pti
--------------------------------------------------------------------------------
/examples/bbb.pti:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/bbb.pti
--------------------------------------------------------------------------------
/examples/moodypad.pti:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/moodypad.pti
--------------------------------------------------------------------------------
/examples/original_files/ LEGAL LICENSE INFO READ ME.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | SampleSwap Licensing Information
7 |
8 |
9 |
14 |
15 |
16 | Q&A / LICENSE INFO FOR THE SOUNDS IN THE SAMPLESWAP COLLECTION
17 | Last Updated: February, 2021
18 | If you received a copy of these sounds from a friend (or over a file sharing network like BitTorrent) please come by and visit SampleSwap.org, the community that collected these samples for your enjoyment. Thanks!
19 | You can also find us at facebook.com/sampleswap
20 | Q: Where do these sounds come from?
21 | http://www.sampleswap.org
22 | These sounds were anonymously uploaded by members of the SampleSwap community between 2000 and 2021. Contributors are asked to only upload original sound files to which they own the copyright, and which they are willing to release into the public domain ("royalty free").
23 | Q: Can I re-distribute these samples?
24 | Feel free to give away these samples as you see fit as long as you include this notice along with the sample(s). Please do not sell copies of this collection, or individual samples within the collection. That's not really keeping with the spirit of things, and you may be exposing yourself to legal murkiness.
25 | Q: Can I use these sounds in a public/commercial music production?
26 | IMPORTANT NOTE: Whether you downloaded these sounds, or received them as a gift for a donation to SampleSwap.org, you do NOT have a guarantee that these sounds are free and clear of any copyright restrictions.
27 | Our membership policy is to require that users only upload original sounds that they are giving up into the public domain. But there's no way for us to be sure that 100% of the sounds in this collection are free and clear from any copyright restrictions, and you should assume that some of the sounds in this collection are not appropriate for commercial use (or even use in a non-commercial but publicly performed production.)
28 | The decision to use or refrain from using these samples in a public or commercial production is entirely your own, and SampleSwap.org's owner and members do not grant you any special license or guarantee that these samples can be used without risk of violating copyright. You and you alone are responsible for the legal ramifications of using any sounds in this collection.
29 | Some common-sense (non-legally binding) advice is this: use your own judgement when integrating samples into your songs, whether they come from SampleSwap or from a commercial sample library. If a sample has lyrics, try googling for those lyrics to find out if they're from a well known song. If a loop sounds like it's been lifted from vinyl, maybe it has been. Cut it up, modify, slice and rearrange the samples you use so that they are authentically your own. It makes your music more interesting, and it may help to protect you from unintentionally infringing on someone's copyright.
30 | If you have any questions about this collection, please contact:
31 | - Canton Becker / canton@gmail.com
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/original_files/080_bella-bonga-beat.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/080_bella-bonga-beat.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/cl_hihat.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/cl_hihat.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/claves.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/claves.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/conga1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/conga1.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/cowbell.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/cowbell.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/crashcym.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/crashcym.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/handclap.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/handclap.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/hi_conga.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/hi_conga.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/hightom.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/hightom.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/kick1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/kick1.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/kick2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/kick2.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/maracas.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/maracas.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/open_hh.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/open_hh.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/rimshot.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/rimshot.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/snare.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/snare.wav
--------------------------------------------------------------------------------
/examples/original_files/808_basic/tom1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/808_basic/tom1.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 001.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 001.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 002.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 002.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 003.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 003.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 004.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 004.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 005.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 005.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 006.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 006.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 007.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 007.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 008.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 008.wav
--------------------------------------------------------------------------------
/examples/original_files/bbb_slices/bbb 009.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/bbb_slices/bbb 009.wav
--------------------------------------------------------------------------------
/examples/original_files/moody-pad.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/moody-pad.wav
--------------------------------------------------------------------------------
/examples/original_files/moody_pad_left.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/examples/original_files/moody_pad_left.wav
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/__init__.py
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/creator.py:
--------------------------------------------------------------------------------
1 | from polyend_tracker_pti_creator.utils.args import parse_args
2 | from polyend_tracker_pti_creator.utils.settings import Settings
3 | from polyend_tracker_pti_creator.utils.pti.pti import PTI
4 |
5 |
6 | def main():
7 | args = parse_args()
8 | settings = Settings(args)
9 | create(settings.settings)
10 |
11 |
12 | def create(settings):
13 | pti = PTI(settings)
14 | pti.create()
15 | print("Success! new pti file(s) have been saved to " + settings['files'][0]['destination_path'] + ".")
16 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/__init__.py
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/__pycache__/exceptions.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/__pycache__/exceptions.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/__pycache__/settings.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/__pycache__/settings.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/args.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 |
4 | def parse_args() -> argparse.Namespace:
5 | parser = argparse.ArgumentParser()
6 | parser.add_argument(
7 | "-s",
8 | "--source",
9 | help="source file or source file path - required."
10 | )
11 | parser.add_argument(
12 | "-d",
13 | "--destination",
14 | help="destination directory path - optional, defaults to source directory path."
15 | )
16 | parser.add_argument(
17 | "-m",
18 | "--mode",
19 | help="instrument creation mode - optional. Possible values are normal and merge. Defaults to normal."
20 | )
21 | parser.add_argument(
22 | "-p",
23 | "--playback",
24 | help="instrument playback setting - optional. Possible values are one-shot, "
25 | "forward-loop, backward-loop, ping-pong-loop, slice, beat-slice, wavetable, "
26 | "granular. Defaults to beat-slice in merge mode. "
27 | "Defaults to 'dynamic' in normal mode. 'dynamic' playback setting sets the "
28 | "playback to either forward-loop or one-shot depending on if a loop is found in the .wav file."
29 | )
30 | parser.add_argument(
31 | "-in",
32 | "--instrument-name",
33 | help="instrument name - optional. Must be less than 32 of alpha, numeric, space, +, -, or @ characters. "
34 | "Defaults to file name stripped of invalid characters then trimmed to 32 character length."
35 | )
36 | parser.add_argument(
37 | "-fn",
38 | "--file-name",
39 | help="destination file name - optional. Defaults to first file-name specified in destination parameter, "
40 | "then original source file name if a file name not specified in destination parameter."
41 | )
42 | return parser.parse_args()
43 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/audio/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/audio/__init__.py
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/audio/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/audio/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/audio/__pycache__/audio.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/audio/__pycache__/audio.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/audio/__pycache__/wave_chunk_parser_extended.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/audio/__pycache__/wave_chunk_parser_extended.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/audio/audio.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from pydub.audio_segment import AudioSegment
3 | from lazy_property import LazyProperty
4 | from polyend_tracker_pti_creator.utils.audio.wave_chunk_parser_extended import (
5 | RiffChunkExtended,
6 | )
7 | from polyend_tracker_pti_creator.utils.exceptions import (
8 | FfmpegNotInstalledException,
9 | )
10 |
11 |
12 | class Audio:
13 | def __init__(self, path: str) -> None:
14 | self.path = path
15 |
16 | @LazyProperty
17 | def loop_points(self) -> List[int]:
18 | with open(self.path, "rb") as file:
19 | try:
20 | print(self.path)
21 | frame_count = AudioSegment.from_file(self.path, format='wav').frame_count()
22 | riff_chunk = RiffChunkExtended.from_file(file, user_chunks=[RiffChunkExtended.CHUNK_SAMPLE])
23 | sample_chunk = None
24 | for sc in riff_chunk.sub_chunks:
25 | if sc.get_name == RiffChunkExtended.CHUNK_SAMPLE:
26 | sample_chunk = sc
27 | return [
28 | round((sample_chunk.first_loop_start / frame_count) * 65535),
29 | round((sample_chunk.first_loop_end / frame_count) * 65535)
30 | ] if sample_chunk.number_of_sample_loops > 0 else [0, 0]
31 | except AttributeError:
32 | return [0, 0]
33 |
34 | @LazyProperty
35 | def audio_segment(self) -> AudioSegment:
36 | try:
37 | audio_segment = AudioSegment.from_file(self.path, format='wav')
38 | if audio_segment.frame_rate != 44100:
39 | audio_segment = audio_segment.set_frame_rate(44100)
40 | if audio_segment.channels != 1:
41 | audio_segment = audio_segment.set_channels(1)
42 | if audio_segment.sample_width != 2:
43 | audio_segment = audio_segment.set_sample_width(2)
44 | return audio_segment
45 | except FileNotFoundError as exception:
46 | if exception.filename == "ffprobe":
47 | raise FfmpegNotInstalledException(
48 | "ffmpeg or libav is required to read audio from non-wave files. "
49 | "Please download and install from ffmpeg.org or libav.org then try again."
50 | ) from exception
51 | return None
52 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/audio/wave_chunk_parser_extended.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=too-many-instance-attributes, too-many-arguments, unused-variable
2 | from __future__ import annotations
3 | from wave_chunk_parser.chunks import RiffChunk, Chunk
4 | from typing import BinaryIO
5 | from struct import unpack, pack
6 | from wave_chunk_parser.exceptions import (
7 | InvalidHeaderException,
8 | )
9 | from wave_chunk_parser.utils import seek_and_read
10 |
11 | class SampleChunk(Chunk):
12 | """
13 | The sample chunk defines how the audio is played back by a sampler.
14 | """
15 |
16 | __manufacturer: int
17 | __product: int
18 | __sample_period: int
19 | __midi_unity_note: int
20 | __midi_pitch_fraction: int
21 | __smpte_format: int
22 | __smpte_offset: int
23 | __number_of_sample_loops: int
24 | __sampler_data: int
25 | __first_cue_point_id: int
26 |
27 | LENGTH_CHUNK = 68
28 | LENGTH_STANDARD_SIZE = 60
29 | HEADER_SAMPLE = b"smpl"
30 |
31 | def __init__(
32 | self,
33 | manufacturer: int,
34 | product: int,
35 | sample_period: int,
36 | midi_unity_note: int,
37 | midi_pitch_fraction: int,
38 | smpte_format: int,
39 | smpte_offset: int,
40 | number_of_sample_loops: int,
41 | sampler_data: int,
42 | first_cue_point_id: int,
43 | first_loop_type: int,
44 | first_loop_start: int,
45 | first_loop_end: int,
46 | first_loop_fraction: int,
47 | first_loop_play_count: int,
48 | ) -> None:
49 | """
50 | Creates a new instance of the sample block.
51 | https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#fmt
52 | TODO: extract more than first cue point.
53 | Args:
54 | manufacturer (int): MIDI Manufacturer's Association Manufacturer code.
55 | product (int): MIDI model ID.
56 | sample_period (int): Duration of time that passes during the playback of one sample in nanoseconds.
57 | midi_unity_note (int): Musical note at which the sample will be played.
58 | midi_pitch_fraction (int): Fraction of a semitone up from the specified MIDI unity note field.
59 | smpte_format (int): SMPTE time format used in the following SMPTE Offset field.
60 | smpte_offset (int): SMPTE time offset to be used.
61 | number_of_sample_loops (int): Number of sample loops.
62 | sampler_data (int): Number of bytes that will follow this chunk.
63 | first_cue_point_id (int): Unique ID that corresponds to one of the defined cue points.
64 | first_loop_type (int): Defines how the waveform samples will be looped.
65 | first_loop_start (int): Byte offset into the waveform data of the first sample to be played in the loop.
66 | first_loop_end (int): Byte offset into the waveform data of the last sample to be played in the loop.
67 | first_loop_fraction (int): Fraction of a sample at which to loop.
68 | first_loop_play_count (int): Number of times to play the loop.
69 | """
70 |
71 | self.__manufacturer = manufacturer
72 | self.__product = product
73 | self.__sample_period = sample_period
74 | self.__midi_unity_note = midi_unity_note
75 | self.__midi_pitch_fraction = midi_pitch_fraction
76 | self.__smpte_format = smpte_format
77 | self.__smpte_offset = smpte_offset
78 | self.__number_of_sample_loops = number_of_sample_loops
79 | self.__sampler_data = sampler_data
80 | self.__first_cue_point_id = first_cue_point_id
81 | self.__first_loop_type = first_loop_type
82 | self.__first_loop_start = first_loop_start
83 | self.__first_loop_end = first_loop_end
84 | self.__first_loop_fraction = first_loop_fraction
85 | self.__first_loop_play_count = first_loop_play_count
86 |
87 | @classmethod
88 | def from_file(cls, file_handle: BinaryIO, offset: int) -> SampleChunk:
89 |
90 | # Sanity check
91 |
92 | (header_str, length) = cls.read_header(file_handle, offset)
93 |
94 | if not header_str == cls.HEADER_SAMPLE:
95 | raise InvalidHeaderException("Sample chunk must start with smpl")
96 |
97 | # Read from the chunk
98 |
99 | (
100 | manufacturer,
101 | product,
102 | sample_period,
103 | midi_unity_note,
104 | midi_pitch_fraction,
105 | smpte_format,
106 | smpte_offset,
107 | number_of_sample_loops,
108 | sampler_data,
109 | first_cue_point_id,
110 | first_loop_type,
111 | first_loop_start,
112 | first_loop_end,
113 | first_loop_fraction,
114 | first_loop_play_count,
115 | ) = unpack(
116 | " int:
146 | """
147 | MIDI Manufacturer's Association Manufacturer code.
148 | """
149 | return self.__manufacturer
150 |
151 | @property
152 | def product(self) -> int:
153 | """
154 | MIDI model ID.
155 | """
156 | return self.__product
157 |
158 | @property
159 | def sample_period(self) -> int:
160 | """
161 | Duration of time that passes during the playback of one sample in nanoseconds.
162 | """
163 | return self.__sample_period
164 |
165 | @property
166 | def midi_unity_note(self) -> int:
167 | """
168 | Musical note at which the sample will be played.
169 | """
170 | return self.__midi_unity_note
171 |
172 | @property
173 | def midi_pitch_fraction(self) -> int:
174 | """
175 | Fraction of a semitone up from the specified MIDI unity note field.
176 | """
177 | return self.__midi_pitch_fraction
178 |
179 | @property
180 | def smpte_format(self) -> int:
181 | """
182 | SMPTE time format used in the following SMPTE Offset field.
183 | """
184 | return self.__smpte_format
185 |
186 | @property
187 | def smpte_offset(self) -> int:
188 | """
189 | SMPTE time offset to be used.
190 | """
191 | return self.__smpte_offset
192 |
193 | @property
194 | def number_of_sample_loops(self) -> int:
195 | """
196 | Number of sample loops.
197 | """
198 | return self.__number_of_sample_loops
199 |
200 | @property
201 | def sampler_data(self) -> int:
202 | """
203 | Number of bytes that will follow this chunk.
204 | """
205 | return self.__sampler_data
206 |
207 | @property
208 | def first_cue_point_id(self) -> int:
209 | """
210 | Unique ID that corresponds to one of the defined cue points.
211 | """
212 | return self.__first_cue_point_id
213 |
214 | @property
215 | def first_loop_type(self) -> int:
216 | """
217 | Defines how the waveform samples will be looped.
218 | """
219 | return self.__first_loop_type
220 |
221 | @property
222 | def first_loop_start(self) -> int:
223 | """
224 | Byte offset into the waveform data of the first sample to be played in the loop.
225 | """
226 | return self.__first_loop_start
227 |
228 | @property
229 | def first_loop_end(self) -> int:
230 | """
231 | Byte offset into the waveform data of the last sample to be played in the loop.
232 | """
233 | return self.__first_loop_end
234 |
235 | @property
236 | def first_loop_fraction(self) -> int:
237 | """
238 | Fraction of a sample at which to loop.
239 | """
240 | return self.__first_loop_fraction
241 |
242 | @property
243 | def first_loop_play_count(self) -> int:
244 | """
245 | Number of times to play the loop.
246 | """
247 | return self.__first_loop_play_count
248 |
249 | @property
250 | def get_name(self) -> bytes:
251 | return self.HEADER_SAMPLE
252 |
253 | def to_bytes(self) -> bytes:
254 |
255 | # Build up our chunk
256 |
257 | return pack(
258 | "<4sIIIIIIIIIIIIIIII",
259 | self.HEADER_SAMPLE,
260 | self.LENGTH_STANDARD_SIZE,
261 | self.manufacturer,
262 | self.product,
263 | self.sample_period,
264 | self.midi_unity_note,
265 | self.midi_pitch_fraction,
266 | self.smpte_format,
267 | self.smpte_offset,
268 | self.number_of_sample_loops,
269 | self.sampler_data,
270 | self.first_cue_point_id,
271 | self.first_loop_type,
272 | self.first_loop_start,
273 | self.first_loop_end,
274 | self.first_loop_fraction,
275 | self.first_loop_play_count,
276 | )
277 |
278 | class RiffChunkExtended(RiffChunk):
279 | CHUNK_SAMPLE = b"smpl"
280 |
281 | @classmethod
282 | def from_file(cls, file_handle: BinaryIO, offset: int = 0, **kwargs) -> Chunk:
283 | kwargs["user_chunks"] = { cls.CHUNK_SAMPLE: SampleChunk }
284 | return super().from_file(file_handle, offset, **kwargs)
285 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=unnecessary-pass
2 | class CreatorSourceMissingException(Exception):
3 | """
4 | Indicates the source argument is missing.
5 | """
6 |
7 | pass
8 |
9 |
10 | class CreatorDestinationInvalidException(Exception):
11 | """
12 | Indicates the source argument is missing.
13 | """
14 |
15 | pass
16 |
17 |
18 | class CreatorNoSourceWavFilesException(Exception):
19 | """
20 | Indicates the source is either a non-wave file or path contains no wave files
21 | """
22 |
23 | pass
24 |
25 |
26 | class CreatorModeInvalidException(Exception):
27 | """
28 | Indicates the mode argument is invalid value.
29 | Valid Values:
30 | normal
31 | merge
32 | """
33 |
34 | pass
35 |
36 |
37 | class CreatorPlaybackInvalidException(Exception):
38 | """
39 | Indicates the playback argument is invalid value.
40 | Valid Values:
41 | one-shot
42 | forward-loop
43 | backward-loop
44 | ping-pong-loop
45 | slice
46 | beat-slice
47 | wavetable
48 | granular
49 | dynamic
50 | """
51 |
52 | pass
53 |
54 |
55 | class CreatorTooManyMergeFilesException(Exception):
56 | """
57 | Indicates the number of files to merge (and slice) is greater than 48.
58 | """
59 |
60 | pass
61 |
62 |
63 | class CreatorSampleTooLongException(Exception):
64 | """
65 | Indicates the sample is longer than 30 seconds, the maximum length supported by Polyend Tracker.
66 | """
67 |
68 | pass
69 |
70 |
71 | class FfmpegNotInstalledException(Exception):
72 | """
73 | Indicates that ffmpeg is not installed.
74 | """
75 |
76 | pass
77 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/pti/__init__.py
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/pti/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/__pycache__/constants.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/pti/__pycache__/constants.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/__pycache__/header.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/pti/__pycache__/header.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/__pycache__/pti.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/polyend_tracker_pti_creator/utils/pti/__pycache__/pti.cpython-38.pyc
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/constants.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=too-many-lines
2 | # adapted from https://github.com/jaap3/pti-file-format/blob/main/pti.rst
3 |
4 | PLAYBACK_VALUES = {
5 | 'one-shot': 0,
6 | 'forward-loop': 1,
7 | 'backward-loop': 2,
8 | 'ping-pong-loop': 3,
9 | 'slice': 4,
10 | 'beat-slice': 5,
11 | 'wavetable': 6,
12 | 'granular': 7
13 | }
14 |
15 | PTI_HEADER_DEFINITION = {
16 | 'file_type_indicator': {
17 | 'description': 'File type indicator',
18 | 'type': '2s',
19 | 'value': b'TI',
20 | },
21 | 'unknown_01': {
22 | 'description': 'unknown - 1',
23 | 'type': 'B',
24 | 'value': 1,
25 | },
26 | 'unknown_02': {
27 | 'description': 'unknown - 0',
28 | 'type': 'B',
29 | 'value': 0,
30 | },
31 | 'unknown_03': {
32 | 'description': 'unknown - 1',
33 | 'type': 'B',
34 | 'value': 1,
35 | },
36 | 'unknown_04': {
37 | 'description': 'unknown - 2, 4, 5',
38 | 'type': 'B',
39 | 'value': 4,
40 | },
41 | 'unknown_05': {
42 | 'description': 'unknown - 0, 1, 2',
43 | 'type': 'B',
44 | 'value': 1,
45 | },
46 | 'unknown_06': {
47 | 'description': 'unknown - 1',
48 | 'type': 'B',
49 | 'value': 1,
50 | },
51 | 'unknown_07': {
52 | 'description': 'unknown - 6, 9',
53 | 'type': 'B',
54 | 'value': 9,
55 | },
56 | 'unknown_08': {
57 | 'description': 'unknown - 6, 9',
58 | 'type': 'B',
59 | 'value': 9,
60 | },
61 | 'unknown_09': {
62 | 'description': 'unknown - 6, 9',
63 | 'type': 'B',
64 | 'value': 9,
65 | },
66 | 'unknown_10': {
67 | 'description': 'unknown - 6, 9',
68 | 'type': 'B',
69 | 'value': 9,
70 | },
71 | 'unknown_11': {
72 | 'description': 'unknown - 116',
73 | 'type': 'B',
74 | 'value': 116,
75 | },
76 | 'unknown_12': {
77 | 'description': 'unknown - 1',
78 | 'type': 'B',
79 | 'value': 1,
80 | },
81 | 'unknown_13': {
82 | 'description': 'unknown - 0, 102, 110',
83 | 'type': 'B',
84 | 'value': 110,
85 | },
86 | 'unknown_14': {
87 | 'description': 'unknown - 0, 102',
88 | 'type': 'B',
89 | 'value': 102,
90 | },
91 | 'unknown_15': {
92 | 'description': 'unknown - 1',
93 | 'type': 'B',
94 | 'value': 1,
95 | },
96 | 'unknown_16': {
97 | 'description': 'unknown - 0',
98 | 'type': 'B',
99 | 'value': 0,
100 | },
101 | 'unknown_17': {
102 | 'description': 'unknown - 0',
103 | 'type': 'B',
104 | 'value': 0,
105 | },
106 | 'unknown_18': {
107 | 'description': 'unknown - 0',
108 | 'type': 'B',
109 | 'value': 0,
110 | },
111 | 'is_wavetable': {
112 | 'description': 'Is set to 1 (True) when Sample playback is set to Wavetable',
113 | 'type': '?',
114 | 'value': False,
115 | },
116 | 'instrument_name': {
117 | 'description': 'ASCII characters',
118 | 'type': '32s',
119 | 'value': b''.join([b'\x00' for x in range(32)]),
120 | },
121 | 'unknown_19': {
122 | 'description': 'unknown - 0',
123 | 'type': 'B',
124 | 'value': 0,
125 | },
126 | 'unknown_20': {
127 | 'description': 'unknown - 0',
128 | 'type': 'B',
129 | 'value': 0,
130 | },
131 | 'unknown_21': {
132 | 'description': 'unknown - 0',
133 | 'type': 'B',
134 | 'value': 0,
135 | },
136 | 'unknown_22': {
137 | 'description': 'unknown - ?',
138 | 'type': 'B',
139 | 'value': 184,
140 | },
141 | 'unknown_23': {
142 | 'description': 'unknown - ?',
143 | 'type': 'B',
144 | 'value': 110,
145 | },
146 | 'unknown_24': {
147 | 'description': 'unknown - ?',
148 | 'type': 'B',
149 | 'value': 2,
150 | },
151 | 'unknown_25': {
152 | 'description': 'unknown - ?',
153 | 'type': 'B',
154 | 'value': 112,
155 | },
156 | 'sample_length': {
157 | 'description': 'long: 0-4294967295',
158 | 'type': 'L',
159 | 'value': 0,
160 | },
161 | 'wavetable_window_size': {
162 | 'description': 'short: 32, 64, 128, 256, 1024, 2048',
163 | 'type': 'H',
164 | 'value': 2048,
165 | },
166 | 'unknown_26': {
167 | 'description': 'unknown - 0',
168 | 'type': 'B',
169 | 'value': 0,
170 | },
171 | 'unknown_27': {
172 | 'description': 'unknown - 0',
173 | 'type': 'B',
174 | 'value': 0,
175 | },
176 | 'wavetable_total_positions': {
177 | 'description': 'short: 0-65535',
178 | 'type': 'H',
179 | 'value': 0,
180 | },
181 | 'unknown_28': {
182 | 'description': 'unknown - 0',
183 | 'type': 'B',
184 | 'value': 0,
185 | },
186 | 'unknown_29': {
187 | 'description': 'unknown - 0',
188 | 'type': 'B',
189 | 'value': 0,
190 | },
191 | 'unknown_30': {
192 | 'description': 'unknown - 0',
193 | 'type': 'B',
194 | 'value': 0,
195 | },
196 | 'unknown_31': {
197 | 'description': 'unknown - 0',
198 | 'type': 'B',
199 | 'value': 0,
200 | },
201 | 'unknown_32': {
202 | 'description': 'unknown - 0',
203 | 'type': 'B',
204 | 'value': 0,
205 | },
206 | 'unknown_33': {
207 | 'description': 'unknown - 0',
208 | 'type': 'B',
209 | 'value': 0,
210 | },
211 | 'sample_playback': {
212 | 'description': '0: 1-Shot (default) '
213 | '1: Forward loop '
214 | '2: Backward loop '
215 | '3: PingPong loop '
216 | '4: Slice '
217 | '5: Beat slice '
218 | '6: Wavetable '
219 | '7: Granular',
220 | 'type': 'B',
221 | 'value': 0,
222 | },
223 | 'unknown_34': {
224 | 'description': 'unknown - 0',
225 | 'type': 'B',
226 | 'value': 0,
227 | },
228 | 'playback_start': {
229 | 'description': 'short: 0-65535',
230 | 'type': 'H',
231 | 'value': 0,
232 | },
233 | 'loop_start': {
234 | 'description': 'short: 1-65534',
235 | 'type': 'H',
236 | 'value': 1,
237 | },
238 | 'loop_end': {
239 | 'description': 'short: 1-65534',
240 | 'type': 'H',
241 | 'value': 65534,
242 | },
243 | 'playback_end': {
244 | 'description': 'short: 0-65535',
245 | 'type': 'H',
246 | 'value': 65535,
247 | },
248 | 'unknown_35': {
249 | 'description': 'unknown - 0',
250 | 'type': 'B',
251 | 'value': 0,
252 | },
253 | 'unknown_36': {
254 | 'description': 'unknown - 0',
255 | 'type': 'B',
256 | 'value': 0,
257 | },
258 | 'wavetable_position': {
259 | 'description': 'short: 0-65535',
260 | 'type': 'H',
261 | 'value': 0,
262 | },
263 | 'unknown_37': {
264 | 'description': 'unknown - 0',
265 | 'type': 'B',
266 | 'value': 0,
267 | },
268 | 'unknown_38': {
269 | 'description': 'unknown - 0',
270 | 'type': 'B',
271 | 'value': 0,
272 | },
273 | # Volume Automation Envelope
274 | 'volume_env_amount': {
275 | 'description': 'float: 0.0 - 1.0 (0-100%)',
276 | 'type': 'f',
277 | 'value': 1.0,
278 | },
279 | 'unknown_39': {
280 | 'description': 'unknown - 0',
281 | 'type': 'B',
282 | 'value': 0,
283 | },
284 | 'unknown_40': {
285 | 'description': 'unknown - 0',
286 | 'type': 'B',
287 | 'value': 0,
288 | },
289 | 'volume_env_attack': {
290 | 'description': 'short: 0-10000 (0-10 seconds)',
291 | 'type': 'H',
292 | 'value': 0,
293 | },
294 | 'unknown_41': {
295 | 'description': 'unknown - 0',
296 | 'type': 'B',
297 | 'value': 0,
298 | },
299 | 'unknown_42': {
300 | 'description': 'unknown - 0',
301 | 'type': 'B',
302 | 'value': 0,
303 | },
304 | 'volume_env_decay': {
305 | 'description': 'short: 0-10000 (0-10 seconds)',
306 | 'type': 'H',
307 | 'value': 0,
308 | },
309 | 'volume_env_sustain': {
310 | 'description': 'float: 0.0 - 1.0 (0-100%)',
311 | 'type': 'f',
312 | 'value': 1.0,
313 | },
314 | 'volume_env_release': {
315 | 'description': 'short: 0-10000 (0-10 seconds)',
316 | 'type': 'H',
317 | 'value': 1000,
318 | },
319 | 'volume_automation_type_1': {
320 | 'description': '00: Off '
321 | '01: Envelope (default) '
322 | '11: LFO',
323 | 'type': 'B',
324 | 'value': 0,
325 | },
326 | 'volume_automation_type_2': {
327 | 'description': '00: Off '
328 | '01: Envelope (default) '
329 | '11: LFO',
330 | 'type': 'B',
331 | 'value': 1,
332 | },
333 | # Panning Automation Envelope
334 | 'panning_env_amount': {
335 | 'description': 'float: 0.0 - 1.0 (0-100%)',
336 | 'type': 'f',
337 | 'value': 1.0,
338 | },
339 | 'unknown_43': {
340 | 'description': 'unknown - 0',
341 | 'type': 'B',
342 | 'value': 0,
343 | },
344 | 'unknown_44': {
345 | 'description': 'unknown - 0',
346 | 'type': 'B',
347 | 'value': 0,
348 | },
349 | 'panning_env_attack': {
350 | 'description': 'short: 0-10000 (0-10 seconds)',
351 | 'type': 'H',
352 | 'value': 0,
353 | },
354 | 'unknown_45': {
355 | 'description': 'unknown - 0',
356 | 'type': 'B',
357 | 'value': 0,
358 | },
359 | 'unknown_46': {
360 | 'description': 'unknown - 0',
361 | 'type': 'B',
362 | 'value': 0,
363 | },
364 | 'panning_env_decay': {
365 | 'description': 'short: 0-10000 (0-10 seconds)',
366 | 'type': 'H',
367 | 'value': 0,
368 | },
369 | 'panning_env_sustain': {
370 | 'description': 'float: 0.0 - 1.0 (0-100%)',
371 | 'type': 'f',
372 | 'value': 1.0,
373 | },
374 | 'panning_env_release': {
375 | 'description': 'short: 0-10000 (0-10 seconds)',
376 | 'type': 'H',
377 | 'value': 1000,
378 | },
379 | 'panning_automation_type_1': {
380 | 'description': '00: Off '
381 | '01: Envelope (default) '
382 | '11: LFO',
383 | 'type': 'B',
384 | 'value': 0,
385 | },
386 | 'panning_automation_type_2': {
387 | 'description': '00: Off '
388 | '01: Envelope (default) '
389 | '11: LFO',
390 | 'type': 'B',
391 | 'value': 0,
392 | },
393 | # Cutoff Automation Envelope
394 | 'cutoff_env_amount': {
395 | 'description': 'float: 0.0 - 1.0 (0-100%)',
396 | 'type': 'f',
397 | 'value': 1.0,
398 | },
399 | 'unknown_47': {
400 | 'description': 'unknown - 0',
401 | 'type': 'B',
402 | 'value': 0,
403 | },
404 | 'unknown_48': {
405 | 'description': 'unknown - 0',
406 | 'type': 'B',
407 | 'value': 0,
408 | },
409 | 'cutoff_env_attack': {
410 | 'description': 'short: 0-10000 (0-10 seconds)',
411 | 'type': 'H',
412 | 'value': 0,
413 | },
414 | 'unknown_49': {
415 | 'description': 'unknown - 0',
416 | 'type': 'B',
417 | 'value': 0,
418 | },
419 | 'unknown_50': {
420 | 'description': 'unknown - 0',
421 | 'type': 'B',
422 | 'value': 0,
423 | },
424 | 'cutoff_env_decay': {
425 | 'description': 'short: 0-10000 (0-10 seconds)',
426 | 'type': 'H',
427 | 'value': 0,
428 | },
429 | 'cutoff_env_sustain': {
430 | 'description': 'float: 0.0 - 1.0 (0-100%)',
431 | 'type': 'f',
432 | 'value': 1.0,
433 | },
434 | 'cutoff_env_release': {
435 | 'description': 'short: 0-10000 (0-10 seconds)',
436 | 'type': 'H',
437 | 'value': 1000,
438 | },
439 | 'cutoff_automation_type_1': {
440 | 'description': '00: Off '
441 | '01: Envelope (default) '
442 | '11: LFO',
443 | 'type': 'B',
444 | 'value': 0,
445 | },
446 | 'cutoff_automation_type_2': {
447 | 'description': '00: Off '
448 | '01: Envelope (default) '
449 | '11: LFO',
450 | 'type': 'B',
451 | 'value': 0,
452 | },
453 | # Wavetable Position Automation Envelope
454 | 'wt_pos_env_amount': {
455 | 'description': 'float: 0.0 - 1.0 (0-100%)',
456 | 'type': 'f',
457 | 'value': 1.0,
458 | },
459 | 'unknown_51': {
460 | 'description': 'unknown - 0',
461 | 'type': 'B',
462 | 'value': 0,
463 | },
464 | 'unknown_52': {
465 | 'description': 'unknown - 0',
466 | 'type': 'B',
467 | 'value': 0,
468 | },
469 | 'wt_pos_env_attack': {
470 | 'description': 'short: 0-10000 (0-10 seconds)',
471 | 'type': 'H',
472 | 'value': 0,
473 | },
474 | 'unknown_53': {
475 | 'description': 'unknown - 0',
476 | 'type': 'B',
477 | 'value': 0,
478 | },
479 | 'unknown_54': {
480 | 'description': 'unknown - 0',
481 | 'type': 'B',
482 | 'value': 0,
483 | },
484 | 'wt_pos_env_decay': {
485 | 'description': 'short: 0-10000 (0-10 seconds)',
486 | 'type': 'H',
487 | 'value': 0,
488 | },
489 | 'wt_pos_env_sustain': {
490 | 'description': 'float: 0.0 - 1.0 (0-100%)',
491 | 'type': 'f',
492 | 'value': 1.0,
493 | },
494 | 'wt_pos_env_release': {
495 | 'description': 'short: 0-10000 (0-10 seconds)',
496 | 'type': 'H',
497 | 'value': 1000,
498 | },
499 | 'wt_pos_automation_type_1': {
500 | 'description': '00: Off '
501 | '01: Envelope (default) '
502 | '11: LFO',
503 | 'type': 'B',
504 | 'value': 0,
505 | },
506 | 'wt_pos_automation_type_2': {
507 | 'description': '00: Off '
508 | '01: Envelope (default) '
509 | '11: LFO',
510 | 'type': 'B',
511 | 'value': 0,
512 | },
513 | # Granular Position Automation Envelope
514 | 'gran_pos_env_amount': {
515 | 'description': 'float: 0.0 - 1.0 (0-100%)',
516 | 'type': 'f',
517 | 'value': 1.0,
518 | },
519 | 'unknown_55': {
520 | 'description': 'unknown - 0',
521 | 'type': 'B',
522 | 'value': 0,
523 | },
524 | 'unknown_56': {
525 | 'description': 'unknown - 0',
526 | 'type': 'B',
527 | 'value': 0,
528 | },
529 | 'gran_pos_env_attack': {
530 | 'description': 'short: 0-10000 (0-10 seconds)',
531 | 'type': 'H',
532 | 'value': 0,
533 | },
534 | 'unknown_57': {
535 | 'description': 'unknown - 0',
536 | 'type': 'B',
537 | 'value': 0,
538 | },
539 | 'unknown_58': {
540 | 'description': 'unknown - 0',
541 | 'type': 'B',
542 | 'value': 0,
543 | },
544 | 'gran_pos_env_decay': {
545 | 'description': 'short: 0-10000 (0-10 seconds)',
546 | 'type': 'H',
547 | 'value': 0,
548 | },
549 | 'gran_pos_env_sustain': {
550 | 'description': 'float: 0.0 - 1.0 (0-100%)',
551 | 'type': 'f',
552 | 'value': 1.0,
553 | },
554 | 'gran_pos_env_release': {
555 | 'description': 'short: 0-10000 (0-10 seconds)',
556 | 'type': 'H',
557 | 'value': 1000,
558 | },
559 | 'gran_pos_automation_type_1': {
560 | 'description': '00: Off '
561 | '01: Envelope (default) '
562 | '11: LFO',
563 | 'type': 'B',
564 | 'value': 0,
565 | },
566 | 'gran_pos_automation_type_2': {
567 | 'description': '00: Off '
568 | '01: Envelope (default) '
569 | '11: LFO',
570 | 'type': 'B',
571 | 'value': 0,
572 | },
573 | # Finetune Automation Envelope
574 | 'finetune_env_amount': {
575 | 'description': 'float: 0.0 - 1.0 (0-100%)',
576 | 'type': 'f',
577 | 'value': 1.0,
578 | },
579 | 'unknown_59': {
580 | 'description': 'unknown - 0',
581 | 'type': 'B',
582 | 'value': 0,
583 | },
584 | 'unknown_60': {
585 | 'description': 'unknown - 0',
586 | 'type': 'B',
587 | 'value': 0,
588 | },
589 | 'finetune_env_attack': {
590 | 'description': 'short: 0-10000 (0-10 seconds)',
591 | 'type': 'H',
592 | 'value': 0,
593 | },
594 | 'unknown_61': {
595 | 'description': 'unknown - 0',
596 | 'type': 'B',
597 | 'value': 0,
598 | },
599 | 'unknown_62': {
600 | 'description': 'unknown - 0',
601 | 'type': 'B',
602 | 'value': 0,
603 | },
604 | 'finetune_env_decay': {
605 | 'description': 'short: 0-10000 (0-10 seconds)',
606 | 'type': 'H',
607 | 'value': 0,
608 | },
609 | 'finetune_env_sustain': {
610 | 'description': 'float: 0.0 - 1.0 (0-100%)',
611 | 'type': 'f',
612 | 'value': 1.0,
613 | },
614 | 'finetune_env_release': {
615 | 'description': 'short: 0-10000 (0-10 seconds)',
616 | 'type': 'H',
617 | 'value': 1000,
618 | },
619 | 'finetune_automation_type_1': {
620 | 'description': '00: Off '
621 | '01: Envelope (default) '
622 | '11: LFO',
623 | 'type': 'B',
624 | 'value': 0,
625 | },
626 | 'finetune_automation_type_2': {
627 | 'description': '00: Off '
628 | '01: Envelope (default) '
629 | '11: LFO',
630 | 'type': 'B',
631 | 'value': 0,
632 | },
633 | # Volume Automation LFO
634 | 'volume_auto_lfo_type': {
635 | 'description': '0: Rev Saw '
636 | '1: Saw '
637 | '2: Triangle (default) '
638 | '3: Square '
639 | '4: Random',
640 | 'type': 'B',
641 | 'value': 2,
642 | },
643 | 'volume_auto_lfo_steps': {
644 | 'description': '0: 24 steps (default) '
645 | '1: 16 steps '
646 | '2: 12 steps '
647 | '3: 8 steps '
648 | '4: 6 steps '
649 | '5: 4 steps '
650 | '6: 3 steps '
651 | '7: 2 steps '
652 | '8: 3/2 step '
653 | '9: 1 step '
654 | '10: 3/4 step '
655 | '11: 1/2 step '
656 | '12: 3/8 step '
657 | '13: 1/3 step '
658 | '14: 1/4 step '
659 | '15: 3/16 step '
660 | '16: 1/6 step '
661 | '17: 1/8 step '
662 | '18: 1/12 step '
663 | '19: 1/16 step '
664 | '20: 1/24 step '
665 | '21: 1/32 step '
666 | '22: 1/48 step '
667 | '23: 1/64 step',
668 | 'type': 'B',
669 | 'value': 0,
670 | },
671 | 'unknown_63': {
672 | 'description': 'unknown - 0',
673 | 'type': 'B',
674 | 'value': 0,
675 | },
676 | 'unknown_64': {
677 | 'description': 'unknown - 0',
678 | 'type': 'B',
679 | 'value': 0,
680 | },
681 | 'volume_auto_lfo_amount': {
682 | 'description': 'float: 0.0 - 1.0 (0-100%)',
683 | 'type': 'f',
684 | 'value': 0.5,
685 | },
686 | # Panning Automation LFO
687 | 'panning_auto_lfo_type': {
688 | 'description': '0: Rev Saw '
689 | '1: Saw '
690 | '2: Triangle (default) '
691 | '3: Square '
692 | '4: Random',
693 | 'type': 'B',
694 | 'value': 2,
695 | },
696 | 'panning_auto_lfo_steps': {
697 | 'description': '0: 24 steps (default) '
698 | '1: 16 steps '
699 | '2: 12 steps '
700 | '3: 8 steps '
701 | '4: 6 steps '
702 | '5: 4 steps '
703 | '6: 3 steps '
704 | '7: 2 steps '
705 | '8: 3/2 step '
706 | '9: 1 step '
707 | '10: 3/4 step '
708 | '11: 1/2 step '
709 | '12: 3/8 step '
710 | '13: 1/3 step '
711 | '14: 1/4 step '
712 | '15: 3/16 step '
713 | '16: 1/6 step '
714 | '17: 1/8 step '
715 | '18: 1/12 step '
716 | '19: 1/16 step '
717 | '20: 1/24 step '
718 | '21: 1/32 step '
719 | '22: 1/48 step '
720 | '23: 1/64 step',
721 | 'type': 'B',
722 | 'value': 0,
723 | },
724 | 'unknown_65': {
725 | 'description': 'unknown - 0',
726 | 'type': 'B',
727 | 'value': 0,
728 | },
729 | 'unknown_66': {
730 | 'description': 'unknown - 0',
731 | 'type': 'B',
732 | 'value': 0,
733 | },
734 | 'panning_auto_lfo_amount': {
735 | 'description': 'float: 0.0 - 1.0 (0-100%)',
736 | 'type': 'f',
737 | 'value': 0.5,
738 | },
739 | # Cutoff Automation LFO
740 | 'cutoff_auto_lfo_type': {
741 | 'description': '0: Rev Saw '
742 | '1: Saw '
743 | '2: Triangle (default) '
744 | '3: Square '
745 | '4: Random',
746 | 'type': 'B',
747 | 'value': 2,
748 | },
749 | 'cutoff_auto_lfo_steps': {
750 | 'description': '0: 24 steps (default) '
751 | '1: 16 steps '
752 | '2: 12 steps '
753 | '3: 8 steps '
754 | '4: 6 steps '
755 | '5: 4 steps '
756 | '6: 3 steps '
757 | '7: 2 steps '
758 | '8: 3/2 step '
759 | '9: 1 step '
760 | '10: 3/4 step '
761 | '11: 1/2 step '
762 | '12: 3/8 step '
763 | '13: 1/3 step '
764 | '14: 1/4 step '
765 | '15: 3/16 step '
766 | '16: 1/6 step '
767 | '17: 1/8 step '
768 | '18: 1/12 step '
769 | '19: 1/16 step '
770 | '20: 1/24 step '
771 | '21: 1/32 step '
772 | '22: 1/48 step '
773 | '23: 1/64 step',
774 | 'type': 'B',
775 | 'value': 0,
776 | },
777 | 'unknown_67': {
778 | 'description': 'unknown - 0',
779 | 'type': 'B',
780 | 'value': 0,
781 | },
782 | 'unknown_68': {
783 | 'description': 'unknown - 0',
784 | 'type': 'B',
785 | 'value': 0,
786 | },
787 | 'cutoff_auto_lfo_amount': {
788 | 'description': 'float: 0.0 - 1.0 (0-100%)',
789 | 'type': 'f',
790 | 'value': 0.5,
791 | },
792 | # Wavetable Position Automation LFO
793 | 'wt_pos_auto_lfo_type': {
794 | 'description': '0: Rev Saw '
795 | '1: Saw '
796 | '2: Triangle (default) '
797 | '3: Square '
798 | '4: Random',
799 | 'type': 'B',
800 | 'value': 2,
801 | },
802 | 'wt_pos_auto_lfo_steps': {
803 | 'description': '0: 24 steps (default) '
804 | '1: 16 steps '
805 | '2: 12 steps '
806 | '3: 8 steps '
807 | '4: 6 steps '
808 | '5: 4 steps '
809 | '6: 3 steps '
810 | '7: 2 steps '
811 | '8: 3/2 step '
812 | '9: 1 step '
813 | '10: 3/4 step '
814 | '11: 1/2 step '
815 | '12: 3/8 step '
816 | '13: 1/3 step '
817 | '14: 1/4 step '
818 | '15: 3/16 step '
819 | '16: 1/6 step '
820 | '17: 1/8 step '
821 | '18: 1/12 step '
822 | '19: 1/16 step '
823 | '20: 1/24 step '
824 | '21: 1/32 step '
825 | '22: 1/48 step '
826 | '23: 1/64 step',
827 | 'type': 'B',
828 | 'value': 0,
829 | },
830 | 'unknown_69': {
831 | 'description': 'unknown - 0',
832 | 'type': 'B',
833 | 'value': 0,
834 | },
835 | 'unknown_70': {
836 | 'description': 'unknown - 0',
837 | 'type': 'B',
838 | 'value': 0,
839 | },
840 | 'wt_pos_auto_lfo_amount': {
841 | 'description': 'float: 0.0 - 1.0 (0-100%)',
842 | 'type': 'f',
843 | 'value': 0.5,
844 | },
845 | # Granular Position Automation LFO
846 | 'gran_pos_auto_lfo_type': {
847 | 'description': '0: Rev Saw '
848 | '1: Saw '
849 | '2: Triangle (default) '
850 | '3: Square '
851 | '4: Random',
852 | 'type': 'B',
853 | 'value': 2,
854 | },
855 | 'gran_pos_auto_lfo_steps': {
856 | 'description': '0: 24 steps (default) '
857 | '1: 16 steps '
858 | '2: 12 steps '
859 | '3: 8 steps '
860 | '4: 6 steps '
861 | '5: 4 steps '
862 | '6: 3 steps '
863 | '7: 2 steps '
864 | '8: 3/2 step '
865 | '9: 1 step '
866 | '10: 3/4 step '
867 | '11: 1/2 step '
868 | '12: 3/8 step '
869 | '13: 1/3 step '
870 | '14: 1/4 step '
871 | '15: 3/16 step '
872 | '16: 1/6 step '
873 | '17: 1/8 step '
874 | '18: 1/12 step '
875 | '19: 1/16 step '
876 | '20: 1/24 step '
877 | '21: 1/32 step '
878 | '22: 1/48 step '
879 | '23: 1/64 step',
880 | 'type': 'B',
881 | 'value': 0,
882 | },
883 | 'unknown_71': {
884 | 'description': 'unknown - 0',
885 | 'type': 'B',
886 | 'value': 0,
887 | },
888 | 'unknown_72': {
889 | 'description': 'unknown - 0',
890 | 'type': 'B',
891 | 'value': 0,
892 | },
893 | 'gran_pos_auto_lfo_amount': {
894 | 'description': 'float: 0.0 - 1.0 (0-100%)',
895 | 'type': 'f',
896 | 'value': 0.5,
897 | },
898 | # Finetune Automation LFO
899 | 'finetune_auto_lfo_type': {
900 | 'description': '0: Rev Saw '
901 | '1: Saw '
902 | '2: Triangle (default) '
903 | '3: Square '
904 | '4: Random',
905 | 'type': 'B',
906 | 'value': 2,
907 | },
908 | 'finetune_auto_lfo_steps': {
909 | 'description': '0: 24 steps (default) '
910 | '1: 16 steps '
911 | '2: 12 steps '
912 | '3: 8 steps '
913 | '4: 6 steps '
914 | '5: 4 steps '
915 | '6: 3 steps '
916 | '7: 2 steps '
917 | '8: 3/2 step '
918 | '9: 1 step '
919 | '10: 3/4 step '
920 | '11: 1/2 step '
921 | '12: 3/8 step '
922 | '13: 1/3 step '
923 | '14: 1/4 step '
924 | '15: 3/16 step '
925 | '16: 1/6 step '
926 | '17: 1/8 step '
927 | '18: 1/12 step '
928 | '19: 1/16 step '
929 | '20: 1/24 step '
930 | '21: 1/32 step '
931 | '22: 1/48 step '
932 | '23: 1/64 step',
933 | 'type': 'B',
934 | 'value': 0,
935 | },
936 | 'unknown_73': {
937 | 'description': 'unknown - 0',
938 | 'type': 'B',
939 | 'value': 0,
940 | },
941 | 'unknown_74': {
942 | 'description': 'unknown - 0',
943 | 'type': 'B',
944 | 'value': 0,
945 | },
946 | 'finetune_auto_lfo_amount': {
947 | 'description': 'float: 0.0 - 1.0 (0-100%)',
948 | 'type': 'f',
949 | 'value': 0.5,
950 | },
951 | # Filter
952 | 'cutoff': {
953 | 'description': 'float: 0.0 - 1.0 (0-100%)',
954 | 'type': 'f',
955 | 'value': 1.0,
956 | },
957 | 'resonance': {
958 | 'description': 'float: 0.0 - 4.300000190734863 (0-100%)',
959 | 'type': 'f',
960 | 'value': 0.0,
961 | },
962 | 'filter_type_1': {
963 | 'description': '00: Disabled (default) '
964 | '01: Low-pass '
965 | '11: High-pass '
966 | '21: Band-pass',
967 | 'type': 'B',
968 | 'value': 0,
969 | },
970 | 'filter_type_2': {
971 | 'description': '00: Disabled (default) '
972 | '01: Low-pass '
973 | '11: High-pass '
974 | '21: Band-pass',
975 | 'type': 'B',
976 | 'value': 0,
977 | },
978 | # Instrument parameters / effects
979 | 'tune': {
980 | 'description': 'signed char: -/+24',
981 | 'type': 'b',
982 | 'value': 0,
983 | },
984 | 'finetune': {
985 | 'description': 'signed char: -/+100',
986 | 'type': 'b',
987 | 'value': 0,
988 | },
989 | 'volume': {
990 | 'description': '0: -inf dB '
991 | '1-100: -/+24 dB',
992 | 'type': 'B',
993 | 'value': 50,
994 | },
995 | 'unknown_75': {
996 | 'description': 'unknown - 0',
997 | 'type': 'B',
998 | 'value': 0,
999 | },
1000 | 'unknown_76': {
1001 | 'description': 'unknown - 0',
1002 | 'type': 'B',
1003 | 'value': 0,
1004 | },
1005 | 'unknown_77': {
1006 | 'description': 'unknown - 0',
1007 | 'type': 'B',
1008 | 'value': 0,
1009 | },
1010 | 'panning': {
1011 | 'description': '0-100: -/+50',
1012 | 'type': 'B',
1013 | 'value': 50,
1014 | },
1015 | 'unknown_78': {
1016 | 'description': 'unknown - 0',
1017 | 'type': 'B',
1018 | 'value': 0,
1019 | },
1020 | 'delay_send': {
1021 | 'description': '0: -inf dB '
1022 | '1-100: -39.6/+0 dB',
1023 | 'type': 'B',
1024 | 'value': 0,
1025 | },
1026 | 'unknown_79': {
1027 | 'description': 'unknown - 0',
1028 | 'type': 'B',
1029 | 'value': 0,
1030 | },
1031 | # Slices
1032 | 'slice_01_adjust': {
1033 | 'description': 'short: 0-65535 '
1034 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1035 | "Value range is limited by the preceeding slice's adjust value",
1036 | 'type': 'H',
1037 | 'value': 0,
1038 | },
1039 | 'slice_02_adjust': {
1040 | 'description': 'short: 0-65535 '
1041 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1042 | "Value range is limited by the preceeding slice's adjust value",
1043 | 'type': 'H',
1044 | 'value': 0,
1045 | },
1046 | 'slice_03_adjust': {
1047 | 'description': 'short: 0-65535 '
1048 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1049 | "Value range is limited by the preceeding slice's adjust value",
1050 | 'type': 'H',
1051 | 'value': 0,
1052 | },
1053 | 'slice_04_adjust': {
1054 | 'description': 'short: 0-65535 '
1055 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1056 | "Value range is limited by the preceeding slice's adjust value",
1057 | 'type': 'H',
1058 | 'value': 0,
1059 | },
1060 | 'slice_05_adjust': {
1061 | 'description': 'short: 0-65535 '
1062 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1063 | "Value range is limited by the preceeding slice's adjust value",
1064 | 'type': 'H',
1065 | 'value': 0,
1066 | },
1067 | 'slice_06_adjust': {
1068 | 'description': 'short: 0-65535 '
1069 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1070 | "Value range is limited by the preceeding slice's adjust value",
1071 | 'type': 'H',
1072 | 'value': 0,
1073 | },
1074 | 'slice_07_adjust': {
1075 | 'description': 'short: 0-65535 '
1076 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1077 | "Value range is limited by the preceeding slice's adjust value",
1078 | 'type': 'H',
1079 | 'value': 0,
1080 | },
1081 | 'slice_08_adjust': {
1082 | 'description': 'short: 0-65535 '
1083 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1084 | "Value range is limited by the preceeding slice's adjust value",
1085 | 'type': 'H',
1086 | 'value': 0,
1087 | },
1088 | 'slice_09_adjust': {
1089 | 'description': 'short: 0-65535 '
1090 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1091 | "Value range is limited by the preceeding slice's adjust value",
1092 | 'type': 'H',
1093 | 'value': 0,
1094 | },
1095 | 'slice_10_adjust': {
1096 | 'description': 'short: 0-65535 '
1097 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1098 | "Value range is limited by the preceeding slice's adjust value",
1099 | 'type': 'H',
1100 | 'value': 0,
1101 | },
1102 | 'slice_11_adjust': {
1103 | 'description': 'short: 0-65535 '
1104 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1105 | "Value range is limited by the preceeding slice's adjust value",
1106 | 'type': 'H',
1107 | 'value': 0,
1108 | },
1109 | 'slice_12_adjust': {
1110 | 'description': 'short: 0-65535 '
1111 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1112 | "Value range is limited by the preceeding slice's adjust value",
1113 | 'type': 'H',
1114 | 'value': 0,
1115 | },
1116 | 'slice_13_adjust': {
1117 | 'description': 'short: 0-65535 '
1118 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1119 | "Value range is limited by the preceeding slice's adjust value",
1120 | 'type': 'H',
1121 | 'value': 0,
1122 | },
1123 | 'slice_14_adjust': {
1124 | 'description': 'short: 0-65535 '
1125 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1126 | "Value range is limited by the preceeding slice's adjust value",
1127 | 'type': 'H',
1128 | 'value': 0,
1129 | },
1130 | 'slice_15_adjust': {
1131 | 'description': 'short: 0-65535 '
1132 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1133 | "Value range is limited by the preceeding slice's adjust value",
1134 | 'type': 'H',
1135 | 'value': 0,
1136 | },
1137 | 'slice_16_adjust': {
1138 | 'description': 'short: 0-65535 '
1139 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1140 | "Value range is limited by the preceeding slice's adjust value",
1141 | 'type': 'H',
1142 | 'value': 0,
1143 | },
1144 | 'slice_17_adjust': {
1145 | 'description': 'short: 0-65535 '
1146 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1147 | "Value range is limited by the preceeding slice's adjust value",
1148 | 'type': 'H',
1149 | 'value': 0,
1150 | },
1151 | 'slice_18_adjust': {
1152 | 'description': 'short: 0-65535 '
1153 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1154 | "Value range is limited by the preceeding slice's adjust value",
1155 | 'type': 'H',
1156 | 'value': 0,
1157 | },
1158 | 'slice_19_adjust': {
1159 | 'description': 'short: 0-65535 '
1160 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1161 | "Value range is limited by the preceeding slice's adjust value",
1162 | 'type': 'H',
1163 | 'value': 0,
1164 | },
1165 | 'slice_20_adjust': {
1166 | 'description': 'short: 0-65535 '
1167 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1168 | "Value range is limited by the preceeding slice's adjust value",
1169 | 'type': 'H',
1170 | 'value': 0,
1171 | },
1172 | 'slice_21_adjust': {
1173 | 'description': 'short: 0-65535 '
1174 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1175 | "Value range is limited by the preceeding slice's adjust value",
1176 | 'type': 'H',
1177 | 'value': 0,
1178 | },
1179 | 'slice_22_adjust': {
1180 | 'description': 'short: 0-65535 '
1181 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1182 | "Value range is limited by the preceeding slice's adjust value",
1183 | 'type': 'H',
1184 | 'value': 0,
1185 | },
1186 | 'slice_23_adjust': {
1187 | 'description': 'short: 0-65535 '
1188 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1189 | "Value range is limited by the preceeding slice's adjust value",
1190 | 'type': 'H',
1191 | 'value': 0,
1192 | },
1193 | 'slice_24_adjust': {
1194 | 'description': 'short: 0-65535 '
1195 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1196 | "Value range is limited by the preceeding slice's adjust value",
1197 | 'type': 'H',
1198 | 'value': 0,
1199 | },
1200 | 'slice_25_adjust': {
1201 | 'description': 'short: 0-65535 '
1202 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1203 | "Value range is limited by the preceeding slice's adjust value",
1204 | 'type': 'H',
1205 | 'value': 0,
1206 | },
1207 | 'slice_26_adjust': {
1208 | 'description': 'short: 0-65535 '
1209 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1210 | "Value range is limited by the preceeding slice's adjust value",
1211 | 'type': 'H',
1212 | 'value': 0,
1213 | },
1214 | 'slice_27_adjust': {
1215 | 'description': 'short: 0-65535 '
1216 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1217 | "Value range is limited by the preceeding slice's adjust value",
1218 | 'type': 'H',
1219 | 'value': 0,
1220 | },
1221 | 'slice_28_adjust': {
1222 | 'description': 'short: 0-65535 '
1223 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1224 | "Value range is limited by the preceeding slice's adjust value",
1225 | 'type': 'H',
1226 | 'value': 0,
1227 | },
1228 | 'slice_29_adjust': {
1229 | 'description': 'short: 0-65535 '
1230 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1231 | "Value range is limited by the preceeding slice's adjust value",
1232 | 'type': 'H',
1233 | 'value': 0,
1234 | },
1235 | 'slice_30_adjust': {
1236 | 'description': 'short: 0-65535 '
1237 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1238 | "Value range is limited by the preceeding slice's adjust value",
1239 | 'type': 'H',
1240 | 'value': 0,
1241 | },
1242 | 'slice_31_adjust': {
1243 | 'description': 'short: 0-65535 '
1244 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1245 | "Value range is limited by the preceeding slice's adjust value",
1246 | 'type': 'H',
1247 | 'value': 0,
1248 | },
1249 | 'slice_32_adjust': {
1250 | 'description': 'short: 0-65535 '
1251 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1252 | "Value range is limited by the preceeding slice's adjust value",
1253 | 'type': 'H',
1254 | 'value': 0,
1255 | },
1256 | 'slice_33_adjust': {
1257 | 'description': 'short: 0-65535 '
1258 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1259 | "Value range is limited by the preceeding slice's adjust value",
1260 | 'type': 'H',
1261 | 'value': 0,
1262 | },
1263 | 'slice_34_adjust': {
1264 | 'description': 'short: 0-65535 '
1265 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1266 | "Value range is limited by the preceeding slice's adjust value",
1267 | 'type': 'H',
1268 | 'value': 0,
1269 | },
1270 | 'slice_35_adjust': {
1271 | 'description': 'short: 0-65535 '
1272 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1273 | "Value range is limited by the preceeding slice's adjust value",
1274 | 'type': 'H',
1275 | 'value': 0,
1276 | },
1277 | 'slice_36_adjust': {
1278 | 'description': 'short: 0-65535 '
1279 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1280 | "Value range is limited by the preceeding slice's adjust value",
1281 | 'type': 'H',
1282 | 'value': 0,
1283 | },
1284 | 'slice_37_adjust': {
1285 | 'description': 'short: 0-65535 '
1286 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1287 | "Value range is limited by the preceeding slice's adjust value",
1288 | 'type': 'H',
1289 | 'value': 0,
1290 | },
1291 | 'slice_38_adjust': {
1292 | 'description': 'short: 0-65535 '
1293 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1294 | "Value range is limited by the preceeding slice's adjust value",
1295 | 'type': 'H',
1296 | 'value': 0,
1297 | },
1298 | 'slice_39_adjust': {
1299 | 'description': 'short: 0-65535 '
1300 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1301 | "Value range is limited by the preceeding slice's adjust value",
1302 | 'type': 'H',
1303 | 'value': 0,
1304 | },
1305 | 'slice_40_adjust': {
1306 | 'description': 'short: 0-65535 '
1307 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1308 | "Value range is limited by the preceeding slice's adjust value",
1309 | 'type': 'H',
1310 | 'value': 0,
1311 | },
1312 | 'slice_41_adjust': {
1313 | 'description': 'short: 0-65535 '
1314 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1315 | "Value range is limited by the preceeding slice's adjust value",
1316 | 'type': 'H',
1317 | 'value': 0,
1318 | },
1319 | 'slice_42_adjust': {
1320 | 'description': 'short: 0-65535 '
1321 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1322 | "Value range is limited by the preceeding slice's adjust value",
1323 | 'type': 'H',
1324 | 'value': 0,
1325 | },
1326 | 'slice_43_adjust': {
1327 | 'description': 'short: 0-65535 '
1328 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1329 | "Value range is limited by the preceeding slice's adjust value",
1330 | 'type': 'H',
1331 | 'value': 0,
1332 | },
1333 | 'slice_44_adjust': {
1334 | 'description': 'short: 0-65535 '
1335 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1336 | "Value range is limited by the preceeding slice's adjust value",
1337 | 'type': 'H',
1338 | 'value': 0,
1339 | },
1340 | 'slice_45_adjust': {
1341 | 'description': 'short: 0-65535 '
1342 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1343 | "Value range is limited by the preceeding slice's adjust value",
1344 | 'type': 'H',
1345 | 'value': 0,
1346 | },
1347 | 'slice_46_adjust': {
1348 | 'description': 'short: 0-65535 '
1349 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1350 | "Value range is limited by the preceeding slice's adjust value",
1351 | 'type': 'H',
1352 | 'value': 0,
1353 | },
1354 | 'slice_47_adjust': {
1355 | 'description': 'short: 0-65535 '
1356 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1357 | "Value range is limited by the preceeding slice's adjust value",
1358 | 'type': 'H',
1359 | 'value': 0,
1360 | },
1361 | 'slice_48_adjust': {
1362 | 'description': 'short: 0-65535 '
1363 | 'Calculate the offset using (value / 65535) * (sample length in ms) '
1364 | "Value range is limited by the preceeding slice's adjust value",
1365 | 'type': 'H',
1366 | 'value': 0,
1367 | },
1368 | 'number_of_slices': {
1369 | 'description': '0-48',
1370 | 'type': 'B',
1371 | 'value': 0,
1372 | },
1373 | 'active_slice': {
1374 | 'description': '0-47',
1375 | 'type': 'B',
1376 | 'value': 0,
1377 | },
1378 | # Granular
1379 | 'granular_length': {
1380 | 'description': 'Short: 44-44100 (1.0 - 1000.0 ms)',
1381 | 'type': 'H',
1382 | 'value': 441,
1383 | },
1384 | 'granular_position': {
1385 | 'description': 'Short: 0-65535 (start-end)',
1386 | 'type': 'H',
1387 | 'value': 0,
1388 | },
1389 | 'granular_shape': {
1390 | 'description': '0: Square (default) '
1391 | '1: Triangle '
1392 | '2: Gauss',
1393 | 'type': 'B',
1394 | 'value': 0,
1395 | },
1396 | 'granular_loop_mode': {
1397 | 'description': '0: Forward (default) '
1398 | '1: Backward '
1399 | '2: PingPong',
1400 | 'type': 'B',
1401 | 'value': 0,
1402 | },
1403 | # More Effects
1404 | 'reverb_send': {
1405 | 'description': '0: -inf dB '
1406 | '1-100: -39.6/+0 dB',
1407 | 'type': 'B',
1408 | 'value': 0,
1409 | },
1410 | 'overdrive': {
1411 | 'description': '0-100: 0-100%',
1412 | 'type': 'B',
1413 | 'value': 0,
1414 | },
1415 | 'bit_depth': {
1416 | 'description': '4-16: 4-16 bit',
1417 | 'type': 'B',
1418 | 'value': 16,
1419 | },
1420 | 'unknown_80': {
1421 | 'description': 'unknown - 0',
1422 | 'type': 'B',
1423 | 'value': 0,
1424 | },
1425 | 'unknown_81': {
1426 | 'description': 'unknown - ?',
1427 | 'type': 'B',
1428 | 'value': 171,
1429 | },
1430 | 'unknown_82': {
1431 | 'description': 'unknown - ?',
1432 | 'type': 'B',
1433 | 'value': 44,
1434 | },
1435 | 'unknown_83': {
1436 | 'description': 'unknown - ?',
1437 | 'type': 'B',
1438 | 'value': 201,
1439 | },
1440 | 'unknown_84': {
1441 | 'description': 'unknown - ?',
1442 | 'type': 'B',
1443 | 'value': 240,
1444 | },
1445 | }
1446 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/header.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=consider-using-generator
2 | import struct
3 | from copy import deepcopy
4 | from lazy_property import LazyProperty
5 | from polyend_tracker_pti_creator.utils.pti.constants import PTI_HEADER_DEFINITION
6 |
7 |
8 | class Header:
9 | def __init__(self, settings):
10 | self.definition = deepcopy(PTI_HEADER_DEFINITION)
11 | for key, value in settings.items():
12 | if isinstance(value, str):
13 | value = self.encode_string(value)
14 | self.definition[key]['value'] = value
15 |
16 | @LazyProperty
17 | def format(self) -> str:
18 | return '<' + ''.join([value['type'] for value in self.definition.values()])
19 |
20 | @LazyProperty
21 | def data(self) -> tuple:
22 | return tuple([value['value'] for value in self.definition.values()])
23 |
24 | @LazyProperty
25 | def data_bytes(self) -> bytearray:
26 | return bytearray(struct.pack(self.format, * self.data))
27 |
28 | @staticmethod
29 | def encode_string(string):
30 | ascii_string = string.encode('ASCII')
31 | pad_length = 32 - len(ascii_string)
32 | pad = b''.join([b'\x00' for x in range(pad_length)])
33 | return ascii_string + pad
34 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/pti/pti.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import List, Dict
3 | from pydub.audio_segment import AudioSegment
4 | from polyend_tracker_pti_creator.utils.audio.audio import Audio
5 | from polyend_tracker_pti_creator.utils.exceptions import (
6 | CreatorTooManyMergeFilesException,
7 | CreatorSampleTooLongException,
8 | )
9 | from polyend_tracker_pti_creator.utils.pti.header import Header
10 | from polyend_tracker_pti_creator.utils.pti.constants import PLAYBACK_VALUES
11 |
12 |
13 | class PTI:
14 | def __init__(self, settings: dict) -> None:
15 | self.files = settings['files']
16 | self.mode = settings['mode']
17 | self.playback = settings['playback']
18 | if self.mode == 'merge':
19 | self.merge_audio()
20 | else:
21 | self.get_audio()
22 |
23 | def merge_audio(self) -> None:
24 | if len(self.files) > 48:
25 | raise CreatorTooManyMergeFilesException(
26 | 'Error! Too many files selected to merge into a single pti. Limited to 48 (maximum slices).'
27 | )
28 | combined = AudioSegment.empty()
29 | slice_points = [0]
30 | for file in self.files:
31 | audio = Audio(os.path.join(file['source_path'], file['source_file_name'] + file['source_extension']))
32 | combined += audio.audio_segment
33 | slice_points.append(slice_points[-1] + int(audio.audio_segment.frame_count()))
34 | slice_points.pop()
35 | combined_length = int(combined.frame_count())
36 | slice_points = [
37 | round((point / combined_length) * 65535) for point in slice_points
38 | ]
39 | self.files[0]['audio'] = combined
40 | self.files[0]['playback'] = self.playback
41 | self.files[0]['slice_points'] = slice_points
42 | self.files = [self.files[0]]
43 |
44 | def get_audio(self) -> None:
45 | for file in self.files:
46 | audio = Audio(os.path.join(file['source_path'], file['source_file_name'] + file['source_extension']))
47 | file['audio'] = audio.audio_segment
48 | if audio.loop_points != [0, 0]:
49 | file['loop_points'] = audio.loop_points
50 | if self.playback == 'dynamic':
51 | file['playback'] = 'forward-loop' if 'loop_points' in file else 'one-shot'
52 | else:
53 | file['playback'] = self.playback
54 |
55 | def create(self) -> None:
56 | for file in self.files:
57 | if len(file['audio']) > 30000:
58 | raise CreatorSampleTooLongException(
59 | f"Error! File "
60 | f"{os.path.join(file['source_path'], file['source_file_name'] + file['source_extension'])} "
61 | f"is longer than 30 seconds (maximum .pti supported length)."
62 | )
63 | sample_length = self.get_sample_length(audio=file['audio'])
64 | settings = {
65 | 'sample_length': sample_length,
66 | 'instrument_name': file['instrument_name'],
67 | 'sample_playback': PLAYBACK_VALUES[file['playback']]
68 | }
69 | if 'loop_points' in file:
70 | settings['loop_start'] = file['loop_points'][0]
71 | settings['loop_end'] = file['loop_points'][1]
72 | if 'slice_points' in file:
73 | settings = self.set_header_slice_points(settings=settings, slice_points=file['slice_points'])
74 | header = Header(settings)
75 | pti_data = header.data_bytes
76 | wav_bytes = file['audio'].raw_data
77 | pti_data.extend(wav_bytes)
78 | with open(os.path.join(
79 | file['destination_path'],
80 | file['destination_file_name'] +
81 | file['destination_extension']
82 | ), mode='wb') as pti_file:
83 | pti_file.write(pti_data)
84 |
85 | @staticmethod
86 | def get_sample_length(audio: AudioSegment) -> int:
87 | return round((len(audio) / 1000) * 44100)
88 |
89 | @staticmethod
90 | def set_header_slice_points(settings: Dict, slice_points: List[int]) -> Dict:
91 | settings['number_of_slices'] = len(slice_points)
92 | for i, point in enumerate(slice_points):
93 | settings[f'slice_{str(i+1).zfill(2)}_adjust'] = point
94 | return settings
95 |
--------------------------------------------------------------------------------
/polyend_tracker_pti_creator/utils/settings.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict
2 | from argparse import Namespace
3 | import os
4 | import re
5 | import pathlib
6 | from lazy_property import LazyProperty
7 | from polyend_tracker_pti_creator.utils.exceptions import (
8 | CreatorSourceMissingException,
9 | CreatorDestinationInvalidException,
10 | CreatorNoSourceWavFilesException,
11 | CreatorModeInvalidException,
12 | CreatorPlaybackInvalidException,
13 | )
14 |
15 | MODES = ['normal', 'merge']
16 | PLAYBACK_TYPES = [
17 | 'one-shot',
18 | 'forward-loop',
19 | 'backward-loop',
20 | 'ping-pong-loop',
21 | 'slice',
22 | 'beat-slice',
23 | 'wavetable',
24 | 'granular',
25 | 'dynamic',
26 | ]
27 |
28 |
29 | class Settings:
30 | def __init__(self, args: Namespace) -> None:
31 | self.args = args
32 | for index, file in enumerate(self.files):
33 | file['destination_path'] = self.destination
34 | file['destination_file_name'] = f"{self.file_name.replace('.pti', '')}" \
35 | f"{index + 1 if self.batch and self.mode != 'merge' else ''}" \
36 | if self.file_name else file['source_file_name']
37 | file['destination_extension'] = '.pti'
38 | if self.instrument_name:
39 | base_instrument_name = self.instrument_name
40 | else:
41 | base_instrument_name = file['source_file_name']
42 | base_instrument_name = re.sub(r'[^a-zA-Z0-9@ +-]', '', base_instrument_name)
43 | file['instrument_name'] = f"{base_instrument_name[0:32 - len(str(index + 1))]}{index + 1}" \
44 | if self.batch and self.instrument_name and self.mode != 'merge' else base_instrument_name[0:32]
45 |
46 | self.settings = {
47 | "files": self.files,
48 | "mode": self.mode,
49 | "playback": self.playback,
50 | }
51 |
52 | @LazyProperty
53 | def mode(self) -> str:
54 | mode = self.args.mode
55 | mode = mode if mode else 'normal'
56 | if mode not in MODES:
57 | raise CreatorModeInvalidException(
58 | "Error! Gave an invalid mode. Valid values are 'normal' and 'merge'."
59 | )
60 | return mode
61 |
62 | @LazyProperty
63 | def source(self) -> str:
64 | if not self.args.source:
65 | raise CreatorSourceMissingException(
66 | "Error! Please specify required --source (-s) argument."
67 | )
68 |
69 | if not os.path.exists(self.args.source):
70 | raise CreatorSourceMissingException(
71 | "Error! Please specify a valid source path."
72 | )
73 | return self.args.source
74 |
75 | @LazyProperty
76 | def destination(self) -> str:
77 | destination = self.args.destination
78 | if destination:
79 | if not os.path.exists(destination):
80 | raise CreatorDestinationInvalidException(
81 | "Error! Please specify a valid destination path."
82 | )
83 | else:
84 | destination = self.source
85 | if os.path.isfile(destination):
86 | destination = os.path.dirname(destination)
87 | return destination
88 |
89 | @LazyProperty
90 | def instrument_name(self) -> str:
91 | return self.args.instrument_name \
92 | if self.args.instrument_name or not self.args.file_name \
93 | else self.args.file_name.replace('.pti', '')
94 |
95 | @LazyProperty
96 | def file_name(self) -> str:
97 | file_name = self.args.file_name
98 | if file_name:
99 | return file_name
100 | return os.path.basename(self.source) if os.path.isfile(self.source) else None
101 |
102 | @LazyProperty
103 | def files(self) -> List[Dict[str, str]]:
104 | source_files = [self.source]
105 | if not os.path.isfile(self.source):
106 | source_files = os.listdir(self.source)
107 | source_files.sort()
108 | source_files = [os.path.join(self.source, file) for file in source_files]
109 | files = [
110 | {
111 | 'source_path': os.path.dirname(file),
112 | 'source_file_name': pathlib.Path(os.path.basename(file)).stem,
113 | 'source_extension': pathlib.Path(os.path.basename(file)).suffix
114 | } for file in source_files if pathlib.Path(os.path.basename(file)).suffix.lower() == '.wav'
115 | ]
116 |
117 | if not files:
118 | raise CreatorNoSourceWavFilesException(
119 | "Error! No .wav files selected. Currently only .wav files supported. "
120 | "If you're trying to use non-wav files, please convert to .wav then try again."
121 | )
122 | return files
123 |
124 | @LazyProperty
125 | def playback(self) -> str:
126 | playback = self.args.playback
127 | if not playback:
128 | if self.mode == 'merge':
129 | playback = 'beat-slice'
130 | else:
131 | playback = 'dynamic'
132 |
133 | if playback not in PLAYBACK_TYPES:
134 | raise CreatorPlaybackInvalidException(
135 | "Error! Gave an invalid playback type. Valid values are 'one-shot', 'forward-loop', 'backward-loop', "
136 | "'ping-pong-loop', 'slice', 'beat-slice', 'wavetable', 'granular', and 'dynamic'"
137 | )
138 | return playback
139 |
140 | @LazyProperty
141 | def batch(self) -> bool:
142 | return len(self.files) > 1
143 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | wavchunk == 1.0.1
2 | wave-chunk-parser == 1.5.0
3 | pydub == 0.25.1
4 | lazy-property == 0.0.1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """ Polyend Tracker .pti creator setup """
2 | import distutils.cmd
3 | import distutils.log
4 | import os
5 | import subprocess
6 | import setuptools.command.build_py
7 | import setuptools.command.install
8 | from setuptools import setup
9 |
10 | sources = ["./setup.py", "./polyend_tracker_pti_creator", "./tests"]
11 |
12 |
13 | class PylintCommand(distutils.cmd.Command):
14 | """A custom command to run Pylint on all Python source files."""
15 |
16 | description = "run Pylint on Python source files"
17 | user_options = [
18 | # The format is (long option, short option, description).
19 | ("pylint-rcfile=", None, "path to Pylint config file"),
20 | ]
21 |
22 | def initialize_options(self):
23 | """Set default values for options."""
24 | # Each user option must be listed here with their default value.
25 | self.pylint_rcfile = "standard.rc"
26 |
27 | def finalize_options(self):
28 | """Post-process options."""
29 | if self.pylint_rcfile:
30 | assert os.path.exists(self.pylint_rcfile), (
31 | f"Pylint config file {self.pylint_rcfile} does not exist."
32 | )
33 |
34 | def run(self):
35 | """Run command."""
36 | command = ["pylint"]
37 | if self.pylint_rcfile:
38 | command.append(f"--rcfile={self.pylint_rcfile}")
39 | command = command + sources
40 | self.announce(f"Running command: {str(command)}", level=distutils.log.INFO)
41 | subprocess.check_call(command)
42 |
43 |
44 | class BlackCommand(distutils.cmd.Command):
45 | """A custom command to run Python Black on all Python source files."""
46 |
47 | description = "run Black on Python source files"
48 | user_options = [
49 | # The format is (long option, short option, description).
50 | ("black-config=", None, "path to black config file"),
51 | ]
52 |
53 | def initialize_options(self):
54 | """Set default values for options."""
55 | # Each user option must be listed here with their default value.
56 | self.black_config_file = ""
57 |
58 | def finalize_options(self):
59 | """Post-process options."""
60 | if self.black_config_file:
61 | assert os.path.exists(self.black_config_file), (
62 | f"black config file {self.black_config_file} does not exist."
63 | )
64 |
65 | def run(self):
66 | """Run command."""
67 | command = ["black"]
68 | if self.black_config_file:
69 | command.append(f"--config={self.black_config_file}")
70 | command = command + sources
71 | self.announce(f"Running command: {str(command)}", level=distutils.log.INFO)
72 | subprocess.check_call(command)
73 |
74 |
75 | class BuildPyCommand(setuptools.command.build_py.build_py):
76 | """Custom build command."""
77 |
78 | def run(self):
79 | setuptools.command.build_py.build_py.run(self)
80 |
81 |
82 | class InstallPyCommand(setuptools.command.install.install):
83 | """Custom install command."""
84 |
85 | def run(self):
86 | pip_process = ["pip", "install", "-r"]
87 | subprocess.check_call(pip_process + ["./requirements.txt"])
88 | subprocess.check_call(pip_process + ["./test_requirements.txt"])
89 | setuptools.command.install.install.run(self)
90 |
91 |
92 | setup(
93 | name="polyend_tracker_pti_creator",
94 | version="0.1.0",
95 | packages=[
96 | "polyend_tracker_pti_creator",
97 | "polyend_tracker_pti_creator.utils",
98 | "polyend_tracker_pti_creator.utils.audio",
99 | "polyend_tracker_pti_creator.utils.pti",
100 | ],
101 | python_requires=">=3",
102 | entry_points={
103 | "console_scripts": ["pet-pti-creator=polyend_tracker_pti_creator.creator:main"]
104 | },
105 | cmdclass={
106 | "format": BlackCommand,
107 | "build_py": BuildPyCommand,
108 | "install": InstallPyCommand,
109 | "lint": PylintCommand,
110 | },
111 | test_suite="tests",
112 | url="https://github.com/adriananders/polyend-tracker-pti-creator",
113 | license="MIT",
114 | author="Adrian Anders",
115 | author_email="realaanders@gmail.com",
116 | description="Polyend Tracker .pti instrument creator",
117 | )
118 |
--------------------------------------------------------------------------------
/standard.rc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # A comma-separated list of package or module names from where C extensions may
4 | # be loaded. Extensions are loading into the active Python interpreter and may
5 | # run arbitrary code.
6 | extension-pkg-whitelist=lxml
7 |
8 | # Specify a score threshold to be exceeded before program exits with error.
9 | fail-under=10.0
10 |
11 | # Add files or directories to the blacklist. They should be base names, not
12 | # paths.
13 | ignore=CVS
14 |
15 | # Add files or directories matching the regex patterns to the blacklist. The
16 | # regex matches against base names, not paths.
17 | ignore-patterns=
18 |
19 | # Python code to execute, usually for sys.path manipulation such as
20 | # pygtk.require().
21 | #init-hook=
22 |
23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
24 | # number of processors available to use.
25 | jobs=1
26 |
27 | # Control the amount of potential inferred values when inferring a single
28 | # object. This can help the performance when dealing with large functions or
29 | # complex, nested conditions.
30 | limit-inference-results=100
31 |
32 | # List of plugins (as comma separated values of python module names) to load,
33 | # usually to register additional checkers.
34 | load-plugins=
35 |
36 | # Pickle collected data for later comparisons.
37 | persistent=yes
38 |
39 | # When enabled, pylint would attempt to guess common misconfiguration and emit
40 | # user-friendly hints instead of false-positive error messages.
41 | suggestion-mode=yes
42 |
43 | # Allow loading of arbitrary C extensions. Extensions are imported into the
44 | # active Python interpreter and may run arbitrary code.
45 | unsafe-load-any-extension=no
46 |
47 |
48 | [MESSAGES CONTROL]
49 |
50 | # Only show warnings with the listed confidence levels. Leave empty to show
51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
52 | confidence=
53 |
54 | # Disable the message, report, category or checker with the given id(s). You
55 | # can either give multiple identifiers separated by comma (,) or put this
56 | # option multiple times (only on the command line, not in the configuration
57 | # file where it should appear only once). You can also use "--disable=all" to
58 | # disable everything first and then reenable specific checks. For example, if
59 | # you want to run only the similarities checker, you can use "--disable=all
60 | # --enable=similarities". If you want to run only the classes checker, but have
61 | # no Warning level messages displayed, use "--disable=all --enable=classes
62 | # --disable=W".
63 | disable=too-few-public-methods,
64 | c-extension-no-member,
65 | no-self-use,
66 | missing-module-docstring,
67 | missing-class-docstring,
68 | missing-function-docstring,
69 | too-many-locals,
70 | print-statement,
71 | parameter-unpacking,
72 | unpacking-in-except,
73 | old-raise-syntax,
74 | backtick,
75 | long-suffix,
76 | old-ne-operator,
77 | old-octal-literal,
78 | import-star-module-level,
79 | non-ascii-bytes-literal,
80 | raw-checker-failed,
81 | bad-inline-option,
82 | locally-disabled,
83 | file-ignored,
84 | suppressed-message,
85 | useless-suppression,
86 | deprecated-pragma,
87 | use-symbolic-message-instead,
88 | apply-builtin,
89 | basestring-builtin,
90 | buffer-builtin,
91 | cmp-builtin,
92 | coerce-builtin,
93 | execfile-builtin,
94 | file-builtin,
95 | long-builtin,
96 | raw_input-builtin,
97 | reduce-builtin,
98 | standarderror-builtin,
99 | unicode-builtin,
100 | xrange-builtin,
101 | coerce-method,
102 | delslice-method,
103 | getslice-method,
104 | setslice-method,
105 | no-absolute-import,
106 | old-division,
107 | dict-iter-method,
108 | dict-view-method,
109 | next-method-called,
110 | metaclass-assignment,
111 | indexing-exception,
112 | raising-string,
113 | reload-builtin,
114 | oct-method,
115 | hex-method,
116 | nonzero-method,
117 | cmp-method,
118 | input-builtin,
119 | round-builtin,
120 | intern-builtin,
121 | unichr-builtin,
122 | map-builtin-not-iterating,
123 | zip-builtin-not-iterating,
124 | range-builtin-not-iterating,
125 | filter-builtin-not-iterating,
126 | using-cmp-argument,
127 | eq-without-hash,
128 | div-method,
129 | idiv-method,
130 | rdiv-method,
131 | exception-message-attribute,
132 | invalid-str-codec,
133 | sys-max-int,
134 | bad-python3-import,
135 | deprecated-string-function,
136 | deprecated-str-translate-call,
137 | deprecated-itertools-function,
138 | deprecated-types-field,
139 | next-method-defined,
140 | dict-items-not-iterating,
141 | dict-keys-not-iterating,
142 | dict-values-not-iterating,
143 | deprecated-operator-function,
144 | deprecated-urllib-function,
145 | xreadlines-attribute,
146 | deprecated-sys-function,
147 | exception-escape,
148 | comprehension-escape
149 |
150 | # Enable the message, report, category or checker with the given id(s). You can
151 | # either give multiple identifier separated by comma (,) or put this option
152 | # multiple time (only on the command line, not in the configuration file where
153 | # it should appear only once). See also the "--disable" option for examples.
154 | enable=c-extension-no-member
155 |
156 |
157 | [REPORTS]
158 |
159 | # Python expression which should return a score less than or equal to 10. You
160 | # have access to the variables 'error', 'warning', 'refactor', and 'convention'
161 | # which contain the number of messages in each category, as well as 'statement'
162 | # which is the total number of statements analyzed. This score is used by the
163 | # global evaluation report (RP0004).
164 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
165 |
166 | # Template used to display messages. This is a python new-style format string
167 | # used to format the message information. See doc for all details.
168 | #msg-template=
169 |
170 | # Set the output format. Available formats are text, parseable, colorized, json
171 | # and msvs (visual studio). You can also give a reporter class, e.g.
172 | # mypackage.mymodule.MyReporterClass.
173 | output-format=text
174 |
175 | # Tells whether to display a full report or only the messages.
176 | reports=no
177 |
178 | # Activate the evaluation score.
179 | score=yes
180 |
181 |
182 | [REFACTORING]
183 |
184 | # Maximum number of nested blocks for function / method body
185 | max-nested-blocks=5
186 |
187 | # Complete name of functions that never returns. When checking for
188 | # inconsistent-return-statements if a never returning function is called then
189 | # it will be considered as an explicit return statement and no message will be
190 | # printed.
191 | never-returning-functions=sys.exit
192 |
193 |
194 | [LOGGING]
195 |
196 | # The type of string formatting that logging methods do. `old` means using %
197 | # formatting, `new` is for `{}` formatting.
198 | logging-format-style=old
199 |
200 | # Logging modules to check that the string format arguments are in logging
201 | # function parameter format.
202 | logging-modules=logging
203 |
204 |
205 | [SPELLING]
206 |
207 | # Limits count of emitted suggestions for spelling mistakes.
208 | max-spelling-suggestions=4
209 |
210 | # Spelling dictionary name. Available dictionaries: none. To make it work,
211 | # install the python-enchant package.
212 | spelling-dict=
213 |
214 | # List of comma separated words that should not be checked.
215 | spelling-ignore-words=
216 |
217 | # A path to a file that contains the private dictionary; one word per line.
218 | spelling-private-dict-file=
219 |
220 | # Tells whether to store unknown words to the private dictionary (see the
221 | # --spelling-private-dict-file option) instead of raising a message.
222 | spelling-store-unknown-words=no
223 |
224 |
225 | [MISCELLANEOUS]
226 |
227 | # List of note tags to take in consideration, separated by a comma.
228 | notes=FIXME,
229 | XXX,
230 | TODO
231 |
232 | # Regular expression of note tags to take in consideration.
233 | #notes-rgx=
234 |
235 |
236 | [TYPECHECK]
237 |
238 | # List of decorators that produce context managers, such as
239 | # contextlib.contextmanager. Add to this list to register other decorators that
240 | # produce valid context managers.
241 | contextmanager-decorators=contextlib.contextmanager
242 |
243 | # List of members which are set dynamically and missed by pylint inference
244 | # system, and so shouldn't trigger E1101 when accessed. Python regular
245 | # expressions are accepted.
246 | generated-members=
247 |
248 | # Tells whether missing members accessed in mixin class should be ignored. A
249 | # mixin class is detected if its name ends with "mixin" (case insensitive).
250 | ignore-mixin-members=yes
251 |
252 | # Tells whether to warn about missing members when the owner of the attribute
253 | # is inferred to be None.
254 | ignore-none=yes
255 |
256 | # This flag controls whether pylint should warn about no-member and similar
257 | # checks whenever an opaque object is returned when inferring. The inference
258 | # can return multiple potential results while evaluating a Python object, but
259 | # some branches might not be evaluated, which results in partial inference. In
260 | # that case, it might be useful to still emit no-member and other checks for
261 | # the rest of the inferred objects.
262 | ignore-on-opaque-inference=yes
263 |
264 | # List of class names for which member attributes should not be checked (useful
265 | # for classes with dynamically set attributes). This supports the use of
266 | # qualified names.
267 | ignored-classes=optparse.Values,thread._local,_thread._local
268 |
269 | # List of module names for which member attributes should not be checked
270 | # (useful for modules/projects where namespaces are manipulated during runtime
271 | # and thus existing member attributes cannot be deduced by static analysis). It
272 | # supports qualified module names, as well as Unix pattern matching.
273 | ignored-modules=
274 |
275 | # Show a hint with possible names when a member name was not found. The aspect
276 | # of finding the hint is based on edit distance.
277 | missing-member-hint=yes
278 |
279 | # The minimum edit distance a name should have in order to be considered a
280 | # similar match for a missing member name.
281 | missing-member-hint-distance=1
282 |
283 | # The total number of similar names that should be taken in consideration when
284 | # showing a hint for a missing member.
285 | missing-member-max-choices=1
286 |
287 | # List of decorators that change the signature of a decorated function.
288 | signature-mutators=
289 |
290 |
291 | [VARIABLES]
292 |
293 | # List of additional names supposed to be defined in builtins. Remember that
294 | # you should avoid defining new builtins when possible.
295 | additional-builtins=
296 |
297 | # Tells whether unused global variables should be treated as a violation.
298 | allow-global-unused-variables=yes
299 |
300 | # List of strings which can identify a callback function by name. A callback
301 | # name must start or end with one of those strings.
302 | callbacks=cb_,
303 | _cb
304 |
305 | # A regular expression matching the name of dummy variables (i.e. expected to
306 | # not be used).
307 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
308 |
309 | # Argument names that match this expression will be ignored. Default to name
310 | # with leading underscore.
311 | ignored-argument-names=_.*|^ignored_|^unused_
312 |
313 | # Tells whether we should check for unused import in __init__ files.
314 | init-import=no
315 |
316 | # List of qualified module names which can have objects that can redefine
317 | # builtins.
318 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
319 |
320 |
321 | [FORMAT]
322 |
323 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
324 | expected-line-ending-format=
325 |
326 | # Regexp for a line that is allowed to be longer than the limit.
327 | ignore-long-lines=^\s*(# )??$
328 |
329 | # Number of spaces of indent required inside a hanging or continued line.
330 | indent-after-paren=4
331 |
332 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
333 | # tab).
334 | indent-string=' '
335 |
336 | # Maximum number of characters on a single line.
337 | max-line-length=120
338 |
339 | # Maximum number of lines in a module.
340 | max-module-lines=1000
341 |
342 | # Allow the body of a class to be on the same line as the declaration if body
343 | # contains single statement.
344 | single-line-class-stmt=no
345 |
346 | # Allow the body of an if to be on the same line as the test if there is no
347 | # else.
348 | single-line-if-stmt=no
349 |
350 |
351 | [SIMILARITIES]
352 |
353 | # Ignore comments when computing similarities.
354 | ignore-comments=yes
355 |
356 | # Ignore docstrings when computing similarities.
357 | ignore-docstrings=yes
358 |
359 | # Ignore imports when computing similarities.
360 | ignore-imports=no
361 |
362 | # Minimum lines number of a similarity.
363 | min-similarity-lines=17
364 |
365 |
366 | [BASIC]
367 |
368 | # Naming style matching correct argument names.
369 | argument-naming-style=snake_case
370 |
371 | # Regular expression matching correct argument names. Overrides argument-
372 | # naming-style.
373 | #argument-rgx=
374 |
375 | # Naming style matching correct attribute names.
376 | attr-naming-style=snake_case
377 |
378 | # Regular expression matching correct attribute names. Overrides attr-naming-
379 | # style.
380 | #attr-rgx=
381 |
382 | # Bad variable names which should always be refused, separated by a comma.
383 | bad-names=foo,
384 | bar,
385 | baz,
386 | toto,
387 | tutu,
388 | tata
389 |
390 | # Bad variable names regexes, separated by a comma. If names match any regex,
391 | # they will always be refused
392 | bad-names-rgxs=
393 |
394 | # Naming style matching correct class attribute names.
395 | class-attribute-naming-style=any
396 |
397 | # Regular expression matching correct class attribute names. Overrides class-
398 | # attribute-naming-style.
399 | #class-attribute-rgx=
400 |
401 | # Naming style matching correct class names.
402 | class-naming-style=PascalCase
403 |
404 | # Regular expression matching correct class names. Overrides class-naming-
405 | # style.
406 | #class-rgx=
407 |
408 | # Naming style matching correct constant names.
409 | const-naming-style=UPPER_CASE
410 |
411 | # Regular expression matching correct constant names. Overrides const-naming-
412 | # style.
413 | #const-rgx=
414 |
415 | # Minimum line length for functions/classes that require docstrings, shorter
416 | # ones are exempt.
417 | docstring-min-length=-1
418 |
419 | # Naming style matching correct function names.
420 | function-naming-style=snake_case
421 |
422 | # Regular expression matching correct function names. Overrides function-
423 | # naming-style.
424 | #function-rgx=
425 |
426 | # Good variable names which should always be accepted, separated by a comma.
427 | good-names=i,
428 | j,
429 | k,
430 | ex,
431 | Run,
432 | _
433 |
434 | # Good variable names regexes, separated by a comma. If names match any regex,
435 | # they will always be accepted
436 | good-names-rgxs=
437 |
438 | # Include a hint for the correct naming format with invalid-name.
439 | include-naming-hint=no
440 |
441 | # Naming style matching correct inline iteration names.
442 | inlinevar-naming-style=any
443 |
444 | # Regular expression matching correct inline iteration names. Overrides
445 | # inlinevar-naming-style.
446 | #inlinevar-rgx=
447 |
448 | # Naming style matching correct method names.
449 | method-naming-style=snake_case
450 |
451 | # Regular expression matching correct method names. Overrides method-naming-
452 | # style.
453 | #method-rgx=
454 |
455 | # Naming style matching correct module names.
456 | module-naming-style=snake_case
457 |
458 | # Regular expression matching correct module names. Overrides module-naming-
459 | # style.
460 | #module-rgx=
461 |
462 | # Colon-delimited sets of names that determine each other's naming style when
463 | # the name regexes allow several styles.
464 | name-group=
465 |
466 | # Regular expression which should only match function or class names that do
467 | # not require a docstring.
468 | no-docstring-rgx=^_
469 |
470 | # List of decorators that produce properties, such as abc.abstractproperty. Add
471 | # to this list to register other decorators that produce valid properties.
472 | # These decorators are taken in consideration only for invalid-name.
473 | property-classes=abc.abstractproperty
474 |
475 | # Naming style matching correct variable names.
476 | variable-naming-style=snake_case
477 |
478 | # Regular expression matching correct variable names. Overrides variable-
479 | # naming-style.
480 | #variable-rgx=
481 |
482 |
483 | [STRING]
484 |
485 | # This flag controls whether inconsistent-quotes generates a warning when the
486 | # character used as a quote delimiter is used inconsistently within a module.
487 | check-quote-consistency=no
488 |
489 | # This flag controls whether the implicit-str-concat should generate a warning
490 | # on implicit string concatenation in sequences defined over several lines.
491 | check-str-concat-over-line-jumps=no
492 |
493 |
494 | [IMPORTS]
495 |
496 | # List of modules that can be imported at any level, not just the top level
497 | # one.
498 | allow-any-import-level=
499 |
500 | # Allow wildcard imports from modules that define __all__.
501 | allow-wildcard-with-all=no
502 |
503 | # Analyse import fallback blocks. This can be used to support both Python 2 and
504 | # 3 compatible code, which means that the block might have code that exists
505 | # only in one or another interpreter, leading to false positives when analysed.
506 | analyse-fallback-blocks=no
507 |
508 | # Deprecated modules which should not be used, separated by a comma.
509 | deprecated-modules=optparse,tkinter.tix
510 |
511 | # Create a graph of external dependencies in the given file (report RP0402 must
512 | # not be disabled).
513 | ext-import-graph=
514 |
515 | # Create a graph of every (i.e. internal and external) dependencies in the
516 | # given file (report RP0402 must not be disabled).
517 | import-graph=
518 |
519 | # Create a graph of internal dependencies in the given file (report RP0402 must
520 | # not be disabled).
521 | int-import-graph=
522 |
523 | # Force import order to recognize a module as part of the standard
524 | # compatibility libraries.
525 | known-standard-library=
526 |
527 | # Force import order to recognize a module as part of a third party library.
528 | known-third-party=enchant
529 |
530 | # Couples of modules and preferred modules, separated by a comma.
531 | preferred-modules=
532 |
533 |
534 | [CLASSES]
535 |
536 | # Warn about protected attribute access inside special methods
537 | check-protected-access-in-special-methods=no
538 |
539 | # List of method names used to declare (i.e. assign) instance attributes.
540 | defining-attr-methods=__init__,
541 | __new__,
542 | setUp,
543 | __post_init__
544 |
545 | # List of member names, which should be excluded from the protected access
546 | # warning.
547 | exclude-protected=_asdict,
548 | _fields,
549 | _replace,
550 | _source,
551 | _make
552 |
553 | # List of valid names for the first argument in a class method.
554 | valid-classmethod-first-arg=cls
555 |
556 | # List of valid names for the first argument in a metaclass class method.
557 | valid-metaclass-classmethod-first-arg=cls
558 |
559 |
560 | [DESIGN]
561 |
562 | # Maximum number of arguments for function / method.
563 | max-args=5
564 |
565 | # Maximum number of attributes for a class (see R0902).
566 | max-attributes=7
567 |
568 | # Maximum number of boolean expressions in an if statement (see R0916).
569 | max-bool-expr=5
570 |
571 | # Maximum number of branch for function / method body.
572 | max-branches=12
573 |
574 | # Maximum number of locals for function / method body.
575 | max-locals=15
576 |
577 | # Maximum number of parents for a class (see R0901).
578 | max-parents=7
579 |
580 | # Maximum number of public methods for a class (see R0904).
581 | max-public-methods=20
582 |
583 | # Maximum number of return / yield for function / method body.
584 | max-returns=6
585 |
586 | # Maximum number of statements in function / method body.
587 | max-statements=50
588 |
589 | # Minimum number of public methods for a class (see R0903).
590 | min-public-methods=2
591 |
592 |
593 | [EXCEPTIONS]
594 |
595 | # Exceptions that will emit a warning when being caught. Defaults to
596 | # "BaseException, Exception".
597 | overgeneral-exceptions=BaseException,
598 | Exception
--------------------------------------------------------------------------------
/test_requirements.txt:
--------------------------------------------------------------------------------
1 | black == 20.8b1
2 | coverage == 7.00
3 | flake8 == 6.0.0
4 | isort == 5.11.4
5 | pep8-naming == 0.13.3
6 | pylint == 2.15.10
7 | sphinx == 6.1.3
8 | parameterized==0.8.1
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/__init__.py
--------------------------------------------------------------------------------
/tests/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/tests/integ/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/integ/__init__.py
--------------------------------------------------------------------------------
/tests/integ/test_integ.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from polyend_tracker_pti_creator.utils.pti.pti import PTI
3 |
4 | class TestInteg(TestCase):
5 | def test_integration(self) -> None:
6 | settings = {
7 | 'files': [
8 | {
9 | 'source_path': './tests/utils/files',
10 | 'source_file_name': 'tone',
11 | 'source_extension': '.wav',
12 | 'destination_path': './tests/utils/files',
13 | 'destination_file_name': 'tone',
14 | 'destination_extension': '.pti',
15 | 'instrument_name': 'test'
16 | },
17 | {
18 | 'source_path': '/Users/adriananders/polyend-tracker-pti-creator/tests/utils/files/integ',
19 | 'source_file_name': 'ss',
20 | 'source_extension': '.wav',
21 | 'destination_path': '/Users/adriananders/polyend-tracker-pti-creator/tests/utils/files/integ',
22 | 'destination_file_name': 'ss',
23 | 'destination_extension': '.pti',
24 | 'instrument_name': 'test'
25 | },
26 | ],
27 | 'mode': 'normal',
28 | 'playback': 'dynamic'
29 | }
30 | PTI(settings).create()
--------------------------------------------------------------------------------
/tests/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/__init__.py
--------------------------------------------------------------------------------
/tests/utils/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/tests/utils/__pycache__/test_settings.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/__pycache__/test_settings.cpython-38.pyc
--------------------------------------------------------------------------------
/tests/utils/audio/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/audio/__init__.py
--------------------------------------------------------------------------------
/tests/utils/audio/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/audio/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/tests/utils/audio/__pycache__/test_audio.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/audio/__pycache__/test_audio.cpython-38.pyc
--------------------------------------------------------------------------------
/tests/utils/audio/__pycache__/test_wave_chunk_parser_extended.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adriananders/polyend-tracker-pti-creator/fd54b28494b59f0fda11b3d89c3a64b0918e76d7/tests/utils/audio/__pycache__/test_wave_chunk_parser_extended.cpython-38.pyc
--------------------------------------------------------------------------------
/tests/utils/audio/test_audio.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import TestCase
3 | from polyend_tracker_pti_creator.utils.audio.audio import (
4 | Audio,
5 | )
6 |
7 | DIR_PATH = os.path.dirname(os.path.realpath(__file__))
8 |
9 | FILE_PATHS = [
10 | os.path.join(DIR_PATH, "../files/tone.wav"),
11 | os.path.join(DIR_PATH, "../files/tone2.wav"),
12 | ]
13 |
14 |
15 | class TestAudio(TestCase):
16 | def test_audio_file(self) -> None:
17 | audio = Audio(FILE_PATHS[0])
18 | self.assertEqual(audio.loop_points, [13968, 65532])
19 | self.assertEqual(audio.audio_segment.frame_rate, 44100)
20 | self.assertEqual(audio.audio_segment.channels, 1)
21 | self.assertEqual(audio.audio_segment.frame_width, 2)
22 | audio2 = Audio(FILE_PATHS[1])
23 | self.assertEqual(audio2.loop_points, [0, 0])
24 |
--------------------------------------------------------------------------------
/tests/utils/audio/test_wave_chunk_parser_extended.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=too-many-instance-attributes, too-many-arguments
2 | from unittest import TestCase
3 | import os
4 | from typing import List
5 | from parameterized import parameterized
6 | import numpy as np
7 | from wave_chunk_parser.exceptions import (
8 | InvalidHeaderException,
9 | )
10 | from wave_chunk_parser.chunks import (
11 | FormatChunk,
12 | DataChunk,
13 | WaveFormat,
14 | )
15 | from polyend_tracker_pti_creator.utils.audio.wave_chunk_parser_extended import (
16 | SampleChunk,
17 | RiffChunkExtended,
18 | )
19 |
20 | DIR_PATH = os.path.dirname(os.path.realpath(__file__))
21 |
22 |
23 | class TestSampleChunk(TestCase):
24 | @parameterized.expand(
25 | [
26 | (
27 | os.path.join(DIR_PATH, "../files/test_tone.wav"),
28 | 75216,
29 | 0,
30 | 0,
31 | 22675,
32 | 60,
33 | 0,
34 | 0,
35 | 0,
36 | 1,
37 | 0,
38 | 0,
39 | 0,
40 | 37485,
41 | 37585,
42 | 0,
43 | 0,
44 | )
45 | ]
46 | )
47 | def test_read_valid_format_chunk(
48 | self,
49 | file_name: str,
50 | chunk_offset: int,
51 | expected_manufacturer: int,
52 | expected_product: int,
53 | expected_sample_period: int,
54 | expected_midi_unity_note: int,
55 | expected_midi_pitch_fraction: int,
56 | expected_smpte_format: int,
57 | expected_smpte_offset: int,
58 | expected_number_of_sample_loops: int,
59 | expected_sampler_data: int,
60 | expected_first_cue_point_id: int,
61 | expected_first_loop_type: int,
62 | expected_first_loop_start: int,
63 | expected_first_loop_end: int,
64 | expected_first_loop_fraction: int,
65 | expected_first_loop_play_count: int,
66 | ) -> None:
67 | """
68 | Valid sample chunks can be read from a file.
69 | """
70 |
71 | # Arrange
72 |
73 | with open(file_name, "rb") as file:
74 |
75 | # Act
76 |
77 | chunk: SampleChunk = SampleChunk.from_file(file, chunk_offset)
78 |
79 | # Assert
80 |
81 | self.assertIsNotNone(chunk)
82 | self.assertEqual(chunk.get_name, b"smpl")
83 | self.assertEqual(chunk.manufacturer, expected_manufacturer)
84 | self.assertEqual(chunk.product, expected_product)
85 | self.assertEqual(chunk.sample_period, expected_sample_period)
86 | self.assertEqual(chunk.midi_unity_note, expected_midi_unity_note)
87 | self.assertEqual(chunk.midi_pitch_fraction, expected_midi_pitch_fraction)
88 | self.assertEqual(chunk.smpte_format, expected_smpte_format)
89 | self.assertEqual(chunk.smpte_offset, expected_smpte_offset)
90 | self.assertEqual(
91 | chunk.number_of_sample_loops, expected_number_of_sample_loops
92 | )
93 | self.assertEqual(chunk.sampler_data, expected_sampler_data)
94 | self.assertEqual(chunk.first_cue_point_id, expected_first_cue_point_id)
95 | self.assertEqual(chunk.first_loop_type, expected_first_loop_type)
96 | self.assertEqual(chunk.first_loop_start, expected_first_loop_start)
97 | self.assertEqual(chunk.first_loop_end, expected_first_loop_end)
98 | self.assertEqual(chunk.first_loop_fraction, expected_first_loop_fraction)
99 | self.assertEqual(
100 | chunk.first_loop_play_count, expected_first_loop_play_count
101 | )
102 |
103 | @parameterized.expand(
104 | [
105 | (
106 | os.path.join(DIR_PATH, "../files/test_tone.wav"),
107 | 36,
108 | )
109 | ]
110 | )
111 | def test_read_wrong_chunk(self, file_name: str, chunk_offset: int) -> None:
112 | """
113 | An appropriate error is raised if the wrong chunk is read.
114 | """
115 |
116 | # Arrange
117 |
118 | with open(file_name, "rb") as file:
119 |
120 | # Act
121 |
122 | with self.assertRaises(InvalidHeaderException) as context:
123 | SampleChunk.from_file(file, chunk_offset)
124 |
125 | # Assert
126 | self.assertIn(
127 | "Sample chunk must start with smpl", context.exception.args[0]
128 | )
129 |
130 | @parameterized.expand(
131 | [
132 | (
133 | 0,
134 | 0,
135 | 22675,
136 | 60,
137 | 0,
138 | 0,
139 | 0,
140 | 1,
141 | 0,
142 | 0,
143 | 0,
144 | 37485,
145 | 37585,
146 | 0,
147 | 0,
148 | b"smpl<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x93X\x00\x00<\x00\x00\x00"
149 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00"
150 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00m\x92\x00\x00"
151 | b"\xd1\x92\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
152 | )
153 | ]
154 | )
155 | def test_encode_chunk(
156 | self,
157 | manufacturer: int,
158 | product: int,
159 | sample_period: int,
160 | midi_unity_note: int,
161 | midi_pitch_fraction: int,
162 | smpte_format: int,
163 | smpte_offset: int,
164 | number_of_sample_loops: int,
165 | sampler_data: int,
166 | first_cue_point_id: int,
167 | first_loop_type: int,
168 | first_loop_start: int,
169 | first_loop_end: int,
170 | first_loop_fraction: int,
171 | first_loop_play_count: int,
172 | expected_bytes: bytes,
173 | ) -> None:
174 | """
175 | The sample chunk encodes correctly.
176 | """
177 |
178 | # Arrage
179 |
180 | chunk = SampleChunk(
181 | manufacturer,
182 | product,
183 | sample_period,
184 | midi_unity_note,
185 | midi_pitch_fraction,
186 | smpte_format,
187 | smpte_offset,
188 | number_of_sample_loops,
189 | sampler_data,
190 | first_cue_point_id,
191 | first_loop_type,
192 | first_loop_start,
193 | first_loop_end,
194 | first_loop_fraction,
195 | first_loop_play_count,
196 | )
197 |
198 | # Act
199 |
200 | converted = chunk.to_bytes()
201 |
202 | # Assert
203 |
204 | self.assertEqual(converted, expected_bytes)
205 |
206 |
207 | class TestRiffChunkExtended(TestCase):
208 | @parameterized.expand(
209 | [
210 | (os.path.join(DIR_PATH, "../files/test_tone.wav"), [b"fmt ", b"data", b"smpl"]),
211 | ]
212 | )
213 | def test_read_valid_wave(self, file_name: str, expected_chunks: List[str]) -> None:
214 | """
215 | Read valid wave files with smpl chunk.
216 | """
217 |
218 | # Arrange
219 |
220 | with open(file_name, "rb") as file:
221 |
222 | # Act
223 |
224 | chunk = RiffChunkExtended.from_file(file)
225 |
226 | # Assert
227 |
228 | self.assertIsNotNone(chunk)
229 | self.assertEqual(chunk.get_name, b"WAVE")
230 | self.assertIsNotNone(chunk.sub_chunks)
231 | for expected_chunk in expected_chunks:
232 | self.assertIn(expected_chunk, [obj.get_name for obj in chunk.sub_chunks])
233 |
234 | def test_encode_wave(self) -> None:
235 | """
236 | A WAVE file with smpl chunk can be encoded.
237 | """
238 |
239 | # Arrange
240 | with open(os.path.join(DIR_PATH, "../files/test_tone.wav"), "rb") as in_file:
241 | samples = np.memmap(
242 | in_file, dtype=np.dtype(" None:
8 | header = Header({})
9 | self.assertEqual(
10 | '<2sBBBBBBBBBBBBBBBBBB?32sBBBBBBB'
11 | 'LHBBHBBBBBBBBHHHHBBHBBfBBHBBHfHBB'
12 | 'fBBHBBHfHBBfBBHBBHfHBBfBBHBBHfHBB'
13 | 'fBBHBBHfHBBfBBHBBHfHBBBBBBfBBBBfB'
14 | 'BBBfBBBBfBBBBfBBBBfffBBbbBBBBBBBB'
15 | 'HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH'
16 | 'HHHHHHHHHHHHHHHBBHHBBBBBBBBBB',
17 | header.format
18 | )
19 |
20 | def test_data(self) -> None:
21 | header = Header({})
22 | self.assertEqual(len(PTI_HEADER_DEFINITION.keys()), len(header.data))
23 |
24 | def test_data_bytes(self) -> None:
25 | Header({})
26 | header = Header({
27 | 'sample_length': 926110,
28 | 'sample_playback': 0,
29 | 'panning_env_attack': 3000,
30 | 'cutoff_env_attack': 3000,
31 | 'wt_pos_env_attack': 3000,
32 | 'gran_pos_env_attack': 3000,
33 | 'finetune_env_attack': 3000,
34 | })
35 | header_empty = Header({})
36 | with open('./tests/utils/files/default_header.pti', mode='rb') as header_file:
37 | expected = bytearray(header_file.read())
38 | self.assertEqual(expected, header.data_bytes)
39 | self.assertNotEquals(header.data_bytes, header_empty.data_bytes)
40 |
--------------------------------------------------------------------------------
/tests/utils/pti/test_pti.py:
--------------------------------------------------------------------------------
1 | import struct
2 | from unittest import TestCase
3 | from polyend_tracker_pti_creator.utils.pti.pti import PTI
4 | from polyend_tracker_pti_creator.utils.pti.header import Header
5 |
6 |
7 | class TestPTI(TestCase):
8 | def test_simple_single_forward_loop(self) -> None:
9 | settings = {
10 | 'files': [
11 | {
12 | 'source_path': './tests/utils/files',
13 | 'source_file_name': 'tone',
14 | 'source_extension': '.wav',
15 | 'destination_path': './tests/utils/files',
16 | 'destination_file_name': 'tone',
17 | 'destination_extension': '.pti',
18 | 'instrument_name': 'test'
19 | },
20 | ],
21 | 'mode': 'normal',
22 | 'playback': 'dynamic'
23 | }
24 | PTI(settings).create()
25 | with open('./tests/utils/files/tone.pti', mode='rb') as file:
26 | file_content = file.read()
27 | header = Header({
28 | 'instrument_name': 'test',
29 | 'sample_length': 21344,
30 | 'sample_playback': 1,
31 | 'loop_start': 13968,
32 | 'loop_end': 65532,
33 | })
34 | actual_header_data = struct.unpack(header.format, file_content[:392])
35 | expected_header_data = header.data
36 | self.assertEqual(expected_header_data, actual_header_data)
37 |
38 | def test_merge_slice(self) -> None:
39 | settings = {
40 | 'files': [
41 | {
42 | 'source_path': './tests/utils/files',
43 | 'source_file_name': 'tone',
44 | 'source_extension': '.wav',
45 | 'destination_path': './tests/utils/files',
46 | 'destination_file_name': 'tone_slice',
47 | 'destination_extension': '.pti',
48 | 'instrument_name': 'testslice'
49 | },
50 | {
51 | 'source_path': './tests/utils/files',
52 | 'source_file_name': 'tone2',
53 | 'source_extension': '.wav',
54 | 'destination_path': './tests/utils/files',
55 | 'destination_file_name': 'tone_slice',
56 | 'destination_extension': '.pti',
57 | 'instrument_name': 'test'
58 | },
59 | ],
60 | 'mode': 'merge',
61 | 'playback': 'beat-slice'
62 | }
63 | PTI(settings).create()
64 | with open('./tests/utils/files/tone_slice.pti', mode='rb') as file:
65 | file_content = file.read()
66 | header = Header({
67 | 'instrument_name': 'testslice',
68 | 'sample_length': 42689,
69 | 'sample_playback': 5,
70 | 'number_of_slices': 2,
71 | 'slice_02_adjust': 32768
72 | })
73 | actual_header_data = struct.unpack(header.format, file_content[:392])
74 | expected_header_data = header.data
75 | self.assertEqual(expected_header_data, actual_header_data)
76 |
77 | def test_multi_single_one_shot(self) -> None:
78 | settings = {
79 | 'files': [
80 | {
81 | 'source_path': './tests/utils/files',
82 | 'source_file_name': 'tone',
83 | 'source_extension': '.wav',
84 | 'destination_path': './tests/utils/files',
85 | 'destination_file_name': 'tone_os_1',
86 | 'destination_extension': '.pti',
87 | 'instrument_name': 'test'
88 | },
89 | {
90 | 'source_path': './tests/utils/files',
91 | 'source_file_name': 'tone2',
92 | 'source_extension': '.wav',
93 | 'destination_path': './tests/utils/files',
94 | 'destination_file_name': 'tone_os_2',
95 | 'destination_extension': '.pti',
96 | 'instrument_name': 'test'
97 | },
98 | ],
99 | 'mode': 'normal',
100 | 'playback': 'one-shot'
101 | }
102 | PTI(settings).create()
103 | with open('./tests/utils/files/tone_os_1.pti', mode='rb') as file:
104 | file_content = file.read()
105 | header = Header({
106 | 'instrument_name': 'test',
107 | 'sample_length': 21344,
108 | 'loop_start': 13968,
109 | 'loop_end': 65532,
110 | })
111 | actual_header_data = struct.unpack(header.format, file_content[:392])
112 | expected_header_data = header.data
113 | self.assertEqual(expected_header_data, actual_header_data)
114 | with open('./tests/utils/files/tone_os_2.pti', mode='rb') as file:
115 | file_content = file.read()
116 | header = Header({
117 | 'instrument_name': 'test',
118 | 'sample_length': 21344,
119 | 'loop_start': 1,
120 | 'loop_end': 65534,
121 | })
122 | actual_header_data = struct.unpack(header.format, file_content[:392])
123 | expected_header_data = header.data
124 | self.assertEqual(expected_header_data, actual_header_data)
125 |
--------------------------------------------------------------------------------
/tests/utils/test_settings.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from polyend_tracker_pti_creator.utils.settings import Settings
3 | from polyend_tracker_pti_creator.utils.exceptions import (
4 | CreatorSourceMissingException,
5 | CreatorNoSourceWavFilesException,
6 | CreatorDestinationInvalidException,
7 | CreatorModeInvalidException,
8 | CreatorPlaybackInvalidException,
9 | )
10 |
11 |
12 | class TestObject:
13 | def __init__(self, settings):
14 | self.source = settings['source']
15 | self.destination = settings['destination'] if 'destination' in settings else None
16 | self.file_name = settings['file_name'] if 'file_name' in settings else None
17 | self.instrument_name = settings['instrument_name'] if 'instrument_name' in settings else None
18 | self.mode = settings['mode'] if 'mode' in settings else None
19 | self.playback = settings['playback'] if 'playback' in settings else None
20 |
21 |
22 | class TestSettings(TestCase):
23 | def test_no_source(self) -> None:
24 | args = TestObject({'source': None})
25 | with self.assertRaises(CreatorSourceMissingException) as context:
26 | Settings(args)
27 | self.assertTrue('Error! Please specify required --source (-s) argument.' in context.exception)
28 |
29 | def test_invalid_source(self) -> None:
30 | args = TestObject({'source': 'testpath'})
31 | with self.assertRaises(CreatorSourceMissingException) as context:
32 | Settings(args)
33 | self.assertTrue('Error! Please specify a valid source path.' in context.exception)
34 |
35 | def test_no_wav_files_source(self) -> None:
36 | args = TestObject({'source': './'})
37 | with self.assertRaises(CreatorNoSourceWavFilesException) as context:
38 | Settings(args)
39 | self.assertTrue("Error! No .wav files selected. Currently only .wav files supported. "
40 | "If you're trying to use non-wav files, please convert to .wav then try again."
41 | in context.exception)
42 |
43 | def test_invalid_destination(self) -> None:
44 | args = TestObject({
45 | 'source': './tests/utils/files',
46 | 'destination': 'invalidpath'
47 | })
48 | with self.assertRaises(CreatorDestinationInvalidException) as context:
49 | Settings(args)
50 | self.assertTrue("Error! Please specify a valid destination path." in context.exception)
51 |
52 | def test_merge_config(self) -> None:
53 | args = TestObject({
54 | 'source': './tests/utils/files',
55 | 'destination': './tests/utils/files/tone.wav',
56 | 'file_name': 'test',
57 | 'instrument_name': 'test',
58 | 'mode': 'merge',
59 | 'playback': None,
60 | })
61 |
62 | self.assertEqual(Settings(args).settings, {
63 | 'files': [
64 | {
65 | 'source_path': './tests/utils/files',
66 | 'source_file_name': 'test_tone',
67 | 'source_extension': '.wav',
68 | 'destination_path': './tests/utils/files',
69 | 'destination_file_name': 'test',
70 | 'destination_extension': '.pti',
71 | 'instrument_name': 'test'
72 | },
73 | {
74 | 'source_path': './tests/utils/files',
75 | 'source_file_name': 'tone',
76 | 'source_extension': '.wav',
77 | 'destination_path': './tests/utils/files',
78 | 'destination_file_name': 'test',
79 | 'destination_extension': '.pti',
80 | 'instrument_name': 'test'
81 | }, {
82 | 'source_path': './tests/utils/files',
83 | 'source_file_name': 'tone2',
84 | 'source_extension': '.wav',
85 | 'destination_path': './tests/utils/files',
86 | 'destination_file_name': 'test',
87 | 'destination_extension': '.pti',
88 | 'instrument_name': 'test'
89 | }
90 | ],
91 | 'mode': 'merge',
92 | 'playback': 'beat-slice'
93 | })
94 |
95 | def test_instrument_name(self) -> None:
96 | args = TestObject({
97 | 'source': './tests/utils/files',
98 | 'destination': None,
99 | 'file_name': None,
100 | 'instrument_name': 'test',
101 | 'mode': None,
102 | 'playback': None,
103 | })
104 | self.assertEqual(Settings(args).settings, {
105 | 'files': [
106 | {
107 | 'source_path': './tests/utils/files',
108 | 'source_file_name': 'test_tone',
109 | 'source_extension': '.wav',
110 | 'destination_path': './tests/utils/files',
111 | 'destination_file_name': 'test_tone',
112 | 'destination_extension': '.pti',
113 | 'instrument_name': 'test1'
114 | },
115 | {
116 | 'source_path': './tests/utils/files',
117 | 'source_file_name': 'tone',
118 | 'source_extension': '.wav',
119 | 'destination_path': './tests/utils/files',
120 | 'destination_file_name': 'tone',
121 | 'destination_extension': '.pti',
122 | 'instrument_name': 'test2'
123 | }, {
124 | 'source_path': './tests/utils/files',
125 | 'source_file_name': 'tone2',
126 | 'source_extension': '.wav',
127 | 'destination_path': './tests/utils/files',
128 | 'destination_file_name': 'tone2',
129 | 'destination_extension': '.pti',
130 | 'instrument_name': 'test3'
131 | }
132 | ],
133 | 'mode': 'normal',
134 | 'playback': 'dynamic'
135 | })
136 |
137 | def test_file_name(self) -> None:
138 | args = TestObject({
139 | 'source': './tests/utils/files',
140 | 'destination': './tests/utils/files',
141 | 'file_name': 'test',
142 | 'instrument_name': None,
143 | 'mode': None,
144 | 'playback': None,
145 | })
146 | self.assertEqual(Settings(args).settings, {
147 | 'files': [
148 | {
149 | 'source_path': './tests/utils/files',
150 | 'source_file_name': 'test_tone',
151 | 'source_extension': '.wav',
152 | 'destination_path': './tests/utils/files',
153 | 'destination_file_name': 'test1',
154 | 'destination_extension': '.pti',
155 | 'instrument_name': 'test1'
156 | },
157 | {
158 | 'source_path': './tests/utils/files',
159 | 'source_file_name': 'tone',
160 | 'source_extension': '.wav',
161 | 'destination_path': './tests/utils/files',
162 | 'destination_file_name': 'test2',
163 | 'destination_extension': '.pti',
164 | 'instrument_name': 'test2'
165 | }, {
166 | 'source_path': './tests/utils/files',
167 | 'source_file_name': 'tone2',
168 | 'source_extension': '.wav',
169 | 'destination_path': './tests/utils/files',
170 | 'destination_file_name': 'test3',
171 | 'destination_extension': '.pti',
172 | 'instrument_name': 'test3'
173 | }
174 | ],
175 | 'mode': 'normal',
176 | 'playback': 'dynamic'
177 | })
178 |
179 | def test_invalid_mode(self) -> None:
180 | args = TestObject({
181 | 'source': './tests/utils/files',
182 | 'destination': None,
183 | 'file_name': 'test',
184 | 'instrument_name': None,
185 | 'mode': 'test',
186 | 'playback': None,
187 | })
188 | with self.assertRaises(CreatorModeInvalidException) as context:
189 | Settings(args)
190 | self.assertTrue("Error! Gave an invalid mode. Valid values are 'normal' and 'merge'." in context.exception)
191 |
192 | def test_invalid_playback(self) -> None:
193 | args = TestObject({
194 | 'source': './tests/utils/files',
195 | 'destination': None,
196 | 'file_name': 'test',
197 | 'instrument_name': None,
198 | 'mode': None,
199 | 'playback': 'test',
200 | })
201 | with self.assertRaises(CreatorPlaybackInvalidException) as context:
202 | Settings(args)
203 | self.assertTrue("Error! Gave an invalid playback type. Valid values are 'one-shot', "
204 | "'forward-loop', 'backward-loop', "
205 | "'ping-pong-loop', 'slice', 'beat-slice', 'wavetable', "
206 | "'granular', and 'dynamic'" in context.exception
207 | )
208 |
--------------------------------------------------------------------------------