diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 5bc9ca8..2c728ef 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -19,7 +19,7 @@ jobs: matrix: os: - windows-latest - - macos-latest + - macos-14 python-ver: - 3.11 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91ac366..08ec328 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,16 +19,16 @@ jobs: fail-fast: false matrix: python-ver: [3.11] - # only build for Windows. Because Mac needs software notarization but I don't have $99 for it. - os: [windows-latest, macos-latest, macos-latest-xlarge] + # Intel mac for macos-13, M1/2 mac for macos-14 + os: [windows-latest, macos-13, macos-14] include: - os: windows-latest build_cmd: build_win.bat zip_name: PlaySK-PianoRoll-Reader-Win.x64.zip - - os: macos-latest + - os: macos-13 build_cmd: bash build_mac.sh zip_name: PlaySK-PianoRoll-Reader-Mac.x64.zip - - os: macos-latest-xlarge + - os: macos-14 build_cmd: bash build_mac.sh zip_name: PlaySK-PianoRoll-Reader-Mac.ARM.zip @@ -95,7 +95,7 @@ jobs: pushd dist brew install create-dmg test -f PlaySK-Installer.dmg && rm PlaySK-Installer.dmg - create-dmg --volname "PlaySK Installer" --background ../docs/dmg-bg.tiff --window-pos 200 120 --window-size 800 500 --icon-size 100 --icon "PlaySK Piano Roll Reader.app" 100 100 --add-file "playsk_config" playsk_config 100 300 --hide-extension "PlaySK Piano Roll Reader.app" --app-drop-link 600 200 "PlaySK-Installer.dmg" "PlaySK Piano Roll Reader.app" + create-dmg --volname "PlaySK Installer" --background ../assets/dmg-bg.tiff --window-pos 200 120 --window-size 800 500 --icon-size 100 --icon "PlaySK Piano Roll Reader.app" 100 100 --add-file "playsk_config" playsk_config 100 300 --hide-extension "PlaySK Piano Roll Reader.app" --app-drop-link 600 200 "PlaySK-Installer.dmg" "PlaySK Piano Roll Reader.app" popd mv dist/PlaySK-Installer.dmg "dist/How to use Mac.png" dist/3rd-party-license.txt . zip -qr ${{ matrix.zip_name }} sample_scans/ PlaySK-Installer.dmg "How to use Mac.png" "3rd-party-license.txt" diff --git a/3rd-party-license.txt b/3rd-party-license.txt index 2051dee..b8c02a3 100644 --- a/3rd-party-license.txt +++ b/3rd-party-license.txt @@ -45,6 +45,32 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +annotated-types +0.6.0 +MIT License +The MIT License (MIT) + +Copyright (c) 2022 the contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + colorama 0.4.6 BSD License @@ -4687,6 +4713,58 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +pydantic +2.6.4 +MIT License +The MIT License (MIT) + +Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pydantic_core +2.16.3 +MIT License +The MIT License (MIT) + +Copyright (c) 2022 Samuel Colvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + pyinstaller 6.1.0 GNU General Public License v2 (GPLv2) @@ -6929,6 +7007,290 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +typing_extensions +4.10.0 +Python Software Foundation License +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + wxPython 4.2.1 wxWindows Library License (https://opensource.org/licenses/wxwindows.php) diff --git a/README.md b/README.md index 3626fca..96e860e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# PlaySK Piano Roll Reader Ver3.2 +# PlaySK Piano Roll Reader Ver3.3 Optically reading a piano roll image, emulates expression and output midi signal in real-time. -![Overall System](./docs/Overall_System.webp) +![Overall System](./assets/Overall_System.webp) The "virtual tracker bar" optically picks up roll holes then emulates note, pedal and expression. Currently, 9 virtual tracker bars are available. @@ -10,12 +10,13 @@ Currently, 9 virtual tracker bars are available. - Ampico B - Duo-Art - Welte-Mignon Licensee +- Welte-Mignon T-98 (Green) - Welte-Mignon T-100 (Red) - Philipps Duca (no expression. experimental) - Recordo version A / B - Artecho -In the future, Ampico A, Welte T-98 will be supported. +In the future, Ampico A will be supported. Support image formats are `.cis`, `.jpg`, `.tif`, `.png`, `.bmp`. diff --git a/assets/How to use Mac.png b/assets/How to use Mac.png new file mode 100644 index 0000000..c74089c Binary files /dev/null and b/assets/How to use Mac.png differ diff --git a/assets/How to use.png b/assets/How to use.png new file mode 100644 index 0000000..1e921b1 Binary files /dev/null and b/assets/How to use.png differ diff --git a/docs/Overall_System.jpg b/assets/Overall_System.jpg similarity index 100% rename from docs/Overall_System.jpg rename to assets/Overall_System.jpg diff --git a/docs/Overall_System.webp b/assets/Overall_System.webp similarity index 100% rename from docs/Overall_System.webp rename to assets/Overall_System.webp diff --git a/docs/dmg-bg.png b/assets/dmg-bg.png similarity index 100% rename from docs/dmg-bg.png rename to assets/dmg-bg.png diff --git a/docs/dmg-bg.tiff b/assets/dmg-bg.tiff similarity index 100% rename from docs/dmg-bg.tiff rename to assets/dmg-bg.tiff diff --git a/docs/dmg-bgx2.png b/assets/dmg-bgx2.png similarity index 100% rename from docs/dmg-bgx2.png rename to assets/dmg-bgx2.png diff --git a/build_mac.sh b/build_mac.sh index b8ddff8..e2352f7 100755 --- a/build_mac.sh +++ b/build_mac.sh @@ -17,5 +17,5 @@ pip-licenses --format=plain-vertical --with-license-file --no-license-path --out # copy files cp -p "3rd-party-license.txt" dist/ -cp -p "docs/How to use Mac.png" dist/ +cp -p "assets/How to use Mac.png" dist/ cp -pr src/playsk_config/ dist/playsk_config/ \ No newline at end of file diff --git a/build_mac.spec b/build_mac.spec index bf32248..88393f6 100644 --- a/build_mac.spec +++ b/build_mac.spec @@ -46,5 +46,5 @@ app = BUNDLE( name='PlaySK Piano Roll Reader.app', icon='src/playsk_config/PlaySK_icon.ico', bundle_identifier=None, - version='3.2.0' + version='3.3.0' ) diff --git a/build_win.bat b/build_win.bat index 4491cb1..b53605e 100644 --- a/build_win.bat +++ b/build_win.bat @@ -17,5 +17,5 @@ pip-licenses --format=plain-vertical --with-license-file --no-license-path --out rem copy files xcopy /i /y "3rd-party-license.txt" ".\dist\PlaySK Piano Roll Reader\" -xcopy /i /y ".\docs\How to use.png" ".\dist\PlaySK Piano Roll Reader\" +xcopy /i /y ".\assets\How to use.png" ".\dist\PlaySK Piano Roll Reader\" xcopy /s /i /y ".\src\playsk_config\" ".\dist\PlaySK Piano Roll Reader\playsk_config\" \ No newline at end of file diff --git a/docs/How to use Mac.png b/docs/How to use Mac.png deleted file mode 100644 index ae999e0..0000000 Binary files a/docs/How to use Mac.png and /dev/null differ diff --git a/docs/How to use.png b/docs/How to use.png deleted file mode 100644 index c46efd1..0000000 Binary files a/docs/How to use.png and /dev/null differ diff --git a/notarize_mac.sh b/notarize_mac.sh index 9a56a9c..d85cc97 100755 --- a/notarize_mac.sh +++ b/notarize_mac.sh @@ -25,7 +25,7 @@ xcrun stapler staple "PlaySK Piano Roll Reader.app" # create dmg echo "~~ create dmg ~~" test -f PlaySK-Installer.dmg && rm PlaySK-Installer.dmg -create-dmg --volname "PlaySK Installer" --background ../docs/dmg-bg.tiff \ +create-dmg --volname "PlaySK Installer" --background ../assets/dmg-bg.tiff \ --window-pos 200 120 --window-size 800 500 \ --icon-size 100 --icon "PlaySK Piano Roll Reader.app" 100 100 \ --add-file "playsk_config" playsk_config 100 300 \ diff --git a/poetry.lock b/poetry.lock index 7c79959..efbf115 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,17 @@ files = [ {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, ] +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -425,6 +436,116 @@ wcwidth = "*" [package.extras] tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] +[[package]] +name = "pydantic" +version = "2.6.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pyinstaller" version = "6.1.0" @@ -627,6 +748,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + [[package]] name = "wcwidth" version = "0.2.13" @@ -671,4 +803,4 @@ six = "*" [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "43a9960657324b93e33586c7567168d2b1c3bd5f2744b49fe9933331006a74ea" +content-hash = "5e575fd92af50bc0fe6c781c92e8042091b46355bdfc0b3fe6076fd4307ccb50" diff --git a/pyproject.toml b/pyproject.toml index 288fd80..0686817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "PlaySK-Piano-Roll-Reader" -version = "3.2.0" +version = "3.3.0" description = "Optically reading a piano roll image, emulates expression and output midi signal in real-time." authors = ["nai-kon "] readme = "README.md" @@ -14,6 +14,7 @@ opencv-python-headless = "^4.8.0.76" numpy = "^1.26.0" mido = "^1.3.0" cython = "^3.0.2" +pydantic = "^2.6.4" [tool.poetry.group.dev.dependencies] pyinstaller = "6.1.0" diff --git a/src/build_mac.spec b/src/build_mac.spec index e18adda..b82f040 100644 --- a/src/build_mac.spec +++ b/src/build_mac.spec @@ -46,5 +46,5 @@ app = BUNDLE( name='PlaySK Piano Roll Reader.app', icon='src/config/PlaySK_icon.ico', bundle_identifier="com.KatzSasaki.PlaySK", - version='3.2.0' + version='3.3.0' ) diff --git a/src/config.py b/src/config.py index 1d308e3..bb2d85b 100644 --- a/src/config.py +++ b/src/config.py @@ -1,39 +1,40 @@ import json import os +from pydantic import BaseModel -class ConfigMng: - _path = os.path.join("playsk_config", "config.json") +CONFIG_PATH = os.path.join("playsk_config", "config.json") - def __init__(self): - self.last_midi_port = "" - self.last_tracker = "" - self.update_notified_version = "" - - self.load_config() +class ConfigMng(BaseModel): + last_midi_port: str = "" + last_tracker: str = "" + update_notified_version: str = "" + window_scale: str = "100%" - def load_config(self): - if not os.path.exists(ConfigMng._path): - return - - with open(ConfigMng._path, encoding="utf-8") as f: - v = json.load(f) - self.last_midi_port = v.get("last_midi_port", "") - self.last_tracker = v.get("last_tracker", "") - self.update_notified_version = v.get("update_notified_version", "") + def __init__(self): + try: + with open(CONFIG_PATH, encoding="utf-8") as f: + data = json.load(f) + except FileNotFoundError: + data = {} + super().__init__(**data) def save_config(self): - with open(ConfigMng._path, "w", encoding="utf-8") as f: - json.dump(self.__dict__, f, indent=4) + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + f.write(self.model_dump_json(indent=4)) + @property + def window_scale_ratio(self): + # return scale ratio. ex) 125% -> 1.25 + return float(self.window_scale.replace("%", "")) / 100 if __name__ == "__main__": obj = ConfigMng() print(obj.__dict__) - obj.last_midi_port = "hoge" + obj.last_midi_port = "hoge日本語" obj.last_tracker = "fuga" - obj = None + obj.save_config() obj = ConfigMng() print(obj.__dict__) diff --git a/src/controls.py b/src/controls.py index 2181ea6..5b35532 100644 --- a/src/controls.py +++ b/src/controls.py @@ -12,23 +12,30 @@ class WelcomeMsg(wx.Panel): def __init__(self, parent, pos=(0, 0), size=(800, 600)): - wx.Panel.__init__(self, parent, wx.ID_ANY, pos, parent.FromDIP(wx.Size(size))) + wx.Panel.__init__(self, parent, wx.ID_ANY, pos, parent.get_dipscaled_size(wx.Size(size))) self.SetForegroundColour("white") dummy = wx.StaticText(self, wx.ID_ANY, "") msg1 = wx.StaticText(self, wx.ID_ANY, "SELECT or DROP FILE here!") - msg1.SetFont(wx.Font(30, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) + text_size = parent.get_scaled_textsize(30) + msg1.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) msg2 = wx.StaticText(self, wx.ID_ANY, "Please donate for continuous development of the software.") - msg2.SetFont(wx.Font(15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) + text_size = parent.get_scaled_textsize(15) + msg2.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) lnk1 = HyperLinkCtrl(self, wx.ID_ANY, "Donate via PayPal", URL="https://paypal.me/KatzSasaki") - lnk1.SetFont(wx.Font(15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) + lnk1.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) + text_size = parent.get_scaled_textsize(10) msg3 = wx.StaticText(self, wx.ID_ANY, APP_TITLE) + msg3.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) msg4 = wx.StaticText(self, wx.ID_ANY, f"Version {APP_VERSION}") + msg4.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) lnk2 = HyperLinkCtrl(self, wx.ID_ANY, "Project page on GitHub", URL="https://github.com/nai-kon/PlaySK-Piano-Roll-Reader") + lnk2.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) msg5 = wx.StaticText(self, wx.ID_ANY, COPY_RIGHT) + msg5.SetFont(wx.Font(text_size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD)) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(dummy, 4, wx.ALIGN_CENTER) @@ -78,7 +85,7 @@ def _value_changed(self, val): def set(self, label, tempo_range, val): self.label = label self.slider.SetRange(tempo_range[0], tempo_range[1]) - self.slider.SetValue(val) + self.slider.SetValue(int(val)) wx.CallAfter(self._value_changed, val) def _slider_changed(self, event): @@ -107,13 +114,17 @@ def __init__(self, parent, pos=(0, 0)): self.right.Bind(wx.EVT_BUTTON, lambda event: self.changed(self.offset + 1)) self.right.Disable() - sizer = wx.GridBagSizer(vgap=2, hgap=2) - sizer.Add(self.auto_tracking, (0, 0), (1, 3), flag=wx.EXPAND) - sizer.Add(self.label, (1, 0), (1, 1), flag=wx.EXPAND) - border_size = parent.FromDIP(5) - sizer.Add(self.left, (1, 1), (1, 1), flag=wx.EXPAND | wx.LEFT, border=border_size) - sizer.Add(self.right, (1, 2), (1, 1), flag=wx.EXPAND | wx.LEFT, border=border_size) - self.SetSizer(sizer) + + sizer1 = wx.BoxSizer(wx.HORIZONTAL) + border_size = parent.get_dipscaled_size(3) + sizer1.Add(self.label, flag=wx.EXPAND | wx.ALL, border=border_size, proportion=1) + sizer1.Add(self.left, flag=wx.EXPAND | wx.ALL, border=border_size, proportion=2) + sizer1.Add(self.right, flag=wx.EXPAND | wx.ALL, border=border_size, proportion=2) + + sizer2 = wx.BoxSizer(wx.VERTICAL) + sizer2.Add(self.auto_tracking, flag=wx.EXPAND) + sizer2.Add(sizer1, flag=wx.EXPAND) + self.SetSizer(sizer2) self.Fit() def _on_auto_checked(self, event): diff --git a/src/input_editor.py b/src/input_editor.py index bc7cf0a..c5e0a53 100644 --- a/src/input_editor.py +++ b/src/input_editor.py @@ -1,21 +1,20 @@ -import platform - import cv2 import wx from cis_image import CisImage, ScannerType class ImgEditDlg(wx.Dialog): - def __init__(self, cis: CisImage): + def __init__(self, parent, cis: CisImage): wx.Dialog.__init__(self, None, title="Adjust roll image") + self.parent = parent self.cis = cis self.panel = SetEdgePane(self, cis.decode_img) - border_size = self.FromDIP(5) + border_size = self.get_dipscaled_size(5) sizer1 = wx.BoxSizer(wx.VERTICAL) sizer1.Add(wx.StaticText(self, label=self.get_show_text()), 1, wx.EXPAND | wx.ALL, border=border_size) - sizer1.Add(wx.Button(self, wx.ID_OK, label="OK"), 1, wx.EXPAND | wx.ALL, border=border_size) - sizer1.Add(wx.Button(self, wx.ID_CANCEL, label="Cancel"), 1, wx.EXPAND | wx.ALL, border=border_size) + sizer1.Add(wx.Button(self, wx.ID_OK, label="OK"), 1, wx.EXPAND | wx.ALL) + sizer1.Add(wx.Button(self, wx.ID_CANCEL, label="Cancel"), 1, wx.EXPAND | wx.ALL) sizer2 = wx.BoxSizer(wx.HORIZONTAL) sizer2.Add(self.panel) @@ -44,10 +43,16 @@ def get_show_text(self): def get_margin_pos(self): return self.panel.get_pos() + def get_dipscaled_size(self, size): + return self.parent.get_dipscaled_size(size) + + def get_dpiscale_factor(self): + return self.parent.get_dpiscale_factor() + class SetEdgePane(wx.Panel): def __init__(self, parent, img): - self.frame_w = parent.FromDIP(950) + self.frame_w = parent.get_dipscaled_size(950) self.frame_h = wx.Display().GetClientArea().height # display height wx.Panel.__init__(self, parent, size=(self.frame_w, self.frame_h)) self.SetDoubleBuffered(True) @@ -61,11 +66,12 @@ def __init__(self, parent, img): self.scroll_y1 = self.img_h // 2 self.scroll_size = 500 # @px self.norm_cursor = wx.Cursor() + self.guild_line_w = parent.get_dipscaled_size(2) self.adjust_cursor = wx.Cursor(wx.CURSOR_SIZEWE) - self.adjust_cursor_slip = self.FromDIP(20) + self.adjust_cursor_slip = parent.get_dipscaled_size(20) self.guide_base_text = "Set this line to the edge of the roll" self.guide_font = wx.Font(15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_SEMIBOLD) - self.scale = self.GetDPIScaleFactor() if platform.system() == "Windows" else 1 + self.scale = parent.get_dpiscale_factor() text = "If there is a margin, set it roughly in the center of the margin. It will be adjusted automatically.\n" \ "If there is no margin, set it strictly to the edge." @@ -90,7 +96,7 @@ def on_paint(self, event): dc.DrawText(self.guide_base_text + " →", self.right_margin_x - text_len[0], self.frame_h // 2) # guide line - dc.SetPen(wx.Pen((180, 0, 0), self.FromDIP(2), wx.SOLID)) + dc.SetPen(wx.Pen((180, 0, 0), self.guild_line_w, wx.SOLID)) dc.DrawLine(self.left_margin_x, 0, self.left_margin_x, self.frame_h) dc.DrawLine(self.right_margin_x, 0, self.right_margin_x, self.frame_h) diff --git a/src/input_src.py b/src/input_src.py index fde4100..82f3063 100644 --- a/src/input_src.py +++ b/src/input_src.py @@ -1,6 +1,5 @@ import math import os -import platform import re import threading import time @@ -67,14 +66,14 @@ def _load_img(path: str, default_tempo: int) -> tuple[np.ndarray | None, int]: return img, tempo -def _load_cis(path: str, default_tempo: int, force_manual_adjust: bool) -> tuple[np.ndarray | None, int]: +def _load_cis(parent: wx.Frame, path: str, default_tempo: int, force_manual_adjust: bool) -> tuple[np.ndarray | None, int]: obj = CisImage() if not obj.load(path): return None, default_tempo # find center of roll margin or manually set if not found left_edge, right_edge = _find_roll_edge(obj.decode_img) if left_edge is None or right_edge is None or force_manual_adjust: - with ImgEditDlg(obj) as dlg: + with ImgEditDlg(parent, obj) as dlg: if dlg.ShowModal() == wx.ID_OK: left_edge, right_edge = dlg.get_margin_pos() else: @@ -88,10 +87,10 @@ def _load_cis(path: str, default_tempo: int, force_manual_adjust: bool) -> tuple return obj.decode_img, tempo -def load_scan(path: str, default_tempo: int, force_manual_adjust: bool = False) -> tuple[np.ndarray | None, int]: +def load_scan(parent: wx.Frame, path: str, default_tempo: int, force_manual_adjust: bool = False) -> tuple[np.ndarray | None, int]: with wx.BusyCursor(): if Path(path).suffix.lower().endswith(".cis"): - return _load_cis(path, default_tempo, force_manual_adjust) + return _load_cis(parent, path, default_tempo, force_manual_adjust) else: return _load_img(path, default_tempo) @@ -114,16 +113,16 @@ def __call__(self): class InputVideo(wx.Panel): - def __init__(self, parent, path, disp_size=(800, 600), callback=None): - wx.Panel.__init__(self, parent, size=parent.FromDIP(wx.Size(disp_size))) + def __init__(self, parent, path, window_scale, callback=None): + self.disp_w, self.disp_h = (800, 600) + wx.Panel.__init__(self, parent, size=parent.get_dipscaled_size(wx.Size((self.disp_w, self.disp_h)))) self.parent = parent self.SetDoubleBuffered(True) - self.disp_w, self.disp_h = disp_size self.bmp = wx.Bitmap(self.disp_w, self.disp_h, depth=24) self.callback = callback self.src = None self.src_path = path - self.scale = self.GetDPIScaleFactor() if platform.system() == "Windows" else 1 + self.scale = parent.get_dpiscale_factor() self.start_play = False self.thread_enable = True @@ -233,8 +232,8 @@ def load_thread(self): class InputWebcam(InputVideo): - def __init__(self, parent, webcam_no=0, disp_size=(800, 600), callback=None): - super().__init__(parent, webcam_no, disp_size, callback) + def __init__(self, parent, webcam_no=0, window_scale=1, callback=None): + super().__init__(parent, webcam_no, window_scale, callback) self.start = True @staticmethod @@ -250,8 +249,8 @@ def list_camera(): class InputScanImg(InputVideo): - def __init__(self, parent, img, spool_diameter=2.72, roll_width=11.25, tempo=80, disp_size=(800, 600), callback=None): - super().__init__(parent, None, disp_size, callback) + def __init__(self, parent, img, spool_diameter=2.72, roll_width=11.25, tempo=80, window_scale=1, callback=None): + super().__init__(parent, None, window_scale, callback) self.src = img self.skip_px = 1 self.spool_rps = 0 @@ -368,7 +367,7 @@ def _get_one_frame_time(self): def slider_value_change(event): obj = event.GetEventObject() panel1.set_tempo(obj.GetValue()) - slider = wx.Slider(frame, value=80, minValue=10, maxValue=140, pos=(0, 600), size=frame.FromDIP(wx.Size((200, 100))), style=wx.SL_HORIZONTAL | wx.SL_LABELS) + slider = wx.Slider(frame, value=80, minValue=10, maxValue=140, pos=(0, 600), size=frame.get_dipscaled_size(wx.Size((200, 100))), style=wx.SL_HORIZONTAL | wx.SL_LABELS) slider.SetPageSize(5) slider.Bind(wx.EVT_SLIDER, slider_value_change) diff --git a/src/main.py b/src/main.py index cd8ae58..0890637 100644 --- a/src/main.py +++ b/src/main.py @@ -43,7 +43,7 @@ def is_exists(self) -> bool: sock.sendall(f"{self.message_path}{sys.argv[1]}".encode()) else: sock.sendall(self.message_notify.encode()) - print("app is already exists.") + print("The software is already exists.") return True except OSError: # app is not exists. run socket server as a daemon diff --git a/src/main_frame.py b/src/main_frame.py index 3b54c0a..3b09526 100644 --- a/src/main_frame.py +++ b/src/main_frame.py @@ -52,15 +52,20 @@ def __init__(self): # wxpython on Windows does not support Darkmode self.SetBackgroundColour("#AAAAAA") + self.conf = ConfigMng() self.img_path = None self.spool = WelcomeMsg(self, size=(800, 600)) self.spool.start_worker() - self.midi_btn = wx.Button(self, size=self.FromDIP(wx.Size((90, 50))), label="MIDI On") + font = wx.Font(self.get_scaled_textsize(10), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) + + self.midi_btn = wx.Button(self, size=self.get_dipscaled_size(wx.Size((90, 50))), label="MIDI On") self.midi_btn.Bind(wx.EVT_BUTTON, self.midi_onoff) + self.midi_btn.SetFont(font) self.midi_btn.Disable() - self.file_btn = wx.Button(self, size=self.FromDIP(wx.Size((90, 50))), label="File") + self.file_btn = wx.Button(self, size=self.get_dipscaled_size(wx.Size((90, 50))), label="File") + self.file_btn.SetFont(font) self.file_btn.Bind(wx.EVT_BUTTON, self.open_file) self.speed = SpeedSlider(self, callback=self.speed_change) @@ -69,16 +74,16 @@ def __init__(self): self.bass_vacuum_lv = VacuumGauge(self, caption="Bass Vacuum (inches of water)") self.treble_vacuum_lv = VacuumGauge(self, caption="Treble Vacuum (inches of water)") - self.adjust_btn = wx.Button(self, size=self.FromDIP(wx.Size((180, 50))), label="Adjust CIS Image") + self.adjust_btn = wx.Button(self, size=self.get_dipscaled_size(wx.Size((180, 50))), label="Adjust CIS Image") self.adjust_btn.Bind(wx.EVT_BUTTON, self.adjust_image) + self.adjust_btn.SetFont(font) self.callback = CallBack(None, self.tracking, self.bass_vacuum_lv, self.treble_vacuum_lv) self.midiobj = MidiWrap() - self.conf = ConfigMng() self.player_mng = PlayerMng() # sizer of controls - border_size = self.FromDIP(5) + border_size = self.get_dipscaled_size(5) self.sizer1 = wx.BoxSizer(wx.HORIZONTAL) self.sizer1.Add(self.midi_btn, flag=wx.EXPAND | wx.ALL, border=border_size, proportion=1) self.sizer1.Add(self.file_btn, flag=wx.EXPAND | wx.ALL, border=border_size, proportion=1) @@ -114,14 +119,34 @@ def __init__(self): # app was opened with file wx.CallAfter(self.load_file, path=sys.argv[1]) + def get_dipscaled_size(self, size:wx.Size | int): + if isinstance(size, int): + return int(self.FromDIP(size) * self.conf.window_scale_ratio) + else: + return self.FromDIP(size) * self.conf.window_scale_ratio + + def get_dpiscale_factor(self): + return self.GetDPIScaleFactor() * self.conf.window_scale_ratio if platform.system() == "Windows" else self.conf.window_scale_ratio + + def get_scaled_textsize(self, size): + return int(size * self.conf.window_scale_ratio) + def create_status_bar(self): - self.sbar = self.CreateStatusBar(5) # midi-port, tracker-bar + self.sbar = self.CreateStatusBar(8) # midi-port, tracker-bar _, h = self.sbar.Size[:2] - self.sbar.SetStatusWidths([-5, -1, -5, -5, -5]) + midiout_caption = "MIDI Output :" + tracker_caption = "Tracker Bar :" + trwsize_caption = "Window Size :" + midiout_caption_w = wx.Window.GetTextExtent(self, midiout_caption).Width + tracker_caption_w = wx.Window.GetTextExtent(self, tracker_caption).Width + wsize_caption_w = wx.Window.GetTextExtent(self, trwsize_caption).Width + + self.sbar.SetStatusWidths([midiout_caption_w, -4, -1, tracker_caption_w, -4, -3, wsize_caption_w, -1]) # midi port + self.sbar.SetStatusText(midiout_caption, 0) ports = self.midiobj.port_list - rect = self.sbar.GetFieldRect(0) + rect = self.sbar.GetFieldRect(1) self.port_sel = wx.Choice(self.sbar, choices=ports, size=(rect.width, h)) self.port_sel.Bind(wx.EVT_CHOICE, self.change_midi_port) self.port_sel.SetPosition((rect.x, 0)) @@ -130,8 +155,9 @@ def create_status_bar(self): self.change_midi_port() # call manually for init # tracker bar + self.sbar.SetStatusText(tracker_caption, 3) players = self.player_mng.player_list - rect = self.sbar.GetFieldRect(2) + rect = self.sbar.GetFieldRect(4) self.player_sel = wx.Choice(self.sbar, choices=players, size=(rect.width, h)) self.player_sel.Bind(wx.EVT_CHOICE, self.change_player) self.player_sel.SetPosition((rect.x, 0)) @@ -139,6 +165,20 @@ def create_status_bar(self): self.player_sel.SetSelection(last_sel) self.change_player() # call manually for init + # window scale + self.sbar.SetStatusText(trwsize_caption, 6) + # calc scales which fit in display size + client_h = wx.Display().GetClientArea().height + cur_h = self.GetSize()[1] + + scales = [f"{v}%" for v in range(100, 300 + 1, 25) if (v / 100) * (cur_h / self.conf.window_scale_ratio) < client_h] + rect = self.sbar.GetFieldRect(7) + self.scale_sel = wx.Choice(self.sbar, choices=scales, size=(rect.width, h)) + self.scale_sel.Bind(wx.EVT_CHOICE, self.change_scale) + self.scale_sel.SetPosition((rect.x, 0)) + last_sel = scales.index(self.conf.window_scale) if self.conf.window_scale in scales else 0 + self.scale_sel.SetSelection(last_sel) + def post_status_msg(self, msg): wx.CallAfter(self.sbar.SetStatusText, text=msg, i=4) @@ -178,6 +218,14 @@ def change_player(self, event=None): player_tmp.auto_tracking = self.tracking.auto_tracking self.callback.player = player_tmp + def change_scale(self, event=None): + idx = self.scale_sel.GetSelection() + select_scale = self.scale_sel.GetString(idx) + if select_scale != self.conf.window_scale: + self.conf.window_scale = select_scale + wx.MessageBox("Exit the software. Please re-start after exit.", "Change Window Size needs Restart") + self.on_close(event=None) + def midi_onoff(self, event): obj = event.GetEventObject() if obj.GetLabel() == "MIDI On": @@ -193,7 +241,7 @@ def load_file(self, path, force_manual_adjust=False): wx.MessageBox("Supported image formats are .cis .jpg, .png, .tif, .bmp", "Unsupported file") return - img, tempo = load_scan(path, self.callback.player.default_tempo, force_manual_adjust) + img, tempo = load_scan(self, path, self.callback.player.default_tempo, force_manual_adjust) if img is None: return self.img_path = path @@ -204,7 +252,7 @@ def load_file(self, path, force_manual_adjust=False): self.callback.player.emulate_off() self.spool.on_destroy() tmp = self.spool - self.spool = InputScanImg(self, img, self.callback.player.spool_diameter, self.callback.player.roll_width, callback=self.callback) + self.spool = InputScanImg(self, img, self.callback.player.spool_diameter, self.callback.player.roll_width, window_scale=self.conf.window_scale, callback=self.callback) self.spool.start_worker() # self.spool.Bind(wx.EVT_KEY_DOWN, self.on_keydown) # self.spool.Bind(wx.EVT_KEY_UP, self.on_keyup) diff --git a/src/player_mng.py b/src/player_mng.py index 8b661e2..c053b42 100644 --- a/src/player_mng.py +++ b/src/player_mng.py @@ -2,15 +2,7 @@ import json import os -from AmpicoB import AmpicoB -from Artecho import Artecho -from DuoArt import DuoArt -from PhilippsDuca import PhilippsDuca -from player import Player -from RecordoA import RecordoA -from RecordoB import RecordoB -from WelteLicensee import WelteLicensee -from WelteT100 import WelteT100 +import players class PlayerMng: @@ -33,20 +25,21 @@ def init_player_map(self): @property def player_list(self): - return list(self.player_conf_map.keys()) + return sorted(self.player_conf_map.keys()) def get_player_obj(self, player_name, midiobj): cls_name = self.player_conf_map.get(player_name, None) cls_map = { - "Player": Player, - "AmpicoB": AmpicoB, - "Duo-Art": DuoArt, - "WelteT100": WelteT100, - "WelteLicensee": WelteLicensee, - "PhillipsDuca": PhilippsDuca, - "RecordoA": RecordoA, - "RecordoB": RecordoB, - "Artecho": Artecho, + "Player": players.BasePlayer, + "AmpicoB": players.AmpicoB, + "Duo-Art": players.DuoArt, + "WelteT100": players.WelteT100, + "WelteT98": players.WelteT98, + "WelteLicensee": players.WelteLicensee, + "PhillipsDuca": players.PhilippsDuca, + "RecordoA": players.RecordoA, + "RecordoB": players.RecordoB, + "Artecho": players.Artecho, } clsobj = cls_map.get(cls_name, None) if clsobj is not None: @@ -61,5 +54,5 @@ def get_player_obj(self, player_name, midiobj): print(obj.player_list) print(type(obj.get_player_obj("88 Note white back", None))) assert obj.get_player_obj("not exists", None) is None - assert type(obj.get_player_obj("88 Note white back", None)) is Player - assert type(obj.get_player_obj("Ampico B white back", None)) is AmpicoB + assert type(obj.get_player_obj("88 Note white back", None)) is players.Player + assert type(obj.get_player_obj("Ampico B white back", None)) is players.AmpicoB diff --git a/src/AmpicoB.py b/src/players/AmpicoB.py similarity index 89% rename from src/AmpicoB.py rename to src/players/AmpicoB.py index fb15faf..2dc7e9a 100644 --- a/src/AmpicoB.py +++ b/src/players/AmpicoB.py @@ -1,8 +1,9 @@ import wx -from player import Player +from .base_player import BasePlayer -class AmpicoB(Player): + +class AmpicoB(BasePlayer): def __init__(self, confpath, midiobj): super().__init__(confpath, midiobj) @@ -72,20 +73,15 @@ def emulate_expression(self, curtime): self.treble_sub_intensity_lock = True self.bass_sub_intensity_lock = True - # trigger intensity holes - if bass_intensities["is_open"][0]: # bass lv 2 - self.bass_intensity_lock[0] = True - if bass_intensities["is_open"][1]: # bass lv 4 - self.bass_intensity_lock[1] = True - if bass_intensities["is_open"][2]: # bass lv 6 - self.bass_intensity_lock[2] = True - - if treble_intensities["is_open"][2]: # treble lv 2 - self.treble_intensity_lock[0] = True - if treble_intensities["is_open"][1]: # treble lv 4 - self.treble_intensity_lock[1] = True - if treble_intensities["is_open"][0]: # treble lv 6 - self.treble_intensity_lock[2] = True + # trigger intensity holes (LV2->4->6) + for idx in range(3): + # bass 2->4->6 + if bass_intensities["is_open"][idx]: + self.bass_intensity_lock[idx] = True + # treble 2->4->6 + if treble_intensities["is_open"][2 - idx]: + self.treble_intensity_lock[idx] = True + self.calc_crescendo(curtime) self.calc_expression() diff --git a/src/Artecho.py b/src/players/Artecho.py similarity index 99% rename from src/Artecho.py rename to src/players/Artecho.py index 339d4e3..1ef72d4 100644 --- a/src/Artecho.py +++ b/src/players/Artecho.py @@ -1,9 +1,10 @@ import numpy as np import wx -from player import Player +from .base_player import BasePlayer -class Artecho(Player): + +class Artecho(BasePlayer): def __init__(self, confpath, midiobj): """ The intensity vacuum is based on crescendo position like Ampico B. diff --git a/src/DuoArt.py b/src/players/DuoArt.py similarity index 98% rename from src/DuoArt.py rename to src/players/DuoArt.py index 113dcad..2a60b9e 100644 --- a/src/DuoArt.py +++ b/src/players/DuoArt.py @@ -1,9 +1,9 @@ from collections import deque -from player import Player +from .base_player import BasePlayer -class DuoArt(Player): +class DuoArt(BasePlayer): def __init__(self, confpath, midiobj): super().__init__(confpath, midiobj) diff --git a/src/PhilippsDuca.py b/src/players/PhilippsDuca.py similarity index 93% rename from src/PhilippsDuca.py rename to src/players/PhilippsDuca.py index 737efbc..47ab66b 100644 --- a/src/PhilippsDuca.py +++ b/src/players/PhilippsDuca.py @@ -1,7 +1,7 @@ -from player import Player +from .base_player import BasePlayer -class PhilippsDuca(Player): +class PhilippsDuca(BasePlayer): def __init__(self, confpath, midiobj): super().__init__(confpath, midiobj) diff --git a/src/RecordoA.py b/src/players/RecordoA.py similarity index 98% rename from src/RecordoA.py rename to src/players/RecordoA.py index 8ecffd8..417ee22 100644 --- a/src/RecordoA.py +++ b/src/players/RecordoA.py @@ -1,7 +1,7 @@ -from player import Player +from .base_player import BasePlayer -class RecordoA(Player): +class RecordoA(BasePlayer): def __init__(self, confpath, midiobj): super().__init__(confpath, midiobj) diff --git a/src/RecordoB.py b/src/players/RecordoB.py similarity index 98% rename from src/RecordoB.py rename to src/players/RecordoB.py index 93e1634..94caec2 100644 --- a/src/RecordoB.py +++ b/src/players/RecordoB.py @@ -1,4 +1,4 @@ -from RecordoA import RecordoA +from .RecordoA import RecordoA class RecordoB(RecordoA): diff --git a/src/WelteLicensee.py b/src/players/WelteLicensee.py similarity index 92% rename from src/WelteLicensee.py rename to src/players/WelteLicensee.py index fcd7e10..01fca53 100644 --- a/src/WelteLicensee.py +++ b/src/players/WelteLicensee.py @@ -1,4 +1,4 @@ -from WelteT100 import WelteT100 +from .WelteT100 import WelteT100 class WelteLicensee(WelteT100): @@ -42,7 +42,7 @@ def emulate_pedals(self): from midi_controller import MidiWrap midiobj = MidiWrap() - player = WelteLicensee(os.path.join("playsk_config", "Ampico B white back.json"), midiobj) + player = WelteLicensee(os.path.join("playsk_config", "Welte Licensee white back.json"), midiobj) frame = np.full((600, 800, 3), 100, np.uint8) start = time.perf_counter() for _ in range(10000): diff --git a/src/WelteT100.py b/src/players/WelteT100.py similarity index 95% rename from src/WelteT100.py rename to src/players/WelteT100.py index 7ca5745..562b259 100644 --- a/src/WelteT100.py +++ b/src/players/WelteT100.py @@ -1,16 +1,17 @@ import numpy as np import wx -from player import Player +from .base_player import BasePlayer -class WelteT100(Player): + +class WelteT100(BasePlayer): def __init__(self, confpath, midiobj): super().__init__(confpath, midiobj) self.mf_hook_pos = 0.47 - self.min_vacuum = 5 # in W.G + self.min_vacuum = 5.5 # in W.G self.max_vacuum = 35 # in W.G - self.cres_pos_to_vacuum = np.poly1d(np.polyfit((0, self.mf_hook_pos, 1), (5, 20, 35), 2)) + self.cres_pos_to_vacuum = np.poly1d(np.polyfit((0, self.mf_hook_pos, 1), (self.min_vacuum, 20, self.max_vacuum), 2)) self.bass_cres_pos = 0 self.bass_cres_state = "slow_decres" @@ -44,7 +45,6 @@ def emulate_off(self): self.treble_vacuum = self.min_vacuum def emulate_expression(self, curtime): - # Check bass expression holes if self.holes["bass_mf_on"]["to_open"]: self.bass_mf_hook = True @@ -154,7 +154,7 @@ def draw_tracker(self, wxdc: wx.PaintDC): from midi_controller import MidiWrap midiobj = MidiWrap() - player = WelteT100(os.path.join("playsk_config", "Ampico B white back.json"), midiobj) + player = WelteT100(os.path.join("playsk_config", "Welte T100 white back.json"), midiobj) frame = np.full((600, 800, 3), 100, np.uint8) start = time.perf_counter() for _ in range(10000): diff --git a/src/players/WelteT98.py b/src/players/WelteT98.py new file mode 100644 index 0000000..c2206fe --- /dev/null +++ b/src/players/WelteT98.py @@ -0,0 +1,80 @@ +from .WelteT100 import WelteT100 + + +class WelteT98(WelteT100): + def __init__(self, confpath, midiobj): + super().__init__(confpath, midiobj) + + self.bass_slow_cres_rate = self.mf_hook_pos / 2.45 # min to mf takes 2.5sec + self.bass_slow_decres_rate = self.mf_hook_pos / 2.45 # mf to min takes 2.5sec + self.bass_fast_cres_rate = 1 / 0.85 + self.bass_fast_decres_rate = 1 / 0.18 + + self.treble_slow_cres_rate = self.mf_hook_pos / 2.45 # min to mf takes 2.5sec + self.treble_slow_decres_rate = self.mf_hook_pos / 2.45 # mf to min takes 2.5sec + self.treble_fast_cres_rate = 1 / 0.85 + self.treble_fast_decres_rate = 1 / 0.18 + + self.bass_vacuum = self.min_vacuum + self.treble_vacuum = self.min_vacuum + + def emulate_pedals(self): + # sustain pedal + sustain = self.holes["sustain"] + if sustain["to_open"]: + self.midi.sustain_on() + + elif sustain["to_close"]: + self.midi.sustain_off() + + # hammer rail lift emulation + soft = self.holes["soft"] + if soft["to_open"]: + self.midi.soft_on() + + elif soft["to_close"]: + self.midi.soft_off() + + def emulate_expression(self, curtime): + # Check bass expression holes + if self.holes["bass_mf"]["is_open"]: + self.bass_mf_hook = True + else: + self.bass_mf_hook = False + + if self.holes["bass_cresc"]["is_open"]: + self.bass_cres_state = "slow_cres" + else: + self.bass_cres_state = "slow_decres" + + # Check treble expression holes + if self.holes["treble_mf"]["is_open"]: + self.treble_mf_hook = True + else: + self.treble_mf_hook = False + + if self.holes["treble_cresc"]["is_open"]: + self.treble_cres_state = "slow_cres" + else: + self.treble_cres_state = "slow_decres" + + self.calc_crescendo(curtime) + self.calc_expression() + + +if __name__ == "__main__": + import os + import time + + import numpy as np + from midi_controller import MidiWrap + + midiobj = MidiWrap() + player = WelteT98(os.path.join("playsk_config", "Ampico B white back.json"), midiobj) + frame = np.full((600, 800, 3), 100, np.uint8) + start = time.perf_counter() + for _ in range(10000): + player.emulate(frame, time.perf_counter()) + end = time.perf_counter() + t = end - start + print(t, "per", (t / 10000) * 1000, "ms") diff --git a/src/players/__init__.py b/src/players/__init__.py new file mode 100644 index 0000000..9b927d3 --- /dev/null +++ b/src/players/__init__.py @@ -0,0 +1,23 @@ +from .AmpicoB import AmpicoB +from .Artecho import Artecho +from .base_player import BasePlayer +from .DuoArt import DuoArt +from .PhilippsDuca import PhilippsDuca +from .RecordoA import RecordoA +from .RecordoB import RecordoB +from .WelteLicensee import WelteLicensee +from .WelteT98 import WelteT98 +from .WelteT100 import WelteT100 + +__all__ = [ + "AmpicoB", + "Artecho", + "BasePlayer", + "DuoArt", + "PhilippsDuca", + "RecordoA", + "RecordoB", + "WelteLicensee", + "WelteT98", + "WelteT100", +] diff --git a/src/player.py b/src/players/base_player.py similarity index 98% rename from src/player.py rename to src/players/base_player.py index e52f5ea..26f8682 100644 --- a/src/player.py +++ b/src/players/base_player.py @@ -102,7 +102,7 @@ def __getitem__(self, key): return ret -class Player: +class BasePlayer: def __init__(self, confpath, midiobj: MidiWrap): self.midi = midiobj @@ -257,7 +257,7 @@ def draw_tracker(self, wxdc: wx.PaintDC): import time midiobj = MidiWrap() - player = Player(os.path.join("playsk_config", "88 Note white back.json"), midiobj) + player = BasePlayer(os.path.join("playsk_config", "88 Note white back.json"), midiobj) frame = np.full((600, 800, 3), 100, np.uint8) player.emulate_on() start = time.perf_counter() diff --git a/src/playsk_config/Welte T98 white back.json b/src/playsk_config/Welte T98 white back.json new file mode 100644 index 0000000..a261d38 --- /dev/null +++ b/src/playsk_config/Welte T98 white back.json @@ -0,0 +1,192 @@ +{ + "note": "Welte Mignon T-98 Greeen of white background scan", + "base_class": "WelteT98", + "roll_width": 11.25, + "spool_diameter": 1.90, + "default_tempo": 72.27, + "expression": { + "stack_split_point": 46 + }, + "tracker_holes": { + "is_dark_hole": false, + "on_brightness": 220, + "lowest_note": 0, + "note": { + "x": [ + 59, + 67, + 75, + 82, + 90, + 98, + 106, + 113, + 121, + 129, + 137, + 145, + 152, + 160, + 168, + 176, + 183, + 191, + 199, + 207, + 214, + 222, + 230, + 238, + 246, + 253, + 261, + 269, + 277, + 284, + 292, + 300, + 308, + 315, + 323, + 331, + 339, + 347, + 354, + 362, + 370, + 378, + 385, + 393, + 401, + 409, + 416, + 424, + 432, + 440, + 447, + 455, + 463, + 471, + 479, + 486, + 494, + 502, + 510, + 517, + 525, + 533, + 541, + 548, + 556, + 564, + 572, + 580, + 587, + 595, + 603, + 611, + 618, + 626, + 634, + 642, + 649, + 657, + 665, + 673, + 681, + 688, + 696, + 704, + 712, + 719, + 727, + 735 + ], + "y": 297, + "w": 5, + "h": 5, + "on_apature": 0.3, + "off_apature": 0.1 + }, + "bass_forz_piano": { + "x": 20, + "y": 297, + "w": 5, + "h": 5, + "on_apature": 0.1, + "off_apature": 0.1 + }, + "bass_mf": { + "x": 28, + "y": 297, + "w": 5, + "h": 6, + "on_apature": 0.1, + "off_apature": 0.05 + }, + "sustain": { + "x": 36, + "y": 295, + "w": 5, + "h": 7, + "on_apature": 0.1, + "off_apature": 0.05 + }, + "bass_cresc": { + "x": 44, + "y": 295, + "w": 5, + "h": 7, + "on_apature": 0.1, + "off_apature": 0.05 + }, + "bass_forz_forte": { + "x": 51, + "y": 297, + "w": 5, + "h": 5, + "on_apature": 0.1, + "off_apature": 0.1 + }, + "treble_forz_forte": { + "x": 743, + "y": 297, + "w": 5, + "h": 5, + "on_apature": 0.1, + "off_apature": 0.1 + }, + "treble_cresc": { + "x": 750, + "y": 295, + "w": 5, + "h": 7, + "on_apature": 0.1, + "off_apature": 0.05 + }, + "soft": { + "x": 758, + "y": 295, + "w": 5, + "h": 7, + "on_apature": 0.1, + "off_apature": 0.05 + }, + "treble_mf": { + "x": 766, + "y": 297, + "w": 5, + "h": 6, + "on_apature": 0.1, + "off_apature": 0.05 + }, + "treble_forz_piano": { + "x": 774, + "y": 297, + "w": 5, + "h": 5, + "on_apature": 0.1, + "off_apature": 0.1 + } + } +} \ No newline at end of file diff --git a/src/playsk_config/config.json b/src/playsk_config/config.json index 689309d..fcc48e4 100644 --- a/src/playsk_config/config.json +++ b/src/playsk_config/config.json @@ -1,5 +1,6 @@ { - "last_midi_port": "loopMIDI Port 1", - "last_tracker": "88 Note white back", - "update_notified_version": "3.1" + "last_midi_port": "Microsoft GS Wavetable Synth 0", + "last_tracker": "Ampico B white back", + "update_notified_version": "3.1", + "window_scale": "100%" } \ No newline at end of file diff --git a/src/vacuum_gauge.py b/src/vacuum_gauge.py index 15a4c5f..31be4eb 100644 --- a/src/vacuum_gauge.py +++ b/src/vacuum_gauge.py @@ -11,7 +11,8 @@ class VacuumGauge(wx.Panel): def __init__(self, parent, pos=(0, 0), caption=""): wx.Panel.__init__(self, parent, wx.ID_ANY, pos=pos) caption = wx.StaticText(self, wx.ID_ANY, caption) - self.meter = OscilloGraph(self) + scale = parent.get_dpiscale_factor() + self.meter = OscilloGraph(self, scale, parent.get_dipscaled_size(wx.Size(200, 150))) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(caption) @@ -32,14 +33,13 @@ def destroy(self): class OscilloGraph(wx.Panel): - def __init__(self, parent, max_scale_scale=50, size=(200, 150)): - size = parent.FromDIP(wx.Size(size)) + def __init__(self, parent, scale, size, max_vacuum=50): wx.Panel.__init__(self, parent, wx.ID_ANY, size=size) self.w = size[0] self.h = size[1] - self.max_scale = max_scale_scale - self.scale = self.GetDPIScaleFactor() if platform.system() == "Windows" else 1 - self.plot_scale = self.h / self.max_scale + self.scale = scale + self.max_vacuum = max_vacuum + self.plot_scale = self.h / self.max_vacuum self.SetDoubleBuffered(True) self.val = 0 @@ -77,7 +77,7 @@ def init_grid(self): # grid line dc.SetPen(wx.Pen("black", int(1 * self.scale), wx.SOLID)) dc.DrawLineList([(x, 0, x, self.h - 1) for x in range(0, self.w, int(50 * self.scale))]) - dc.DrawLineList([(0, int(y * self.plot_scale), self.w - 1, int(y * self.plot_scale)) for y in range(0, self.max_scale, 10)]) + dc.DrawLineList([(0, int(y * self.plot_scale), self.w - 1, int(y * self.plot_scale)) for y in range(0, self.max_vacuum, 10)]) # scale dc.SetFont(wx.Font(int(12 * self.scale), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)) @@ -129,7 +129,7 @@ def slider_value_change(event): panel1.vacuum = obj.GetValue() panel2.vacuum = obj.GetValue() + 10 - slider = wx.Slider(frame, value=0, minValue=0, maxValue=40, size=frame.FromDIP(wx.Size(200, 100)), style=wx.SL_LABELS) + slider = wx.Slider(frame, value=0, minValue=0, maxValue=40, size=frame.get_dipscaled_size(wx.Size(200, 100)), style=wx.SL_LABELS) slider.Bind(wx.EVT_SLIDER, slider_value_change) sizer = wx.BoxSizer(wx.VERTICAL) diff --git a/src/version.py b/src/version.py index 20f3f46..5bfb949 100644 --- a/src/version.py +++ b/src/version.py @@ -1,4 +1,4 @@ APP_TITLE = "PlaySK Piano Roll Reader" -APP_VERSION = "3.2.0" +APP_VERSION = "3.3.0" COPY_RIGHT = "(C)Sasaki Katsumasa 2014-2024" diff --git a/src/version_info.txt b/src/version_info.txt index 7b93619..3c40dc4 100644 --- a/src/version_info.txt +++ b/src/version_info.txt @@ -4,8 +4,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. - filevers=(3, 2, 0, 0), - prodvers=(3, 2, 0, 0), + filevers=(3, 3, 0, 0), + prodvers=(3, 3, 0, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -28,10 +28,10 @@ VSVersionInfo( StringTable( u'040904B0', [StringStruct(u'FileDescription', u'PlaySK Piano Roll Reader'), - StringStruct(u'FileVersion', u'3.2.0.0'), + StringStruct(u'FileVersion', u'3.3.0.0'), StringStruct(u'LegalCopyright', u'(C)Sasaki Katsumasa 2014-2024'), StringStruct(u'ProductName', u'PlaySK Piano Roll Reader'), - StringStruct(u'ProductVersion', u'3.2.0.0')]) + StringStruct(u'ProductVersion', u'3.3.0.0')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) ] diff --git a/test/dummy_config/config.json b/test/dummy_config/config.json index 4f00b48..2cc3d6b 100644 --- a/test/dummy_config/config.json +++ b/test/dummy_config/config.json @@ -1,5 +1,6 @@ { "last_midi_port": "loopMIDI Port 1", "last_tracker": "Duo-Art white back", - "update_notified_version": "3.1" + "update_notified_version": "3.1", + "window_scale": "125%" } \ No newline at end of file diff --git a/test/dummy_config/config2.json b/test/dummy_config/config2.json index 495c5fa..e0aa3f6 100644 --- a/test/dummy_config/config2.json +++ b/test/dummy_config/config2.json @@ -1,5 +1,6 @@ { - "last_midi_port": 1, - "last_tracker": 2, - "update_notified_version": 3 + "last_midi_port": "hogehoge", + "last_tracker": "fugafuga", + "update_notified_version": "4", + "window_scale": "125%" } \ No newline at end of file diff --git a/test/test_AmpicoB.py b/test/test_AmpicoB.py index b70ffda..acad14f 100644 --- a/test/test_AmpicoB.py +++ b/test/test_AmpicoB.py @@ -1,9 +1,11 @@ +import math import sys +import numpy as np import pytest sys.path.append("src/") -from AmpicoB import AmpicoB +import players from midi_controller import MidiWrap @@ -11,7 +13,7 @@ class TestAmpicoB: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = AmpicoB("src/playsk_config/Ampico B white back.json", midiobj) + obj = players.AmpicoB("src/playsk_config/Ampico B white back.json", midiobj) return obj def test_emulate_off(self, player): @@ -32,3 +34,192 @@ def test_emulate_off(self, player): assert not player.treble_sub_intensity_lock assert player.bass_vacuum == player.intensity_range["none"][0] assert player.treble_vacuum == player.intensity_range["none"][0] + + def test_emulate_expression(self, player): + # bass intensity cancel + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_cancel"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.bass_intensity_lock = [True, True, True] + player.bass_sub_intensity_lock = True + player.emulate_expression(0) + assert player.bass_intensity_lock == [False, False, False] + assert not player.bass_sub_intensity_lock + + # treble intensity cancel + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_cancel"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.treble_intensity_lock = [True, True, True] + player.treble_sub_intensity_lock = True + player.emulate_expression(0) + assert player.treble_intensity_lock == [False, False, False] + assert not player.treble_sub_intensity_lock + + # sub intensity + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["subintensity"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.bass_sub_intensity_lock = False + player.treble_sub_intensity_lock = False + player.emulate_expression(0) + assert player.bass_sub_intensity_lock + assert player.treble_sub_intensity_lock + + # intensity holes + for idx in range(3): + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_intensity"]["pos"][idx] + frame[y1:y2, x1:x2, :] = 255 + x1, y1, x2, y2 = player.holes["treble_intensity"]["pos"][2 - idx] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_intensity_lock[idx] + assert player.treble_intensity_lock[idx] + + def test_calc_crescendo(self, player): + player.pre_time = None + player.calc_crescendo(100) + assert player.pre_time == 100 + + # slow crescendeo. 4sec min to max + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.amp_cres_pos = 0 + player.pre_time = 0 + player.calc_crescendo(2) + assert math.isclose(player.amp_cres_pos, 0.5) + player.calc_crescendo(4) + assert math.isclose(player.amp_cres_pos, 1) + + # slow decrescendo. 4sec max to min + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.calc_crescendo(6) + assert math.isclose(player.amp_cres_pos, 0.5) + player.calc_crescendo(8) + assert math.isclose(player.amp_cres_pos, 0) + + # fast crescendeo. 0.8 sec min to max + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + x1, y1, x2, y2 = player.holes["treble_fast_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.amp_cres_pos = 0 + player.pre_time = 0 + player.calc_crescendo(0.4) + assert math.isclose(player.amp_cres_pos, 0.5) + player.calc_crescendo(0.8) + assert math.isclose(player.amp_cres_pos, 1) + + # fast decrescendo. 0.8 sec max to min + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 0 + x1, y1, x2, y2 = player.holes["treble_fast_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.calc_crescendo(1.2) + assert math.isclose(player.amp_cres_pos, 0.5) + player.calc_crescendo(1.6) + assert math.isclose(player.amp_cres_pos, 0) + + # amplifier triggers fast crescendo + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + x1, y1, x2, y2 = player.holes["amplifier"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.amp_cres_pos = 0 + player.pre_time = 0 + player.calc_crescendo(0.4) + assert math.isclose(player.amp_cres_pos, 0.5) + player.calc_crescendo(0.8) + assert math.isclose(player.amp_cres_pos, 1) + + # amplifier triggers fast crescendo + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 0 + x1, y1, x2, y2 = player.holes["amplifier"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.calc_crescendo(1.2) + assert math.isclose(player.amp_cres_pos, 0.5) + player.calc_crescendo(1.6) + assert math.isclose(player.amp_cres_pos, 0) + + # 1st amplifier lock + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.amp_cres_pos = 0 + player.pre_time = 0 + player.calc_crescendo(2) # crescendo position is 0.5 + x1, y1, x2, y2 = player.holes["amplifier"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + frame[y1:y2, x1:x2, :] = 0 # 1st amplifier locked + player.holes.set_frame(frame, 0) + player.calc_crescendo(10) + assert math.isclose(player.amp_cres_pos, 0.85) # max is 0.85 + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.calc_crescendo(20) + assert math.isclose(player.amp_cres_pos, 0.3) # min is 0.3 + assert player.amp_lock_range == [0.3, 0.85] + + # 1st amplifier to no amplifier + x1, y1, x2, y2 = player.holes["amplifier"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.calc_crescendo(20) + frame[y1:y2, x1:x2, :] = 0 # 1st amplifier unlocked + player.holes.set_frame(frame, 0) + player.calc_crescendo(30) + assert math.isclose(player.amp_cres_pos, 0) # min is 0 + assert player.amp_lock_range == [0, 1.0] + + # 2nd amplifier lock + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.amp_cres_pos = 0 + player.pre_time = 0 + player.calc_crescendo(4) # crescendo position is 1.0 + x1, y1, x2, y2 = player.holes["amplifier"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + frame[y1:y2, x1:x2, :] = 0 # 1st amplifier locked + player.holes.set_frame(frame, 0) + player.calc_crescendo(10) + assert math.isclose(player.amp_cres_pos, 1) # max is 1 + x1, y1, x2, y2 = player.holes["treble_slow_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.calc_crescendo(20) + assert math.isclose(player.amp_cres_pos, 0.85) # min is 0.85 + assert player.amp_lock_range == [0.85, 1] + + # 2nd amplifier to no amplifier + x1, y1, x2, y2 = player.holes["amplifier"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.calc_crescendo(20) + frame[y1:y2, x1:x2, :] = 0 # 1st amplifier unlocked + player.holes.set_frame(frame, 0) + player.calc_crescendo(30) + assert math.isclose(player.amp_cres_pos, 0) # min is 0 + assert player.amp_lock_range == [0, 1.0] + diff --git a/test/test_Artecho.py b/test/test_Artecho.py index dedb300..d43fd5d 100644 --- a/test/test_Artecho.py +++ b/test/test_Artecho.py @@ -4,7 +4,7 @@ import pytest sys.path.append("src/") -from Artecho import Artecho +import players from midi_controller import MidiWrap @@ -12,7 +12,7 @@ class TestArtecho: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = Artecho("src/playsk_config/Artecho white back (experimental).json", midiobj) + obj = players.Artecho("src/playsk_config/Artecho white back (experimental).json", midiobj) return obj def test_emulate_off(self, player): @@ -52,7 +52,6 @@ def test_calc_velocity(self, player): assert player.calc_velocity() == (60, 54) def test_emulate_pedals(self, player, mocker): - # sustain on frame = np.full((600, 800, 3), 0, np.uint8) sustain_on_mock = mocker.patch("midi_controller.MidiWrap.sustain_on") @@ -70,3 +69,43 @@ def test_emulate_pedals(self, player, mocker): player.emulate_pedals() sustain_off_mock.assert_called_once() + def test_emulate_expression(self, player): + # bass intensity cancel + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_cancel"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.bass_intensity_lock = [True, True, True] + player.emulate_expression(0) + assert player.bass_intensity_lock == [False, False, False] + + # bass intensity cancel + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_cancel"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.treble_intensity_lock = [True, True, True] + player.emulate_expression(0) + assert player.treble_intensity_lock == [False, False, False] + + # cancel + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["cancel"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.bass_hammer_rail_lock = False + player.treble_hammer_rail_lock = False + player.pianissimo_lock = False + player.emulate_expression(0) + assert not player.bass_hammer_rail_lock + assert not player.treble_hammer_rail_lock + assert not player.pianissimo_lock + + # pianissimo + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["pianissimo"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.pianissimo_lock + diff --git a/test/test_DuoArt.py b/test/test_DuoArt.py index 198d8df..497ec68 100644 --- a/test/test_DuoArt.py +++ b/test/test_DuoArt.py @@ -6,7 +6,7 @@ sys.path.append("src/") from collections import deque -from DuoArt import DuoArt +import players from midi_controller import MidiWrap @@ -14,7 +14,7 @@ class TestDuoArt: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = DuoArt("src/playsk_config/Duo-Art white back.json", midiobj) + obj = players.DuoArt("src/playsk_config/Duo-Art white back.json", midiobj) return obj def test_emulate_off(self, player): diff --git a/test/test_RecordoA.py b/test/test_RecordoA.py index cafde73..303a7f4 100644 --- a/test/test_RecordoA.py +++ b/test/test_RecordoA.py @@ -4,15 +4,15 @@ import pytest sys.path.append("src/") +import players from midi_controller import MidiWrap -from RecordoA import RecordoA class TestRecordoA: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = RecordoA("src/playsk_config/Recordo A (rare) white back.json", midiobj) + obj = players.RecordoA("src/playsk_config/Recordo A (rare) white back.json", midiobj) return obj def test_emulate_off(self, player): diff --git a/test/test_RecordoB.py b/test/test_RecordoB.py index 45cb26f..837105c 100644 --- a/test/test_RecordoB.py +++ b/test/test_RecordoB.py @@ -4,15 +4,15 @@ import pytest sys.path.append("src/") +import players from midi_controller import MidiWrap -from RecordoB import RecordoB class TestRecordoB: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = RecordoB("src/playsk_config/Recordo B white back.json", midiobj) + obj = players.RecordoB("src/playsk_config/Recordo B white back.json", midiobj) return obj @pytest.mark.parametrize("open_ports, expect", [ diff --git a/test/test_WelteT100.py b/test/test_WelteT100.py index fcf7c3a..6305d5e 100644 --- a/test/test_WelteT100.py +++ b/test/test_WelteT100.py @@ -4,16 +4,15 @@ import pytest sys.path.append("src/") - +import players from midi_controller import MidiWrap -from WelteT100 import WelteT100 class TestWelteT100: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = WelteT100("src/playsk_config/Welte T100 white back.json", midiobj) + obj = players.WelteT100("src/playsk_config/Welte T100 white back.json", midiobj) return obj def test_emulate_off(self, player): @@ -36,7 +35,6 @@ def test_emulate_off(self, player): assert player.treble_vacuum == player.min_vacuum def test_pedal(self, player, mocker): - # sustain on frame = np.full((600, 800, 3), 0, np.uint8) sustain_on_mock = mocker.patch("midi_controller.MidiWrap.sustain_on") @@ -72,3 +70,64 @@ def test_pedal(self, player, mocker): player.holes.set_frame(frame, 0) player.emulate_pedals() sustain_off_mock.assert_called_once() + + def test_emulate_expression(self, player): + # bass mf on/off + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_mf_on"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_mf_hook + + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_mf_off"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert not player.bass_mf_hook + + # bass slow crescend on/off + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_cresc_forte"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_cres_state == "slow_cres" + + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_cresc_piano"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_cres_state == "slow_decres" + + # bass mf on/off + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_mf_on"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.treble_mf_hook + + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_mf_off"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert not player.treble_mf_hook + + # treble slow crescend on/off + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_cresc_forte"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.treble_cres_state == "slow_cres" + + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_cresc_piano"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.treble_cres_state == "slow_decres" \ No newline at end of file diff --git a/test/test_WelteT98.py b/test/test_WelteT98.py new file mode 100644 index 0000000..7165d3c --- /dev/null +++ b/test/test_WelteT98.py @@ -0,0 +1,124 @@ +import sys + +import numpy as np +import pytest + +sys.path.append("src/") +import players +from midi_controller import MidiWrap + + +class TestWelteT98: + @pytest.fixture + def player(self): + midiobj = MidiWrap() + obj = players.WelteT98("src/playsk_config/Welte T98 white back.json", midiobj) + return obj + + def test_emulate_off(self, player): + player.bass_cres_pos = 1 + player.bass_cres_state = "slow_cres" + player.bass_mf_hook = True + player.treble_cres_pos = 1 + player.treble_cres_state = "slow_cres" + player.treble_mf_hook = True + player.bass_vacuum = 30 + player.treble_vacuum = 30 + player.emulate_off() + assert player.bass_cres_pos == 0 + assert player.bass_cres_state == "slow_decres" + assert not player.bass_mf_hook + assert player.treble_cres_pos == 0 + assert player.treble_cres_state == "slow_decres" + assert not player.treble_mf_hook + assert player.bass_vacuum == player.min_vacuum + assert player.treble_vacuum == player.min_vacuum + + def test_pedal(self, player, mocker): + # sustain on + frame = np.full((600, 800, 3), 0, np.uint8) + sustain_on_mock = mocker.patch("midi_controller.MidiWrap.sustain_on") + x1, y1, x2, y2 = player.holes["sustain"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_pedals() + sustain_on_mock.assert_called_once() + + # sustain off + sustain_off_mock = mocker.patch("midi_controller.MidiWrap.sustain_off") + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.emulate_pedals() + sustain_off_mock.assert_called_once() + + # soft on + frame = np.full((600, 800, 3), 0, np.uint8) + sustain_on_mock = mocker.patch("midi_controller.MidiWrap.soft_on") + x1, y1, x2, y2 = player.holes["soft"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_pedals() + sustain_on_mock.assert_called_once() + + # soft off + sustain_off_mock = mocker.patch("midi_controller.MidiWrap.soft_off") + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.emulate_pedals() + sustain_off_mock.assert_called_once() + + + def test_emulate_expression(self, player): + # bass mf on + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_mf"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_mf_hook + + # bass mf off + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert not player.bass_mf_hook + + # bass crescendo + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["bass_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_cres_state == "slow_cres" + + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.bass_cres_state == "slow_decres" + + # treble mf on + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_mf"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.treble_mf_hook + + # treble mf off + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert not player.treble_mf_hook + + # treble crescendo + frame = np.full((600, 800, 3), 0, np.uint8) + x1, y1, x2, y2 = player.holes["treble_cresc"]["pos"][0] + frame[y1:y2, x1:x2, :] = 255 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.treble_cres_state == "slow_cres" + + frame[y1:y2, x1:x2, :] = 0 + player.holes.set_frame(frame, 0) + player.emulate_expression(0) + assert player.treble_cres_state == "slow_decres" diff --git a/test/test_config.py b/test/test_config.py index 55e56b0..63b1ed5 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,39 +1,51 @@ from src.config import ConfigMng -def test_load_config(monkeypatch): +def test_load_config(mocker): exists_path = "test/dummy_config/config.json" - monkeypatch.setattr(ConfigMng, "_path", exists_path) + mocker.patch("src.config.CONFIG_PATH", new=exists_path) mngr = ConfigMng() assert mngr.last_midi_port == "loopMIDI Port 1" assert mngr.last_tracker == "Duo-Art white back" assert mngr.update_notified_version == "3.1" + assert mngr.window_scale == "125%" empty_json_path = "test/dummy_config/empty_config.json" - monkeypatch.setattr(ConfigMng, "_path", empty_json_path) + mocker.patch("src.config.CONFIG_PATH", new=empty_json_path) mngr = ConfigMng() assert mngr.last_midi_port == "" assert mngr.last_tracker == "" assert mngr.update_notified_version == "" + assert mngr.window_scale == "100%" not_exists_path = "notexists.json" - monkeypatch.setattr(ConfigMng, "_path", not_exists_path) + mocker.patch("src.config.CONFIG_PATH", new=not_exists_path) mngr = ConfigMng() assert mngr.last_midi_port == "" assert mngr.last_tracker == "" assert mngr.update_notified_version == "" + assert mngr.window_scale == "100%" -def test_save_config(monkeypatch): +def test_save_config(mocker): exists_path = "test/dummy_config/config2.json" - monkeypatch.setattr(ConfigMng, "_path", exists_path) + mocker.patch("src.config.CONFIG_PATH", new=exists_path) mngr = ConfigMng() - mngr.last_midi_port = 1 - mngr.last_tracker = 2 - mngr.update_notified_version = 3 + mngr.last_midi_port = "Japanese日本語" + mngr.last_tracker = "2" + mngr.update_notified_version = "3" + mngr.window_scale = "150%" mngr.save_config() mngr = ConfigMng() - assert mngr.last_midi_port == 1 - assert mngr.last_tracker == 2 - assert mngr.update_notified_version == 3 + assert mngr.last_midi_port == "Japanese日本語" + assert mngr.last_tracker == "2" + assert mngr.update_notified_version == "3" + assert mngr.window_scale == "150%" + + mngr = ConfigMng() + mngr.last_midi_port = "hogehoge" + mngr.last_tracker = "fugafuga" + mngr.update_notified_version = "4" + mngr.window_scale = "125%" + mngr.save_config() \ No newline at end of file diff --git a/test/test_input_src.py b/test/test_input_src.py index b1d0088..5e2f43c 100644 --- a/test/test_input_src.py +++ b/test/test_input_src.py @@ -25,7 +25,7 @@ def test_load_img(img_path, expect): ]) def test_load_cis(img_path, expect, mocker): mocker.patch("wx.MessageBox") # ignore msg box - img, tempo = _load_cis("test/test_images/" + img_path, 80, False) + img, tempo = _load_cis(None, "test/test_images/" + img_path, 80, False) assert (img is not None, tempo) == expect # Test case of using ImgEditDlg is not implemented yet diff --git a/test/test_player.py b/test/test_player.py index 3a6702d..7e9fe47 100644 --- a/test/test_player.py +++ b/test/test_player.py @@ -9,7 +9,7 @@ sys.path.append("src/") from midi_controller import MidiWrap -from player import Player, TrackerHoles +from players.base_player import BasePlayer, TrackerHoles class TestTrackerHoles: @@ -94,7 +94,7 @@ class TestPlayer: @pytest.fixture def player(self): midiobj = MidiWrap() - obj = Player("src/playsk_config/88 Note white back.json", midiobj) + obj = BasePlayer("src/playsk_config/88 Note white back.json", midiobj) return obj def call_emulate_th(self, obj, frame): @@ -156,7 +156,7 @@ def heavy_emulate_notes(self): hole_color = player.holes.th_bright - 1 if player.holes.is_dark_hole else player.holes.th_bright + 1 frame = np.full((600, 800, 3), hole_color, np.uint8) - monkeypatch.setattr(Player, "emulate_notes", heavy_emulate_notes) + monkeypatch.setattr(BasePlayer, "emulate_notes", heavy_emulate_notes) th = threading.Thread(target=self.call_emulate_th, args=(player, frame)) th.start() time.sleep(1) diff --git a/test/test_player_mng.py b/test/test_player_mng.py index a1e6f4e..e547be6 100644 --- a/test/test_player_mng.py +++ b/test/test_player_mng.py @@ -3,16 +3,8 @@ import pytest sys.path.append("src/") -from AmpicoB import AmpicoB -from Artecho import Artecho -from DuoArt import DuoArt -from PhilippsDuca import PhilippsDuca -from player import Player +import players from player_mng import PlayerMng -from RecordoA import RecordoA -from RecordoB import RecordoB -from WelteLicensee import WelteLicensee -from WelteT100 import WelteT100 class TestPlayerMng: @@ -41,17 +33,19 @@ def test_player_list(self, player_mng): "Recordo A (rare) white back", "Recordo B white back", "Artecho white back (experimental)", + "Welte T98 white back", ]) assert player_names == gt_names def test_get_player_obj(self, player_mng): assert player_mng.get_player_obj("not exists player", None) is None - assert type(player_mng.get_player_obj("Ampico B white back", None)) is AmpicoB - assert type(player_mng.get_player_obj("Duo-Art white back", None)) is DuoArt - assert type(player_mng.get_player_obj("Philipps Duca (no expression)", None)) is PhilippsDuca - assert type(player_mng.get_player_obj("88 Note white back", None)) is Player - assert type(player_mng.get_player_obj("Welte Licensee white back", None)) is WelteLicensee - assert type(player_mng.get_player_obj("Welte T100 white back", None)) is WelteT100 - assert type(player_mng.get_player_obj("Recordo A (rare) white back", None)) is RecordoA - assert type(player_mng.get_player_obj("Recordo B white back", None)) is RecordoB - assert type(player_mng.get_player_obj("Artecho white back (experimental)", None)) is Artecho + assert type(player_mng.get_player_obj("Ampico B white back", None)) is players.AmpicoB + assert type(player_mng.get_player_obj("Duo-Art white back", None)) is players.DuoArt + assert type(player_mng.get_player_obj("Philipps Duca (no expression)", None)) is players.PhilippsDuca + assert type(player_mng.get_player_obj("88 Note white back", None)) is players.BasePlayer + assert type(player_mng.get_player_obj("Welte Licensee white back", None)) is players.WelteLicensee + assert type(player_mng.get_player_obj("Welte T100 white back", None)) is players.WelteT100 + assert type(player_mng.get_player_obj("Recordo A (rare) white back", None)) is players.RecordoA + assert type(player_mng.get_player_obj("Recordo B white back", None)) is players.RecordoB + assert type(player_mng.get_player_obj("Artecho white back (experimental)", None)) is players.Artecho + assert type(player_mng.get_player_obj("Welte T98 white back", None)) is players.WelteT98