├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── module.pyx ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Created by .ignore support plugin (hsz.mobi) 35 | ### Python template 36 | # Byte-compiled / optimized / DLL files 37 | __pycache__/ 38 | *.py[cod] 39 | *$py.class 40 | 41 | # C extensions 42 | *.so 43 | 44 | # Distribution / packaging 45 | .Python 46 | build/ 47 | develop-eggs/ 48 | dist/ 49 | downloads/ 50 | eggs/ 51 | .eggs/ 52 | lib/ 53 | lib64/ 54 | parts/ 55 | sdist/ 56 | var/ 57 | wheels/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | MANIFEST 62 | 63 | # PyInstaller 64 | # Usually these files are written by a python script from a template 65 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 66 | *.manifest 67 | *.spec 68 | 69 | # Installer logs 70 | pip-log.txt 71 | pip-delete-this-directory.txt 72 | 73 | # Unit test / coverage reports 74 | htmlcov/ 75 | .tox/ 76 | .coverage 77 | .coverage.* 78 | .cache 79 | nosetests.xml 80 | coverage.xml 81 | *.cover 82 | .hypothesis/ 83 | 84 | # Translations 85 | *.mo 86 | *.pot 87 | 88 | # Django stuff: 89 | *.log 90 | .static_storage/ 91 | .media/ 92 | local_settings.py 93 | 94 | # Flask stuff: 95 | instance/ 96 | .webassets-cache 97 | 98 | # Scrapy stuff: 99 | .scrapy 100 | 101 | # Sphinx documentation 102 | docs/_build/ 103 | 104 | # PyBuilder 105 | target/ 106 | 107 | # Jupyter Notebook 108 | .ipynb_checkpoints 109 | 110 | # pyenv 111 | .python-version 112 | 113 | # celery beat schedule file 114 | celerybeat-schedule 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | 141 | module.cpp 142 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hca2wav"] 2 | path = hca2wav 3 | url = git@github.com:Cryptomelone/hca2wav.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cryptomelone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hcapy 2 | 3 | hca2wav wrapper working on Python3. 4 | 5 | ## 概要 6 | 7 | [@Nyagamon](https://github.com/Nyagamon)さんの[HCADecoder](https://github.com/Nyagamon/HCADecoder)をベースにしたhcaデコーダ、[hca2wav](https://github.com/CrescentApricot/hca2wav)のPython3ラッパーです。
8 | 9 | ## 依存 10 | - Python3 11 | - C++11 12 | 13 | ## インストール 14 | 15 | ``` 16 | pip3 install git+ssh://git@github.com/CrescentApricot/hcapy.git 17 | ``` 18 | 19 | ## 使い方 20 | 21 | ```python 22 | import hcapy 23 | 24 | d = hcapy.Decoder(961961961961961) # 鍵指定 25 | 26 | d.decode_file("target_file.hca") # target_file.hcaを指定した鍵でデコード 27 | 28 | try: 29 | with open("target_file.hca", "rb") as f, open("decoded.wav", "wb") as f2: 30 | f2.write(d.decode(f.read()).read()) # bytesからデコード、io.BytesIOでリターンする 31 | except hcapy.InvalidHCAError: 32 | print("invalid hca!") 33 | ``` 34 | 35 | ### 鍵について 36 | 37 | 上記コードでは `961961961961961` のようなintで指定していますが、`"0x36ae63907b9e9"`のような形式でも、`"36ae63907b9e9"`のような形式でも、`"3907b9e9", "36ae"`のような形式でも指定可能です。 38 | 39 | ### 出力ファイルパスについて 40 | 41 | `decode_file` の場合、出力ファイルパスを引数 `dest` に指定することができますが、指定しないこともできます。指定しない場合、`src` と同じディレクトリに生成されます。 42 | 43 | ## 実装予定 44 | 45 | - ~~`bytes` からのデコード~~ (無理やり実装済み) 46 | - コマンドラインツール 47 | - パッケージ構成の正規化(hcapy.Decoder, hcapy.exceptions.Invalid...) 48 | -------------------------------------------------------------------------------- /module.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | import os 3 | import tempfile 4 | import io 5 | 6 | 7 | class InvalidHCAError(Exception): 8 | pass 9 | 10 | 11 | cdef extern from "hca2wav/include/clHCA.h": 12 | cdef cppclass clHCA: 13 | clHCA(unsigned int, unsigned int) except + 14 | int ciphKey1, ciphKey2 15 | void DecodeToWaveFile(const char*, const char*, float, int, int) 16 | 17 | cdef extern from "hca2wav/main.cpp": 18 | cdef int atoi16(const char *s) 19 | 20 | cdef class clHCApy: 21 | cdef clHCA* thisptr 22 | 23 | def __cinit__(self, long int ciphKey1, long int ciphKey2): 24 | self.thisptr = new clHCA(ciphKey1, ciphKey2) 25 | 26 | def __dealloc__(self): 27 | del self.thisptr 28 | 29 | def decode_file(self, const char* src, const char* dest, float volume=1., int mode=16, int loop=0): 30 | return self.thisptr.DecodeToWaveFile(src, dest, volume, mode, loop) 31 | 32 | 33 | class Decoder: 34 | def __init__(self, cipher_key, cipher_key_2=None): 35 | if cipher_key and not cipher_key_2: 36 | if type(cipher_key) == str: 37 | if cipher_key.startswith("0x"): 38 | h = cipher_key[2:].zfill(16) 39 | else: 40 | h = cipher_key.zfill(16) 41 | elif type(cipher_key) == int: 42 | h = hex(cipher_key)[2:].zfill(16) 43 | else: 44 | raise ValueError("cipher_key is must str(hex) or int.") 45 | self.cipher_key_1 = atoi16(h[8:].encode()) 46 | self.cipher_key_2 = atoi16(h[:8].encode()) 47 | 48 | elif cipher_key and cipher_key_2: 49 | if type(cipher_key) == int: 50 | self.cipher_key_1 = cipher_key 51 | elif type(cipher_key) == str: 52 | self.cipher_key_1 = atoi16(cipher_key.encode()) 53 | else: 54 | raise ValueError("cipher_key is must str(hex) or int.") 55 | if type(cipher_key_2) == int: 56 | self.cipher_key_2 = cipher_key_2 57 | elif type(cipher_key_2) == str: 58 | self.cipher_key_2 = atoi16(cipher_key_2.encode()) 59 | else: 60 | raise ValueError("cipher_key is must str(hex) or int.") 61 | else: 62 | raise ValueError("cipher_key was not spec.") 63 | 64 | self.decoder = clHCApy(self.cipher_key_1, self.cipher_key_2) 65 | 66 | def decode(self, src: bytes, volume: float=1., mode: int=16, loop: int=0) -> io.BytesIO: 67 | with tempfile.TemporaryDirectory() as tmp: 68 | t_src = os.path.join(tmp, "src.bin") 69 | t_dest = os.path.join(tmp, "dest.bin") 70 | with open(t_src, "wb") as t_src_o: 71 | t_src_o.write(src) 72 | self.decoder.decode_file(t_src.encode(), t_dest.encode(), volume, mode, loop) 73 | if os.path.exists(t_dest): 74 | with open(t_dest, "rb") as t_dest_o: 75 | return io.BytesIO(t_dest_o.read()) 76 | else: 77 | raise InvalidHCAError("hca decode failed.") 78 | 79 | def decode_file(self, src: str, dest=None, volume: float=1., mode: int=16, loop: int=0): 80 | if dest: 81 | dest_e = dest.encode() 82 | else: 83 | dest_dir = os.path.dirname(src) 84 | src_file = os.path.basename(src) 85 | src_file_spl = src_file.split(".") 86 | if len(src_file_spl) < 0: 87 | dest_file = src_file + ".wav" 88 | else: 89 | src_file_spl[-1] = "wav" 90 | dest_file = ".".join(src_file_spl) 91 | dest_e = os.path.join(dest_dir, dest_file).encode() 92 | return self.decoder.decode_file(src.encode(), dest_e, volume, mode, loop) 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Cython -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages, Extension 2 | from Cython.Build import build_ext, cythonize 3 | 4 | 5 | with open('README.md') as f: 6 | readme = f.read() 7 | with open('LICENSE') as f: 8 | li = f.read() 9 | 10 | setup( 11 | name='hcapy', 12 | version='0.0.2', 13 | description='hca2wav wrapper working on Python3.', 14 | long_description=readme, 15 | author='CrescentApricot', 16 | author_email='crescentapricot@users.noreply.github.com', 17 | license=li, 18 | url='https://github.com/CrescentApricot/hcapy', 19 | packages=find_packages(exclude=('tests',)), 20 | ext_modules=[ 21 | Extension("hcapy", sources=["module.pyx", "hca2wav/src/clHCA.cpp"], language="c++") 22 | ], 23 | cmdclass={'build_ext': build_ext}, 24 | ) 25 | --------------------------------------------------------------------------------