├── .clang-format ├── .github ├── .gitkeep └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode └── settings.json ├── .well-known └── microsoft-identity-association.json ├── CNAME ├── DropboxIntegration.md ├── EULA.md ├── ExternalAPIs.md ├── LICENSE ├── Makefile ├── PrivacyPolicy.md ├── README.md ├── _config.yml ├── cli ├── call-monitor.mm ├── call-monitor.plist ├── call-recorder.mm ├── call-recorder.plist ├── dtmf-decoder.mm ├── dtmf-decoder.plist ├── mixer.mm ├── mixer.plist ├── player.mm ├── player.plist ├── recorder.mm └── recorder.plist ├── devkit ├── env.sh ├── i18n_lint.py ├── i18n_statistics.py ├── roothide.sh └── rootless.sh ├── include ├── ATAudioTap.h ├── ATAudioTapDescription.h ├── AudioQueue+Private.h ├── CTCall.h ├── CTSetting.h └── CTTelephonyCenter.h ├── layout └── DEBIAN │ └── control ├── requirements.txt └── res ├── en.lproj ├── Compress Recording.shortcut ├── Create Archive.shortcut ├── EULA.md ├── InfoPlist.strings ├── Localizable.strings ├── Localizable.stringsdict ├── PrivacyPolicy.md └── SoftwareLicense.md ├── es.lproj ├── EULA.md ├── InfoPlist.strings ├── Localizable.strings ├── Localizable.stringsdict ├── Localizable.stringsdict.xml └── PrivacyPolicy.md ├── fr.lproj ├── InfoPlist.strings └── Localizable.strings ├── ko.lproj ├── InfoPlist.strings ├── Localizable.strings └── Localizable.stringsdict ├── ru.lproj ├── InfoPlist.strings ├── Localizable.strings └── Localizable.stringsdict ├── screenshot.png ├── stats.png ├── tr.lproj ├── EULA.md ├── InfoPlist.strings ├── Localizable.strings ├── Localizable.stringsdict └── PrivacyPolicy.md ├── ug-CN.lproj ├── InfoPlist.strings ├── Localizable.strings └── Localizable.stringsdict ├── vi.lproj ├── InfoPlist.strings └── Localizable.strings ├── zh-Hans.lproj ├── Compress Recording.shortcut ├── EULA.md ├── InfoPlist.strings ├── Localizable.strings ├── Localizable.stringsdict └── PrivacyPolicy.md ├── zh-Hant-HK.lproj ├── EULA.md ├── InfoPlist.strings ├── Localizable.strings └── PrivacyPolicy.md └── zh-Hant-TW.lproj ├── InfoPlist.strings └── Localizable.strings /.clang-format: -------------------------------------------------------------------------------- 1 | # clang-format 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | AccessModifierOffset: -4 5 | ContinuationIndentWidth: 4 6 | ColumnLimit: 120 -------------------------------------------------------------------------------- /.github/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lessica/TrollRecorder/25ef7b23f001099944f1d5381d20a36e50937ce2/.github/.gitkeep -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Please complete the following information:** 27 | - Device: [e.g. iPhone 13 Pro Max] 28 | - OS: [e.g. iOS 16.6.1] 29 | - Version [e.g. 1.8.5] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .theos 3 | packages 4 | compile_commands.json 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/swift,macos,xcode,python 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,macos,xcode,python 8 | 9 | ### macOS ### 10 | # General 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | 15 | # Icon must end with two \r 16 | Icon 17 | 18 | 19 | # Thumbnails 20 | ._* 21 | 22 | # Files that might appear in the root of a volume 23 | .DocumentRevisions-V100 24 | .fseventsd 25 | .Spotlight-V100 26 | .TemporaryItems 27 | .Trashes 28 | .VolumeIcon.icns 29 | .com.apple.timemachine.donotpresent 30 | 31 | # Directories potentially created on remote AFP share 32 | .AppleDB 33 | .AppleDesktop 34 | Network Trash Folder 35 | Temporary Items 36 | .apdisk 37 | 38 | ### macOS Patch ### 39 | # iCloud generated files 40 | *.icloud 41 | 42 | ### Swift ### 43 | # Xcode 44 | # 45 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 46 | 47 | ## User settings 48 | xcuserdata/ 49 | 50 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 51 | *.xcscmblueprint 52 | *.xccheckout 53 | 54 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 55 | build/ 56 | DerivedData/ 57 | *.moved-aside 58 | *.pbxuser 59 | !default.pbxuser 60 | *.mode1v3 61 | !default.mode1v3 62 | *.mode2v3 63 | !default.mode2v3 64 | *.perspectivev3 65 | !default.perspectivev3 66 | 67 | ## Obj-C/Swift specific 68 | *.hmap 69 | 70 | ## App packaging 71 | *.ipa 72 | *.dSYM.zip 73 | *.dSYM 74 | 75 | ## Playgrounds 76 | timeline.xctimeline 77 | playground.xcworkspace 78 | 79 | # Swift Package Manager 80 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 81 | # Packages/ 82 | # Package.pins 83 | # Package.resolved 84 | # *.xcodeproj 85 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 86 | # hence it is not needed unless you have added a package configuration file to your project 87 | # .swiftpm 88 | 89 | .build/ 90 | 91 | # CocoaPods 92 | # We recommend against adding the Pods directory to your .gitignore. However 93 | # you should judge for yourself, the pros and cons are mentioned at: 94 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 95 | # Pods/ 96 | # Add this line if you want to avoid checking in source code from the Xcode workspace 97 | # *.xcworkspace 98 | 99 | # Carthage 100 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 101 | # Carthage/Checkouts 102 | 103 | Carthage/Build/ 104 | 105 | # Accio dependency management 106 | Dependencies/ 107 | .accio/ 108 | 109 | # fastlane 110 | # It is recommended to not store the screenshots in the git repo. 111 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 112 | # For more information about the recommended setup visit: 113 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 114 | 115 | fastlane/report.xml 116 | fastlane/Preview.html 117 | fastlane/screenshots/**/*.png 118 | fastlane/test_output 119 | 120 | # Code Injection 121 | # After new code Injection tools there's a generated folder /iOSInjectionProject 122 | # https://github.com/johnno1962/injectionforxcode 123 | 124 | iOSInjectionProject/ 125 | 126 | ### Xcode ### 127 | 128 | ## Xcode 8 and earlier 129 | 130 | ### Xcode Patch ### 131 | *.xcodeproj/* 132 | !*.xcodeproj/project.pbxproj 133 | !*.xcodeproj/xcshareddata/ 134 | !*.xcodeproj/project.xcworkspace/ 135 | !*.xcworkspace/contents.xcworkspacedata 136 | /*.gcno 137 | **/xcshareddata/WorkspaceSettings.xcsettings 138 | 139 | ### Python ### 140 | # Byte-compiled / optimized / DLL files 141 | __pycache__/ 142 | *.py[cod] 143 | *$py.class 144 | 145 | # C extensions 146 | *.so 147 | 148 | # Distribution / packaging 149 | .Python 150 | build/ 151 | develop-eggs/ 152 | dist/ 153 | downloads/ 154 | eggs/ 155 | .eggs/ 156 | lib/ 157 | lib64/ 158 | parts/ 159 | sdist/ 160 | var/ 161 | wheels/ 162 | share/python-wheels/ 163 | *.egg-info/ 164 | .installed.cfg 165 | *.egg 166 | MANIFEST 167 | 168 | # PyInstaller 169 | # Usually these files are written by a python script from a template 170 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 171 | *.manifest 172 | *.spec 173 | 174 | # Installer logs 175 | pip-log.txt 176 | pip-delete-this-directory.txt 177 | 178 | # Unit test / coverage reports 179 | htmlcov/ 180 | .tox/ 181 | .nox/ 182 | .coverage 183 | .coverage.* 184 | .cache 185 | nosetests.xml 186 | coverage.xml 187 | *.cover 188 | *.py,cover 189 | .hypothesis/ 190 | .pytest_cache/ 191 | cover/ 192 | 193 | # Translations 194 | *.mo 195 | *.pot 196 | 197 | # Django stuff: 198 | *.log 199 | local_settings.py 200 | db.sqlite3 201 | db.sqlite3-journal 202 | 203 | # Flask stuff: 204 | instance/ 205 | .webassets-cache 206 | 207 | # Scrapy stuff: 208 | .scrapy 209 | 210 | # Sphinx documentation 211 | docs/_build/ 212 | 213 | # PyBuilder 214 | .pybuilder/ 215 | target/ 216 | 217 | # Jupyter Notebook 218 | .ipynb_checkpoints 219 | 220 | # IPython 221 | profile_default/ 222 | ipython_config.py 223 | 224 | # pyenv 225 | # For a library or package, you might want to ignore these files since the code is 226 | # intended to run in multiple environments; otherwise, check them in: 227 | # .python-version 228 | 229 | # pipenv 230 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 231 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 232 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 233 | # install all needed dependencies. 234 | #Pipfile.lock 235 | 236 | # poetry 237 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 238 | # This is especially recommended for binary packages to ensure reproducibility, and is more 239 | # commonly ignored for libraries. 240 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 241 | #poetry.lock 242 | 243 | # pdm 244 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 245 | #pdm.lock 246 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 247 | # in version control. 248 | # https://pdm.fming.dev/#use-with-ide 249 | .pdm.toml 250 | 251 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 252 | __pypackages__/ 253 | 254 | # Celery stuff 255 | celerybeat-schedule 256 | celerybeat.pid 257 | 258 | # SageMath parsed files 259 | *.sage.py 260 | 261 | # Environments 262 | .env 263 | .venv 264 | env/ 265 | venv/ 266 | ENV/ 267 | env.bak/ 268 | venv.bak/ 269 | 270 | # Spyder project settings 271 | .spyderproject 272 | .spyproject 273 | 274 | # Rope project settings 275 | .ropeproject 276 | 277 | # mkdocs documentation 278 | /site 279 | 280 | # mypy 281 | .mypy_cache/ 282 | .dmypy.json 283 | dmypy.json 284 | 285 | # Pyre type checker 286 | .pyre/ 287 | 288 | # pytype static type analyzer 289 | .pytype/ 290 | 291 | # Cython debug symbols 292 | cython_debug/ 293 | 294 | # PyCharm 295 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 296 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 297 | # and can be added to the global gitignore or merged into this file. For a more nuclear 298 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 299 | #.idea/ 300 | 301 | ### Python Patch ### 302 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 303 | poetry.toml 304 | 305 | # ruff 306 | .ruff_cache/ 307 | 308 | # LSP config files 309 | pyrightconfig.json 310 | 311 | # End of https://www.toptal.com/developers/gitignore/api/swift,macos,xcode,python 312 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.well-known/microsoft-identity-association.json: -------------------------------------------------------------------------------- 1 | { 2 | "associatedApplications": [ 3 | { 4 | "applicationId": "7615346f-200f-4255-98d7-215330d658ce" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | trollrecorder.app -------------------------------------------------------------------------------- /DropboxIntegration.md: -------------------------------------------------------------------------------- 1 | # Dropbox Integration 2 | 3 | See video tutorial below: 4 | 5 | https://github.com/user-attachments/assets/21755372-84aa-4a4b-a3e2-b517f36cb444 6 | 7 | -------------------------------------------------------------------------------- /ExternalAPIs.md: -------------------------------------------------------------------------------- 1 | # External APIs 2 | 3 | ## 全局通知 4 | 5 | - `wiki.qaq.trapp.command.recording.stop`: 停止现有的通话录音会话。 6 | - `wiki.qaq.trapp.command.recording.start`: 开始新的通话录音。 7 | - `wiki.qaq.trapp.command.recording.toggle`: 开始新的通话录音,或停止现有的通话录音会话。 8 | 9 | ## Global Notifications 10 | 11 | - `wiki.qaq.trapp.command.recording.stop`: Stop an existing call recording session. 12 | - `wiki.qaq.trapp.command.recording.start`: Start a new call recording. 13 | - `wiki.qaq.trapp.command.recording.toggle`: Start a new call recoring, or stop an existing one. 14 | 15 | ## URL Scheme 支持 16 | 17 | - `trapp://hud/on`: 打开悬浮窗。 18 | - `trapp://hud/off`: 关闭悬浮窗。 19 | - `trapp://hud/toggle`: 打开/关闭悬浮窗。 20 | - `trapp://call-recording/on`: 开始新的通话录音。 21 | - `trapp://call-recording/off`: 停止现有的通话录音会话。 22 | - `trapp://call-recording/toggle`: 开始新的通话录音,或停止现有的通话录音会话。 23 | - `trapp://voice-memo/on`: 开始新的语音备忘录。 24 | - `trapp://voice-memo/off`: 停止现有的语音备忘录会话。 25 | - `trapp://voice-memo/toggle`: 开始新的语音备忘录,或停止现有的语音备忘录会话。 26 | - `trapp://system-audio/on`: 开始新的系统音频录制。 27 | - `trapp://system-audio/off`: 停止现有的系统音频录制。 28 | - `trapp://system-audio/toggle`: 开始新的系统音频录制,或停止现有的系统音频录制。 29 | 30 | ## URL Scheme Support 31 | 32 | - `trapp://hud/on`: Toggle ON the hoverball. 33 | - `trapp://hud/off`: Toggle OFF the hoverball. 34 | - `trapp://hud/toggle`: Toggle ON/OFF the hoverball. 35 | - `trapp://call-recording/on`: Start a new call recording. 36 | - `trapp://call-recording/off`: Stop an existing call recording session. 37 | - `trapp://call-recording/toggle`: Start a new call recoring, or stop an existing one. 38 | - `trapp://voice-memo/on`: Start a new voice memo. 39 | - `trapp://voice-memo/off`: Stop an existing voice memo session. 40 | - `trapp://voice-memo/toggle`: Start a new voice memo, or stop an existing one. 41 | - `trapp://system-audio/on`: Start a new system audio recording. 42 | - `trapp://system-audio/off`: Stop an existing system audio recording session. 43 | - `trapp://system-audio/toggle`: Start a new system audio recording, or stop an existing one. 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARCHS := arm64 # arm64e 2 | TARGET := iphone:clang:latest:15.0 3 | 4 | INSTALL_TARGET_PROCESSES += audio-player 5 | INSTALL_TARGET_PROCESSES += audio-recorder 6 | INSTALL_TARGET_PROCESSES += audio-mixer 7 | 8 | INSTALL_TARGET_PROCESSES += call-recorder 9 | INSTALL_TARGET_PROCESSES += call-monitor 10 | 11 | INSTALL_TARGET_PROCESSES += dtmf-decoder 12 | 13 | include $(THEOS)/makefiles/common.mk 14 | 15 | TOOL_NAME += audio-player 16 | TOOL_NAME += audio-recorder 17 | TOOL_NAME += audio-mixer 18 | 19 | TOOL_NAME += call-recorder 20 | TOOL_NAME += call-monitor 21 | 22 | TOOL_NAME += dtmf-decoder 23 | 24 | audio-player_USE_MODULES := 0 25 | audio-player_FILES += cli/player.mm 26 | audio-player_CFLAGS += -fobjc-arc 27 | audio-player_CFLAGS += -Iinclude 28 | audio-player_CCFLAGS += -std=gnu++17 29 | audio-player_CODESIGN_FLAGS += -Scli/player.plist 30 | audio-player_FRAMEWORKS += AudioToolbox AVFAudio 31 | audio-player_INSTALL_PATH += /usr/local/bin 32 | 33 | audio-recorder_USE_MODULES := 0 34 | audio-recorder_FILES += cli/recorder.mm 35 | audio-recorder_CFLAGS += -fobjc-arc 36 | audio-recorder_CFLAGS += -Iinclude 37 | audio-recorder_CCFLAGS += -std=gnu++17 38 | audio-recorder_CODESIGN_FLAGS += -Scli/recorder.plist 39 | audio-recorder_FRAMEWORKS += AudioToolbox AVFAudio 40 | audio-recorder_INSTALL_PATH += /usr/local/bin 41 | 42 | audio-mixer_USE_MODULES := 0 43 | audio-mixer_FILES += cli/mixer.mm 44 | audio-mixer_CFLAGS += -fobjc-arc 45 | audio-mixer_CFLAGS += -Iinclude 46 | audio-mixer_CCFLAGS += -std=gnu++17 47 | audio-mixer_CODESIGN_FLAGS += -Scli/mixer.plist 48 | audio-mixer_FRAMEWORKS += AudioToolbox AVFAudio 49 | audio-mixer_INSTALL_PATH += /usr/local/bin 50 | 51 | call-recorder_USE_MODULES := 0 52 | call-recorder_FILES += cli/call-recorder.mm 53 | call-recorder_CFLAGS += -fobjc-arc 54 | call-recorder_CFLAGS += -Iinclude 55 | call-recorder_CCFLAGS += -std=gnu++17 56 | call-recorder_CODESIGN_FLAGS += -Scli/call-recorder.plist 57 | call-recorder_FRAMEWORKS += AudioToolbox AVFAudio 58 | call-recorder_INSTALL_PATH += /usr/local/bin 59 | 60 | call-monitor_USE_MODULES := 0 61 | call-monitor_FILES += cli/call-monitor.mm 62 | call-monitor_CFLAGS += -fobjc-arc 63 | call-monitor_CFLAGS += -Iinclude 64 | call-monitor_CCFLAGS += -std=gnu++17 65 | call-monitor_CODESIGN_FLAGS += -Scli/call-monitor.plist 66 | call-monitor_FRAMEWORKS += Foundation CallKit CoreTelephony 67 | call-monitor_INSTALL_PATH += /usr/local/bin 68 | 69 | dtmf-decoder_USE_MODULES := 0 70 | dtmf-decoder_FILES += cli/dtmf-decoder.mm 71 | dtmf-decoder_CFLAGS += -fobjc-arc 72 | dtmf-decoder_CFLAGS += -Iinclude 73 | dtmf-decoder_CFLAGS += -Wno-unused-variable 74 | dtmf-decoder_CFLAGS += -Wno-unused-but-set-variable 75 | dtmf-decoder_CODESIGN_FLAGS += -Scli/dtmf-decoder.plist 76 | dtmf-decoder_FRAMEWORKS += AudioToolbox AVFAudio 77 | dtmf-decoder_INSTALL_PATH += /usr/local/bin 78 | 79 | include $(THEOS_MAKE_PATH)/tool.mk -------------------------------------------------------------------------------- /PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | “TrollRecorder” is committed to safeguarding your privacy. This privacy policy outlines how we handle your personal information. 4 | 5 | ## Personal Data Handling 6 | 7 | “TrollRecorder” does not collect or upload any personal data or recordings. 8 | 9 | All permissions requested by the app are solely to provide the necessary functionality. Most data processing occurs locally on your device. 10 | 11 | However, data related to technical errors may be uploaded to our servers to assist in improving app functionality. Additionally, cookies and similar technologies may be used to recognize your purchase record. 12 | 13 | ## Technical Error Reporting 14 | 15 | We collect data on technical errors encountered while using our app to help improve its functionality. This data is securely uploaded to our servers and handled according to our data security protocols. For more information on how this data is used, please refer to the security information at [Bugsnag Security](https://www.bugsnag.com/product/security/). 16 | 17 | ## Cookies and Similar Technologies 18 | 19 | We employ cookies or similar technologies to recognize your purchase record, facilitating the activation of paid features. 20 | 21 | ## Third Parties 22 | 23 | Your personal information remains confidential, and we do not share it with third parties. 24 | 25 | ## Cloud Storage Integration 26 | 27 | “TrollRecorder” allows users to upload their recordings to their own Google Drive, Dropbox, and Microsoft OneDrive accounts. This feature is optional and requires user authentication with the respective cloud storage service. The app securely stores your cloud storage credentials on your device; authentication is handled securely through the respective service’s API. Your recordings are uploaded directly from your device to your chosen cloud storage service, and “TrollRecorder” does not have access to your recordings stored in these services. 28 | 29 | ## Links to Other Websites 30 | 31 | While we may include links in our app for your convenience and reference, we are not responsible for the privacy policies of these external sites. We advise you to be aware that these sites’ privacy policies may differ from ours. 32 | 33 | ## Data Security 34 | 35 | We prioritize the security of your personal information. While we utilize commercially acceptable means to protect your personal information, please note that no method of electronic storage or internet transmission is entirely secure. We cannot guarantee absolute security, but we strive to provide the highest level of protection possible. 36 | 37 | ## Changes to This Privacy Policy 38 | 39 | This privacy policy is effective as of 2024/2/29. We reserve the right to update or make changes to our privacy policy at any time. Any changes will take effect immediately after being posted on this page. 40 | 41 | We recommend that you periodically review this privacy policy. If we make any significant changes to this privacy policy, we will notify you either through the email address you have provided to us or by posting a prominent notice on our app. 42 | 43 | ## Contact Us 44 | 45 | If you have any questions or concerns regarding this privacy policy, please contact us at [82flex@gmail.com](mailto:82flex@gmail.com). 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TrollRecorder 2 | 3 | Not the first, but the best phone call recorder with [TrollStore](https://ios.cfw.guide/installing-trollstore/). 4 | 5 | [now-on-havoc]: https://havoc.app/package/trollrecorder 6 | 7 | [Get It On Havoc][now-on-havoc] 8 | 9 | ![Screenshot](./res/screenshot.png) 10 | 11 | - Supports iOS 15.0 to iOS 17.0 12 | - **No iOS 14 support.** 13 | 14 | ## Jailbreak Version 15 | 16 | Popular jailbreaks are also supported. Get [TrollRecorder JB](https://havoc.app/package/trollrecorderjb). 17 | 18 | - [RootHide](https://github.com/roothide/Dopamine2-roothide) 19 | - [Dopamine](https://github.com/opa334/Dopamine) / [palera1n](https://palera.in/) 20 | 21 | ## ⚠️ Not Supported 22 | 23 | - There is no additional support for [Bootstrap (RootHide)](https://github.com/roothide/Bootstrap) nor [NathanLR](https://github.com/verygenericname/nathanlr). 24 | - Use TrollStore version instead. 25 | - [eSign](https://twitter.com/EsignPatch) or [LiveContainer](https://github.com/khanhduytran0/LiveContainer) are not supported due to their own limitations. 26 | 27 | ## Pro Version 28 | 29 | Core features of TrollRecorder are free to use. You can buy a Pro license to unlock advanced features. 30 | Paid licenses are valid for lifetime, and up to 5 devices. 31 | 32 | ## Special Thanks 33 | 34 | - [TrollStore](https://github.com/opa334/TrollStore) and [Dopamine](https://github.com/opa334/Dopamine) by [@opa334dev](https://twitter.com/opa334dev) 35 | - [AudioRecorder XS](https://limneos.net/audiorecorderxs/) by [@limneos](https://twitter.com/limneos) 36 | - [CallAssist](https://buy.htv123.com) by [@xybp888](https://github.com/xybp888) 37 | 38 | ## Translators 39 | 40 | - French by [@DzMoha](https://twitter.com/contact_nadhir) 41 | - Korean by **@SUB** 42 | - Spanish by [@Deci8BelioS](https://github.com/Deci8BelioS) 43 | - Traditional Chinese (Hong Kong) by [@CaslenZ](https://github.com/CaslenZ) 44 | - Traditional Chinese (Taiwan) by [@雲端戰神一刀秒](https://github.com/mp614t) 45 | - Turkish by [**Altay T.**](https://x.com/yamenhuu) 46 | - Uyghur by **BlacЖinG** 47 | - Vietnamese by [@2311WithLuv](https://www.facebook.com/If2019) 48 | - Russian by [**RAYMONCE**](https://t.me/raymonce) 49 | 50 | ## Localization 51 | 52 | At least, you need to provide `Localizable.strings` and `InfoPlist.strings`. Example: [Localizable.strings](https://github.com/Lessica/TrollRecorder/blob/main/res/en.lproj/Localizable.strings). 53 | 54 | **🙇 PLEASE HELP US TRANSLATE!** 55 | 56 | ![Statistics](./res/stats.png) 57 | 58 | ## Privacy Policy 59 | 60 | See [Privacy Policy](./PrivacyPolicy.md). 61 | 62 | ## End-User License Agreement 63 | 64 | See [E.U.L.A.](./EULA.md). 65 | 66 | ## License 67 | 68 | The core of TrollRecorder (command line tool / CLI), and only itself, is open-sourced here. 69 | 70 | The command line tools of TrollRecorder are [Free Software](https://www.gnu.org/philosophy/free-sw.html) licensed under the [GNU General Public License](LICENSE). 71 | 72 | ## 解锁全部功能 73 | 74 | - 🌟 超过 50 个高级功能 75 | - 🌟 绑定 Havoc 账号,在多达 5 台设备上同时使用 76 | - 🌟 无订阅,无额外付费,一次购买终身可用 77 | - 🌟 安全的支付方式 78 | - 🌟 精英群技术支持 79 | 80 | ➡️ [购买巨魔版](https://havoc.app/package/trollrecorder) ➡️ [购买越狱版](https://havoc.app/package/trollrecorderjb) 81 | 82 | ### 免费版 vs 正式版 83 | 84 | | 功能 | 免费版 | 正式版 | 备注 | 85 | |------|------|------|------| 86 | |   语音备忘录 |  ✅ |  ✅ | 录制环境音 | 87 | |   基础通话录音 |  ✅ |  ✅ | 电话与 FaceTime 录音 | 88 | |   CallKit 录音 |  ✅ |  ✅ | 支持 CallKit 的第三方 App 录音 | 89 | |   微信和其他 App 录音 |  ✅ |  ✅ | 非 CallKit 的第三方 App 录音 | 90 | |   系统音频录制 | |  ✅ | 录制设备发出的声音 | 91 | | 👍 微信通话助理 | |  ✅ | 获取并显示微信的联系人备注 | 92 | | 👍 首次解锁后启动 | |  ✅ | 需将小组件添加到锁定屏幕或主屏幕 | 93 | | 👍 稳定持久不漏录 | |  ✅ | 需将小组件添加到锁定屏幕或主屏幕 | 94 | | 👍 优秀的功耗控制 |  ✅ |  ✅ | 基于事件驱动,对续航影响极小 | 95 | |   通知与提醒 |  ✅ |  ✅ | 振动、触感反馈和推送通知 | 96 | |   位置服务 |  ✅ |  ✅ | 记录录音时的地理位置 | 97 | |   悬浮球 |  ✅ |  ✅ | 精美的服务和录音状态指示器 | 98 | |   悬浮球+ | |  ✅ | 自定义悬浮球尺寸、样式和效果 | 99 | |   显示与外观 | |  ✅ | 多点图标,多点新意 | 100 | |   自定义分享 | |  ✅ | 分享录音时携带详细信息,自定义文件名称 | 101 | |   回收站 | |  ✅ | 世上没有后悔药,但我们有 | 102 | | 👍 个人收藏/过期清理 | |  ✅ | 小容量 iPhone 的福音 | 103 | | 👍 智能云归档 | |  ✅ | 充分利用 iCloud 云盘归档过往录音 | 104 | | 👍 通过 iCloud 备份 | |  ✅ | 跟随 iCloud 整机增量备份 | 105 | | 👍 组合模式/保留通道 | |  ✅ | 分别保留和听取扬声器和麦克风通道 | 106 | |   多种文件和音频格式 | |  ✅ | 支持 m4a/caf/wav,支持 aac 编码 | 107 | |   自定义采样率 | |  ✅ | 更好的音频质量 | 108 | |   触控/面容 ID | |  ✅ | | 109 | |   隐秘语音备忘录 | |  ✅ | 让语音备忘录保持隐身录制 | 110 | |   电话/联系人联动 | |  ✅ | 仅越狱版提供,在「最近通话」中查看关联录音 | 111 | |   更多网络存储 | |  ✅ | Google Drive / Microsoft OneDrive / Dropbox | 112 | 113 | ### 巨魔版 vs 越狱版 114 | 115 | | 功能 | 巨魔版 | 越狱版 | 备注 | 116 | |------|------|------|------| 117 | | 稳定持久不漏录 |  ✅ |  ✅ | | 118 | | 首次解锁后启动 |  ✅ | | 需将小组件添加到锁定屏幕或主屏幕 | 119 | | 越狱后自启动 | |  ✅ | | 120 | | 隐秘语音备忘录 |  ✅ |  ✅ | 让语音备忘录保持隐身录制 | 121 | | 电话/联系人联动 | |  ✅ | 在「最近通话」中查看关联录音 | 122 | 123 | ### 智能云归档 vs 通过 iCloud 备份 124 | 125 | | 特性 | 智能云归档 | 通过 iCloud 备份 | 126 | |------|------|------| 127 | | 位置 | iCloud 云盘 | iCloud 备份 | 128 | | 前提条件 | ✅ iCloud 套餐空间足够
✅ 设置 -> Apple ID -> iCloud -> 打开「iCloud 云盘」 | ✅ iCloud 套餐空间足够
✅ 设置 -> Apple ID -> iCloud -> 打开「iCloud 云备份」 | 129 | | 开启方式 | App -> 设置 -> 内容共享 -> 打开「智能云归档」 | App -> 设置 -> 本地存储 -> 打开「通过 iCloud 备份」 | 130 | | 范围 | ⚠️ 除当前月份外的所有月份的录音 | ✅ 所有录音 | 131 | | 在「文件」中查看 | ✅ | ✅ | 132 | | 按月整理 | ✅ | ✅ | 133 | | 只增不删 | ✅ | ⚠️ 跟随本地录音增删改 | 134 | | 自定义归档名称 | ✅ | ⚠️ 原始文件名称 | 135 | | 不占用额外存储空间 | ✅ APFS 克隆 | ✅ 硬链接 | 136 | 137 | ### 网络存储模式 138 | 139 | | 模式 | 可恢复 | 描述 | 140 | |------|------|------| 141 | | 归档 | | 使用 “内容共享” → “自定义导出名称” **重命名** 混合和组合模式的本地录音并上传到云盘。你可以在云盘上按月查看、按备注后的文件名搜索录音。被 “归档” 到云盘的录音无法恢复到巨魔录音机 App 当中。 | 142 | | 上传 | ✅ | 将本地录音(包含分离通道)和元数据以初始形态上传到云盘,云盘上的录音 **只增不删**。你可以通过 “双向同步” 或手动迁移将这些录音恢复到巨魔录音机 App 当中。 | 143 | | 同步 | ✅ | 在 “上传” 的基础之上:如果你删除了本地录音,云盘上对应的录音也会被删除(或移动到云盘的回收站)。 | 144 | | 双向同步 | ✅ | 在 “同步” 的基础之上:如果你删除了云盘上的录音,本地对应的录音也会被删除(不会移动到回收站),云盘上的录音和本地始终保持一致。 | 145 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pages-themes/minimal@v0.2.0 2 | plugins: 3 | - jekyll-remote-theme 4 | title: TrollRecorder 5 | description: Record your phone calls like never before. 6 | show_downloads: true 7 | include: 8 | - ".well-known" -------------------------------------------------------------------------------- /cli/call-monitor.mm: -------------------------------------------------------------------------------- 1 | // 2 | // call-monitor.mm 3 | // TrollRecorder 4 | // 5 | // Created by Lessica on 2024/2/10. 6 | // 7 | 8 | #import "CTCall.h" 9 | #import "CTSetting.h" 10 | #import "CTTelephonyCenter.h" 11 | #import 12 | #import 13 | #import 14 | 15 | static NSString *_CTCallStatusStringRepresentation(CTCallStatus status) { 16 | switch (status) { 17 | case kCTCallStatusUnknown: 18 | return @"Unknown"; 19 | case kCTCallStatusAnswered: 20 | return @"Answered"; 21 | case kCTCallStatusDroppedInterrupted: 22 | return @"Dropped Interrupted"; 23 | case kCTCallStatusOutgoingInitiated: 24 | return @"Outgoing Initiated"; 25 | case kCTCallStatusIncomingCall: 26 | return @"Incoming"; 27 | case kCTCallStatusIncomingCallEnded: 28 | return @"Incoming Ended"; 29 | default: 30 | return @"Unknown"; 31 | } 32 | } 33 | 34 | static NSString *_CTCallTypeStringRepresentation(CTCallType type) { 35 | if (CFStringCompare(type, kCTCallTypeNormal, 0) == kCFCompareEqualTo) 36 | return @"Normal"; 37 | else if (CFStringCompare(type, kCTCallTypeVOIP, 0) == kCFCompareEqualTo) 38 | return @"VOIP"; 39 | else if (CFStringCompare(type, kCTCallTypeVideoConference, 0) == kCFCompareEqualTo) 40 | return @"Video Conference"; 41 | else if (CFStringCompare(type, kCTCallTypeVoicemail, 0) == kCFCompareEqualTo) 42 | return @"Voicemail"; 43 | else 44 | return @"Unknown"; 45 | } 46 | 47 | static void _TelephonyEventCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, 48 | const void *object, CFDictionaryRef userInfo) { 49 | 50 | NSLog(@"CoreTelephony event name = %@", name); 51 | 52 | if (CFStringCompare(name, kCTCallIdentificationChangeNotification, 0) == kCFCompareEqualTo || 53 | CFStringCompare(name, kCTCallStatusChangeNotification, 0) == kCFCompareEqualTo) { 54 | 55 | CTCallRef call = (CTCallRef)object; 56 | 57 | BOOL callIsTheOnlyCall = [[(__bridge NSDictionary *)userInfo objectForKey:@"kCTCallIsTheOnlyCall"] boolValue]; 58 | int callCount = CTGetCurrentCallCount(); 59 | 60 | CTCallStatus callStatus = 61 | (CTCallStatus)[[(__bridge NSDictionary *)userInfo objectForKey:@"kCTCallStatus"] integerValue]; 62 | CTCallType callType = CTCallGetCallType(call); 63 | 64 | CFStringRef callAddress = CTCallCopyAddress(kCFAllocatorDefault, call); 65 | CFStringRef callName = CTCallCopyName(kCFAllocatorDefault, call); 66 | CFStringRef callCountryCode = CTCallCopyCountryCode(kCFAllocatorDefault, call); 67 | CFStringRef callNetworkCode = CTCallCopyNetworkCode(kCFAllocatorDefault, call); 68 | CFStringRef callUniqueStringID = CTCallCopyUniqueStringID(kCFAllocatorDefault, call); 69 | 70 | NSLog(@" Count = %d", callCount); 71 | NSLog(@" IsTheOnlyCall = %@", callIsTheOnlyCall ? @"YES" : @"NO"); 72 | 73 | NSLog(@" Status = %@", _CTCallStatusStringRepresentation(callStatus)); 74 | NSLog(@" Type = %@", _CTCallTypeStringRepresentation(callType)); 75 | 76 | NSLog(@" Name = %@", callName); 77 | NSLog(@" Address = %@", callAddress); 78 | NSLog(@" CountryCode = %@", callCountryCode); 79 | NSLog(@" NetworkCode = %@", callNetworkCode); 80 | NSLog(@" UniqueStringID = %@", callUniqueStringID); 81 | 82 | if (callAddress) 83 | CFRelease(callAddress); 84 | 85 | if (callName) 86 | CFRelease(callName); 87 | 88 | if (callCountryCode) 89 | CFRelease(callCountryCode); 90 | 91 | if (callNetworkCode) 92 | CFRelease(callNetworkCode); 93 | 94 | if (callUniqueStringID) 95 | CFRelease(callUniqueStringID); 96 | 97 | } else { 98 | NSLog(@" object = %@", object); 99 | NSLog(@" userInfo = %@", userInfo); 100 | } 101 | } 102 | 103 | static NSString *_CXCallStatusStringRepresentation(CXCall *call) { 104 | if (!call.outgoing && !call.onHold && !call.hasConnected && !call.hasEnded) { 105 | return @"Incoming"; 106 | } else if (!call.outgoing && !call.onHold && !call.hasConnected && call.hasEnded) { 107 | return @"Incoming Terminated"; 108 | } else if (!call.outgoing && !call.onHold && call.hasConnected && !call.hasEnded) { 109 | return @"Incoming Answered"; 110 | } else if (!call.outgoing && !call.onHold && call.hasConnected && call.hasEnded) { 111 | return @"Incoming Ended"; 112 | } else if (call.outgoing && !call.onHold && !call.hasConnected && !call.hasEnded) { 113 | return @"Outgoing Initiated"; 114 | } else if (call.outgoing && !call.onHold && !call.hasConnected && call.hasEnded) { 115 | return @"Outgoing Terminated"; 116 | } else if (call.outgoing && !call.onHold && call.hasConnected && !call.hasEnded) { 117 | return @"Outgoing Answered"; 118 | } else if (call.outgoing && !call.onHold && call.hasConnected && call.hasEnded) { 119 | return @"Outgoing Ended"; 120 | } 121 | return @"Unknown"; 122 | } 123 | 124 | @interface CXCallController (Private) 125 | - (void)setCallObserver:(CXCallObserver *)arg1; 126 | @end 127 | 128 | @interface CallMonitorCallObserverDelegate : NSObject 129 | @end 130 | 131 | @implementation CallMonitorCallObserverDelegate 132 | 133 | - (void)callObserver:(CXCallObserver *)callObserver callChanged:(CXCall *)call { 134 | NSLog(@"CallKit call changed"); 135 | 136 | NSLog(@" Count = %lu", callObserver.calls.count); 137 | NSLog(@" IsTheOnlyCall = %@", callObserver.calls.count <= 1 ? @"YES" : @"NO"); 138 | 139 | NSLog(@" Status = %@", _CXCallStatusStringRepresentation(call)); 140 | NSLog(@" Type = %@", @"CallKit"); 141 | 142 | NSLog(@" UniqueStringID = %@", call.UUID.UUIDString); 143 | } 144 | 145 | @end 146 | 147 | int main(int argc, const char *argv[]) { 148 | 149 | @autoreleasepool { 150 | 151 | /* Register for CoreTelephony notifications */ 152 | CTTelephonyCenterAddObserver(CTTelephonyCenterGetDefault(), NULL, _TelephonyEventCallback, 153 | kCTCallStatusChangeNotification, NULL, 154 | CFNotificationSuspensionBehaviorDeliverImmediately); 155 | 156 | CTTelephonyCenterAddObserver(CTTelephonyCenterGetDefault(), NULL, _TelephonyEventCallback, 157 | kCTCallIdentificationChangeNotification, NULL, 158 | CFNotificationSuspensionBehaviorDeliverImmediately); 159 | 160 | /* Register for CallKit notifications */ 161 | static CXCallController *mCallController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()]; 162 | static CallMonitorCallObserverDelegate *mCallObserverDelegate = [CallMonitorCallObserverDelegate new]; 163 | [mCallController.callObserver setDelegate:mCallObserverDelegate queue:dispatch_get_main_queue()]; 164 | 165 | CFRunLoopRun(); 166 | } 167 | return 0; 168 | } -------------------------------------------------------------------------------- /cli/call-monitor.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | platform-application 6 | 7 | com.apple.private.security.no-container 8 | 9 | com.apple.private.security.container-manager 10 | 11 | com.apple.CommCenter.fine-grained 12 | 13 | sms 14 | spi 15 | identity 16 | bb-xpc 17 | phone 18 | 19 | com.apple.telephonyutilities.callservicesd 20 | 21 | access-calls 22 | modify-calls 23 | modify-status-bar 24 | access-call-capabilities 25 | modify-call-capabilities 26 | access-call-providers 27 | access-moments 28 | modify-moments 29 | 30 | 31 | -------------------------------------------------------------------------------- /cli/call-recorder.mm: -------------------------------------------------------------------------------- 1 | // 2 | // call-recorder.mm 3 | // TrollRecorder 4 | // 5 | // Created by Lessica on 2024/9/19. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | #import "ATAudioTap.h" 13 | #import "ATAudioTapDescription.h" 14 | #import "AudioQueue+Private.h" 15 | 16 | static const int kNumberOfBuffers = 3; 17 | static const int kMaximumBufferSize = 0x50000; 18 | static const int kMinimumBufferSize = 0x4000; 19 | 20 | static AudioStreamBasicDescription mDataFormat = {0}; 21 | static AudioQueueRef mQueueRef = NULL; 22 | static AudioQueueBufferRef mBuffers[kNumberOfBuffers] = {0}; 23 | static AudioFileID mAudioFile = 0; 24 | static UInt32 mBufferByteSize = 0; 25 | static SInt64 mCurrentPacket = 0; 26 | static bool mIsWaitingForDevice = false; 27 | static bool mIsRecording = false; 28 | static bool mIsPaused = false; 29 | static int mATAudioTapDescriptionPID = 0; 30 | static AudioTimeStamp mRecordedTime = {0}; 31 | static ATAudioTap *mAudioTap = nil; 32 | 33 | __used static void _RecorderCallback(void *ptr, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, 34 | const AudioTimeStamp *timestamp, UInt32 inNumPackets, 35 | const AudioStreamPacketDescription *inPacketDesc) { 36 | 37 | OSStatus status = noErr; 38 | 39 | if (inNumPackets == 0 && mDataFormat.mBytesPerPacket != 0) { 40 | inNumPackets = inBuffer->mAudioDataByteSize / mDataFormat.mBytesPerPacket; 41 | } 42 | 43 | status = AudioFileWritePackets(mAudioFile, false, inBuffer->mAudioDataByteSize, inPacketDesc, mCurrentPacket, 44 | &inNumPackets, inBuffer->mAudioData); 45 | 46 | if (status == noErr) { 47 | mCurrentPacket += inNumPackets; 48 | } else { 49 | // NSLog(@"AudioFileWritePackets (%d)", status); 50 | } 51 | 52 | if (mIsRecording) { 53 | status = AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL); 54 | 55 | // if (status != noErr) { 56 | // NSLog(@"AudioQueueEnqueueBuffer (%d)", status); 57 | // } 58 | } 59 | } 60 | 61 | __used static void _RecorderListenerCallback(void *user_data, AudioQueueRef queue, AudioQueuePropertyID prop) { 62 | 63 | UInt32 res = 0; 64 | UInt32 resSize = 0; 65 | OSStatus status = noErr; 66 | 67 | resSize = sizeof(res); 68 | 69 | status = AudioQueueGetProperty(queue, kAudioQueueProperty_IsRunning, &res, &resSize); 70 | 71 | if (status != noErr) { 72 | NSLog(@"AudioQueueGetProperty (%d)", status); 73 | } 74 | 75 | NSLog(@"_RecorderListenerCallback: %d", res); 76 | if (status == noErr) { 77 | if (res == 0 && !mIsWaitingForDevice) { 78 | mIsRecording = false; 79 | } else if (res != 0 && mIsWaitingForDevice) { 80 | mIsWaitingForDevice = false; 81 | mIsRecording = true; 82 | } 83 | } 84 | } 85 | 86 | __used static void _CalculateDerivedBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription streamDesc, 87 | Float64 seconds, UInt32 *outBufferSize) { 88 | 89 | UInt32 maxPacketSize = 0; 90 | UInt32 maxVBRPacketSize = 0; 91 | Float64 numBytesForTime = 0; 92 | OSStatus status = noErr; 93 | 94 | maxPacketSize = streamDesc.mBytesPerPacket; 95 | 96 | if (maxPacketSize == 0) { 97 | maxVBRPacketSize = sizeof(maxPacketSize); 98 | 99 | status = AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, 100 | &maxVBRPacketSize); 101 | 102 | if (status != noErr) { 103 | // NSLog(@"AudioQueueGetProperty (%d)", status); 104 | } 105 | } 106 | 107 | numBytesForTime = streamDesc.mSampleRate * maxPacketSize * seconds; 108 | 109 | if (numBytesForTime < kMinimumBufferSize) { 110 | *outBufferSize = kMinimumBufferSize; 111 | } else if (numBytesForTime > kMaximumBufferSize) { 112 | *outBufferSize = kMaximumBufferSize; 113 | } else { 114 | *outBufferSize = numBytesForTime; 115 | } 116 | } 117 | 118 | __used static OSStatus _RecorderSetup(CFURLRef fileURL) { 119 | 120 | OSStatus status = noErr; 121 | UInt32 dataFormatSize = 0; 122 | UInt32 *magicCookie = NULL; 123 | UInt32 cookieSize = 0; 124 | 125 | Float64 sampleRate = 0; 126 | UInt32 numberOfChannels = 0; 127 | 128 | sampleRate = 44100.0; 129 | numberOfChannels = 130 | (mATAudioTapDescriptionPID == kATAudioTapDescriptionPIDMicrophone ? 1 : 2); // built-in mono microphone 131 | 132 | mDataFormat.mFormatID = kAudioFormatMPEG4AAC; 133 | mDataFormat.mSampleRate = sampleRate; 134 | mDataFormat.mChannelsPerFrame = numberOfChannels; 135 | 136 | dataFormatSize = sizeof(mDataFormat); 137 | 138 | status = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &dataFormatSize, &mDataFormat); 139 | 140 | if (status != noErr) { 141 | NSLog(@"AudioFormatGetProperty (%d)", status); 142 | return status; 143 | } 144 | 145 | mCurrentPacket = 0; 146 | 147 | AVAudioFormat *audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 148 | sampleRate:(double)sampleRate 149 | channels:(AVAudioChannelCount)numberOfChannels 150 | interleaved:YES]; 151 | 152 | if (!audioFormat) { 153 | status = -1; 154 | NSLog(@"AVAudioFormat (%d)", status); 155 | return status; 156 | } 157 | 158 | mDataFormat = *([audioFormat streamDescription]); 159 | 160 | ATAudioTapDescription *audioTapDescription = nil; 161 | if ([ATAudioTapDescription instancesRespondToSelector:@selector(initTapInternalWithFormat:PIDs:)]) { 162 | audioTapDescription = 163 | [[ATAudioTapDescription alloc] initTapInternalWithFormat:audioFormat 164 | PIDs:@[ @(mATAudioTapDescriptionPID) ]]; 165 | } else { 166 | audioTapDescription = 167 | [[ATAudioTapDescription alloc] initProcessTapInternalWithFormat:audioFormat PID:mATAudioTapDescriptionPID]; 168 | } 169 | 170 | mAudioTap = [[ATAudioTap alloc] initWithTapDescription:audioTapDescription]; 171 | 172 | status = AudioQueueNewInput(&mDataFormat, _RecorderCallback, NULL, NULL, NULL, 0x800, &mQueueRef); 173 | 174 | if (status != noErr) { 175 | NSLog(@"AudioQueueNewInput (%d)", status); 176 | return status; 177 | } 178 | 179 | status = AudioQueueSetProperty(mQueueRef, kAudioQueueProperty_TapOutputBypass, (__bridge void *)mAudioTap, 8); 180 | 181 | if (status != noErr) { 182 | NSLog(@"AudioQueueSetProperty (%d)", status); 183 | return status; 184 | } 185 | 186 | _CalculateDerivedBufferSize(mQueueRef, mDataFormat, 0.5, &mBufferByteSize); 187 | 188 | for (int i = 0; i < kNumberOfBuffers; i++) { 189 | AudioQueueAllocateBuffer(mQueueRef, mBufferByteSize, &mBuffers[i]); 190 | AudioQueueEnqueueBuffer(mQueueRef, mBuffers[i], 0, NULL); 191 | } 192 | 193 | status = AudioFileCreateWithURL(fileURL, kAudioFileCAFType, &mDataFormat, kAudioFileFlags_EraseFile, &mAudioFile); 194 | 195 | if (status != noErr) { 196 | NSLog(@"AudioFileCreateWithURL (%d)", status); 197 | return status; 198 | } 199 | 200 | cookieSize = sizeof(UInt32); 201 | status = AudioQueueGetPropertySize(mQueueRef, kAudioQueueProperty_MagicCookie, &cookieSize); 202 | 203 | if (status == noErr) { 204 | magicCookie = (UInt32 *)malloc(cookieSize); 205 | 206 | status = AudioQueueGetProperty(mQueueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize); 207 | if (status == noErr) { 208 | status = AudioFileSetProperty(mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie); 209 | 210 | if (status != noErr) { 211 | NSLog(@"AudioFileSetProperty (%d)", status); 212 | } 213 | } else { 214 | NSLog(@"AudioQueueGetProperty (%d)", status); 215 | } 216 | 217 | free(magicCookie); 218 | } else { 219 | // NSLog(@"AudioQueueGetPropertySize (%d)", status); 220 | } 221 | 222 | // Ignore the error 223 | status = noErr; 224 | 225 | status = AudioQueueAddPropertyListener(mQueueRef, kAudioQueueProperty_IsRunning, _RecorderListenerCallback, NULL); 226 | 227 | if (status != noErr) { 228 | NSLog(@"AudioQueueAddPropertyListener (%d)", status); 229 | } 230 | 231 | // Ignore the error 232 | status = noErr; 233 | 234 | return status; 235 | } 236 | 237 | __used static OSStatus _RecorderStart(void) { 238 | 239 | if (mIsRecording) { 240 | return noErr; 241 | } 242 | 243 | OSStatus status = noErr; 244 | 245 | status = AudioQueueStart(mQueueRef, NULL); 246 | 247 | if (status != noErr) { 248 | NSLog(@"AudioQueueStart (%d)", status); 249 | return status; 250 | } 251 | 252 | mIsPaused = false; 253 | mIsRecording = true; 254 | mIsWaitingForDevice = true; 255 | 256 | return status; 257 | } 258 | 259 | __used static OSStatus _RecorderPause(void) { 260 | 261 | if (!mIsRecording || mIsPaused) { 262 | return noErr; 263 | } 264 | 265 | OSStatus status = noErr; 266 | 267 | status = AudioQueuePause(mQueueRef); 268 | 269 | if (status != noErr) { 270 | NSLog(@"AudioQueuePause (%d)", status); 271 | } 272 | 273 | mIsPaused = true; 274 | mIsRecording = false; 275 | mIsWaitingForDevice = false; 276 | 277 | return status; 278 | } 279 | 280 | __used static OSStatus _RecorderStop(bool stopImmediately, bool deactivateSession) { 281 | 282 | if (!mIsRecording) { 283 | return noErr; 284 | } 285 | 286 | OSStatus status = noErr; 287 | 288 | UInt32 *magicCookie = NULL; 289 | UInt32 cookieSize = 0; 290 | 291 | status = AudioQueueStop(mQueueRef, stopImmediately); 292 | 293 | if (status != noErr) { 294 | NSLog(@"AudioQueueStop (%d)", status); 295 | } 296 | 297 | cookieSize = sizeof(UInt32); 298 | status = AudioQueueGetPropertySize(mQueueRef, kAudioQueueProperty_MagicCookie, &cookieSize); 299 | 300 | if (status == noErr) { 301 | magicCookie = (UInt32 *)malloc(cookieSize); 302 | 303 | status = AudioQueueGetProperty(mQueueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize); 304 | if (status == noErr) { 305 | status = AudioFileSetProperty(mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie); 306 | if (status != noErr) { 307 | NSLog(@"AudioFileSetProperty (%d)", status); 308 | } 309 | } else { 310 | NSLog(@"AudioQueueGetProperty (%d)", status); 311 | } 312 | 313 | free(magicCookie); 314 | } else { 315 | NSLog(@"AudioQueueGetPropertySize (%d)", status); 316 | } 317 | 318 | // Ignore the error 319 | status = noErr; 320 | 321 | if (deactivateSession) { 322 | } 323 | 324 | if (stopImmediately) { 325 | mIsPaused = false; 326 | mIsRecording = false; 327 | mIsWaitingForDevice = false; 328 | } 329 | 330 | return status; 331 | } 332 | 333 | __used static OSStatus _RecorderDispose(void) { 334 | 335 | OSStatus status = noErr; 336 | 337 | status = AudioFileClose(mAudioFile); 338 | 339 | if (status != noErr) { 340 | NSLog(@"AudioFileClose (%d)", status); 341 | } 342 | 343 | status = AudioQueueDispose(mQueueRef, true); 344 | 345 | if (status != noErr) { 346 | NSLog(@"AudioQueueDispose (%d)", status); 347 | } 348 | 349 | return status; 350 | } 351 | 352 | __used static void _SignalInterrupted(int signal) { 353 | 354 | NSLog(@"Stopped by signal %d", signal); 355 | _RecorderStop(false, false); 356 | 357 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 358 | _RecorderStop(true, true); 359 | }); 360 | } 361 | 362 | __used static void _SignalStopped(int signal) { 363 | 364 | NSLog(@"Paused by signal %d", signal); 365 | _RecorderPause(); 366 | } 367 | 368 | __used static void _SignalResumed(int signal) { 369 | 370 | NSLog(@"Resumed by signal %d", signal); 371 | _RecorderStart(); 372 | } 373 | 374 | int main(int argc, const char *argv[]) { 375 | 376 | @autoreleasepool { 377 | 378 | if (argc < 3) { 379 | printf("Usage: %s \n", argv[0]); 380 | return EXIT_FAILURE; 381 | } 382 | 383 | NSString *channel = [[NSString stringWithUTF8String:argv[1]] lowercaseString]; 384 | if ([channel isEqualToString:@"sys"] || [channel isEqualToString:@"system"]) { 385 | mATAudioTapDescriptionPID = kATAudioTapDescriptionPIDSystemAudio; 386 | } else if ([channel isEqualToString:@"mic"] || [channel isEqualToString:@"microphone"]) { 387 | mATAudioTapDescriptionPID = kATAudioTapDescriptionPIDMicrophone; 388 | } else if ([channel isEqualToString:@"speaker"]) { 389 | mATAudioTapDescriptionPID = kATAudioTapDescriptionPIDSpeaker; 390 | } else { 391 | printf("Invalid channel: %s\n", argv[1]); 392 | return EXIT_FAILURE; 393 | } 394 | 395 | NSString *audioFilePath = [NSString stringWithUTF8String:argv[2]]; 396 | CFURLRef fileURL = 397 | CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)audioFilePath, kCFURLPOSIXPathStyle, false); 398 | 399 | OSStatus status = _RecorderSetup(fileURL); 400 | 401 | if (status != noErr) { 402 | NSLog(@"_RecorderSetup (%d)", status); 403 | return EXIT_FAILURE; 404 | } 405 | 406 | status = _RecorderStart(); 407 | 408 | if (status != noErr) { 409 | NSLog(@"_RecorderStart (%d)", status); 410 | return EXIT_FAILURE; 411 | } 412 | 413 | { 414 | struct sigaction act = {{0}}; 415 | struct sigaction oldact = {{0}}; 416 | act.sa_handler = &_SignalInterrupted; 417 | sigaction(SIGINT, &act, &oldact); 418 | } 419 | 420 | { 421 | struct sigaction act = {{0}}; 422 | struct sigaction oldact = {{0}}; 423 | act.sa_handler = &_SignalStopped; 424 | sigaction(SIGUSR1, &act, &oldact); 425 | } 426 | 427 | { 428 | struct sigaction act = {{0}}; 429 | struct sigaction oldact = {{0}}; 430 | act.sa_handler = &_SignalResumed; 431 | sigaction(SIGUSR2, &act, &oldact); 432 | } 433 | 434 | if (mIsWaitingForDevice) { 435 | printf("Recording > Waiting for device...\n"); 436 | } 437 | 438 | printf("Recording > Press to stop.\n"); 439 | 440 | OSStatus timingStatus = noErr; 441 | NSTimeInterval lastReportedTimeInSeconds = 0.0; 442 | NSTimeInterval currentTimeInSeconds = 0.0; 443 | 444 | while (mIsRecording || mIsPaused || mIsWaitingForDevice) { 445 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1e-2, true); 446 | 447 | timingStatus = AudioQueueGetCurrentTime(mQueueRef, NULL, &mRecordedTime, NULL); 448 | 449 | if (timingStatus == noErr) { 450 | currentTimeInSeconds = mRecordedTime.mSampleTime / mDataFormat.mSampleRate; 451 | 452 | if (currentTimeInSeconds - lastReportedTimeInSeconds > 1.0) { 453 | lastReportedTimeInSeconds = currentTimeInSeconds; 454 | 455 | printf("Recording > %02d:%02d:%02d\n", (int)currentTimeInSeconds / 3600, 456 | (int)currentTimeInSeconds / 60 % 60, (int)currentTimeInSeconds % 60); 457 | } 458 | } 459 | } 460 | 461 | status = _RecorderStop(true, true); 462 | if (status != noErr) { 463 | NSLog(@"_RecorderStop (%d)", status); 464 | return EXIT_FAILURE; 465 | } 466 | 467 | status = _RecorderDispose(); 468 | if (status != noErr) { 469 | NSLog(@"_RecorderDispose (%d)", status); 470 | return EXIT_FAILURE; 471 | } 472 | 473 | CFRelease(fileURL); 474 | } 475 | 476 | return EXIT_SUCCESS; 477 | } 478 | -------------------------------------------------------------------------------- /cli/call-recorder.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | platform-application 6 | 7 | 8 | com.apple.coreaudio.CanRecordWithoutSessionActivation 9 | 10 | com.apple.coreaudio.CanTapTelephony 11 | 12 | com.apple.coreaudio.app-tap 13 | 14 | com.apple.coreaudio.private.SystemWideTap 15 | 16 | 17 | com.apple.private.security.no-container 18 | 19 | com.apple.private.security.container-manager 20 | 21 | com.apple.private.tcc.allow 22 | 23 | kTCCServiceMicrophone 24 | 25 | com.apple.private.tcc.manager.check-by-audit-token 26 | 27 | kTCCServiceMicrophone 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /cli/dtmf-decoder.mm: -------------------------------------------------------------------------------- 1 | /* 2 | $Id: DTMFDecoder.m 125 2010-09-19 00:01:02Z veg $ 3 | 4 | Dreadtech DTMF Decoder - Copyright 2010 Martin Wellard 5 | 6 | This file is part of Dreadtech DTMF Decoder. 7 | 8 | Dreadtech DTMF Decoder is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | Dreadtech DTMF Decoder is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with Dreadtech DTMF Decoder. If not, see . 20 | */ 21 | 22 | /* NOTE: VoLTE must be disabled to validate the DTMF tones. */ 23 | 24 | #import 25 | #import 26 | #import 27 | #import 28 | #import 29 | 30 | #import "ATAudioTap.h" 31 | #import "ATAudioTapDescription.h" 32 | #import "AudioQueue+Private.h" 33 | 34 | #pragma mark - Defines 35 | 36 | #define SAMPLE_RATE 8000.0 37 | #define NUM_BUF 40 38 | #define DETECT_BUFLEN 8192 39 | 40 | #define MIN_TONE_LENGTH 0.045 41 | #define FRAMES_PER_TONE 2 42 | #define BYTES_PER_CHANNEL 2 43 | #define BUFFER_SIZE ((int)(MIN_TONE_LENGTH * SAMPLE_RATE * BYTES_PER_CHANNEL) / FRAMES_PER_TONE) 44 | 45 | #define NUM_FREQ 8 46 | 47 | #define kMinNoiseToleranceFactor 1.5 48 | #define kMaxNoiseToleranceFactor 6.5 49 | 50 | #pragma mark - Structs 51 | 52 | struct FilterCoefficientsEntry { 53 | double unityGainCorrection; 54 | double coeff1; 55 | double coeff2; 56 | }; 57 | 58 | typedef struct { 59 | AudioStreamBasicDescription dataFormat; 60 | AudioQueueRef queue; 61 | AudioQueueBufferRef buffers[NUM_BUF]; 62 | BOOL isRecording; 63 | SInt64 currentPacket; 64 | short filteredBuffer[BUFFER_SIZE]; 65 | char *detectBuffer; 66 | BOOL bufferChanged; 67 | ATAudioTap *audioTap; 68 | } RecordState; 69 | 70 | #pragma mark - Constants 71 | 72 | // Filter coefficients 73 | static const struct FilterCoefficientsEntry filterCoefficients[NUM_FREQ] = { 74 | {0.002729634465943104, 1.703076309365611, 0.994540731068114}, // 697 Hz 75 | {0.003014658069540622, 1.640321076289727, 0.9939706838609188}, // 770 Hz 76 | {0.003334626751652912, 1.563455998285116, 0.9933307464966943}, // 852 Hz 77 | {0.003681676706860666, 1.472762296913335, 0.9926366465862788}, // 941 Hz 78 | {0.00472526211613835, 1.158603326387692, 0.9905494757677233}, // 1209 Hz 79 | {0.005219030413485972, 0.991170124246961, 0.989561939173028}, // 1336 Hz 80 | {0.005766653227008568, 0.7940130339147109, 0.9884666935459827}, // 1477 Hz 81 | {0.006371827557152048, 0.5649101144069607, 0.9872563448856961} // 1633 Hz 82 | }; 83 | 84 | static const char dtmfCodes[4][4] = { 85 | {'1', '2', '3', 'A'}, 86 | {'4', '5', '6', 'B'}, 87 | {'7', '8', '9', 'C'}, 88 | {'*', '0', '#', 'D'}, 89 | }; 90 | 91 | #pragma mark - Variables 92 | 93 | static double powers[NUM_FREQ]; // Location to store the powers for all the frequencies. 94 | static double filterBuf0[NUM_FREQ]; // Buffer for the IIR filter slot 0. 95 | static double filterBuf1[NUM_FREQ]; // Buffer for the IIR filter slot 1. 96 | static char holdingBuffer[2]; 97 | static int holdingBufferCount[2]; 98 | static int powerMeasurementMethod; // 0 = Peak Value -> RMS, 1 = Sqrt of Sum of Squares, 2 = Sum of Abs Values 99 | static BOOL rawOutput; 100 | static double noiseToleranceFactor; 101 | 102 | #pragma mark - Functions 103 | 104 | // BpRe/100/frequency == Bandpass resonator, Q=100 (0=>Inf), frequency 105 | // e.g. ./fiview 8000 -i BpRe/100/1336 106 | // Generated using http://uazu.net/fiview/ 107 | static double bandPassFilter(register double val, int filterIndex) { 108 | register double tmp, fir, iir; 109 | tmp = filterBuf0[filterIndex]; 110 | filterBuf0[filterIndex] = filterBuf1[filterIndex]; 111 | val *= filterCoefficients[filterIndex].unityGainCorrection; 112 | iir = val + filterCoefficients[filterIndex].coeff1 * filterBuf0[filterIndex] - 113 | filterCoefficients[filterIndex].coeff2 * tmp; 114 | fir = iir - tmp; 115 | filterBuf1[filterIndex] = iir; 116 | val = fir; 117 | return val; 118 | } 119 | 120 | static char lookupDTMFCode(void) { 121 | // Find the highest powered frequency index. 122 | int max1Index = 0; 123 | for (int i = 0; i < NUM_FREQ; i++) { 124 | if (powers[i] >= powers[max1Index]) 125 | max1Index = i; 126 | } 127 | 128 | // Find the 2nd highest powered frequency index. 129 | int max2Index; 130 | 131 | if (max1Index == 0) { 132 | max2Index = 1; 133 | } else { 134 | max2Index = 0; 135 | } 136 | 137 | for (int i = 0; i < NUM_FREQ; i++) { 138 | if ((powers[i] >= powers[max2Index]) && (i != max1Index)) 139 | max2Index = i; 140 | } 141 | 142 | // Check that fequency 1 and 2 are substantially bigger than any other frequencies. 143 | BOOL valid = YES; 144 | for (int i = 0; i < NUM_FREQ; i++) { 145 | if ((i == max1Index) || (i == max2Index)) 146 | continue; 147 | 148 | if (powers[i] > (powers[max2Index] / noiseToleranceFactor)) { 149 | valid = NO; 150 | break; 151 | } 152 | } 153 | 154 | if (valid) { 155 | HBLogDebug(@"Highest frequencies found: %d %d", max1Index, max2Index); 156 | 157 | // Figure out which one is a row and which one is a column. 158 | int row = -1; 159 | int col = -1; 160 | if ((max1Index >= 0) && (max1Index <= 3)) { 161 | row = max1Index; 162 | } else { 163 | col = max1Index; 164 | } 165 | 166 | if ((max2Index >= 4) && (max2Index <= 7)) { 167 | col = max2Index; 168 | } else { 169 | row = max2Index; 170 | } 171 | 172 | // Check we have both the row and column and fail if we have 2 rows or 2 columns. 173 | if ((row == -1) || (col == -1)) { 174 | // We have to rows or 2 cols, fail. 175 | HBLogError(@"We have 2 rows or 2 columns, must have gotten it wrong."); 176 | } else { 177 | HBLogDebug(@"DTMF code %c", dtmfCodes[row][col - 4]); 178 | return dtmfCodes[row][col - 4]; 179 | } 180 | } 181 | 182 | return ' '; 183 | } 184 | 185 | #pragma mark - Audio Callback 186 | 187 | static void gAudioInputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, 188 | const AudioTimeStamp *inStartTime, UInt32 inNumberPacketDescriptions, 189 | const AudioStreamPacketDescription *inPacketDescs) { 190 | 191 | RecordState *recordState = (RecordState *)inUserData; 192 | 193 | if (!recordState->isRecording) { 194 | HBLogError(@"Not recording, returning."); 195 | } 196 | 197 | recordState->currentPacket += inNumberPacketDescriptions; 198 | 199 | size_t i, numberOfSamples = inBuffer->mAudioDataByteSize / 2; 200 | short *p = (short *)inBuffer->mAudioData; 201 | short min, max; 202 | 203 | // Normalize - AKA Automatic Gain 204 | min = p[0]; 205 | max = p[0]; 206 | long zerocount = 0; 207 | for (i = 0L; i < numberOfSamples; i++) { 208 | if (p[i] == 0) 209 | zerocount++; 210 | if (p[i] < min) 211 | min = p[i]; 212 | if (p[i] > max) 213 | max = p[i]; 214 | } 215 | min = abs(min); 216 | max = abs(max); 217 | if (max < min) { 218 | max = min; // Pick bigger of max and min. 219 | } 220 | 221 | for (i = 0L; i < numberOfSamples; i++) { 222 | p[i] = (short)(((double)p[i] / (double)max) * (double)32767); 223 | } 224 | 225 | HBLogDebug(@"%d %d %ld %lf", min, max, zerocount, inStartTime->mSampleTime); 226 | 227 | // Reset all previous power calculations. 228 | int t; 229 | double val; 230 | 231 | for (t = 0; t < NUM_FREQ; t++) { 232 | powers[t] = (double)0.0; 233 | } 234 | 235 | // Run the bandpass filter and calculate the power. 236 | for (i = 0L; i < numberOfSamples; i++) { 237 | for (t = 0; t < NUM_FREQ; t++) { 238 | 239 | // Find the highest value. 240 | switch (powerMeasurementMethod) { 241 | case 0: 242 | val = fabs(bandPassFilter((double)p[i], t)); 243 | if (val > powers[t]) 244 | powers[t] = val; 245 | break; 246 | case 1: 247 | val = bandPassFilter((double)p[i], t); 248 | powers[t] += val * val; 249 | break; 250 | default: 251 | powers[t] += fabs(bandPassFilter((double)p[i], t)); 252 | break; 253 | } 254 | } 255 | } 256 | 257 | // Scale 0 - 1, then convert into an power value. 258 | for (t = 0; t < NUM_FREQ; t++) { 259 | switch (powerMeasurementMethod) { 260 | case 0: 261 | powers[t] = (powers[t] / (double)32768.0) * ((double)1.0 / sqrt((double)2.0)); 262 | break; 263 | case 1: 264 | powers[t] = sqrt(powers[t] / (double)numberOfSamples) / (double)32768.0; 265 | break; 266 | default: 267 | powers[t] = (powers[t] / (double)numberOfSamples) / (double)32768.0; 268 | break; 269 | } 270 | } 271 | 272 | HBLogDebug(@"HB %d %d", holdingBuffer[0], holdingBuffer[1]); 273 | HBLogDebug(@"RMS Powers: %0.3lf, %0.3lf, %0.3lf, %0.3lf, %0.3lf, %0.3lf, %0.3lf, %0.3lf", powers[0], powers[1], 274 | powers[2], powers[3], powers[4], powers[5], powers[6], powers[7]); 275 | 276 | // Figure out the dtmf code is nothing recognized. 277 | char chr = lookupDTMFCode(); 278 | 279 | // Add it to the buffer. 280 | BOOL showBuffer = NO; 281 | 282 | if (chr == holdingBuffer[1]) { 283 | holdingBufferCount[1]++; 284 | // To deal with the case where we've received nothing for a while, 285 | // spit out the buffer. 286 | if ((holdingBuffer[1] == ' ') && (holdingBufferCount[1] >= 40)) 287 | showBuffer = YES; 288 | } else { 289 | showBuffer = YES; 290 | } 291 | 292 | if (showBuffer) { 293 | // Combine the buffer entries if they're the same. 294 | if (holdingBuffer[1] == holdingBuffer[0]) { 295 | holdingBufferCount[1] += holdingBufferCount[0]; 296 | holdingBuffer[0] = 0; 297 | holdingBufferCount[0] = 0; 298 | } 299 | 300 | // Archive the current value if we have more than 2 samples. 301 | if ((holdingBufferCount[1] > 1) || (rawOutput)) { 302 | if ((holdingBuffer[0] != 0) && (holdingBuffer[0] != ' ')) { 303 | char tmp[20] = ""; 304 | if (rawOutput) { 305 | snprintf(tmp, 20, "%c(%d) ", holdingBuffer[0], holdingBufferCount[0]); 306 | } else { 307 | snprintf(tmp, 20, "%c", holdingBuffer[0]); 308 | } 309 | if (strlen(recordState->detectBuffer) + strlen(tmp) < DETECT_BUFLEN) { 310 | strcat(recordState->detectBuffer, tmp); 311 | recordState->bufferChanged = YES; 312 | } 313 | } 314 | 315 | holdingBuffer[0] = holdingBuffer[1]; 316 | holdingBufferCount[0] = holdingBufferCount[1]; 317 | } 318 | 319 | holdingBuffer[1] = chr; 320 | holdingBufferCount[1] = 1; 321 | } 322 | 323 | AudioQueueEnqueueBuffer(recordState->queue, inBuffer, 0, NULL); 324 | } 325 | 326 | #pragma mark - DTMFDecoder 327 | 328 | @interface DTMFDecoder : NSObject { 329 | AudioStreamBasicDescription mAudioFormat; 330 | RecordState mRecordState; 331 | float mNoiseLevel; 332 | NSInteger mPowerMethod; 333 | } 334 | 335 | + (instancetype)sharedDecoder; 336 | - (instancetype)init NS_UNAVAILABLE; 337 | - (void)resetBuffer; 338 | - (void)startRecording; 339 | - (void)stopRecording; 340 | - (NSString *)copyBuffer; 341 | 342 | @property(nonatomic, assign, readonly, getter=isRecording) BOOL recording; 343 | @property(nonatomic, assign, getter=isBufferChanged) BOOL bufferChanged; 344 | @property(nonatomic, assign) float noiseLevel; 345 | @property(nonatomic, assign) NSInteger powerMethod; 346 | 347 | @end 348 | 349 | @implementation DTMFDecoder 350 | 351 | + (instancetype)sharedDecoder { 352 | static DTMFDecoder *sharedDecoder = nil; 353 | static dispatch_once_t onceToken; 354 | dispatch_once(&onceToken, ^{ 355 | sharedDecoder = [[self alloc] init]; 356 | }); 357 | return sharedDecoder; 358 | } 359 | 360 | - (instancetype)init { 361 | self = [super init]; 362 | if (self) { 363 | mRecordState.detectBuffer = (char *)calloc(1, DETECT_BUFLEN); 364 | [self setNoiseLevel:0]; 365 | [self setPowerMethod:0]; 366 | [self resetBuffer]; 367 | } 368 | return self; 369 | } 370 | 371 | - (void)startRecording { 372 | if (mRecordState.isRecording) { 373 | return; 374 | } 375 | 376 | for (int i = 0; i < 2; i++) { 377 | holdingBufferCount[i] = 0; 378 | holdingBuffer[i] = ' '; 379 | } 380 | 381 | AudioQueueBufferRef qref[NUM_BUF]; 382 | 383 | mAudioFormat.mSampleRate = SAMPLE_RATE; 384 | mAudioFormat.mFormatID = kAudioFormatLinearPCM; 385 | mAudioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; 386 | mAudioFormat.mFramesPerPacket = 1; 387 | mAudioFormat.mChannelsPerFrame = 1; 388 | mAudioFormat.mBitsPerChannel = 16; 389 | mAudioFormat.mBytesPerPacket = 2; 390 | mAudioFormat.mBytesPerFrame = 2; 391 | mAudioFormat.mReserved = 0; 392 | 393 | AVAudioFormat *avAudioFormat = [[AVAudioFormat alloc] initWithStreamDescription:&mAudioFormat]; 394 | 395 | ATAudioTapDescription *audioTapDescription = nil; 396 | if ([ATAudioTapDescription instancesRespondToSelector:@selector(initTapInternalWithFormat:PIDs:)]) { 397 | audioTapDescription = 398 | [[ATAudioTapDescription alloc] initTapInternalWithFormat:avAudioFormat 399 | PIDs:@[ @(kATAudioTapDescriptionPIDSpeaker) ]]; 400 | } else { 401 | audioTapDescription = 402 | [[ATAudioTapDescription alloc] initProcessTapInternalWithFormat:avAudioFormat 403 | PID:kATAudioTapDescriptionPIDSpeaker]; 404 | } 405 | 406 | mRecordState.audioTap = [[ATAudioTap alloc] initWithTapDescription:audioTapDescription]; 407 | 408 | OSStatus status; 409 | status = AudioQueueNewInput(&mAudioFormat, gAudioInputCallback, 410 | &mRecordState, // User Data 411 | CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 412 | 0x800, // Reserved 413 | &mRecordState.queue); 414 | 415 | if (status != noErr) { 416 | HBLogError(@"AudioQueueNewInput (%d)", status); 417 | return; 418 | } 419 | 420 | status = AudioQueueSetProperty(mRecordState.queue, kAudioQueueProperty_TapOutputBypass, 421 | (__bridge void *)mRecordState.audioTap, 8); 422 | 423 | if (status != noErr) { 424 | HBLogError(@"AudioQueueSetProperty (%d)", status); 425 | return; 426 | } 427 | 428 | // Get the *actual* recording format back from the queue's audio converter. 429 | // We may not have been given what we asked for. 430 | UInt32 fsize = sizeof(mAudioFormat); 431 | 432 | AudioQueueGetProperty(mRecordState.queue, kAudioQueueProperty_StreamDescription, &mAudioFormat, &fsize); 433 | 434 | if (mAudioFormat.mSampleRate != SAMPLE_RATE) { 435 | HBLogError(@"Expected sample rate is %.1f", mAudioFormat.mSampleRate); 436 | return; 437 | } 438 | 439 | for (int i = 0; i < NUM_BUF; ++i) { 440 | // Allocate buffer. Size is in bytes. 441 | AudioQueueAllocateBuffer(mRecordState.queue, BUFFER_SIZE, &qref[i]); 442 | AudioQueueEnqueueBuffer(mRecordState.queue, qref[i], 0, NULL); 443 | } 444 | 445 | AudioQueueStart(mRecordState.queue, NULL); 446 | mRecordState.isRecording = YES; 447 | } 448 | 449 | - (void)resetBuffer { 450 | memset(mRecordState.detectBuffer, '\0', DETECT_BUFLEN); 451 | mRecordState.bufferChanged = YES; 452 | } 453 | 454 | - (void)stopRecording { 455 | if (!mRecordState.isRecording) { 456 | return; 457 | } 458 | mRecordState.isRecording = NO; 459 | AudioQueueStop(mRecordState.queue, true); 460 | for (int i = 0; i < NUM_BUF; i++) { 461 | AudioQueueFreeBuffer(mRecordState.queue, mRecordState.buffers[i]); 462 | } 463 | AudioQueueDispose(mRecordState.queue, true); 464 | } 465 | 466 | - (float)noiseLevel { 467 | return mNoiseLevel; 468 | } 469 | 470 | - (void)setNoiseLevel:(float)noiseLevel { 471 | if (noiseLevel <= 0 || noiseLevel > 1) { 472 | noiseLevel = 0.5; 473 | } 474 | mNoiseLevel = noiseLevel; 475 | noiseToleranceFactor = (double)(((1.0 - noiseLevel) * (kMaxNoiseToleranceFactor - kMinNoiseToleranceFactor)) + 476 | kMinNoiseToleranceFactor); 477 | } 478 | 479 | - (NSInteger)powerMethod { 480 | return mPowerMethod; 481 | } 482 | 483 | - (void)setPowerMethod:(NSInteger)powerMethod { 484 | if (powerMethod > 2 || powerMethod < 0) { 485 | powerMethod = 1; 486 | } 487 | mPowerMethod = powerMethod; 488 | powerMeasurementMethod = (int)powerMethod; 489 | } 490 | 491 | - (BOOL)isBufferChanged { 492 | return mRecordState.bufferChanged; 493 | } 494 | 495 | - (void)setBufferChanged:(BOOL)newData { 496 | mRecordState.bufferChanged = newData; 497 | } 498 | 499 | - (NSString *)copyBuffer { 500 | return @(mRecordState.detectBuffer); 501 | } 502 | 503 | - (BOOL)isRecording { 504 | return mRecordState.isRecording; 505 | } 506 | 507 | @end 508 | 509 | #pragma mark - Main Procedure 510 | 511 | int main(int argc, const char *argv[]) { 512 | @autoreleasepool { 513 | static DTMFDecoder *gDecoder; 514 | gDecoder = [DTMFDecoder sharedDecoder]; 515 | [gDecoder startRecording]; 516 | printf("DTMF > Press to stop.\n"); 517 | while ([gDecoder isRecording]) { 518 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1e-2, true); 519 | if ([gDecoder isBufferChanged]) { 520 | NSLog(@"%@", [gDecoder copyBuffer]); 521 | [gDecoder setBufferChanged:NO]; 522 | } 523 | } 524 | } 525 | return EXIT_SUCCESS; 526 | } 527 | -------------------------------------------------------------------------------- /cli/dtmf-decoder.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | platform-application 6 | 7 | 8 | com.apple.coreaudio.CanRecordWithoutSessionActivation 9 | 10 | com.apple.coreaudio.CanTapTelephony 11 | 12 | com.apple.coreaudio.app-tap 13 | 14 | com.apple.coreaudio.private.SystemWideTap 15 | 16 | 17 | com.apple.private.security.no-container 18 | 19 | com.apple.private.security.container-manager 20 | 21 | com.apple.private.tcc.allow 22 | 23 | kTCCServiceMicrophone 24 | 25 | com.apple.private.tcc.manager.check-by-audit-token 26 | 27 | kTCCServiceMicrophone 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /cli/mixer.mm: -------------------------------------------------------------------------------- 1 | // 2 | // mixer.mm 3 | // TrollRecorder 4 | // 5 | // Created by Lessica on 2024/2/10. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #define BUFFER_SIZE 8192 12 | static const Float64 kMaxSampleRate = 44100.0; 13 | 14 | static bool mIsCombinerMode = false; 15 | static Float64 mPreferredSampleRate = 0; 16 | static AudioFileTypeID mOutputAudioFileTypeID = kAudioFileWAVEType; 17 | 18 | __used static AVAudioFormat *_SetupStreamDescription(AudioStreamBasicDescription *audioFormatPtr, Float64 sampleRate, 19 | UInt32 numChannels) { 20 | bzero(audioFormatPtr, sizeof(AudioStreamBasicDescription)); 21 | AVAudioFormat *audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 22 | sampleRate:(double)sampleRate 23 | channels:(AVAudioChannelCount)numChannels 24 | interleaved:YES]; 25 | *audioFormatPtr = *([audioFormat streamDescription]); 26 | return audioFormat; 27 | } 28 | 29 | int main(int argc, const char *argv[]) { 30 | 31 | @autoreleasepool { 32 | 33 | if (argc < 4) { 34 | printf("Usage: %s \n", argv[0]); 35 | return EXIT_FAILURE; 36 | } 37 | 38 | NSString *binaryPath = [NSString stringWithUTF8String:argv[0]]; 39 | if ([binaryPath hasSuffix:@"/audio-combiner"] || [binaryPath isEqualToString:@"audio-combiner"]) { 40 | mIsCombinerMode = true; 41 | NSLog(@"Running in combiner mode"); 42 | } else { 43 | mIsCombinerMode = false; 44 | NSLog(@"Running in mixer mode"); 45 | } 46 | 47 | NSString *audioPath1 = [NSString stringWithUTF8String:argv[1]]; 48 | NSString *audioPath2 = [NSString stringWithUTF8String:argv[2]]; 49 | NSString *outputPath = [NSString stringWithUTF8String:argv[3]]; 50 | NSString *outputExt = [[outputPath pathExtension] lowercaseString]; 51 | 52 | if ([outputExt isEqualToString:@"m4a"]) { 53 | mOutputAudioFileTypeID = kAudioFileM4AType; 54 | } else if ([outputExt isEqualToString:@"wav"]) { 55 | mOutputAudioFileTypeID = kAudioFileWAVEType; 56 | } else if ([outputExt isEqualToString:@"caf"]) { 57 | mOutputAudioFileTypeID = kAudioFileCAFType; 58 | } else { 59 | NSLog(@"Unsupported output file type: %@", outputExt); 60 | return EXIT_FAILURE; 61 | } 62 | 63 | if (argc > 4) { 64 | mPreferredSampleRate = MIN([[NSString stringWithUTF8String:argv[4]] doubleValue], kMaxSampleRate); 65 | } 66 | 67 | OSStatus err = noErr; 68 | UInt32 propertySize = sizeof(AudioStreamBasicDescription); 69 | AudioStreamBasicDescription inputStreamDesc1 = {0}; 70 | AudioStreamBasicDescription inputStreamDesc2 = {0}; 71 | AudioStreamBasicDescription outStreamDesc = {0}; 72 | AVAudioFormat *inputAudioFormat1 = nil; 73 | AVAudioFormat *inputAudioFormat2 = nil; 74 | AVAudioFormat *outputAudioFormat = nil; 75 | ExtAudioFileRef inputAudioFileRef1 = NULL; 76 | ExtAudioFileRef inputAudioFileRef2 = NULL; 77 | ExtAudioFileRef outputAudioFileRef = NULL; 78 | 79 | CFURLRef inURL1 = NULL; 80 | CFURLRef inURL2 = NULL; 81 | CFURLRef outURL = NULL; 82 | 83 | NSTimeInterval lastReportedTimeInSeconds = 0.0; 84 | NSTimeInterval currentTimeInSeconds = 0.0; 85 | 86 | do { 87 | inURL1 = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)audioPath1, kCFURLPOSIXPathStyle, 88 | false); 89 | inURL2 = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)audioPath2, kCFURLPOSIXPathStyle, 90 | false); 91 | outURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)outputPath, kCFURLPOSIXPathStyle, 92 | false); 93 | 94 | err = ExtAudioFileOpenURL(inURL1, &inputAudioFileRef1); 95 | 96 | if (err != noErr) { 97 | NSLog(@"ExtAudioFileOpenURL (%d)", (int)err); 98 | break; 99 | } 100 | 101 | err = ExtAudioFileOpenURL(inURL2, &inputAudioFileRef2); 102 | 103 | if (err != noErr) { 104 | NSLog(@"ExtAudioFileOpenURL (%d)", (int)err); 105 | break; 106 | } 107 | 108 | bzero(&inputStreamDesc1, sizeof(inputStreamDesc1)); 109 | err = ExtAudioFileGetProperty(inputAudioFileRef1, kExtAudioFileProperty_FileDataFormat, &propertySize, 110 | &inputStreamDesc1); 111 | 112 | if (err != noErr) { 113 | NSLog(@"ExtAudioFileGetProperty (%d)", (int)err); 114 | break; 115 | } 116 | 117 | bzero(&inputStreamDesc2, sizeof(inputStreamDesc2)); 118 | err = ExtAudioFileGetProperty(inputAudioFileRef2, kExtAudioFileProperty_FileDataFormat, &propertySize, 119 | &inputStreamDesc2); 120 | if (err != noErr) { 121 | NSLog(@"ExtAudioFileGetProperty (%d)", (int)err); 122 | break; 123 | } 124 | 125 | UInt32 outputNumberOfChannels = 2; 126 | Float64 outputSampleRate = 127 | mPreferredSampleRate > 0 128 | ? mPreferredSampleRate 129 | : MIN(MAX(inputStreamDesc1.mSampleRate, inputStreamDesc2.mSampleRate), kMaxSampleRate); 130 | 131 | inputAudioFormat1 = _SetupStreamDescription(&inputStreamDesc1, outputSampleRate, outputNumberOfChannels); 132 | err = ExtAudioFileSetProperty(inputAudioFileRef1, kExtAudioFileProperty_ClientDataFormat, 133 | sizeof(inputStreamDesc1), &inputStreamDesc1); 134 | 135 | if (err != noErr) { 136 | NSLog(@"ExtAudioFileSetProperty (%d)", (int)err); 137 | break; 138 | } 139 | 140 | inputAudioFormat2 = _SetupStreamDescription(&inputStreamDesc2, outputSampleRate, outputNumberOfChannels); 141 | err = ExtAudioFileSetProperty(inputAudioFileRef2, kExtAudioFileProperty_ClientDataFormat, 142 | sizeof(inputStreamDesc2), &inputStreamDesc2); 143 | 144 | if (err != noErr) { 145 | NSLog(@"ExtAudioFileSetProperty (%d)", (int)err); 146 | break; 147 | } 148 | 149 | AudioChannelLayout channelLayout = {0}; 150 | channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo; 151 | 152 | outputAudioFormat = _SetupStreamDescription(&outStreamDesc, outputSampleRate, outputNumberOfChannels); 153 | err = ExtAudioFileCreateWithURL(outURL, mOutputAudioFileTypeID, &outStreamDesc, &channelLayout, 154 | kAudioFileFlags_EraseFile, &outputAudioFileRef); 155 | 156 | if (err != noErr) { 157 | NSLog(@"ExtAudioFileCreateWithURL (%d)", (int)err); 158 | break; 159 | } 160 | 161 | err = ExtAudioFileSetProperty(outputAudioFileRef, kExtAudioFileProperty_ClientDataFormat, 162 | sizeof(outStreamDesc), &outStreamDesc); 163 | 164 | if (err != noErr) { 165 | NSLog(@"ExtAudioFileSetProperty (%d)", (int)err); 166 | break; 167 | } 168 | 169 | AVAudioPCMBuffer *convPCMBuffer1 = 170 | [[AVAudioPCMBuffer alloc] initWithPCMFormat:inputAudioFormat1 171 | frameCapacity:BUFFER_SIZE / inputStreamDesc1.mBytesPerFrame]; 172 | convPCMBuffer1.frameLength = convPCMBuffer1.frameCapacity; 173 | AudioBufferList *conversionBufferList1 = convPCMBuffer1.mutableAudioBufferList; 174 | UInt16 *conversionBuffer1 = (UInt16 *)conversionBufferList1->mBuffers[0].mData; 175 | 176 | AVAudioPCMBuffer *convPCMBuffer2 = 177 | [[AVAudioPCMBuffer alloc] initWithPCMFormat:inputAudioFormat2 178 | frameCapacity:BUFFER_SIZE / inputStreamDesc2.mBytesPerFrame]; 179 | convPCMBuffer2.frameLength = convPCMBuffer2.frameCapacity; 180 | AudioBufferList *conversionBufferList2 = convPCMBuffer2.mutableAudioBufferList; 181 | UInt16 *conversionBuffer2 = (UInt16 *)conversionBufferList2->mBuffers[0].mData; 182 | 183 | AVAudioPCMBuffer *outPCMBuffer = 184 | [[AVAudioPCMBuffer alloc] initWithPCMFormat:outputAudioFormat 185 | frameCapacity:BUFFER_SIZE / outStreamDesc.mBytesPerFrame]; 186 | outPCMBuffer.frameLength = outPCMBuffer.frameCapacity; 187 | AudioBufferList *outBufferList = outPCMBuffer.mutableAudioBufferList; 188 | UInt16 *outBuffer = (UInt16 *)outBufferList->mBuffers[0].mData; 189 | 190 | BOOL writeLeftChannel = YES; 191 | 192 | while (1) { 193 | 194 | convPCMBuffer1.frameLength = convPCMBuffer1.frameCapacity; 195 | convPCMBuffer2.frameLength = convPCMBuffer2.frameCapacity; 196 | outPCMBuffer.frameLength = outPCMBuffer.frameCapacity; 197 | 198 | UInt32 convFrameLength1 = convPCMBuffer1.frameLength; 199 | UInt32 convFrameLength2 = convPCMBuffer2.frameLength; 200 | 201 | err = ExtAudioFileRead(inputAudioFileRef1, &convFrameLength1, conversionBufferList1); 202 | 203 | if (err != noErr) { 204 | NSLog(@"ExtAudioFileRead (%d)", (int)err); 205 | break; 206 | } 207 | 208 | err = ExtAudioFileRead(inputAudioFileRef2, &convFrameLength2, conversionBufferList2); 209 | 210 | if (err != noErr) { 211 | NSLog(@"ExtAudioFileRead (%d)", (int)err); 212 | break; 213 | } 214 | 215 | if (convFrameLength1 == 0 && convFrameLength2 == 0) { 216 | break; 217 | } 218 | 219 | UInt32 maximumFrameLength = MAX(convFrameLength1, convFrameLength2); 220 | UInt32 minimumFrameLength = MIN(convFrameLength1, convFrameLength2); 221 | 222 | outPCMBuffer.frameLength = maximumFrameLength; 223 | 224 | UInt32 stereoCount = maximumFrameLength * 2; 225 | if (mIsCombinerMode) { 226 | goto combiner; 227 | } else { 228 | goto mixer; 229 | } 230 | 231 | combiner: 232 | for (UInt32 j = 0; j < stereoCount; j++) { 233 | if (j / 2 < minimumFrameLength) { 234 | *(outBuffer + j) = writeLeftChannel ? *(conversionBuffer1 + j) : *(conversionBuffer2 + j); 235 | } else { 236 | if (maximumFrameLength == convFrameLength1) { 237 | *(outBuffer + j) = writeLeftChannel ? *(conversionBuffer1 + j) : 0; 238 | } else { 239 | *(outBuffer + j) = writeLeftChannel ? 0 : *(conversionBuffer2 + j); 240 | } 241 | } 242 | writeLeftChannel = !writeLeftChannel; 243 | } 244 | goto writer; 245 | 246 | mixer: 247 | /* FIXME: I removed the mixer code for brevity. */ 248 | goto combiner; 249 | 250 | writer: 251 | err = ExtAudioFileWrite(outputAudioFileRef, maximumFrameLength, outBufferList); 252 | 253 | if (err != noErr) { 254 | NSLog(@"ExtAudioFileWrite (%d)", (int)err); 255 | break; 256 | } 257 | 258 | SInt64 outFrameOffset = 0; 259 | err = ExtAudioFileTell(outputAudioFileRef, &outFrameOffset); 260 | 261 | if (err != noErr) { 262 | NSLog(@"ExtAudioFileTell (%d)", (int)err); 263 | break; 264 | } 265 | 266 | currentTimeInSeconds = (NSTimeInterval)outFrameOffset / outStreamDesc.mSampleRate; 267 | if (currentTimeInSeconds - lastReportedTimeInSeconds >= 1.0) { 268 | lastReportedTimeInSeconds = currentTimeInSeconds; 269 | 270 | printf("Converting > %02d:%02d:%02d\n", (int)currentTimeInSeconds / 3600, 271 | (int)currentTimeInSeconds / 60, (int)currentTimeInSeconds % 60); 272 | } 273 | } 274 | } while (NO); 275 | 276 | if (currentTimeInSeconds > 0) { 277 | printf("Converting > %02d:%02d:%02d\n", (int)currentTimeInSeconds / 3600, (int)currentTimeInSeconds / 60, 278 | (int)currentTimeInSeconds % 60); 279 | } 280 | 281 | if (inURL1) { 282 | CFRelease(inURL1); 283 | } 284 | 285 | if (inURL2) { 286 | CFRelease(inURL2); 287 | } 288 | 289 | if (outURL) { 290 | CFRelease(outURL); 291 | } 292 | 293 | if (inputAudioFileRef1) { 294 | ExtAudioFileDispose(inputAudioFileRef1); 295 | } 296 | 297 | if (inputAudioFileRef2) { 298 | ExtAudioFileDispose(inputAudioFileRef2); 299 | } 300 | 301 | if (outputAudioFileRef) { 302 | ExtAudioFileDispose(outputAudioFileRef); 303 | } 304 | 305 | return err == noErr ? EXIT_SUCCESS : EXIT_FAILURE; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /cli/mixer.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | platform-application 6 | 7 | com.apple.private.security.no-container 8 | 9 | com.apple.private.security.container-manager 10 | 11 | 12 | -------------------------------------------------------------------------------- /cli/player.mm: -------------------------------------------------------------------------------- 1 | // 2 | // player.mm 3 | // TrollRecorder 4 | // 5 | // Created by Lessica on 2024/2/10. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | static const int kNumberOfBuffers = 3; 13 | static const int kMaximumBufferSize = 0x50000; 14 | static const int kMinimumBufferSize = 0x4000; 15 | 16 | static AudioStreamBasicDescription mDataFormat = {0}; 17 | static AudioQueueRef mQueueRef = NULL; 18 | static AudioQueueBufferRef mBuffers[kNumberOfBuffers] = {0}; 19 | static AudioFileID mAudioFile = 0; 20 | static UInt32 mBufferByteSize = 0; 21 | static SInt64 mCurrentPacket = 0; 22 | static UInt32 mPacketsToRead = 0; 23 | static AudioStreamPacketDescription *mPacketDescs = NULL; 24 | static bool mIsPlaying = false; 25 | static bool mIsPaused = false; 26 | static Float64 mFileDuration = 0; 27 | static AudioTimeStamp mPlayedTime = {0}; 28 | static Float32 mGain = 1.0f; 29 | 30 | __used static void _PlayerCallback(void *ptr, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) { 31 | 32 | UInt32 numBytesReadFromFile = 0; 33 | UInt32 numPackets = 0; 34 | OSStatus status = noErr; 35 | 36 | numBytesReadFromFile = mBufferByteSize; 37 | numPackets = mPacketsToRead; 38 | 39 | status = AudioFileReadPacketData(mAudioFile, false, &numBytesReadFromFile, mPacketDescs, mCurrentPacket, 40 | &numPackets, inBuffer->mAudioData); 41 | 42 | if (status != noErr) { 43 | NSLog(@"AudioFileReadPacketData (%d)", status); 44 | } 45 | 46 | if (numPackets > 0) { 47 | inBuffer->mAudioDataByteSize = numBytesReadFromFile; 48 | mCurrentPacket += numPackets; 49 | status = AudioQueueEnqueueBuffer(mQueueRef, inBuffer, mPacketDescs ? numPackets : 0, mPacketDescs); 50 | 51 | // if (status != noErr) { 52 | // NSLog(@"AudioQueueEnqueueBuffer (%d)", status); 53 | // } 54 | } else { 55 | status = AudioQueueStop(mQueueRef, false); 56 | 57 | if (status != noErr) { 58 | NSLog(@"AudioQueueStop (%d)", status); 59 | } 60 | } 61 | } 62 | 63 | __used static void _PlayerListenerCallback(void *user_data, AudioQueueRef queue, AudioQueuePropertyID prop) { 64 | 65 | UInt32 res = 0; 66 | UInt32 resSize = 0; 67 | OSStatus status = noErr; 68 | 69 | resSize = sizeof(res); 70 | 71 | status = AudioQueueGetProperty(queue, kAudioQueueProperty_IsRunning, &res, &resSize); 72 | 73 | if (status != noErr) { 74 | NSLog(@"AudioQueueGetProperty (%d)", status); 75 | } 76 | 77 | NSLog(@"_PlayerListenerCallback: %d", res); 78 | 79 | if (status == noErr && res == 0) { 80 | mIsPlaying = false; 81 | } 82 | } 83 | 84 | __used static void _CalculatePlayerBufferSize(AudioStreamBasicDescription basicDesc, UInt32 maxPacketSize, 85 | Float64 seconds, UInt32 *outBufferSize, UInt32 *outNumPacketsToRead) { 86 | 87 | Float64 numPacketsForTime = 0; 88 | 89 | if (basicDesc.mFramesPerPacket != 0) { 90 | numPacketsForTime = basicDesc.mSampleRate / basicDesc.mFramesPerPacket * seconds; 91 | *outBufferSize = numPacketsForTime * maxPacketSize; 92 | } else { 93 | *outBufferSize = kMaximumBufferSize > maxPacketSize ? kMaximumBufferSize : maxPacketSize; 94 | } 95 | 96 | if (*outBufferSize > kMaximumBufferSize && *outBufferSize > maxPacketSize) { 97 | *outBufferSize = kMaximumBufferSize; 98 | } else { 99 | if (*outBufferSize < kMinimumBufferSize) { 100 | *outBufferSize = kMinimumBufferSize; 101 | } 102 | } 103 | 104 | *outNumPacketsToRead = *outBufferSize / maxPacketSize; 105 | } 106 | 107 | __used static OSStatus _PlayerSetup(CFURLRef fileURL) { 108 | 109 | OSStatus status = noErr; 110 | UInt32 dataFormatSize = 0; 111 | UInt32 maxPacketSize = 0; 112 | UInt32 propertySize = 0; 113 | UInt64 nPackets = 0; 114 | UInt32 *magicCookie = NULL; 115 | UInt32 cookieSize = 0; 116 | bool isVBRFormat = false; 117 | 118 | status = AudioFileOpenURL(fileURL, kAudioFileReadPermission, kAudioFileCAFType, &mAudioFile); 119 | 120 | if (status != noErr) { 121 | NSLog(@"AudioFileOpenURL (%d)", status); 122 | return status; 123 | } 124 | 125 | dataFormatSize = sizeof(mDataFormat); 126 | AudioFileGetProperty(mAudioFile, kAudioFilePropertyDataFormat, &dataFormatSize, &mDataFormat); 127 | 128 | status = AudioQueueNewOutput(&mDataFormat, _PlayerCallback, NULL, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, 129 | &mQueueRef); 130 | 131 | if (status != noErr) { 132 | NSLog(@"AudioQueueNewOutput (%d)", status); 133 | AudioFileClose(mAudioFile); 134 | return status; 135 | } 136 | 137 | propertySize = sizeof(maxPacketSize); 138 | status = AudioFileGetProperty(mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize); 139 | 140 | if (status != noErr) { 141 | NSLog(@"AudioFileGetProperty (%d)", status); 142 | AudioFileClose(mAudioFile); 143 | AudioQueueDispose(mQueueRef, true); 144 | return status; 145 | } 146 | 147 | _CalculatePlayerBufferSize(mDataFormat, maxPacketSize, 0.5, &mBufferByteSize, &mPacketsToRead); 148 | 149 | mCurrentPacket = 0; 150 | 151 | isVBRFormat = mDataFormat.mBytesPerPacket == 0 || mDataFormat.mFramesPerPacket == 0; 152 | 153 | if (isVBRFormat) { 154 | mPacketDescs = (AudioStreamPacketDescription *)malloc(mPacketsToRead * sizeof(AudioStreamPacketDescription)); 155 | } else { 156 | mPacketDescs = NULL; 157 | } 158 | 159 | propertySize = sizeof(nPackets); 160 | status = AudioFileGetProperty(mAudioFile, kAudioFilePropertyAudioDataPacketCount, &propertySize, &nPackets); 161 | 162 | if (status != noErr) { 163 | NSLog(@"AudioFileGetProperty (%d)", status); 164 | AudioFileClose(mAudioFile); 165 | AudioQueueDispose(mQueueRef, true); 166 | return status; 167 | } 168 | 169 | mFileDuration = (nPackets * mDataFormat.mFramesPerPacket) / mDataFormat.mSampleRate; 170 | 171 | cookieSize = sizeof(UInt32); 172 | status = AudioFileGetPropertyInfo(mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, NULL); 173 | if (status == noErr && cookieSize) { 174 | magicCookie = (UInt32 *)malloc(cookieSize); 175 | 176 | status = AudioFileGetProperty(mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, magicCookie); 177 | if (status == noErr) { 178 | status = AudioQueueSetProperty(mQueueRef, kAudioQueueProperty_MagicCookie, magicCookie, cookieSize); 179 | if (status != noErr) { 180 | NSLog(@"AudioQueueSetProperty (%d)", status); 181 | } 182 | } else { 183 | NSLog(@"AudioFileGetProperty (%d)", status); 184 | } 185 | 186 | free(magicCookie); 187 | } else { 188 | NSLog(@"AudioFileGetPropertyInfo (%d)", status); 189 | } 190 | 191 | // Ignore the error 192 | status = noErr; 193 | 194 | for (int i = 0; i < kNumberOfBuffers; i++) { 195 | AudioQueueAllocateBuffer(mQueueRef, mBufferByteSize, &mBuffers[i]); 196 | _PlayerCallback(NULL, mQueueRef, mBuffers[i]); 197 | } 198 | 199 | status = AudioQueueSetParameter(mQueueRef, kAudioQueueParam_Volume, mGain); 200 | 201 | if (status != noErr) { 202 | NSLog(@"AudioQueueSetParameter (%d)", status); 203 | } 204 | 205 | status = AudioQueueAddPropertyListener(mQueueRef, kAudioQueueProperty_IsRunning, _PlayerListenerCallback, NULL); 206 | 207 | if (status != noErr) { 208 | NSLog(@"AudioQueueAddPropertyListener (%d)", status); 209 | } 210 | 211 | // Ignore the error 212 | status = noErr; 213 | 214 | return status; 215 | } 216 | 217 | __used static OSStatus _PlayerStart(void) { 218 | 219 | if (mIsPlaying) { 220 | return noErr; 221 | } 222 | 223 | OSStatus status = noErr; 224 | NSError *error = nil; 225 | BOOL succeed = NO; 226 | 227 | succeed = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; 228 | 229 | if (!succeed) { 230 | NSLog(@"- [AVAudioSession setCategory:error:] error = %@", error); 231 | return -1; 232 | } 233 | 234 | succeed = [[AVAudioSession sharedInstance] setActive:YES 235 | withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 236 | error:&error]; 237 | if (!succeed) { 238 | NSLog(@"- [AVAudioSession setActive:withOptions:error:] error = %@", error); 239 | return -1; 240 | } 241 | 242 | status = AudioQueueStart(mQueueRef, mPlayedTime.mHostTime > 0 ? &mPlayedTime : NULL); 243 | 244 | if (status != noErr) { 245 | NSLog(@"AudioQueueStart (%d)", status); 246 | return status; 247 | } 248 | 249 | mIsPaused = false; 250 | mIsPlaying = true; 251 | 252 | return status; 253 | } 254 | 255 | __used static OSStatus _PlayerPause(void) { 256 | 257 | if (!mIsPlaying || mIsPaused) { 258 | return noErr; 259 | } 260 | 261 | OSStatus status = noErr; 262 | NSError *error = nil; 263 | BOOL succeed = NO; 264 | 265 | status = AudioQueueGetCurrentTime(mQueueRef, NULL, &mPlayedTime, NULL); 266 | 267 | if (status != noErr) { 268 | NSLog(@"AudioQueueGetCurrentTime (%d)", status); 269 | } 270 | 271 | status = AudioQueuePause(mQueueRef); 272 | 273 | if (status != noErr) { 274 | NSLog(@"AudioQueuePause (%d)", status); 275 | } 276 | 277 | mIsPaused = true; 278 | mIsPlaying = false; 279 | 280 | succeed = [[AVAudioSession sharedInstance] setActive:NO 281 | withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 282 | error:&error]; 283 | 284 | if (!succeed) { 285 | NSLog(@"- [AVAudioSession setActive:withOptions:error:] error = %@", error); 286 | } 287 | 288 | return status; 289 | } 290 | 291 | __used static OSStatus _PlayerStop(bool stopImmediately) { 292 | 293 | if (!mIsPlaying) { 294 | return noErr; 295 | } 296 | 297 | OSStatus status = noErr; 298 | NSError *error = nil; 299 | BOOL succeed = NO; 300 | 301 | status = AudioQueueStop(mQueueRef, stopImmediately); 302 | 303 | if (status != noErr) { 304 | NSLog(@"AudioQueueStop (%d)", status); 305 | } 306 | 307 | succeed = [[AVAudioSession sharedInstance] setActive:NO 308 | withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 309 | error:&error]; 310 | 311 | if (!succeed) { 312 | NSLog(@"- [AVAudioSession setActive:withOptions:error:] error = %@", error); 313 | } 314 | 315 | if (stopImmediately) { 316 | mIsPaused = false; 317 | mIsPlaying = false; 318 | } 319 | 320 | return status; 321 | } 322 | 323 | __used static OSStatus _PlayerDispose(void) { 324 | 325 | OSStatus status = noErr; 326 | 327 | status = AudioFileClose(mAudioFile); 328 | 329 | if (status != noErr) { 330 | NSLog(@"AudioFileClose (%d)", status); 331 | } 332 | 333 | status = AudioQueueDispose(mQueueRef, true); 334 | 335 | if (status != noErr) { 336 | NSLog(@"AudioQueueDispose (%d)", status); 337 | } 338 | 339 | if (mPacketDescs) { 340 | free(mPacketDescs); 341 | } 342 | 343 | return status; 344 | } 345 | 346 | __used static void _SignalInterrupted(int signal) { 347 | 348 | NSLog(@"Stopped by signal %d", signal); 349 | _PlayerStop(true); 350 | } 351 | 352 | __used static void _SignalStopped(int signal) { 353 | 354 | NSLog(@"Paused by signal %d", signal); 355 | _PlayerPause(); 356 | } 357 | 358 | __used static void _SignalResumed(int signal) { 359 | 360 | NSLog(@"Resumed by signal %d", signal); 361 | _PlayerStart(); 362 | } 363 | 364 | int main(int argc, const char *argv[]) { 365 | 366 | @autoreleasepool { 367 | 368 | if (argc < 2) { 369 | printf("Usage: %s \n", argv[0]); 370 | return EXIT_FAILURE; 371 | } 372 | 373 | NSString *audioFilePath = [NSString stringWithUTF8String:argv[1]]; 374 | CFURLRef fileURL = 375 | CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)audioFilePath, kCFURLPOSIXPathStyle, false); 376 | 377 | if (argc > 2) { 378 | mGain = MIN(MAX(atof(argv[2]), 0.0f), 1.0f); 379 | } else { 380 | mGain = 1.0f; 381 | } 382 | 383 | OSStatus status = _PlayerSetup(fileURL); 384 | 385 | if (status != noErr) { 386 | NSLog(@"_PlayerSetup (%d)", status); 387 | return EXIT_FAILURE; 388 | } 389 | 390 | status = _PlayerStart(); 391 | 392 | if (status != noErr) { 393 | NSLog(@"_PlayerStart (%d)", status); 394 | return EXIT_FAILURE; 395 | } 396 | 397 | { 398 | struct sigaction act = {{0}}; 399 | struct sigaction oldact = {{0}}; 400 | act.sa_handler = &_SignalInterrupted; 401 | sigaction(SIGINT, &act, &oldact); 402 | } 403 | 404 | { 405 | struct sigaction act = {{0}}; 406 | struct sigaction oldact = {{0}}; 407 | act.sa_handler = &_SignalStopped; 408 | sigaction(SIGUSR1, &act, &oldact); 409 | } 410 | 411 | { 412 | struct sigaction act = {{0}}; 413 | struct sigaction oldact = {{0}}; 414 | act.sa_handler = &_SignalResumed; 415 | sigaction(SIGUSR2, &act, &oldact); 416 | } 417 | 418 | printf("Playing > Press to stop.\n"); 419 | 420 | OSStatus timingStatus = noErr; 421 | NSTimeInterval lastReportedTimeInSeconds = 0.0; 422 | NSTimeInterval currentTimeInSeconds = 0.0; 423 | 424 | while (mIsPlaying || mIsPaused) { 425 | 426 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1e-2, true); 427 | 428 | timingStatus = AudioQueueGetCurrentTime(mQueueRef, NULL, &mPlayedTime, NULL); 429 | 430 | if (timingStatus == noErr) { 431 | currentTimeInSeconds = mPlayedTime.mSampleTime / mDataFormat.mSampleRate; 432 | 433 | if (currentTimeInSeconds - lastReportedTimeInSeconds > 1.0) { 434 | lastReportedTimeInSeconds = currentTimeInSeconds; 435 | 436 | printf("Playing > %02d:%02d:%02d / %02d:%02d:%02d\n", (int)currentTimeInSeconds / 3600, 437 | (int)currentTimeInSeconds / 60 % 60, (int)currentTimeInSeconds % 60, 438 | (int)mFileDuration / 3600, (int)mFileDuration / 60 % 60, (int)mFileDuration % 60); 439 | } 440 | } 441 | } 442 | 443 | status = _PlayerStop(true); 444 | if (status != noErr) { 445 | NSLog(@"_PlayerStop (%d)", status); 446 | return EXIT_FAILURE; 447 | } 448 | 449 | status = _PlayerDispose(); 450 | if (status != noErr) { 451 | NSLog(@"_PlayerDispose (%d)", status); 452 | return EXIT_FAILURE; 453 | } 454 | 455 | CFRelease(fileURL); 456 | } 457 | 458 | return EXIT_SUCCESS; 459 | } 460 | -------------------------------------------------------------------------------- /cli/player.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | platform-application 6 | 7 | com.apple.private.security.no-container 8 | 9 | com.apple.private.security.container-manager 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /cli/recorder.mm: -------------------------------------------------------------------------------- 1 | // 2 | // recorder.mm 3 | // TrollRecorder 4 | // 5 | // Created by Lessica on 2024/2/10. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | #import "ATAudioTap.h" 13 | #import "ATAudioTapDescription.h" 14 | #import "AudioQueue+Private.h" 15 | 16 | static const int kNumberOfBuffers = 3; 17 | static const int kMaximumBufferSize = 0x50000; 18 | static const int kMinimumBufferSize = 0x4000; 19 | 20 | static AudioStreamBasicDescription mDataFormat = {0}; 21 | static AudioQueueRef mQueueRef = NULL; 22 | static AudioQueueBufferRef mBuffers[kNumberOfBuffers] = {0}; 23 | static AudioFileID mAudioFile = 0; 24 | static UInt32 mBufferByteSize = 0; 25 | static SInt64 mCurrentPacket = 0; 26 | static bool mIsRecording = false; 27 | static bool mIsPaused = false; 28 | static int mATAudioTapDescriptionPID = 0; 29 | static AudioTimeStamp mRecordedTime = {0}; 30 | 31 | __used static void _RecorderCallback(void *ptr, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, 32 | const AudioTimeStamp *timestamp, UInt32 inNumPackets, 33 | const AudioStreamPacketDescription *inPacketDesc) { 34 | 35 | OSStatus status = noErr; 36 | 37 | if (inNumPackets == 0 && mDataFormat.mBytesPerPacket != 0) { 38 | inNumPackets = inBuffer->mAudioDataByteSize / mDataFormat.mBytesPerPacket; 39 | } 40 | 41 | status = AudioFileWritePackets(mAudioFile, false, inBuffer->mAudioDataByteSize, inPacketDesc, mCurrentPacket, 42 | &inNumPackets, inBuffer->mAudioData); 43 | 44 | if (status == noErr) { 45 | mCurrentPacket += inNumPackets; 46 | } else { 47 | // NSLog(@"AudioFileWritePackets (%d)", status); 48 | } 49 | 50 | if (mIsRecording) { 51 | status = AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL); 52 | 53 | // if (status != noErr) { 54 | // NSLog(@"AudioQueueEnqueueBuffer (%d)", status); 55 | // } 56 | } 57 | } 58 | 59 | __used static void _RecorderListenerCallback(void *user_data, AudioQueueRef queue, AudioQueuePropertyID prop) { 60 | 61 | UInt32 res = 0; 62 | UInt32 resSize = 0; 63 | OSStatus status = noErr; 64 | 65 | resSize = sizeof(res); 66 | 67 | status = AudioQueueGetProperty(queue, kAudioQueueProperty_IsRunning, &res, &resSize); 68 | 69 | if (status != noErr) { 70 | NSLog(@"AudioQueueGetProperty (%d)", status); 71 | } 72 | 73 | NSLog(@"_RecorderListenerCallback: %d", res); 74 | if (status == noErr && res == 0) { 75 | mIsRecording = false; 76 | } 77 | } 78 | 79 | __used static void _CalculateDerivedBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription streamDesc, 80 | Float64 seconds, UInt32 *outBufferSize) { 81 | 82 | UInt32 maxPacketSize = 0; 83 | UInt32 maxVBRPacketSize = 0; 84 | Float64 numBytesForTime = 0; 85 | OSStatus status = noErr; 86 | 87 | maxPacketSize = streamDesc.mBytesPerPacket; 88 | 89 | if (maxPacketSize == 0) { 90 | maxVBRPacketSize = sizeof(maxPacketSize); 91 | 92 | status = AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, 93 | &maxVBRPacketSize); 94 | 95 | if (status != noErr) { 96 | // NSLog(@"AudioQueueGetProperty (%d)", status); 97 | } 98 | } 99 | 100 | numBytesForTime = streamDesc.mSampleRate * maxPacketSize * seconds; 101 | 102 | if (numBytesForTime < kMinimumBufferSize) { 103 | *outBufferSize = kMinimumBufferSize; 104 | } else if (numBytesForTime > kMaximumBufferSize) { 105 | *outBufferSize = kMaximumBufferSize; 106 | } else { 107 | *outBufferSize = numBytesForTime; 108 | } 109 | } 110 | 111 | __used static OSStatus _RecorderSetup(CFURLRef fileURL) { 112 | 113 | OSStatus status = noErr; 114 | UInt32 *magicCookie = NULL; 115 | UInt32 cookieSize = 0; 116 | UInt32 numberOfChannels = 0; 117 | AVAudioFormat *audioFormat = nil; 118 | 119 | numberOfChannels = 120 | (mATAudioTapDescriptionPID == kATAudioTapDescriptionPIDMicrophone ? 1 : 2); // built-in mono microphone 121 | audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 122 | sampleRate:44100.0 123 | channels:(AVAudioChannelCount)numberOfChannels 124 | interleaved:YES]; 125 | if (!audioFormat) { 126 | NSLog(@"AVAudioFormat"); 127 | return -1; 128 | } 129 | 130 | mCurrentPacket = 0; 131 | mDataFormat = *([audioFormat streamDescription]); 132 | 133 | status = AudioQueueNewInput(&mDataFormat, _RecorderCallback, NULL, NULL, NULL, 0, &mQueueRef); 134 | 135 | if (status != noErr) { 136 | NSLog(@"AudioQueueNewInput (%d)", status); 137 | return status; 138 | } 139 | 140 | _CalculateDerivedBufferSize(mQueueRef, mDataFormat, 0.5, &mBufferByteSize); 141 | 142 | for (int i = 0; i < kNumberOfBuffers; i++) { 143 | AudioQueueAllocateBuffer(mQueueRef, mBufferByteSize, &mBuffers[i]); 144 | AudioQueueEnqueueBuffer(mQueueRef, mBuffers[i], 0, NULL); 145 | } 146 | 147 | status = AudioFileCreateWithURL(fileURL, kAudioFileCAFType, &mDataFormat, kAudioFileFlags_EraseFile, &mAudioFile); 148 | 149 | if (status != noErr) { 150 | NSLog(@"AudioFileCreateWithURL (%d)", status); 151 | return status; 152 | } 153 | 154 | cookieSize = sizeof(UInt32); 155 | status = AudioQueueGetPropertySize(mQueueRef, kAudioQueueProperty_MagicCookie, &cookieSize); 156 | 157 | if (status == noErr) { 158 | magicCookie = (UInt32 *)malloc(cookieSize); 159 | 160 | status = AudioQueueGetProperty(mQueueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize); 161 | if (status == noErr) { 162 | status = AudioFileSetProperty(mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie); 163 | 164 | if (status != noErr) { 165 | NSLog(@"AudioFileSetProperty (%d)", status); 166 | } 167 | } else { 168 | NSLog(@"AudioQueueGetProperty (%d)", status); 169 | } 170 | 171 | free(magicCookie); 172 | } else { 173 | // NSLog(@"AudioQueueGetPropertySize (%d)", status); 174 | } 175 | 176 | // Ignore the error 177 | status = noErr; 178 | 179 | status = AudioQueueAddPropertyListener(mQueueRef, kAudioQueueProperty_IsRunning, _RecorderListenerCallback, NULL); 180 | 181 | if (status != noErr) { 182 | NSLog(@"AudioQueueAddPropertyListener (%d)", status); 183 | } 184 | 185 | // Ignore the error 186 | status = noErr; 187 | 188 | return status; 189 | } 190 | 191 | __used static OSStatus _RecorderStart(void) { 192 | 193 | if (mIsRecording) { 194 | return noErr; 195 | } 196 | 197 | NSError *error = nil; 198 | BOOL succeed = NO; 199 | OSStatus status = noErr; 200 | 201 | /* FIXME: We need some additional setup to avoid AVAudioSession activation here. */ 202 | /* I removed some codes shamefully stolen from AudioRecorder XS by @limneos... */ 203 | /* See _headers_ for further details. */ 204 | 205 | succeed = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord 206 | mode:AVAudioSessionModeDefault 207 | options:AVAudioSessionCategoryOptionMixWithOthers 208 | error:&error]; 209 | 210 | if (!succeed) { 211 | NSLog(@"- [AVAudioSession setCategory:error:] error = %@", error); 212 | return -1; 213 | } 214 | 215 | succeed = [[AVAudioSession sharedInstance] setActive:YES error:&error]; 216 | 217 | if (!succeed) { 218 | NSLog(@"- [AVAudioSession setActive:error:] error = %@", error); 219 | return -1; 220 | } 221 | 222 | status = AudioQueueStart(mQueueRef, NULL); 223 | 224 | if (status != noErr) { 225 | NSLog(@"AudioQueueStart (%d)", status); 226 | return status; 227 | } 228 | 229 | mIsPaused = false; 230 | mIsRecording = true; 231 | 232 | return status; 233 | } 234 | 235 | __used static OSStatus _RecorderPause(void) { 236 | 237 | if (!mIsRecording || mIsPaused) { 238 | return noErr; 239 | } 240 | 241 | OSStatus status = noErr; 242 | NSError *error = nil; 243 | BOOL succeed = NO; 244 | 245 | status = AudioQueuePause(mQueueRef); 246 | 247 | if (status != noErr) { 248 | NSLog(@"AudioQueuePause (%d)", status); 249 | } 250 | 251 | mIsPaused = true; 252 | mIsRecording = false; 253 | 254 | succeed = [[AVAudioSession sharedInstance] setActive:NO 255 | withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 256 | error:&error]; 257 | 258 | if (!succeed) { 259 | NSLog(@"- [AVAudioSession setActive:withOptions:error:] error = %@", error); 260 | } 261 | 262 | return status; 263 | } 264 | 265 | __used static OSStatus _RecorderStop(bool stopImmediately, bool deactivateSession) { 266 | 267 | if (!mIsRecording) { 268 | return noErr; 269 | } 270 | 271 | OSStatus status = noErr; 272 | NSError *error = nil; 273 | BOOL succeed = NO; 274 | 275 | UInt32 *magicCookie = NULL; 276 | UInt32 cookieSize = 0; 277 | 278 | status = AudioQueueStop(mQueueRef, stopImmediately); 279 | 280 | if (status != noErr) { 281 | NSLog(@"AudioQueueStop (%d)", status); 282 | } 283 | 284 | cookieSize = sizeof(UInt32); 285 | status = AudioQueueGetPropertySize(mQueueRef, kAudioQueueProperty_MagicCookie, &cookieSize); 286 | 287 | if (status == noErr) { 288 | magicCookie = (UInt32 *)malloc(cookieSize); 289 | 290 | status = AudioQueueGetProperty(mQueueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize); 291 | if (status == noErr) { 292 | status = AudioFileSetProperty(mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie); 293 | if (status != noErr) { 294 | NSLog(@"AudioFileSetProperty (%d)", status); 295 | } 296 | } else { 297 | NSLog(@"AudioQueueGetProperty (%d)", status); 298 | } 299 | 300 | free(magicCookie); 301 | } else { 302 | NSLog(@"AudioQueueGetPropertySize (%d)", status); 303 | } 304 | 305 | // Ignore the error 306 | status = noErr; 307 | 308 | if (deactivateSession) { 309 | succeed = [[AVAudioSession sharedInstance] setActive:NO 310 | withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 311 | error:&error]; 312 | 313 | if (!succeed) { 314 | NSLog(@"- [AVAudioSession setActive:withOptions:error:] error = %@", error); 315 | } 316 | } 317 | 318 | if (stopImmediately) { 319 | mIsPaused = false; 320 | mIsRecording = false; 321 | } 322 | 323 | return status; 324 | } 325 | 326 | __used static OSStatus _RecorderDispose(void) { 327 | 328 | OSStatus status = noErr; 329 | 330 | status = AudioFileClose(mAudioFile); 331 | 332 | if (status != noErr) { 333 | NSLog(@"AudioFileClose (%d)", status); 334 | } 335 | 336 | status = AudioQueueDispose(mQueueRef, true); 337 | 338 | if (status != noErr) { 339 | NSLog(@"AudioQueueDispose (%d)", status); 340 | } 341 | 342 | return status; 343 | } 344 | 345 | __used static void _SignalInterrupted(int signal) { 346 | 347 | NSLog(@"Stopped by signal %d", signal); 348 | _RecorderStop(false, false); 349 | 350 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 351 | _RecorderStop(true, true); 352 | }); 353 | } 354 | 355 | __used static void _SignalStopped(int signal) { 356 | 357 | NSLog(@"Paused by signal %d", signal); 358 | _RecorderPause(); 359 | } 360 | 361 | __used static void _SignalResumed(int signal) { 362 | 363 | NSLog(@"Resumed by signal %d", signal); 364 | _RecorderStart(); 365 | } 366 | 367 | int main(int argc, const char *argv[]) { 368 | 369 | @autoreleasepool { 370 | 371 | if (argc < 3) { 372 | printf("Usage: %s \n", argv[0]); 373 | return EXIT_FAILURE; 374 | } 375 | 376 | NSString *channel = [[NSString stringWithUTF8String:argv[1]] lowercaseString]; 377 | if ([channel isEqualToString:@"mic"] || [channel isEqualToString:@"microphone"]) { 378 | mATAudioTapDescriptionPID = kATAudioTapDescriptionPIDMicrophone; 379 | } else if ([channel isEqualToString:@"speaker"]) { 380 | mATAudioTapDescriptionPID = kATAudioTapDescriptionPIDSpeaker; 381 | } else { 382 | printf("Invalid channel: %s\n", argv[1]); 383 | return EXIT_FAILURE; 384 | } 385 | 386 | NSString *audioFilePath = [NSString stringWithUTF8String:argv[2]]; 387 | CFURLRef fileURL = 388 | CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)audioFilePath, kCFURLPOSIXPathStyle, false); 389 | 390 | OSStatus status = _RecorderSetup(fileURL); 391 | 392 | if (status != noErr) { 393 | NSLog(@"_RecorderSetup (%d)", status); 394 | return EXIT_FAILURE; 395 | } 396 | 397 | status = _RecorderStart(); 398 | 399 | if (status != noErr) { 400 | NSLog(@"_RecorderStart (%d)", status); 401 | return EXIT_FAILURE; 402 | } 403 | 404 | { 405 | struct sigaction act = {{0}}; 406 | struct sigaction oldact = {{0}}; 407 | act.sa_handler = &_SignalInterrupted; 408 | sigaction(SIGINT, &act, &oldact); 409 | } 410 | 411 | { 412 | struct sigaction act = {{0}}; 413 | struct sigaction oldact = {{0}}; 414 | act.sa_handler = &_SignalStopped; 415 | sigaction(SIGUSR1, &act, &oldact); 416 | } 417 | 418 | { 419 | struct sigaction act = {{0}}; 420 | struct sigaction oldact = {{0}}; 421 | act.sa_handler = &_SignalResumed; 422 | sigaction(SIGUSR2, &act, &oldact); 423 | } 424 | 425 | printf("Recording > Press to stop.\n"); 426 | 427 | OSStatus timingStatus = noErr; 428 | NSTimeInterval lastReportedTimeInSeconds = 0.0; 429 | NSTimeInterval currentTimeInSeconds = 0.0; 430 | 431 | while (mIsRecording || mIsPaused) { 432 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1e-2, true); 433 | 434 | timingStatus = AudioQueueGetCurrentTime(mQueueRef, NULL, &mRecordedTime, NULL); 435 | 436 | if (timingStatus == noErr) { 437 | currentTimeInSeconds = mRecordedTime.mSampleTime / mDataFormat.mSampleRate; 438 | 439 | if (currentTimeInSeconds - lastReportedTimeInSeconds > 1.0) { 440 | lastReportedTimeInSeconds = currentTimeInSeconds; 441 | 442 | printf("Recording > %02d:%02d:%02d\n", (int)currentTimeInSeconds / 3600, 443 | (int)currentTimeInSeconds / 60 % 60, (int)currentTimeInSeconds % 60); 444 | } 445 | } 446 | } 447 | 448 | status = _RecorderStop(true, true); 449 | if (status != noErr) { 450 | NSLog(@"_RecorderStop (%d)", status); 451 | return EXIT_FAILURE; 452 | } 453 | 454 | status = _RecorderDispose(); 455 | if (status != noErr) { 456 | NSLog(@"_RecorderDispose (%d)", status); 457 | return EXIT_FAILURE; 458 | } 459 | 460 | CFRelease(fileURL); 461 | } 462 | 463 | return EXIT_SUCCESS; 464 | } 465 | -------------------------------------------------------------------------------- /cli/recorder.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | platform-application 7 | 8 | com.apple.private.security.no-container 9 | 10 | com.apple.private.security.container-manager 11 | 12 | com.apple.private.tcc.allow 13 | 14 | kTCCServiceMicrophone 15 | 16 | com.apple.private.tcc.manager.check-by-audit-token 17 | 18 | kTCCServiceMicrophone 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /devkit/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos 4 | export THEOS_PACKAGE_SCHEME= 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | export THEOS_DEVICE_SIMULATOR= 8 | -------------------------------------------------------------------------------- /devkit/i18n_lint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | 6 | def lint_strings_files(directory): 7 | for root, _, files in os.walk(directory): 8 | for file in files: 9 | if file.endswith('.strings') or file.endswith('.stringsdict'): 10 | file_path = os.path.join(root, file) 11 | try: 12 | subprocess.run(['plutil', '-lint', file_path], check=True) 13 | print(f"Linting passed for {file_path}") 14 | except subprocess.CalledProcessError: 15 | print(f"Linting failed for {file_path}") 16 | 17 | if __name__ == "__main__": 18 | lint_strings_files('res') 19 | -------------------------------------------------------------------------------- /devkit/i18n_statistics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import matplotlib.pyplot as plt 4 | import pathlib 5 | import plistlib 6 | 7 | 8 | lproj_base = pathlib.Path('res') 9 | 10 | available_languages = [ 11 | 'en', 12 | 'es', 13 | 'fr', 14 | 'ko', 15 | 'ru', 16 | 'tr', 17 | 'ug-CN', 18 | 'vi', 19 | 'zh-Hans', 20 | 'zh-Hant-HK', 21 | 'zh-Hant-TW', 22 | ] 23 | 24 | def get_lproj_path(lang) -> pathlib.Path: 25 | return lproj_base / f'{lang}.lproj' 26 | 27 | def get_lproj_strings_paths(lang) -> list[pathlib.Path]: 28 | return [ 29 | get_lproj_path(lang) / 'Localizable.strings', 30 | get_lproj_path(lang) / 'InfoPlist.strings', 31 | ] 32 | 33 | def get_lproj_strings_count(lang) -> tuple[int, int]: 34 | all_lines = [] 35 | for strings_path in get_lproj_strings_paths(lang): 36 | if not strings_path.exists(): 37 | continue 38 | with open(strings_path, 'r') as f: 39 | all_lines.extend(f.readlines()) 40 | todo_count = 0 41 | for line in all_lines: 42 | if line.find('/* TODO */') != -1 or line.find('/* Translated with ') != -1: 43 | todo_count += 1 44 | return int((len(all_lines) + 1) / 3), todo_count 45 | 46 | def get_lproj_stringsdict_paths(lang) -> list[pathlib.Path]: 47 | return [ 48 | get_lproj_path(lang) / 'Localizable.stringsdict', 49 | ] 50 | 51 | def get_lproj_stringsdict_count(lang) -> int: 52 | all_count = 0 53 | for stringsdict_path in get_lproj_stringsdict_paths(lang): 54 | if not stringsdict_path.exists(): 55 | continue 56 | stringsdict = plistlib.load(open(stringsdict_path, 'rb')) 57 | all_count += len(stringsdict) 58 | return all_count 59 | 60 | if __name__ == '__main__': 61 | x_labels = available_languages 62 | y_values = [] 63 | for lang in available_languages: 64 | total, todo = get_lproj_strings_count(lang) 65 | total += get_lproj_stringsdict_count(lang) 66 | percent = (total - todo) / total 67 | y_values.append((lang, percent, total - todo, total)) 68 | print(f'{lang}: {total - todo}/{total} translated, {percent * 100:.2f}%') 69 | y_values.sort(key=lambda x: 1.0 - x[1]) 70 | y_values.reverse() 71 | plt.figure().set_figwidth(15) 72 | plt.barh([x[0] for x in y_values], [x[2] for x in y_values]) 73 | plt.xlabel('Strings Translated') 74 | plt.ylabel('Language') 75 | plt.title('Translation Progress') 76 | plt.savefig(lproj_base / 'stats.png') 77 | -------------------------------------------------------------------------------- /devkit/roothide.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos-roothide 4 | export THEOS_PACKAGE_SCHEME=roothide 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | export THEOS_DEVICE_SIMULATOR= 8 | -------------------------------------------------------------------------------- /devkit/rootless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos 4 | export THEOS_PACKAGE_SCHEME=rootless 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | export THEOS_DEVICE_SIMULATOR= 8 | -------------------------------------------------------------------------------- /include/ATAudioTap.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "ATAudioTapDescription.h" 3 | 4 | @interface ATAudioTap : NSObject 5 | - (instancetype)initWithTapDescription:(ATAudioTapDescription *)arg1; 6 | @end 7 | -------------------------------------------------------------------------------- /include/ATAudioTapDescription.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #define kATAudioTapDescriptionPIDMicrophone 0xFFFFFFFD // -3 4 | #define kATAudioTapDescriptionPIDSpeaker 0xFFFFFFFE // -2 5 | #define kATAudioTapDescriptionPIDSystemAudio 0xFFFFFFFF // -1 6 | 7 | @interface ATAudioTapDescription : NSObject 8 | - (instancetype)initTapInternalWithFormat:(AVAudioFormat *)arg1 PIDs:(id)arg2; // iOS 16+ 9 | - (instancetype)initProcessTapInternalWithFormat:(AVAudioFormat *)arg1 PID:(int)arg2; 10 | @end 11 | -------------------------------------------------------------------------------- /include/AudioQueue+Private.h: -------------------------------------------------------------------------------- 1 | #define kAudioQueueProperty_TapOutputBypass 'qtob' // The name of this property is a guess... 2 | -------------------------------------------------------------------------------- /include/CTCall.h: -------------------------------------------------------------------------------- 1 | /** 2 | * CoreTelephony calling support. 3 | * 4 | * Copyright (c) 2013, Cykey (David Murray) 5 | * All rights reserved. 6 | */ 7 | 8 | #ifndef CTCALL_H_ 9 | #define CTCALL_H_ 10 | 11 | #include 12 | 13 | #if __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | #pragma mark - Definitions 18 | 19 | /* 20 | * Opaque structure definition. 21 | */ 22 | 23 | typedef struct __CTCall *CTCallRef; 24 | 25 | typedef enum { 26 | kCTCallStatusUnknown = 0, 27 | kCTCallStatusAnswered, 28 | kCTCallStatusDroppedInterrupted, 29 | kCTCallStatusOutgoingInitiated, 30 | kCTCallStatusIncomingCall, 31 | kCTCallStatusIncomingCallEnded 32 | } CTCallStatus; 33 | 34 | typedef CFStringRef CTCallType; 35 | 36 | extern CTCallType kCTCallTypeNormal; 37 | extern CTCallType kCTCallTypeVOIP; 38 | extern CTCallType kCTCallTypeVideoConference; 39 | extern CTCallType kCTCallTypeVoicemail; 40 | 41 | /* For use with the CoreTelephony notification system. */ 42 | 43 | extern CFStringRef kCTCallStatusChangeNotification; 44 | extern CFStringRef kCTCallIdentificationChangeNotification; 45 | 46 | #pragma mark - API 47 | 48 | CFArrayRef CTCopyCurrentCalls(CFAllocatorRef allocator); 49 | 50 | /* The 'types' argument is an array of CTCallTypes. */ 51 | CFArrayRef CTCopyCurrentCallsWithTypes(CFAllocatorRef allocator, CFArrayRef types); 52 | 53 | int CTGetCurrentCallCount(); 54 | 55 | CFStringRef CTCallCopyAddress(CFAllocatorRef allocator, CTCallRef call); 56 | CFStringRef CTCallCopyName(CFAllocatorRef allocator, CTCallRef call); 57 | CFStringRef CTCallCopyCountryCode(CFAllocatorRef allocator, CTCallRef call); 58 | CFStringRef CTCallCopyNetworkCode(CFAllocatorRef allocator, CTCallRef call); 59 | 60 | CFStringRef CTCallCopyUniqueStringID(CFAllocatorRef allocator, CTCallRef call); 61 | 62 | BOOL CTCallGetStartTime(CTCallRef, double *); 63 | BOOL CTCallGetDuration(CTCallRef, double *); 64 | 65 | CTCallStatus CTCallGetStatus(CTCallRef call); 66 | CTCallType CTCallGetCallType(CTCallRef call); 67 | 68 | /* Pass NULL to delete all calls. */ 69 | void CTCallDeleteAllCallsBeforeDate(CFDateRef date); 70 | void CTCallHistoryInvalidateCaches(); 71 | 72 | void CTCallTimersReset(); 73 | 74 | void CTCallAnswer(CTCallRef call); 75 | void CTCallHold(CTCallRef call); 76 | void CTCallResume(CTCallRef call); 77 | void CTCallDisconnect(CTCallRef call); 78 | 79 | void CTCallListDisconnectAll(); 80 | 81 | Boolean CTCallIsConferenced(CTCallRef call); 82 | Boolean CTCallIsAlerting(CTCallRef call); 83 | Boolean CTCallIsToVoicemail(CTCallRef call); 84 | Boolean CTCallIsOutgoing(CTCallRef call); 85 | 86 | void CTCallJoinConference(CTCallRef call); 87 | void CTCallLeaveConference(CTCallRef call); 88 | 89 | /* 90 | * The phone number passed in the dial functions must be normalized. 91 | * E.g. +1 (555) 555-5555 would become 15555555555. 92 | * One can use CPPhoneNumberCopyNormalized from AppSupport.framework to normalize phone numbers. 93 | */ 94 | 95 | CTCallRef CTCallDial(CFStringRef number); 96 | CTCallRef CTCallDialEmergency(CFStringRef number); 97 | 98 | /* Returns the call history. */ 99 | CFArrayRef _CTCallCopyAllCalls(); 100 | 101 | #if __cplusplus 102 | } 103 | #endif 104 | 105 | #endif /* CTCALL_H_ */ -------------------------------------------------------------------------------- /include/CTSetting.h: -------------------------------------------------------------------------------- 1 | /** 2 | * CoreTelephony setting. 3 | * 4 | * Copyright (c) 2013-2014 Cykey (David Murray) 5 | * All rights reserved. 6 | */ 7 | 8 | #ifndef CTSETTING_H_ 9 | #define CTSETTING_H_ 10 | 11 | #include 12 | 13 | #if __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | #pragma mark - API 18 | 19 | CFStringRef CTSettingCopyMyPhoneNumber(); 20 | CFDictionaryRef CTSettingCopyMyPhoneNumberExtended(); 21 | 22 | #if __cplusplus 23 | } 24 | #endif 25 | 26 | #endif /* CTSETTING_H_ */ -------------------------------------------------------------------------------- /include/CTTelephonyCenter.h: -------------------------------------------------------------------------------- 1 | /** 2 | * CoreTelephony notification support. 3 | * 4 | * Copyright (c) 2013-2014 Cykey (David Murray) 5 | * All rights reserved. 6 | */ 7 | 8 | #ifndef CTTELEPHONYCENTER_H_ 9 | #define CTTELEPHONYCENTER_H_ 10 | 11 | #include 12 | 13 | #if __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | #pragma mark - API 18 | 19 | /* This API is a mimic of CFNotificationCenter. */ 20 | 21 | CFNotificationCenterRef CTTelephonyCenterGetDefault(); 22 | void CTTelephonyCenterAddObserver(CFNotificationCenterRef center, const void *observer, CFNotificationCallback callBack, CFStringRef name, const void *object, CFNotificationSuspensionBehavior suspensionBehavior); 23 | void CTTelephonyCenterRemoveObserver(CFNotificationCenterRef center, const void *observer, CFStringRef name, const void *object); 24 | void CTTelephonyCenterRemoveEveryObserver(CFNotificationCenterRef center, const void *observer); 25 | 26 | #if __cplusplus 27 | } 28 | #endif 29 | 30 | #endif /* CTTELEPHONYCENTER_H_ */ -------------------------------------------------------------------------------- /layout/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: audio-commands 2 | Name: Audio Commands 3 | Version: 0.1 4 | Section: Scripting 5 | Depends: firmware (>= 13.0) 6 | Architecture: iphoneos-arm 7 | Author: Lessica <82flex@gmail.com> 8 | Maintainer: Lessica <82flex@gmail.com> 9 | Description: Something shamefully stolen from the Internet. 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | contourpy==1.3.1 2 | cycler==0.12.1 3 | fonttools==4.55.0 4 | kiwisolver==1.4.7 5 | matplotlib==3.9.2 6 | numpy==2.1.3 7 | packaging==24.2 8 | pillow==11.0.0 9 | pyparsing==3.2.0 10 | python-dateutil==2.9.0.post0 11 | six==1.16.0 12 | -------------------------------------------------------------------------------- /res/en.lproj/Compress Recording.shortcut: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lessica/TrollRecorder/25ef7b23f001099944f1d5381d20a36e50937ce2/res/en.lproj/Compress Recording.shortcut -------------------------------------------------------------------------------- /res/en.lproj/Create Archive.shortcut: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lessica/TrollRecorder/25ef7b23f001099944f1d5381d20a36e50937ce2/res/en.lproj/Create Archive.shortcut -------------------------------------------------------------------------------- /res/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“TrollRecorder” needs access to your contacts to display your friend’s name instead of phone number."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“TrollRecorder” needs access to Face ID to verify your identity."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“TrollRecorder” needs access to your location to save it with your recordings."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“TrollRecorder” needs access to your location to save it with your recordings."; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“TrollRecorder” needs access to your location to save it with your recordings."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“TrollRecorder” needs access to your location to save it with your recordings."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“TrollRecorder” needs access to your location to save it with your recordings."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“TrollRecorder” needs access to your microphone to implement the most basic recording features."; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“TrollRecorder” wants to use speech recognition to make transcript for your recordings."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "Toggle Call Recording"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "Toggle Hoverball"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "Toggle Recording"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "Toggle System Audio Recording"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "Toggle Voice Memo Recording"; 48 | -------------------------------------------------------------------------------- /res/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | Content Type 15 | other 16 | Content Types 17 | 18 | 19 | %lld content types 20 | 21 | NSStringLocalizedFormatKey 22 | %#@VARIABLE@ 23 | VARIABLE 24 | 25 | NSStringFormatSpecTypeKey 26 | NSStringPluralRuleType 27 | NSStringFormatValueTypeKey 28 | lld 29 | zero 30 | %lld content types 31 | one 32 | %lld content type 33 | other 34 | %lld content types 35 | 36 | 37 | Information Field 38 | 39 | NSStringLocalizedFormatKey 40 | %#@VARIABLE@ 41 | VARIABLE 42 | 43 | NSStringFormatSpecTypeKey 44 | NSStringPluralRuleType 45 | one 46 | Information Field 47 | other 48 | Information Fields 49 | 50 | 51 | %lld fields 52 | 53 | NSStringLocalizedFormatKey 54 | %#@VARIABLE@ 55 | VARIABLE 56 | 57 | NSStringFormatSpecTypeKey 58 | NSStringPluralRuleType 59 | NSStringFormatValueTypeKey 60 | lld 61 | zero 62 | %lld fields 63 | one 64 | %lld field 65 | other 66 | %lld fields 67 | 68 | 69 | Voice Channel 70 | 71 | NSStringLocalizedFormatKey 72 | %#@VARIABLE@ 73 | VARIABLE 74 | 75 | NSStringFormatSpecTypeKey 76 | NSStringPluralRuleType 77 | one 78 | Voice Channel 79 | other 80 | Voice Channels 81 | 82 | 83 | %lld channels 84 | 85 | NSStringLocalizedFormatKey 86 | %#@VARIABLE@ 87 | VARIABLE 88 | 89 | NSStringFormatSpecTypeKey 90 | NSStringPluralRuleType 91 | NSStringFormatValueTypeKey 92 | lld 93 | zero 94 | %lld channels 95 | one 96 | %lld channel 97 | other 98 | %lld channels 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /res/en.lproj/PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | “TrollRecorder” is committed to safeguarding your privacy. This privacy policy outlines how we handle your personal information. 4 | 5 | ## Personal Data Handling 6 | 7 | “TrollRecorder” does not collect or upload any personal data or recordings. 8 | 9 | All permissions requested by the app are solely to provide the necessary functionality. Most data processing occurs locally on your device. 10 | 11 | However, data related to technical errors may be uploaded to our servers to assist in improving app functionality. Additionally, cookies and similar technologies may be used to recognize your purchase record. 12 | 13 | ## Technical Error Reporting 14 | 15 | We collect data on technical errors encountered while using our app to help improve its functionality. This data is securely uploaded to our servers and handled according to our data security protocols. For more information on how this data is used, please refer to the security information at [Bugsnag Security](https://www.bugsnag.com/product/security/). 16 | 17 | ## Cookies and Similar Technologies 18 | 19 | We employ cookies or similar technologies to recognize your purchase record, facilitating the activation of paid features. 20 | 21 | ## Third Parties 22 | 23 | Your personal information remains confidential, and we do not share it with third parties. 24 | 25 | ## Cloud Storage Integration 26 | 27 | “TrollRecorder” allows users to upload their recordings to their own Google Drive, Dropbox, and Microsoft OneDrive accounts. This feature is optional and requires user authentication with the respective cloud storage service. The app securely stores your cloud storage credentials on your device; authentication is handled securely through the respective service’s API. Your recordings are uploaded directly from your device to your chosen cloud storage service, and “TrollRecorder” does not have access to your recordings stored in these services. 28 | 29 | ## Links to Other Websites 30 | 31 | While we may include links in our app for your convenience and reference, we are not responsible for the privacy policies of these external sites. We advise you to be aware that these sites’ privacy policies may differ from ours. 32 | 33 | ## Data Security 34 | 35 | We prioritize the security of your personal information. While we utilize commercially acceptable means to protect your personal information, please note that no method of electronic storage or internet transmission is entirely secure. We cannot guarantee absolute security, but we strive to provide the highest level of protection possible. 36 | 37 | ## Changes to This Privacy Policy 38 | 39 | This privacy policy is effective as of 2024/2/29. We reserve the right to update or make changes to our privacy policy at any time. Any changes will take effect immediately after being posted on this page. 40 | 41 | We recommend that you periodically review this privacy policy. If we make any significant changes to this privacy policy, we will notify you either through the email address you have provided to us or by posting a prominent notice on our app. 42 | 43 | ## Contact Us 44 | 45 | If you have any questions or concerns regarding this privacy policy, please contact us at [82flex@gmail.com](mailto:82flex@gmail.com). 46 | -------------------------------------------------------------------------------- /res/es.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“TrollRecorder” necesita acceder a tus contactos para mostrar el nombre de tu amigo en lugar de su número de teléfono."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“TrollRecorder” necesita acceder a Face ID para verificar tu identidad."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“TrollRecorder” necesita acceder a tu ubicación para guardarla con tus grabaciones."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“TrollRecorder” necesita acceder a tu ubicación para guardarla con tus grabaciones."; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“TrollRecorder” necesita acceder a tu ubicación para guardarla con tus grabaciones."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“TrollRecorder” necesita acceder a tu ubicación para guardarla con tus grabaciones."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“TrollRecorder” necesita acceder a tu ubicación para guardarla con tus grabaciones."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“TrollRecorder” necesita acceso a su micrófono para implementar las funciones de grabación más básicas."; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“TrollRecorder” requiere el uso de tecnología de reconocimiento de voz para producir una copia de texto de su grabación."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "Activar grabación de llamadas"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "Activar/Desactivar Bola flotante"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "Activar/Desactivar Grabación"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "Alternar grabación de audio del sistema"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "Activar grabación de notas de voz"; 48 | -------------------------------------------------------------------------------- /res/es.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | Tipo de contenido 15 | other 16 | Tipos de contenido 17 | 18 | 19 | %lld content types 20 | 21 | NSStringLocalizedFormatKey 22 | %#@VARIABLE@ 23 | VARIABLE 24 | 25 | NSStringFormatSpecTypeKey 26 | NSStringPluralRuleType 27 | NSStringFormatValueTypeKey 28 | lld 29 | zero 30 | %lld tipos de contenido 31 | one 32 | %lld tipo de contenido 33 | other 34 | %lld tipos de contenido 35 | 36 | 37 | Information Field 38 | 39 | NSStringLocalizedFormatKey 40 | %#@VARIABLE@ 41 | VARIABLE 42 | 43 | NSStringFormatSpecTypeKey 44 | NSStringPluralRuleType 45 | one 46 | Campo de información 47 | other 48 | Campos de información 49 | 50 | 51 | %lld fields 52 | 53 | NSStringLocalizedFormatKey 54 | %#@VARIABLE@ 55 | VARIABLE 56 | 57 | NSStringFormatSpecTypeKey 58 | NSStringPluralRuleType 59 | NSStringFormatValueTypeKey 60 | lld 61 | zero 62 | %lld campos 63 | one 64 | %lld campo 65 | other 66 | %lld campos 67 | 68 | 69 | Voice Channel 70 | 71 | NSStringLocalizedFormatKey 72 | %#@VARIABLE@ 73 | VARIABLE 74 | 75 | NSStringFormatSpecTypeKey 76 | NSStringPluralRuleType 77 | one 78 | Canal de voz 79 | other 80 | Canales de voz 81 | 82 | 83 | %lld channels 84 | 85 | NSStringLocalizedFormatKey 86 | %#@VARIABLE@ 87 | VARIABLE 88 | 89 | NSStringFormatSpecTypeKey 90 | NSStringPluralRuleType 91 | NSStringFormatValueTypeKey 92 | lld 93 | zero 94 | %lld canales 95 | one 96 | %lld canal 97 | other 98 | %lld canales 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /res/es.lproj/Localizable.stringsdict.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | Tipo de contenido 15 | other 16 | Tipos de contenido 17 | 18 | 19 | %lld content types 20 | 21 | NSStringLocalizedFormatKey 22 | %#@VARIABLE@ 23 | VARIABLE 24 | 25 | NSStringFormatSpecTypeKey 26 | NSStringPluralRuleType 27 | NSStringFormatValueTypeKey 28 | lld 29 | zero 30 | %lld tipos de contenido 31 | one 32 | %lld tipo de contenido 33 | other 34 | %lld tipos de contenido 35 | 36 | 37 | Information Field 38 | 39 | NSStringLocalizedFormatKey 40 | %#@VARIABLE@ 41 | VARIABLE 42 | 43 | NSStringFormatSpecTypeKey 44 | NSStringPluralRuleType 45 | one 46 | Campo de información 47 | other 48 | Campos de información 49 | 50 | 51 | %lld fields 52 | 53 | NSStringLocalizedFormatKey 54 | %#@VARIABLE@ 55 | VARIABLE 56 | 57 | NSStringFormatSpecTypeKey 58 | NSStringPluralRuleType 59 | NSStringFormatValueTypeKey 60 | lld 61 | zero 62 | %lld campos 63 | one 64 | %lld campo 65 | other 66 | %lld campos 67 | 68 | 69 | Voice Channel 70 | 71 | NSStringLocalizedFormatKey 72 | %#@VARIABLE@ 73 | VARIABLE 74 | 75 | NSStringFormatSpecTypeKey 76 | NSStringPluralRuleType 77 | one 78 | Canal de voz 79 | other 80 | Canales de voz 81 | 82 | 83 | %lld channels 84 | 85 | NSStringLocalizedFormatKey 86 | %#@VARIABLE@ 87 | VARIABLE 88 | 89 | NSStringFormatSpecTypeKey 90 | NSStringPluralRuleType 91 | NSStringFormatValueTypeKey 92 | lld 93 | zero 94 | %lld canales 95 | one 96 | %lld canal 97 | other 98 | %lld canales 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /res/es.lproj/PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # Política de privacidad 2 | 3 | “TrollRecorder” se compromete a salvaguardar su privacidad. Esta política de privacidad describe cómo manejamos su información personal. 4 | 5 | ## Tratamiento de datos personales 6 | 7 | “TrollRecorder” no recoge ni carga datos personales ni grabaciones. 8 | 9 | Todos los permisos solicitados por la aplicación son únicamente para proporcionar la funcionalidad necesaria. La mayor parte del procesamiento de datos se realiza localmente en su dispositivo. 10 | 11 | Sin embargo, los datos relacionados con errores técnicos pueden cargarse en nuestros servidores para ayudar a mejorar la funcionalidad de la aplicación. Además, pueden utilizarse cookies y tecnologías similares para reconocer su historial de compras. 12 | 13 | ## Notificación de errores técnicos 14 | 15 | Recopilamos datos sobre errores técnicos encontrados al utilizar nuestra aplicación para ayudar a mejorar su funcionalidad. Estos datos se cargan de forma segura en nuestros servidores y se manejan de acuerdo con nuestros protocolos de seguridad de datos. Para obtener más información sobre cómo se utilizan estos datos, consulte la información de seguridad en [Bugsnag Security](https://www.bugsnag.com/product/security/). 16 | 17 | ## Cookies y tecnologías similares 18 | 19 | Empleamos cookies o tecnologías similares para reconocer su registro de compra, facilitando la activación de funciones de pago. 20 | 21 | ## Terceros 22 | 23 | Sus datos personales son confidenciales y no los compartimos con terceros. 24 | 25 | ## Integración de almacenamiento en la nube 26 | 27 | “TrollRecorder” permite a los usuarios subir sus grabaciones a sus propias cuentas de Google Drive, Dropbox y Microsoft OneDrive. Esta función es opcional y requiere la autenticación del usuario con el respectivo servicio de almacenamiento en la nube. La aplicación almacena de forma segura sus credenciales de almacenamiento en la nube en su dispositivo; la autenticación se gestiona de forma segura a través de la API del servicio respectivo. Las grabaciones se suben directamente desde el dispositivo al servicio de almacenamiento en la nube elegido, y “TrollRecorder” no tiene acceso a las grabaciones almacenadas en estos servicios. 28 | 29 | ## Enlaces a otros sitios web 30 | 31 | Aunque podemos incluir enlaces en nuestra aplicación para su comodidad y referencia, no somos responsables de las políticas de privacidad de estos sitios externos. Le aconsejamos que tenga en cuenta que las políticas de privacidad de estos sitios pueden diferir de las nuestras. 32 | 33 | ## Seguridad de los datos 34 | 35 | Damos prioridad a la seguridad de su información personal. Aunque utilizamos medios comercialmente aceptables para proteger su información personal, tenga en cuenta que ningún método de almacenamiento electrónico o transmisión por Internet es totalmente seguro. No podemos garantizar una seguridad absoluta, pero nos esforzamos por ofrecer el mayor nivel de protección posible. 36 | 37 | ## Cambios en esta política de privacidad 38 | 39 | Esta política de privacidad es efectiva a partir del 2024/2/29. Nos reservamos el derecho a actualizar o realizar cambios en nuestra política de privacidad en cualquier momento. Cualquier cambio entrará en vigor inmediatamente después de su publicación en esta página. 40 | 41 | Le recomendamos que revise periódicamente esta política de privacidad. Si realizamos algún cambio significativo en esta política de privacidad, se lo notificaremos a través de la dirección de correo electrónico que nos haya proporcionado o publicando un aviso destacado en nuestra aplicación. 42 | 43 | ## Contacte con nosotros 44 | 45 | Si tiene alguna pregunta o duda sobre esta política de privacidad, póngase en contacto con nosotros en [82flex@gmail.com](mailto:82flex@gmail.com). -------------------------------------------------------------------------------- /res/fr.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“TrollRecorder” a besoin d'accéder à vos contacts pour afficher le nom de vos amis au lieu du numéro de téléphone."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“TrollRecorder” a besoin d'accéder à Face ID pour vérifier votre identité."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“TrollRecorder” a besoin d'accéder à votre emplacement pour l'enregistrer avec vos enregistrements."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“TrollRecorder” a besoin d'accéder à votre emplacement pour l'enregistrer avec vos enregistrements"; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“TrollRecorder” a besoin d'accéder à votre localisation pour l'enregistrer avec vos enregistrements."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“TrollRecorder” a besoin d'accéder à votre localisation pour l'enregistrer avec vos enregistrements."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“TrollRecorder” a besoin d'accéder à votre localisation pour l'enregistrer avec vos enregistrements."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“TrollRecorder” a besoin d'accéder à votre microphone pour mettre en œuvre les fonctionnalités d'enregistrement les plus basiques"; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“TrollRecorder” souhaite utiliser la reconnaissance vocale pour créer des transcriptions de vos enregistrements."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* TODO */ 35 | "Toggle Call Recording" = "Toggle Call Recording"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "Activer Hoverball"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "Activer l'enregistrement"; 42 | 43 | /* TODO */ 44 | "Toggle System Audio Recording" = "Toggle System Audio Recording"; 45 | 46 | /* TODO */ 47 | "Toggle Voice Memo Recording" = "Toggle Voice Memo Recording"; 48 | -------------------------------------------------------------------------------- /res/ko.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“TrollRecorder”는 녹음에 연락처 이름을 표시하기 위해 연락처에 접근해야 합니다."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“TrollRecorder”는 신원 확인을 위해 Face ID가 필요합니다."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“TrollRecorder”는 녹음에 위치 정보를 저장하기 위해 사용자의 위치에 접근해야 합니다."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“TrollRecorder”는 녹음에 위치 정보를 저장하기 위해 사용자의 위치에 접근해야 합니다."; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“TrollRecorder”는 녹음에 위치 정보를 저장하기 위해 사용자의 위치에 접근해야 합니다."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“TrollRecorder”는 녹음에 위치 정보를 저장하기 위해 사용자의 위치에 접근해야 합니다."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“TrollRecorder”는 녹음에 위치 정보를 저장하기 위해 사용자의 위치에 접근해야 합니다."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“TrollRecorder”는 가장 기본적인 녹음 기능 수행을 위해 마이크에 접근해야 합니다."; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“TrollRecorder”는 녹음의 전사문을 만들기 위해 음성 인식 기술을 사용해야 합니다."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "통화 녹음 시작/중지"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "호버볼 켜기/끄기"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "녹음 시작/중지"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "시스템 오디오 녹음 시작/중지"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "음성 메모 녹음 시작/중지"; 48 | -------------------------------------------------------------------------------- /res/ko.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | 콘텐츠 유형 15 | other 16 | 콘텐츠 유형 17 | 18 | 19 | %lld content types 20 | 21 | NSStringLocalizedFormatKey 22 | %#@VARIABLE@ 23 | VARIABLE 24 | 25 | NSStringFormatSpecTypeKey 26 | NSStringPluralRuleType 27 | NSStringFormatValueTypeKey 28 | lld 29 | zero 30 | %lld개 콘텐츠 유형 31 | one 32 | %lld개 콘텐츠 유형 33 | other 34 | %lld개 콘텐츠 유형 35 | 36 | 37 | Information Field 38 | 39 | NSStringLocalizedFormatKey 40 | %#@VARIABLE@ 41 | VARIABLE 42 | 43 | NSStringFormatSpecTypeKey 44 | NSStringPluralRuleType 45 | one 46 | 정보 필드 47 | other 48 | 정보 필드 49 | 50 | 51 | %lld fields 52 | 53 | NSStringLocalizedFormatKey 54 | %#@VARIABLE@ 55 | VARIABLE 56 | 57 | NSStringFormatSpecTypeKey 58 | NSStringPluralRuleType 59 | NSStringFormatValueTypeKey 60 | lld 61 | zero 62 | %lld개 항목 63 | one 64 | %lld개 항목 65 | other 66 | %lld개 항목 67 | 68 | 69 | Voice Channel 70 | 71 | NSStringLocalizedFormatKey 72 | %#@VARIABLE@ 73 | VARIABLE 74 | 75 | NSStringFormatSpecTypeKey 76 | NSStringPluralRuleType 77 | one 78 | 음성 채널 79 | other 80 | 음성 채널 81 | 82 | 83 | %lld channels 84 | 85 | NSStringLocalizedFormatKey 86 | %#@VARIABLE@ 87 | VARIABLE 88 | 89 | NSStringFormatSpecTypeKey 90 | NSStringPluralRuleType 91 | NSStringFormatValueTypeKey 92 | lld 93 | zero 94 | %lld 채널 95 | one 96 | %lld 채널 97 | other 98 | %lld 채널 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /res/ru.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "«TrollRecorder» требует доступ к вашим контактам, чтобы отображать имя вашего друга вместо номера телефона."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "«TrollRecorder» требует доступ к Face ID для подтверждения вашей личности."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "«TrollRecorder» требует доступ к вашему местоположению, чтобы сохранять его вместе с вашими записями."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "«TrollRecorder» требует доступ к вашему местоположению, чтобы сохранять его вместе с вашими записями."; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "«TrollRecorder» требует доступ к вашему местоположению, чтобы сохранять его вместе с вашими записями."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "«TrollRecorder» требует доступ к вашему местоположению, чтобы сохранять его вместе с вашими записями."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "«TrollRecorder» требует доступ к вашему местоположению, чтобы сохранять его вместе с вашими записями."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "«TrollRecorder» требует доступ к вашему микрофону для реализации основных функций записи."; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "«TrollRecorder» хочет использовать распознавание речи для создания расшифровки ваших записей."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "Переключить запись звонков"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "Переключить плавающий шар"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "Переключить запись"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "Переключить запись системного звука"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "Переключить запись голосовых заметок"; 48 | -------------------------------------------------------------------------------- /res/ru.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | Тип контента 15 | few 16 | Типы контента 17 | many 18 | Типы контента 19 | other 20 | Типы контента 21 | 22 | 23 | %lld content types 24 | 25 | NSStringLocalizedFormatKey 26 | %#@VARIABLE@ 27 | VARIABLE 28 | 29 | NSStringFormatSpecTypeKey 30 | NSStringPluralRuleType 31 | NSStringFormatValueTypeKey 32 | lld 33 | zero 34 | %lld типов контента 35 | one 36 | %lld тип контента 37 | few 38 | %lld типа контента 39 | many 40 | %lld типов контента 41 | other 42 | %lld типов контента 43 | 44 | 45 | Information Field 46 | 47 | NSStringLocalizedFormatKey 48 | %#@VARIABLE@ 49 | VARIABLE 50 | 51 | NSStringFormatSpecTypeKey 52 | NSStringPluralRuleType 53 | one 54 | Информационное поле 55 | few 56 | Информационные поля 57 | many 58 | Информационные поля 59 | other 60 | Информационные поля 61 | 62 | 63 | %lld fields 64 | 65 | NSStringLocalizedFormatKey 66 | %#@VARIABLE@ 67 | VARIABLE 68 | 69 | NSStringFormatSpecTypeKey 70 | NSStringPluralRuleType 71 | NSStringFormatValueTypeKey 72 | lld 73 | zero 74 | %lld полей 75 | one 76 | %lld поле 77 | few 78 | %lld поля 79 | many 80 | %lld полей 81 | other 82 | %lld полей 83 | 84 | 85 | Voice Channel 86 | 87 | NSStringLocalizedFormatKey 88 | %#@VARIABLE@ 89 | VARIABLE 90 | 91 | NSStringFormatSpecTypeKey 92 | NSStringPluralRuleType 93 | one 94 | Голосовой канал 95 | few 96 | Голосовые каналы 97 | many 98 | Голосовые каналы 99 | other 100 | Голосовые каналы 101 | 102 | 103 | %lld channels 104 | 105 | NSStringLocalizedFormatKey 106 | %#@VARIABLE@ 107 | VARIABLE 108 | 109 | NSStringFormatSpecTypeKey 110 | NSStringPluralRuleType 111 | NSStringFormatValueTypeKey 112 | lld 113 | zero 114 | %lld каналов 115 | one 116 | %lld канал 117 | few 118 | %lld канала 119 | many 120 | %lld каналов 121 | other 122 | %lld каналов 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /res/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lessica/TrollRecorder/25ef7b23f001099944f1d5381d20a36e50937ce2/res/screenshot.png -------------------------------------------------------------------------------- /res/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lessica/TrollRecorder/25ef7b23f001099944f1d5381d20a36e50937ce2/res/stats.png -------------------------------------------------------------------------------- /res/tr.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“TrollRecorder” uygulamasının, telefon numarası yerine arkadaşlarınızın isimlerini gösterebilmesi için kişilerinize erişmek için izin gerekiyor."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“TrollRecorder” uygulamasının kimliğinizi doğrulayabilmesi için Face ID'ye erişmek için izin gerekiyor."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“TrollRecorder” uygulamasının, kayıtlarınızla birlikte konumunuzu kaydedebilmesi için sürekli konum erişimi gerekiyor."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“TrollRecorder” uygulamasının, kayıtlarınızla birlikte konumunuzu kaydedebilmesi için sürekli konum erişimi gerekiyor"; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“TrollRecorder” uygulamasının, kayıtlarınızla birlikte konumunuzu kaydedebilmesi için geçici konum erişimi gerekiyor."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“TrollRecorder” uygulamasının, kayıtlarınızla birlikte konumunuzu kaydedebilmesi için konum erişimi gerekiyor."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“TrollRecorder” uygulamasının, kayıtlarınızla birlikte konumunuzu kaydedebilmesi için kullanım sırasında konum erişimi gerekiyor."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“TrollRecorder” uygulamasının temel kayıt özelliklerini kullanabilmesi için mikrofon erişimi gerekiyor"; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“TrollRecorder” uygulamasının, kayıtlarınızın transkripsiyonunu oluşturabilmesi için ses tanıma özelliğini kullanmak istiyor."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "Çağrı Kaydını Aç / Kapat"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "Hoverball'ı Aç/Kapat"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "Kaydı Aç/Kapat"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "Sistem Ses Kaydını Aç / Kapat"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "Sesli Not Kaydını Aç / Kapat"; 48 | -------------------------------------------------------------------------------- /res/tr.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %lld channels 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | lld 15 | one 16 | %lld kanal 17 | other 18 | %lld kanallar 19 | zero 20 | %lld kanallar 21 | 22 | 23 | %lld content types 24 | 25 | NSStringLocalizedFormatKey 26 | %#@VARIABLE@ 27 | VARIABLE 28 | 29 | NSStringFormatSpecTypeKey 30 | NSStringPluralRuleType 31 | NSStringFormatValueTypeKey 32 | lld 33 | one 34 | %lld İçerik Tipi 35 | other 36 | %lld İçerik türleri 37 | zero 38 | %lld i̇çeri̇k türleri̇ 39 | 40 | 41 | %lld fields 42 | 43 | NSStringLocalizedFormatKey 44 | %#@VARIABLE@ 45 | VARIABLE 46 | 47 | NSStringFormatSpecTypeKey 48 | NSStringPluralRuleType 49 | NSStringFormatValueTypeKey 50 | lld 51 | one 52 | %lld Alan 53 | other 54 | %lld Alanlar 55 | zero 56 | %lld Alanlar 57 | 58 | 59 | Content Type 60 | 61 | NSStringLocalizedFormatKey 62 | %#@VARIABLE@ 63 | VARIABLE 64 | 65 | NSStringFormatSpecTypeKey 66 | NSStringPluralRuleType 67 | one 68 | İçerik türü 69 | other 70 | İçerik türleri 71 | 72 | 73 | Information Field 74 | 75 | NSStringLocalizedFormatKey 76 | %#@VARIABLE@ 77 | VARIABLE 78 | 79 | NSStringFormatSpecTypeKey 80 | NSStringPluralRuleType 81 | one 82 | Bilgi Alanı 83 | other 84 | Bilgi Alanları 85 | 86 | 87 | Voice Channel 88 | 89 | NSStringLocalizedFormatKey 90 | %#@VARIABLE@ 91 | VARIABLE 92 | 93 | NSStringFormatSpecTypeKey 94 | NSStringPluralRuleType 95 | one 96 | Ses kanalı 97 | other 98 | Ses kanalları 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /res/tr.lproj/PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # Gizlilik Politikası 2 | 3 | “TrollRecorder” gizliliğinizi korumayı taahhüt eder. Bu gizlilik politikası, kişisel bilgilerinizi nasıl işlediğimizi özetlemektedir. 4 | 5 | ## Kişisel Verilerin İşlenmesi 6 | 7 | “TrollRecorder” herhangi bir kişisel veri veya kayıt toplamaz veya yüklemez. 8 | 9 | Uygulama tarafından talep edilen tüm izinler yalnızca gerekli işlevselliği sağlamak içindir. Veri işlemenin çoğu cihazınızda yerel olarak gerçekleşir. 10 | 11 | Ancak, teknik hatalarla ilgili veriler, uygulama işlevselliğini geliştirmeye yardımcı olmak için sunucularımıza yüklenebilir. Ayrıca, satın alma kaydınızı tanımak için çerezler ve benzer teknolojiler kullanılabilir. 12 | 13 | ## Teknik Hata Raporlama 14 | 15 | İşlevselliğini geliştirmeye yardımcı olmak için uygulamamızı kullanırken karşılaşılan teknik hatalar hakkında veri topluyoruz. Bu veriler güvenli bir şekilde sunucularımıza yüklenir ve veri güvenliği protokollerimize göre işlenir. Bu verilerin nasıl kullanıldığı hakkında daha fazla bilgi için lütfen [Bugsnag Security](https://www.bugsnag.com/product/security/) adresindeki güvenlik bilgilerine bakın. 16 | 17 | ## Çerezler ve Benzer Teknolojiler 18 | 19 | Satın alma kaydınızı tanımak ve ücretli özelliklerin etkinleştirilmesini kolaylaştırmak için çerezleri veya benzer teknolojileri kullanırız. 20 | 21 | ## Üçüncü Taraflar 22 | 23 | Kişisel bilgileriniz gizli kalır ve üçüncü taraflarla paylaşmayız. 24 | 25 | ## Bulut Depolama Entegrasyonu 26 | 27 | “TrollRecorder”, kullanıcıların kayıtlarını kendi Google Drive, Dropbox ve Microsoft OneDrive hesaplarına yüklemelerine olanak tanır. Bu özellik isteğe bağlıdır ve ilgili bulut depolama hizmeti ile kullanıcı kimlik doğrulaması gerektirir. Uygulama, bulut depolama kimlik bilgilerinizi cihazınızda güvenli bir şekilde saklar; kimlik doğrulama, ilgili hizmetin API'si aracılığıyla güvenli bir şekilde gerçekleştirilir. Kayıtlarınız doğrudan cihazınızdan seçtiğiniz bulut depolama hizmetine yüklenir ve “TrollRecorder” bu hizmetlerde depolanan kayıtlarınıza erişemez. 28 | 29 | ## Diğer Web Sitelerine Bağlantılar 30 | 31 | Size kolaylık sağlamak ve referans olması için uygulamamıza bağlantılar ekleyebilsek de, bu harici sitelerin gizlilik politikalarından sorumlu değiliz. Bu sitelerin gizlilik politikalarının bizimkinden farklı olabileceğini bilmenizi tavsiye ederiz. 32 | 33 | ## Veri Güvenliği 34 | 35 | Kişisel bilgilerinizin güvenliğine öncelik veriyoruz. Kişisel bilgilerinizi korumak için ticari olarak kabul edilebilir araçlar kullansak da, hiçbir elektronik depolama veya internet iletimi yönteminin tamamen güvenli olmadığını lütfen unutmayın. Mutlak güvenliği garanti edemeyiz, ancak mümkün olan en yüksek düzeyde koruma sağlamaya çalışıyoruz. 36 | 37 | ## Bu Gizlilik Politikasındaki Değişiklikler 38 | 39 | Bu gizlilik politikası 2024/2/29 tarihinden itibaren geçerlidir. Gizlilik politikamızı istediğimiz zaman güncelleme veya değişiklik yapma hakkımızı saklı tutarız. Herhangi bir değişiklik, bu sayfada yayınlandıktan hemen sonra yürürlüğe girecektir. 40 | 41 | Bu gizlilik politikasını periyodik olarak gözden geçirmenizi öneririz. Bu gizlilik politikasında önemli bir değişiklik yaparsak, bize sağladığınız e-posta adresi aracılığıyla veya uygulamamızda göze çarpan bir bildirim yayınlayarak sizi bilgilendireceğiz. 42 | 43 | ## Bize Ulaşın 44 | 45 | Bu gizlilik politikası ile ilgili herhangi bir sorunuz veya endişeniz varsa, lütfen [82flex@gmail.com](mailto:82flex@gmail.com) adresinden bizimle iletişime geçin. 46 | -------------------------------------------------------------------------------- /res/ug-CN.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "جۈمو ئۈن ئالغۇ"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "«جۈمو ئۈن ئالغۇ» ئۈن خاتىرىلەشتە ئالاقىداشلار ئىسمىنى كۆرسىتىش ئۈچۈن سىزنىڭ ئالاقىداشلىرىڭىزغا زىيارەت قىلىشى كېرەك."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "«جۈمو ئۈن ئالغۇ» سىزنىڭ كىملىكىڭىزنى دەلىللەش ئۈچۈن چىراي ID نى ئىشلىتىشى كېرەك."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "«جۈمو ئۈن ئالغۇ» ئۈن خاتىرىسىدە ئورۇن ئۇچۇرىنى ساقلاش ئۈچۈن سىزنىڭ ئورنىڭىزغا زىيارەت قىلىشى كېرەك."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "«جۈمو ئۈن ئالغۇ» ئۈن خاتىرىسىدە ئورۇن ئۇچۇرىنى ساقلاش ئۈچۈن سىزنىڭ ئورنىڭىزغا زىيارەت قىلىشى كېرەك."; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "«جۈمو ئۈن ئالغۇ» ئۈن خاتىرىسىدە ئورۇن ئۇچۇرىنى ساقلاش ئۈچۈن سىزنىڭ ئورنىڭىزغا زىيارەت قىلىشى كېرەك."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "«جۈمو ئۈن ئالغۇ» ئۈن خاتىرىسىدە ئورۇن ئۇچۇرىنى ساقلاش ئۈچۈن سىزنىڭ ئورنىڭىزغا زىيارەت قىلىشى كېرەك."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "«جۈمو ئۈن ئالغۇ» ئۈن خاتىرىسىدە ئورۇن ئۇچۇرىنى ساقلاش ئۈچۈن سىزنىڭ ئورنىڭىزغا زىيارەت قىلىشى كېرەك."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "«جۈمو ئۈن ئالغۇ» ئەڭ ئاساسىي ئۈن خاتىرىلەش ئىقتىدارىنى ئىشقا ئاشۇرۇش ئۈچۈن سىزنىڭ مىكروفونىڭىزغا زىيارەت قىلىشى كېرەك."; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "«جۈمو ئۈن ئالغۇ» سىزنىڭ ئۈن خاتىرىلىرىڭىزگە تېكىست نۇسخىسى تەييارلاش ئۈچۈن ئاۋاز تونۇش تېخنىكىسىنى ئىشلىتىشى كېرەك."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "جۈمو ئۈن ئالغۇ"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "تېلېفون خاتىرىلەشنى قوزغىتىش-توختىتىش"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "لەيلىمە توپنى ئالماشتۇرۇش"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "ئۈن خاتىرىلەشنى قوزغىتىش-توختىتىش"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "سىستېما ئاۋاز خاتىرىلەشنى قوزغىتىش-توختىتىش"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "ئاۋاز خاتىرىسىنى قوزغىتىش-توختىتىش"; 48 | -------------------------------------------------------------------------------- /res/ug-CN.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | مەزمۇن تىپى 15 | other 16 | مەزمۇن تىپى 17 | 18 | 19 | %lld content types 20 | 21 | NSStringLocalizedFormatKey 22 | %#@VARIABLE@ 23 | VARIABLE 24 | 25 | NSStringFormatSpecTypeKey 26 | NSStringPluralRuleType 27 | NSStringFormatValueTypeKey 28 | lld 29 | zero 30 | %lld مەزمۇن تىپى 31 | one 32 | %lld مەزمۇن تىپى 33 | other 34 | %lld مەزمۇن تىپى 35 | 36 | 37 | Information Field 38 | 39 | NSStringLocalizedFormatKey 40 | %#@VARIABLE@ 41 | VARIABLE 42 | 43 | NSStringFormatSpecTypeKey 44 | NSStringPluralRuleType 45 | one 46 | ئۇچۇر خانىسى 47 | other 48 | ئۇچۇر خانىسى 49 | 50 | 51 | %lld fields 52 | 53 | NSStringLocalizedFormatKey 54 | %#@VARIABLE@ 55 | VARIABLE 56 | 57 | NSStringFormatSpecTypeKey 58 | NSStringPluralRuleType 59 | NSStringFormatValueTypeKey 60 | lld 61 | zero 62 | %lld تۈر 63 | one 64 | %lld تۈر 65 | other 66 | %lld تۈر 67 | 68 | 69 | Voice Channel 70 | 71 | NSStringLocalizedFormatKey 72 | %#@VARIABLE@ 73 | VARIABLE 74 | 75 | NSStringFormatSpecTypeKey 76 | NSStringPluralRuleType 77 | one 78 | ئاۋاز قانىلى 79 | other 80 | ئاۋاز قانىلى 81 | 82 | 83 | %lld channels 84 | 85 | NSStringLocalizedFormatKey 86 | %#@VARIABLE@ 87 | VARIABLE 88 | 89 | NSStringFormatSpecTypeKey 90 | NSStringPluralRuleType 91 | NSStringFormatValueTypeKey 92 | lld 93 | zero 94 | %lld قانال 95 | one 96 | %lld قانال 97 | other 98 | %lld قانال 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /res/vi.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "TrollRecorder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“TrollRecorder” cần quyền truy cập vào danh bạ của bạn để hiển thị tên bạn bè của bạn thay vì số điện thoại."; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“TrollRecorder” cần quyền truy cập vào Face ID để xác minh danh tính của bạn."; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“TrollRecorder” cần quyền truy cập vào vị trí của bạn để lưu nó với các bản ghi âm của bạn."; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“TrollRecorder” cần quyền truy cập vào vị trí của bạn để lưu nó với các bản ghi âm của bạn."; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“TrollRecorder” cần quyền truy cập vào vị trí của bạn để lưu nó với các bản ghi âm của bạn."; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“TrollRecorder” cần quyền truy cập vào vị trí của bạn để lưu nó với các bản ghi âm của bạn."; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“TrollRecorder” cần quyền truy cập vào vị trí của bạn để lưu nó với các bản ghi âm của bạn."; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“TrollRecorder” cần truy cập vào micrô của bạn để thực hiện các tính năng ghi âm cơ bản nhất."; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“TrollRecorder” muốn sử dụng nhận dạng giọng nói để tạo bảng điểm cho các bản ghi âm của bạn."; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "TrollRecorder"; 33 | 34 | /* TODO */ 35 | "Toggle Call Recording" = "Toggle Call Recording"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "Bật/Tắt Bong bóng"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "Bật/Tắt Ghi âm"; 42 | 43 | /* TODO */ 44 | "Toggle System Audio Recording" = "Toggle System Audio Recording"; 45 | 46 | /* TODO */ 47 | "Toggle Voice Memo Recording" = "Toggle Voice Memo Recording"; 48 | -------------------------------------------------------------------------------- /res/zh-Hans.lproj/Compress Recording.shortcut: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lessica/TrollRecorder/25ef7b23f001099944f1d5381d20a36e50937ce2/res/zh-Hans.lproj/Compress Recording.shortcut -------------------------------------------------------------------------------- /res/zh-Hans.lproj/EULA.md: -------------------------------------------------------------------------------- 1 | # 最终用户许可协议 2 | 3 | 欢迎您使用“巨魔录音机”软件及相关服务!在此特别提醒您(用户)在使用和购买之前,请认真阅读《巨魔录音机用户协议与隐私条款》(以下简称“本协议”),确保您充分理解本协议中各条款,特别是涉及免除或者限制责任的条款、权利许可和信息使用的条款、同意开通的条款、法律适用和争议解决条款等。其中,免除或者限制责任条款等重要内容将以加粗形式提示您注意,您应重点阅读。如您未满 18 周岁,请您在法定监护人陪同下仔细阅读并充分理解本协议,并征得法定监护人的同意后下载本软件。请您审慎阅读并选择接受或不接受本协议。 4 | 5 | 除非您接受本协议所有条款,否则您无权购买或使用(以下统称“使用”)“巨魔录音机”软件或者通过任何方式使用“巨魔录音机”服务,或者获得“巨魔录音机”软件提供的任何服务。若您使用“巨魔录音机”软件及相关服务(以下简称“本服务”),则视为您已充分理解本协议,并承诺作为本协议的一方当事人接受本协议各项条款的约束。“巨魔录音机”软件服务由江宁区你好软件开发服务工作室(以下简称“你好工作室”)开发并提供。本协议约定“巨魔录音机”软件开发者与用户之间关于“巨魔录音机”软件服务(以下简称 “服务”)的权利义务。 6 | 7 | ## 定义 8 | 9 | 1. 用户:指所有直接或间接获取和使用“巨魔录音机”软件及相关服务的自然人、法人和其他组织等。在本协议中称为“用户”或称“您”。 10 | 2. “巨魔录音机”指由你好工作室合法拥有并运营的、标注名称为“巨魔录音机”的客户端应用程序、淘宝店铺、微信公众号、小程序、视频号、微博以及 [TrollRecorder](https://trollrecorder.app) 的网站等。 11 | 3. 本协议内容同时包括“巨魔录音机”软件开发者已经发布及后续可能不断发布的关于“巨魔录音机”软件及相关服务的相关协议、规则等内容。前述内容一经正式发布,并以适当的方式送达用户(系统通知、短信、电话等),即为本协议不可分割的组成部分,您应同样遵守。若用户不接受修改后的条款,请立即停止使用服务,用户继续使用服务视为接受修改后的协议。 12 | 13 | ## 软件购买 14 | 15 | ### 免费使用 16 | 17 | “巨魔录音机”提供免费使用版本,在功能上相较于收费版本区别如下: 18 | 19 | 1. 免费版本不提供任何形式的技术支持。 20 | 2. 免费版本的部分设置选项(包括但不限于悬浮球、地理位置、混音器与合并器选项)将被限制使用。 21 | 3. 免费版本的录音文件将被加入水印。 22 | 23 | 用户在使用免费版本的“巨魔录音机”时也应遵守本协议内约定的全部内容。 24 | 25 | ### 授权使用 26 | 27 | 用户在使用“巨魔录音机”收费服务前需要在 [浩劫商店](https://havoc.app) 购买一个“巨魔录音机”使用授权。“巨魔录音机”使用授权应当使用设备唯一标识符绑定注册,单个授权至多可绑定 5 台设备。 28 | 29 | ### 您必须承诺和保证 30 | 31 | 1. 您了解并同意,用户须对购买信息的真实性、合法性、有效性承担全部责任;用户不得冒充他人,不得利用他人的名义发布任何信息;不得恶意使用授权设备导致其他用户误认;否则我们有权立即停止提供服务,您独自承担由此而产生的一切法律责任。 32 | 2. 您的授权在丢失或遗忘后,可遵照相应的申诉途径及时申诉请求找回。 33 | 3. 因您自身原因或其他不可抗因素而导致授权被盗、丢失,均由您本人承担责任,“巨魔录音机”软件开发者不承担任何责任。 34 | 35 | ## 隐私政策 36 | 37 | “巨魔录音机”软件开发者依据法律法规收集、使用个人信息,坚持遵循个人信息收集、使用的合法、正当、必要的原则。在使用“巨魔录音机”软件及相关服务前,请您务必仔细阅读并透彻理解本隐私政策,“巨魔录音机”软件开发者可能会收集和使用您的相关信息,您一旦选择使用“巨魔录音机”软件及相关服务,即意味着同意“巨魔录音机”软件开发者按照本隐私政策收集、保存、使用、共享、披露及保护您的信息。 38 | 39 | ### 本软件可能收集的信息 40 | 41 | #### 您可能提供的信息 42 | 43 | 网络身份标识信息(包括系统账号、IP 地址、电子邮箱地址等);通讯信息;设备信息(包括设备型号、设备 MAC 地址、操作系统类型、设备设置);软件列表唯一设备识别码(如 IMEI/IDFA/UDID/GUID、SIM 卡 IMSI 信息等在内的描述个人常用设备基本情况的信息);位置信息(包括行程信息、精准定位信息、住宿信息、经纬度等)。 44 | 45 | #### “巨魔录音机”软件开发者可能收集的信息 46 | 47 | 1. 日志信息 48 | 当您使用“巨魔录音机”软件开发者提供的服务时,可能会通过 Cookie 或其他方式自动采集一些与您个人身份无关的日志信息。这些信息包括:设备或应用信息,例如您使用的硬件设备、网页浏览器或用于接入本平台服务的其他程序所提供的配置信息、您的 IP 地址、您所使用的硬件设备的版本及设备识别码等。 49 | 50 | 2. 位置信息 51 | 当您通过具有定位功能的移动设备使用本软件提供的服务时,系统会通过 GPS 或 Wi-Fi 等方式收集您的地理位置信息。您可以通过关闭定位功能,停止对您的地理位置信息的收集。 52 | 53 | 3. 个人信息 54 | 当您使用 Google Drive、 Dropbox 等第三方网络存储服务时,我们可能会访问您相关账户的基础信息(例如:电子邮件地址)。 55 | 56 | ### “巨魔录音机”软件开发者可能如何使用信息 57 | 58 | “巨魔录音机”软件开发者可能将所收集的信息用作如下用途: 59 | 60 | 1. 向您提供服务; 61 | 2. 帮助本产品及服务的设计、优化和升级; 62 | 3. 向您提供个性化服务,例如向您展示、推送与您更加相关的知识内容和广告; 63 | 4. 用于身份验证、安全防范、存档和备份,确保服务的安全性。 64 | 65 | 当您使用 Google Drive、 Dropbox 等第三方网络存储服务时,我们可能会在您所选择的第三方网络存储服务中创建和使用以下数据信息: 66 | 67 | 1. 文件存储:将您的录音文件上传到您所选择的第三方网络存储服务账户中。 68 | 2. 文件管理:读取和管理存储在您所选择的第三方网络存储服务账户中的录音文件。 69 | 70 | ### 信息的保护 71 | 72 | 1. “巨魔录音机”软件开发者将运用各种安全技术和程序来建立完善的管理制度来保护您的个人信息; 73 | 2. 当您选择使用第三方网络存储服务保存您的录音及相关数据时,第三方网络存储服务对您的个人信息及数据承担保护责任。 74 | 75 | ### 信息的合理利用及披露原则 76 | 77 | “巨魔录音机”软件开发者不会将您的个人信息转移或披露给任何非关联的第三方,除非: 78 | 79 | 1. 事先获得您的明确授权同意; 80 | 2. 相关法律法规或法院、政府机关要求; 81 | 3. 为完成合并、分立、收购或资产转让而转移,但“巨魔录音机”软件开发者会要求新的持有您的个人信息的公司、组织或个人继续受本隐私政策的约束,否则,“巨魔录音机”软件开发者有权要求该公司、组织或个人重新取得您的授权同意; 82 | 4. 在法律法规允许的范围内,为维护“巨魔录音机”其他用户、“巨魔录音机”软件开发者的生命、财产等合法权益或维权产品或服务的安全稳定运行所必需的,例如查找、预防、处理欺诈等违法活动和减少信用风险等; 83 | 5. “巨魔录音机”软件开发者为维护合法权益而向用户提起诉讼或仲裁; 84 | 6. 以维护公共利益或学术研究为目的; 85 | 86 | 请您注意,“巨魔录音机”软件开发者在您使用相关产品或服务时不会让您提供任何财产账户、银行卡、信用卡、第三方支付账户及对应密码等,请勿在使用服务中透露自己的各类财产账户、银行卡、信用卡、第三方支付账户及对应密码等重要资料,否则由此带来的任何损失由您自行承担。 87 | 88 | ## 用户使用规范 89 | 90 | 1. 本条所述内容是指用户使用“巨魔录音机”的过程中所传播、上传的任何内容,包括但不限于文字、语音、图片、视频、图文等发送、回复或自动回复消息和相关链接页面,以及其他使用本服务所产生的内容。 91 | 2. 用户不得利用“巨魔录音机”服务传播如下法律法规和政策禁止的内容: 92 | 1. 反对宪法所确定的基本原则的; 93 | 2. 危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的; 94 | 3. 损害国家荣誉和利益的; 95 | 4. 煽动民族仇恨、民族歧视,破坏民族团结的; 96 | 5. 破坏国家宗教政策,宣扬邪教和封建迷信的; 97 | 6. 散布谣言,扰乱社会秩序,破坏社会稳定的; 98 | 7. 散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的; 99 | 8. 侮辱或者诽谤他人,侵害他人合法权益的; 100 | 9. 含有法律、行政法规禁止的其他内容的信息。 101 | 3. 用户不得利用“巨魔录音机”服务传播如下干扰“巨魔录音机”正常运营,或侵犯其他用户或第三方合法权益的内容: 102 | 1. 含有任何性或暗示性的; 103 | 2. 含有辱骂、恐吓、威胁内容的; 104 | 3. 含有骚扰、垃圾广告、恶意信息、诱骗信息的; 105 | 4. 涉及他人隐私、个人信息或资料的; 106 | 5. 侵害他人名誉权、肖像权、知识产权、商业秘密等合法权利权益的; 107 | 6. 含有其他干扰本服务正常运营和侵权其他用户或第三方合法权益内容的信息。 108 | 4. 未经“巨魔录音机”软件开发者书面许可,您不得自行或授权、允许、协助任何第三人对本协议“巨魔录音机”软件及相关服务中信息内容进行如下行为: 109 | 1. 复制、读取、采用“巨魔录音机”软件及相关服务的信息内容,用于包括但不限于宣传、增加阅读量、浏览量等商业用途; 110 | 2. 擅自编辑、整理、编排“巨魔录音机”软件及相关服务的信息内容后在“巨魔录音机”软件及相关服务的源页面以外的渠道进行展示; 111 | 3. 采用包括但不限于特殊标识、特殊代码等任何形式的识别方法,自行或协助第三人对“巨魔录音机”软件及相关服务的信息内容产生流量、阅读量引导、转移、劫持等不利影响; 112 | 4. 其他非法获取或使用“巨魔录音机”软件及相关服务的信息内容的行为。 113 | 5. 如果“巨魔录音机”软件开发者发现或者收到他人举报或投诉用户违反本协议约定的,“巨魔录音机”软件开发者有权不经通知随时处以包括但不限于警告、设备封禁的处罚,且通知用户处理结果。 114 | 6. 因违反用户协议被封禁的用户,可以自行与“巨魔录音机”软件开发者联系。封禁用户可提交申诉,“巨魔录音机”软件开发者将对申诉进行审查,并自行合理判断决定是否变更处罚措施。 115 | 7. 用户理解并同意,“巨魔录音机”软件开发者有权依合理判断对违反有关法律法规或本协议的行为进行处罚,对违法违规的任何用户采取适当的法律行动,并依据法律法规保存有关信息向有关部门报告等,用户应承担由此而产生的一切法律责任。 116 | 8. 用户理解并同意,因用户违反本协议约定,导致或产生的任何第三方主张的任何索赔、要求或损失,包括合理的律师费,用户应当赔偿“巨魔录音机”软件开发者,并使之免受损害。 117 | 118 | ## 基于软件提供服务 119 | 120 | “巨魔录音机”软件开发者依托“巨魔录音机”软件向您提供服务,您还应遵守以下约定: 121 | 122 | 1. 您在使用本服务的过程中可能需要下载软件,对于这些软件,“巨魔录音机”软件开发者给予您一项个人的、不可转让及非排他性的许可。您仅可为访问或使用本服务的目的而使用这些软件。 123 | 2. 您使用“巨魔录音机”软件及相关服务,可以通过访问“巨魔录音机”官网相关网站。若您并非从“巨魔录音机”软件开发者或经“巨魔录音机”软件开发者授权的第三方获取本软件的,“巨魔录音机”软件开发者无法保证非官方版本的“巨魔录音机”软件能够正常使用,您因此遭受的损失与“巨魔录音机”软件开发者无关。 124 | 3. 为了改善用户体验、保证服务的安全性及产品功能的一致性,“巨魔录音机”软件开发者可能会对软件进行更新。您应该将相关软件更新到最新版本,否则“巨魔录音机”软件开发者并不保证其能正常使用,您有权选择接受更新版本或服务,如您不接受,部分功能将受到限制或不能继续使用。如您不再需要使用“巨魔录音机”软件及相关服务可自行卸载。 125 | 4. 除非“巨魔录音机”软件开发者书面许可,您不得从事下列任一行为: 126 | 1. 删除软件及其副本上关于著作权的信息; 127 | 2. 对软件进行反向工程、反向汇编、反向编译,或者以其他方式尝试发现软件的源代码; 128 | 3. 对“巨魔录音机”软件开发者拥有知识产权的内容进行使用、出租、出借、复制、修改、链接、转载、汇编、发表、出版、建立镜像站点等; 129 | 4. 对软件或者软件运行过程中释放到任何终端内存中的数据、软件运行过程中客户端与服务器端的交互数据,以及软件运行所必需的系统数据,进行复制、修改、增加、删除、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非经“巨魔录音机”软件开发者授权的第三方工具/服务接入软件和相关系统; 130 | 5. 通过修改或伪造软件运行中的指令、数据,增加、删减、变动软件的功能或运行效果,或者将用于上述用途的软件、方法进行运营或向公众传播,无论这些行为是否为商业目的; 131 | 6. 通过非“巨魔录音机”软件开发者开发、授权的第三方软件、插件、外挂、系统,登录或使用“巨魔录音机”软件开发者软件及服务,或制作、发布、传播非“巨魔录音机”软件开发者开发、授权的第三方软件、插件、外挂、系统。 132 | 133 | ## 用户发送、传播、使用规则 134 | 135 | 1. 用户在本服务中或通过本服务所传送、发布的任何内容并不反映或代表,也不得被视为反映或代表“巨魔录音机”软件开发者的观点、立场或政策,“巨魔录音机”软件开发者对此不承担任何责任。 136 | 2. 用户不得利用服务进行如下行为: 137 | 1. 提交、发布虚假信息,或盗用他人头像或资料,冒充、利用他人名义; 138 | 2. 强制、诱导其他用户关注、点击链接页面或分享信息; 139 | 3. 虚构事实、隐瞒真相以误导、欺骗他人; 140 | 4. 利用技术手段批量建立虚假账号; 141 | 5. 利用“巨魔录音机”服务从事任何违法犯罪活动; 142 | 6. 制作、发布与以上行为相关的方法、工具,或对此类方法、工具进行运营或传播,无论这些行为是否为商业目的; 143 | 7. 其他违反法律法规规定、侵犯其他用户合法权益、干扰“巨魔录音机”正常运营或“巨魔录音机”软件开发者未明示授权的行为。 144 | 3. 用户须对利用服务传送信息(包括但不限于网页、文字、图片、音频、视频、图表等)的真实性、合法性、无害性、准确性、有效性等全权负责、与用户所传播的信息相关的任何法律责任由用户自行承担,与“巨魔录音机”软件开发者无关。如因此给“巨魔录音机”软件开发者或第三方造成损害的,用户应当依法予以赔偿。 145 | 4. 您发送或传播的内容应有合法来源,相关内容为您所有或您已获得权利人的授权。 146 | 5. 您使用本服务时不得违反国家法律法规、侵害他人合法权益。您理解并同意,如您被他人投诉侵权或您投诉他人侵权,“巨魔录音机”软件开发者有权将争议中相关方的主体、联系方式、投诉相关内容等必要信息提供给其他争议方或相关部门,以便及时解决投诉纠纷,保护他人合法权益。 147 | 6. “巨魔录音机”软件开发者提供的服务中可能包括广告,用户同意在使用过程中显示“巨魔录音机”软件开发者和第三方供应商、合作伙伴提供的广告。除法律法规明确规定外,用户应自行对该广告进行的交易负责,对用户因依该广告信息进行的交易或前述广告商提供的内容而遭受的损失或损害,“巨魔录音机”软件开发者不承担任何责任。 148 | 7. 用户不得对“巨魔录音机”软件开发者服务中涉及的链接或推广内容进行域名劫持、恶意跳转、恶意刷量、作弊等,否则,“巨魔录音机”软件开发者有权立即扣除对应部分应获得的收益,因此给“巨魔录音机”软件开发者造成损失的,用户应全额赔偿该损失。 149 | 150 | ## 遵守当地法律监管 151 | 152 | 1. 您在使用本服务过程中应当遵守当地相关的法律法规,并尊重当地的道德和风俗习惯。如果您的行为违反了当地法律法规或道德风俗,您应当为此独立承担责任。 153 | 2. 您应避免因使用本服务而使“巨魔录音机”软件开发者卷入政治和公共事件,否则“巨魔录音机”软件开发者有权暂停或终止对您的服务。 154 | 155 | ## 违约处理 156 | 157 | 1. 针对您违反本协议或其他服务条款的行为,“巨魔录音机”软件开发者有权独立判断并视情况采取预先警示、永久封禁等措施。“巨魔录音机”软件开发者有权公告处理结果,且有权根据实际情况决定是否恢复使用。对涉嫌违反法律法规、涉嫌违法犯罪的行为将保存有关记录,并依法向有关主管部门报告、配合有关主管部门调查。 158 | 2. 因您违反本协议或其他服务条款规定,引起第三方投诉或诉讼索赔的,您应当自行承担全部法律责任。因您的违法或违约行为导致“巨魔录音机”软件开发者向任何第三方赔偿或遭受国家机关处罚的,您还应足额赔偿“巨魔录音机”软件开发者因此遭受的全部损失。 159 | 3. 因您违反本协议或其他服务条款规定,造成“巨魔录音机”软件开发者向任何第三方承担责任的,“巨魔录音机”软件开发者在承担所述责任后有权向用户足额追偿,由此产生的合理费用包括但不限于,律师费、公证费、鉴定费、差旅费、诉讼费等亦由用户承担。 160 | 4. “巨魔录音机”软件开发者尊重并保护法人、公民的知识产权、名誉权、姓名权、隐私权等合法权益。您保证,在使用相关服务时上传的文字、图片、视频、音频、链接等不侵犯任何第三方的知识产权、名誉权、姓名权、隐私权等权利及合法权益。针对第三方提出的全部权利主张,您应自行承担全部法律责任;如因您的侵权行为导致“巨魔录音机”软件开发者遭受损失的(包括经济、商誉等损失),您还应足额赔偿“巨魔录音机”软件开发者遭受的全部损失。 161 | 162 | ## 不可抗力及其他免责事由 163 | 164 | 1. 您理解并同意,在使用本服务的过程中,可能会遇到不可抗力等风险因素,使本服务发生中断。不可抗力是指不能预见、不能克服并不能避免且对一方或双方造成重大影响的客观事件,包括但不限于自然灾害如洪水、地震、瘟疫流行和风暴等以及社会事件如战争、动乱、政府行为等。出现上述情况时,“巨魔录音机”软件开发者将努力在第一时间与相关单位配合,及时进行修复,但是由此给您造成的损失“巨魔录音机”软件开发者在法律允许的范围内免责。 165 | 2. 在法律允许的范围内,“巨魔录音机”软件开发者对以下情形导致的服务中断或受阻不承担责任: 166 | 1. 受到计算机病毒、木马或其他恶意程序、黑客攻击的破坏; 167 | 2. 用户或“巨魔录音机”软件开发者的电脑软件、系统、硬件和通信线路出现故障; 168 | 3. 用户操作不当; 169 | 4. 用户通过非“巨魔录音机”软件开发者授权的方式使用本服务; 170 | 5. 其他“巨魔录音机”软件开发者无法控制或合理预见的情形。 171 | 3. 您理解并同意,“巨魔录音机”软件及相关服务可能会受多种因素的影响或干扰,“巨魔录音机”软件开发者不保证(包括但不限于): 172 | 1. “巨魔录音机”软件开发者完全适合用户的使用要求; 173 | 2. “巨魔录音机”软件开发者不受干扰,及时、安全、可靠或不出现错误;您经由“巨魔录音机”软件开发者取得的任何软件、服务或其他材料符合用户的期望; 174 | 3. 软件中任何错误都将能得到更正。 175 | 4. 您理解并同意,在使用本服务的过程中,可能会遇到网络信息或其他用户行为带来的风险,“巨魔录音机”软件开发者不对任何信息的真实性、适用性、合法性承担责任,也不对因侵权行为给您造成的损害负责。这些风险包括但不限于: 176 | 1. 来自他人匿名或冒名的含有威胁、诽谤、令人反感或非法内容的信息; 177 | 2. 因使用本协议项下的服务,遭受他人误导、欺骗或其他导致或可能导致的任何心理、生理上的伤害以及经济上的损失; 178 | 3. 其他因网络信息或用户行为引起的风险。 179 | 5. 对于涉嫌借款、理财、证券、保险以及其他涉财产的网络信息、账户密码、广告或推广等信息的,请您谨慎对待并自行进行判断,基于前述原因您因此遭受的利润、商业信誉、资料损失或其他有形或无形损失,“巨魔录音机”软件开发者不承担任何直接、间接、附带、特别、衍生性或惩罚性的赔偿责任。 180 | 6. 当您选择使用第三方网络存储服务保存您的录音及相关数据时,若造成您个人数据及信息的包括但不限于丢失、损毁等情况,“巨魔录音机”软件开发者不承担任何直接、间接、附带、特别、衍生性或惩罚性的赔偿责任。 181 | 7. 您理解并同意,本服务并非为某些特定目的而设计,包括但不限于核设施、军事用途、医疗设施、交通通讯等重要领域。如果因为软件或服务的原因导致上述操作失败而带来的人员伤亡、财产损失和环境破坏等,“巨魔录音机”软件开发者不承担法律责任。 182 | 8. “巨魔录音机”软件开发者依据本协议约定获得处理违法违规内容的权利,该权利不构成“巨魔录音机”软件开发者的义务或承诺,“巨魔录音机”软件开发者不能保证及时发现违法行为或进行相应处理。 183 | 184 | ## 服务的变更、中断、终止 185 | 186 | 1. 您理解并同意,“巨魔录音机”软件开发者提供的服务是按照现有技术和条件所能达到的现状提供的。“巨魔录音机”软件开发者会尽最大努力向您提供服务,确保服务的连贯性和安全性。您理解,“巨魔录音机”软件开发者不能随时预见和防范技术以及其他风险,包括但不限于不可抗力、病毒、木马、黑客攻击、系统不稳定、第三方服务瑕疵及其他各种安全问题的侵扰等原因可能导致的服务中断、数据丢失以及其他的损失和风险。 187 | 2. 您理解并同意,“巨魔录音机”软件开发者为了服务整体运营的需要,有权在公告通知后修改、中断、中止或终止相关服务,而无须向用户负责或承担任何赔偿责任。 188 | 189 | ## 管辖与法律适用 190 | 191 | 1. 本协议的成立、生效、履行、解释及纠纷解决,适用中华人民共和国大陆地区法律(不包括冲突法)。 192 | 2. 本协议签订地为中华人民共和国江苏省南京市江宁区。 193 | 3. 若您和“巨魔录音机”软件开发者之间发生任何纠纷或争议,首先应友好协商解决;协商不成的,您同意将纠纷或争议提交本协议签订地(即中华人民共和国江苏省南京市江宁区)有管辖权的人民法院管辖。 194 | 4. 本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。 195 | 5. 本协议条款无论因何种原因部分无效或不可执行,其余条款仍有效,对双方具有约束力。 196 | -------------------------------------------------------------------------------- /res/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "巨魔录音机"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“巨魔录音机” 需要访问你的联系人,以便在录音中显示联系人姓名。"; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“巨魔录音机” 需要使用面容 ID 以验证你的身份。"; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“巨魔录音机” 需要访问你的位置,以便在录音中保存位置信息。"; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“巨魔录音机” 需要访问你的位置,以便在录音中保存位置信息。"; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“巨魔录音机” 需要访问你的位置,以便在录音中保存位置信息。"; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“巨魔录音机” 需要访问你的位置,以便在录音中保存位置信息。"; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“巨魔录音机” 需要访问你的位置,以便在录音中保存位置信息。"; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“巨魔录音机” 需要访问你的麦克风,以便实现最基本的录音功能。"; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“巨魔录音机” 需要使用语音识别技术为你的录音制作文字副本。"; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "巨魔录音机"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "启停通话录音"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "切换悬浮球"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "启停录音"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "启停系统音频录制"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "启停语音备忘录"; 48 | -------------------------------------------------------------------------------- /res/zh-Hans.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Content Type 6 | 7 | NSStringLocalizedFormatKey 8 | %#@VARIABLE@ 9 | VARIABLE 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | one 14 | 内容类型 15 | other 16 | 内容类型 17 | 18 | 19 | %lld content types 20 | 21 | NSStringLocalizedFormatKey 22 | %#@VARIABLE@ 23 | VARIABLE 24 | 25 | NSStringFormatSpecTypeKey 26 | NSStringPluralRuleType 27 | NSStringFormatValueTypeKey 28 | lld 29 | zero 30 | %lld 种内容类型 31 | one 32 | %lld 种内容类型 33 | other 34 | %lld 种内容类型 35 | 36 | 37 | Information Field 38 | 39 | NSStringLocalizedFormatKey 40 | %#@VARIABLE@ 41 | VARIABLE 42 | 43 | NSStringFormatSpecTypeKey 44 | NSStringPluralRuleType 45 | one 46 | 信息字段 47 | other 48 | 信息字段 49 | 50 | 51 | %lld fields 52 | 53 | NSStringLocalizedFormatKey 54 | %#@VARIABLE@ 55 | VARIABLE 56 | 57 | NSStringFormatSpecTypeKey 58 | NSStringPluralRuleType 59 | NSStringFormatValueTypeKey 60 | lld 61 | zero 62 | %lld 项 63 | one 64 | %lld 项 65 | other 66 | %lld 项 67 | 68 | 69 | Voice Channel 70 | 71 | NSStringLocalizedFormatKey 72 | %#@VARIABLE@ 73 | VARIABLE 74 | 75 | NSStringFormatSpecTypeKey 76 | NSStringPluralRuleType 77 | one 78 | 语音通道 79 | other 80 | 语音通道 81 | 82 | 83 | %lld channels 84 | 85 | NSStringLocalizedFormatKey 86 | %#@VARIABLE@ 87 | VARIABLE 88 | 89 | NSStringFormatSpecTypeKey 90 | NSStringPluralRuleType 91 | NSStringFormatValueTypeKey 92 | lld 93 | zero 94 | %lld 通道 95 | one 96 | %lld 通道 97 | other 98 | %lld 通道 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /res/zh-Hans.lproj/PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # 隐私政策 2 | 3 | “巨魔录音机” 承诺保护您的隐私。本隐私政策概述了我们如何处理您的个人信息。 4 | 5 | ## 个人数据收集与使用 6 | 7 | “巨魔录音机“ 不会收集或上传任何个人数据和录音。应用请求的所有权限都是为了提供应用程序功能所需的服务。大多数数据处理在您的设备上本地进行。然而,与技术错误有关的数据可能会被上传到的服务器,以帮助改进应用程序功能,同时可能会使用 Cookie 和类似技术来识别您的购买记录。 8 | 9 | ## 技术错误报告 10 | 11 | 我们收集使用应用程序时遇到的技术错误时候的数据,以帮助改善其功能。这些数据将安全上传到 Bugsnag 的服务器,并按照其数据安全协议处理。有关如何使用这些数据的更多信息,请参阅 [Bugsnag Security](https://www.bugsnag.com/product/security/) 上有关数据安全的信息。 12 | 13 | ## Cookie 和类似技术 14 | 15 | 我们使用 Cookie 或类似技术识别您的购买记录,以便激活付费功能。 16 | 17 | ## 数据共享和披露 18 | 19 | 我们不会与任何第三方共享您的数据,除非: 20 | 21 | 1. 获得您的明确同意。 22 | 2. 法律要求:在法律要求的情况下披露信息,例如配合法律程序、法庭命令或政府要求。 23 | 3. 保护权利:为保护我们的权利、隐私、安全或财产,以及我们的用户或公众的权利、隐私、安全或财产。 24 | 25 | ## 云端存储集成 26 | 27 | 在您使用 “巨魔录音机“ 时,我们可能会访问您的 Google Drive、Dropbox 或 Microsoft OneDrive 数据来提供服务。 28 | 29 | 1. 信息的收集: 30 | 1. 个人信息:您使用 Google、Dropbox 或 Microsoft 账户登录时,我们可能会访问您 Google、Dropbox 或 Microsoft 账户的基础信息(例如:电子邮件地址)。 31 | 2. 录音数据:您创建的音频文件和相关数据。 32 | 2. 信息的使用: 33 | 1. 文件存储:将用户的录音文件上传到用户的 Google Drive、Dropbox 或 Microsoft OneDrive 云端存储。 34 | 2. 文件管理:读取和管理存储在用户 Google Drive、Dropbox 或 Microsoft OneDrive 云端的录音文件。 35 | 36 | ## 与其他网站的链接 37 | 38 | 虽然我们可能会在应用程序中包含链接,以方便您使用和参考,但我们不对这些外部网站的隐私政策负责。我们建议您注意这些网站的隐私政策可能与我们的不同。 39 | 40 | ## 数据安全 41 | 42 | 我们优先考虑您个人信息的安全。然而,尽管我们采用商业上可接受的方式来保护您的个人信息,但请注意没有任何电子存储或互联网传输方式是完全安全的。我们不能保证绝对安全,但我们会尽力提供最高级别的保护。 43 | 44 | ## 用户的权利 45 | 46 | 用户有权访问、更正或删除其在我们这里存储的个人信息。用户可以通过下方联系方式与我们联系以行使这些权利。 47 | 48 | - 电子邮件:[82flex@gmail.com](mailto:82flex@gmail.com) 49 | - 官方网站:[TrollRecorder](https://github.com/Lessica/TrollRecorder/issues/) 50 | 51 | ## 本隐私政策的变更 52 | 53 | 本隐私政策自 2024/06/07 起生效。我们保留随时更新或更改隐私政策的权利。任何变更在本页面发布后立即生效。 54 | 55 | 我们建议您定期查看本隐私政策。如果我们对本隐私政策作出任何重大变更,我们将通过您提供给我们的电子邮件地址通知您,或在我们的应用程序上发布醒目通知。 56 | 57 | ## 联系我们 58 | 59 | 如果您对本隐私政策有任何问题或疑虑,请通过 [82flex@gmail.com](mailto:82flex@gmail.com) 联系我们。 60 | -------------------------------------------------------------------------------- /res/zh-Hant-HK.lproj/EULA.md: -------------------------------------------------------------------------------- 1 | # 最終用戶授權合約 2 | 3 | 歡迎您使用“巨魔錄音機”軟體及相關服務!在此特別提醒您(用戶)在使用和購買之前,請認真閱讀《巨魔錄音機用戶協議與隱私條款》(以下簡稱“本協定”),確保您充分理解本協定中各條款,特別是涉及免除或者限制責任的條款、權利許可和資訊使用的條款、同意開通的條款、法律適用和爭議解决條款等。其中,免除或者限制責任條款等重要內容將以加粗形式提示您注意,您應重點閱讀。如您未滿18周歲,請您在法定監護人陪同下仔細閱讀並充分理解本協定,並征得法定監護人的同意後下載本軟體。請您審慎閱讀並選擇接受或不接受本協定。 4 | 5 | 除非您接受本協定所有條款,否則您無權購買或使用(以下統稱“使用”)“巨魔錄音機”軟體或者通過任何方式使用“巨魔錄音機”服務,或者獲得“巨魔錄音機”軟體提供的任何服務。若您使用“巨魔錄音機”軟體及相關服務(以下簡稱“本服務”),則視為您已充分理解本協定,並承諾作為本協定的一方當事人接受本協定各項條款的約束。“巨魔錄音機”軟體服務由 [@Lessica](https://github.com/Lessica) 開發並發佈。本協議約定“巨魔錄音機”軟體開發者與用戶之間關於“巨魔錄音機”軟體服務(以下簡稱“服務”)的權利義務。 6 | 7 | ## 定義 8 | 9 | 1. 用戶:指所有直接或間接獲取和使用“巨魔錄音機”軟體及相關服務的自然人、法人和其他組織等。在本協定中稱為“用戶”或稱“您”。 10 | 2. “巨魔錄音機”指由 [@Lessica](https://github.com/Lessica) 合法擁有並營運的、標注名稱為“巨魔錄音機”的用戶端應用程序、小程式、微信訂閲賬號、今日頭條頭條號、微博以及 [TrollRecorder](https://trollrecorder.app) 的網站等。 11 | 3. 本協定內容同時包括“巨魔錄音機”軟體開發者已經發佈及後續可能不斷發佈的關於“巨魔錄音機”軟體及相關服務的相關協定、規則等內容。前述內容一經正式發佈,並以適當的方式送達用戶(系統通知、簡訊、電話等),即為本協定不可分割的組成部分,您應同樣遵守。若用戶不接受修改後的條款,請立即停止使用服務,用戶繼續使用服務視為接受修改後的協定。 12 | 13 | ## 軟體購買 14 | 15 | ### 免費使用 16 | 17 | “巨魔錄音機”提供免費使用版本,在功能上相較於收費版本區別如下: 18 | 19 | 1. 免費版本不提供任何形式的技術支援。 20 | 2. 免費版本的部分設定選項(包括但不限於懸浮球、地理位置、混音器與合併器選項)將被限制使用。 21 | 3. 免費版本的錄音檔案將被加入浮水印。 22 | 23 | 用戶在使用免費版本的“巨魔錄音機”時也應遵守本協定內約定的全部內容。 24 | 25 | ### 授權使用 26 | 27 | 用戶在使用“巨魔錄音機”收費服務前需要購買一個“巨魔錄音機”使用授權。“巨魔錄音機”使用授權應當使用設備 UDID 號碼綁定注册,單個授權可綁定一臺設備 UDID。 28 | 29 | ### 您必須承諾和保證 30 | 31 | 1. 您瞭解並同意,用戶須對購買資訊的真實性、合法性、有效性承擔全部責任;用戶不得冒充他人,不得利用他人的名義發佈任何資訊;不得惡意使用授權設備導致其他用戶誤認;否則我們有權立即停止提供服務,您獨自承擔由此而產生的一切法律責任。 32 | 2. 您的授權在遺失或遺忘後,可遵照相應的申訴途徑及時申訴請求找回。 33 | 3. 因您自身原因或其他不可抗因素而導致授權被盜、遺失,均由您本人承擔責任,“巨魔錄音機”軟體開發者不承擔任何責任。 34 | 35 | ## 隱私政策 36 | 37 | “巨魔錄音機”軟體開發者依據法律法規收集、使用個人資訊,堅持遵循個人資訊收集、使用的合法、正當、必要的原則。在使用“巨魔錄音機”軟體及相關服務前,請您務必仔細閱讀並透徹理解本隱私政策,“巨魔錄音機”軟體開發者可能會收集和使用您的相關資訊,您一旦選擇使用“巨魔錄音機”軟體及相關服務,即意味著同意“巨魔錄音機”軟體開發者按照本隱私政策收集、保存、使用、共亯、披露及保護您的資訊。 38 | 39 | ### 本軟體可能收集的資訊 40 | 41 | #### 您可能提供的資訊 42 | 43 | 網絡身份標識資訊(包括系統帳號、IP 地址、電子郵箱地址等);通訊資訊;設備資訊(包括設備型號、設備 MAC 地址、操作系統類型、設備設定);軟體列表唯一設備識別碼(如 IMEI/IDFA/UDID/GUID、SIM 卡 IMSI 資訊等在內的描述個人常用設備基本情况的資訊);位置資訊(包括行程資訊、精准定位資訊、住宿資訊、經緯度等)。 44 | 45 | #### “巨魔錄音機”軟體開發者可能收集的資訊 46 | 47 | 1. 日誌資訊 48 | 當您使用“巨魔錄音機”軟體開發者提供的服務時,可能會通過 Cookie 或其他方式自動採集一些與您個人身份無關的日誌資訊。這些資訊包括:設備或應用資訊,例如您使用的硬體設備、網頁瀏覽器或用於接入本平臺服務的其他程式所提供的配置資訊、您的IP地址、您所使用的硬體設備的版本及設備識別碼等。 49 | 50 | 2. 位置資訊 51 | 當您通過具有定位功能的移動設備使用本軟體提供的服務時,系統會通過 GPS 或 Wi-Fi 等管道收集您的地理位置資訊。您可以通過關閉定位功能,停止對您的地理位置資訊的收集。 52 | 53 | ### “巨魔錄音機”軟體開發者可能如何使用資訊 54 | 55 | “巨魔錄音機”軟體開發者可能將所收集的資訊用作如下用途: 56 | 57 | 1. 向您提供服務; 58 | 2. 幫助本產品及服務的設計、優化和升級; 59 | 3. 向您提供個性化服務,例如向您展示、推送與您更加相關的知識內容和廣告; 60 | 4. 用於身份驗證、安全防範、存檔和備份,確保服務的安全性。 61 | 62 | ### 資訊的保護 63 | 64 | “巨魔錄音機”軟體開發者將運用各種安全技術和程式來建立完善的管理制度來保護您的個人資訊。 65 | 66 | ### 資訊的合理利用及披露原則 67 | 68 | “巨魔錄音機”軟體開發者不會將您的個人資訊轉移或披露給任何非關聯的第三方,除非: 69 | 70 | 1. 事先獲得您的明確授權同意; 71 | 2. 相關法律法規或法院、政府機關要求; 72 | 3. 為完成合併、分立、收購或資產轉讓而轉移,但“巨魔錄音機”軟體開發者會要求新的持有您的個人資訊的公司、組織或個人繼續受本隱私政策的約束,否則,“巨魔錄音機”軟體開發者有權要求該公司、組織或個人重新取得您的授權同意; 73 | 4. 在法律法規允許的範圍內,為維護“巨魔錄音機”其他用戶、“巨魔錄音機”軟體開發者的生命、財產等合法權益或維權產品或服務的安全穩定運行所必需的,例如查找、預防、處理欺詐等違法活動和减少信用風險等; 74 | 5. “巨魔錄音機”軟體開發者為維護合法權益而向用戶提起訴訟或仲裁; 75 | 6. 以維護公共利益或學術研究為目的; 76 | 77 | 請您注意,“巨魔錄音機”軟體開發者在您使用相關產品或服務時不會讓您提供任何財產帳戶、銀行卡、信用卡、第三方支付帳戶及對應密碼等,請勿在使用服務中透露自己的各類財產帳戶、銀行卡、信用卡、第三方支付帳戶及對應密碼等重要資訊,否則由此帶來的任何損失由您自行承擔。 78 | 79 | ## 用戶使用規範 80 | 81 | 1. 本條所述內容是指用戶使用“巨魔錄音機”的過程中所傳播、上傳的任何內容,包括但不限於文字、語音、圖片、視頻、圖文等發送、回復或自動回復消息和相關連結頁面,以及其他使用本服務所產生的內容。 82 | 2. 用戶不得利用“巨魔錄音機”服務傳播如下法律法規和政策禁止的內容: 83 | 1. 反對憲法所確定的基本原則的; 84 | 2. 危害國家安全,洩露國家秘密,顛覆國家政權,破壞國家統一的; 85 | 3. 損害國家榮譽和利益的; 86 | 4. 煽動民族仇恨、民族歧視,破壞民族團結的; 87 | 5. 破壞國家宗教政策,宣揚邪教和封建迷信的; 88 | 6. 散佈謠言,擾亂社會秩序,破壞社會穩定的; 89 | 7. 散佈淫穢、色情、賭博、暴力、兇殺、恐怖或者教唆犯罪的; 90 | 8. 侮辱或者誹謗他人,侵害他人合法權益的; 91 | 9. 含有法律、行政法規禁止的其他內容的資訊。 92 | 3. 用戶不得利用“巨魔錄音機”服務傳播如下干擾“巨魔錄音機”正常運營,或侵犯其他用戶或第三方合法權益的內容: 93 | 1. 含有任何性或暗示性的; 94 | 2. 含有辱駡、恐嚇、威脅內容的; 95 | 3. 含有騷擾、垃圾廣告、惡意資訊、誘騙資訊的; 96 | 4. 涉及他人隱私、個人資訊的; 97 | 5. 侵害他人名譽權、肖像權、知識產權、商業秘密等合法權利權益的; 98 | 6. 含有其他干擾本服務正常運營和侵權其他用戶或第三方合法權益內容的資訊。 99 | 4. 未經“巨魔錄音機”軟體開發者書面許可,您不得自行或授權、允許、協助任何第三人對本協定“巨魔錄音機”軟體及相關服務中資訊內容進行如下行為: 100 | 1. 複製、讀取、採用“巨魔錄音機”軟體及相關服務的資訊內容,用於包括但不限於文宣、新增閱讀量、流覽量等商業用途; 101 | 2. 擅自編輯、整理、編排“巨魔錄音機”軟體及相關服務的資訊內容後在“巨魔錄音機”軟體及相關服務的源頁面以外的渠道進行展示; 102 | 3. 採用包括但不限於特殊標識、特殊程式碼等任何形式的識別方法,自行或協助第三人對“巨魔錄音機”軟體及相關服務的資訊內容產生流量、閱讀量引導、轉移、劫持等不利影響; 103 | 4. 其他非法獲取或使用“巨魔錄音機”軟體及相關服務的資訊內容的行為。 104 | 5. 如果“巨魔錄音機”軟體開發者發現或者收到他人舉報或投訴用戶違反本協議約定的,“巨魔錄音機”軟體開發者有權不經通知隨時處以包括但不限於警告、設備封禁的處罰,且通知用戶處理結果。 105 | 6. 因違反使用者協定被封禁的用戶,可以自行與“巨魔錄音機”軟體開發者聯繫。封禁用戶可提交申訴,“巨魔錄音機”軟體開發者將對申訴進行審查,並自行合理判斷决定是否變更處罰措施。 106 | 7. 用戶理解並同意,“巨魔錄音機”軟體開發者有權依合理判斷對違反有關法律法規或本協定的行為進行處罰,對違法違規的任何用戶採取適當的法律行動,並依據法律法規保存有關資訊向有關部門報告等,用戶應承擔由此而產生的一切法律責任。 107 | 8. 用戶理解並同意,因用戶違反本協議約定,導致或產生的任何第三方主張的任何索賠、要求或損失,包括合理的律師費,用戶應當賠償“巨魔錄音機”軟體開發者,並使之免受損害。 108 | 109 | ## 基於軟體提供服務 110 | 111 | “巨魔錄音機”軟體開發者依託“巨魔錄音機”軟體向您提供服務,您還應遵守以下約定: 112 | 113 | 1. 您在使用本服務的過程中可能需要下載軟體,對於這些軟體,“巨魔錄音機”軟體開發者給予您一項個人的、不可轉讓及非排他性的許可。您僅可為訪問或使用本服務的目的而使用這些軟體。 114 | 2. 您使用“巨魔錄音機”軟體及相關服務,可以通過訪問“巨魔錄音機”官網相關網站。若您並非從“巨魔錄音機”軟體開發者或經“巨魔錄音機”軟體開發者授權第三方獲取本軟體的,“巨魔錄音機”軟體開發者無法保證非官方版本的“巨魔錄音機”軟體能够正常使用,您囙此遭受的損失與“巨魔錄音機”軟體開發者無關。 115 | 3. 為了改善用戶體驗、保證服務的安全性及產品功能的一致性,“巨魔錄音機”軟體開發者可能會對軟體進行更新。您應該將相關軟體更新到最新版本,否則“巨魔錄音機”軟體開發者並不保證其能正常使用,您有權選擇接受更新版本或服務,如您不接受,部分功能將受到限制或不能繼續使用。如您不再需要使用“巨魔錄音機”軟體及相關服務可自行卸載。 116 | 4. 除非“巨魔錄音機”軟體開發者書面許可,您不得從事下列任一行為: 117 | 1. 删除軟體及其副本上關於著作權的資訊; 118 | 2. 對軟體進行反向工程、反向彙編、反向編譯,或者以其他方式嘗試發現軟體的原始程式碼; 119 | 3. 對“巨魔錄音機”軟體開發者擁有知識產權的內容進行使用、出租、出借、複製、修改、連結、轉載、匯編、發表、出版、建立鏡像網站等; 120 | 4. 對軟體或者軟體運行過程中釋放到任何終端記憶體中的數據、軟體運行過程中用戶端與伺服器端的互動數據,以及軟體運行所必需的系統數據,進行複製、修改、新增、删除、掛接運行或創作任何衍生作品,形式包括但不限於使用挿件、外掛或非經“巨魔錄音機”軟體開發者授權的第三方工具/服務接入軟體和相關系統; 121 | 5. 通過修改或偽造軟體運行中的指令、數據,新增、删减、變動軟體的功能或運行效果,或者將用於上述用途的軟體、方法進行運營或向公眾傳播,無論這些行為是否為商業目的; 122 | 6. 通過非“巨魔錄音機”軟體開發者開發、授權的第三方軟體、挿件、外掛、系統,登入或使用“巨魔錄音機”軟體開發者軟體及服務,或製作、發佈、傳播非“巨魔錄音機”軟體開發者開發、授權的第三方軟體、挿件、外掛、系統。 123 | 124 | ## 用戶發送、傳播、使用規則 125 | 126 | 1. 用戶在本服務中或通過本服務所傳送、發佈的任何內容並不反映或代表,也不得被視為反映或代表“巨魔錄音機”軟體開發者的觀點、立場或政策,“巨魔錄音機”軟體開發者對此不承擔任何責任。 127 | 2. 用戶不得利用服務進行如下行為: 128 | 1. 提交、發佈虛假資訊,或盜用他人頭像或資料,冒充、利用他人名義; 129 | 2. 強制、誘導其他用戶關注、點擊連結頁面或分享資訊; 130 | 3. 虛構事實、隱瞞真相以誤導、欺騙他人; 131 | 4. 利用科技手段批量建立虛假帳號; 132 | 5. 利用“巨魔錄音機”服務從事任何違法犯罪活動; 133 | 6. 製作、發佈與以上行為相關的方法、工具,或對此類方法、工具進行運營或傳播,無論這些行為是否為商業目的; 134 | 7. 其他違反法律法規規定、侵犯其他用戶合法權益、干擾“巨魔錄音機”正常運營或“巨魔錄音機”軟體開發者未明示授權的行為。 135 | 3. 用戶須對利用服務傳送資訊(包括但不限於網頁、文字、圖片、音訊、視頻、圖表等)的真實性、合法性、無害性、準確性、有效性等全權負責、與用戶所傳播的資訊相關的任何法律責任由用戶自行承擔,與“巨魔錄音機”軟體開發者無關。如因此給“巨魔錄音機”軟體開發者或第三方造成損害的,用戶應當依法予以賠償。 136 | 4. 您發送或傳播的內容應有合法來源,相關內容為您所有或您已獲得權利人的授權。 137 | 5. 您使用本服務時不得違反國家法律法規、侵害他人合法權益。您理解並同意,如您被他人投訴侵權或您投訴他人侵權,“巨魔錄音機”軟體開發者有權將爭議中相關方的主體、聯繫方式、投訴相關內容等必要資訊提供給其他爭議方或相關部門,以便及時解决投訴糾紛,保護他人合法權益。 138 | 6. “巨魔錄音機”軟體開發者提供的服務中可能包括廣告,用戶同意在使用過程中顯示“巨魔錄音機”軟體開發者和第三方供應商、合作夥伴提供的廣告。除法律法規明確規定外,用戶應自行對該廣告進行的交易負責,對用戶因依該廣告資訊進行的交易或前述廣告商提供的內容而遭受的損失或損害,“巨魔錄音機”軟體開發者不承擔任何責任。 139 | 7. 用戶不得對“巨魔錄音機”軟體開發者服務中涉及的連結或推廣內容進行域名劫持、惡意跳轉、惡意刷量、作弊等,否則,“巨魔錄音機”軟體開發者有權立即扣除對應部分應獲得的收益,因此給“巨魔錄音機”軟體開發者造成損失的,用戶應全額賠償該損失。 140 | 141 | ## 遵守當地法律監管 142 | 143 | 1. 您在使用本服務過程中應當遵守當地相關的法律法規,並尊重當地的道德和風俗習慣。如果您的行為違反了當地法律法規或道德風俗,您應當為此獨立承擔責任。 144 | 2. 您應避免因使用本服務而使“巨魔錄音機”軟體開發者捲入政治和公共事件,否則“巨魔錄音機”軟體開發者有權暫停或終止對您的服務。 145 | 146 | ## 違約處理 147 | 148 | 1. 針對您違反本協定或其他服務條款的行為,“巨魔錄音機”軟體開發者有權獨立判斷並視情况採取預先警示、永久封禁等措施。“巨魔錄音機”軟體開發者有權公告處理結果,且有權根據實際情況决定是否恢復使用。對涉嫌違反法律法規、涉嫌違法犯罪的行為將保存有關記錄,並依法向有關主管部門報告、配合有關主管部門調查。 149 | 2. 因您違反本協定或其他服務條款規定,引起第三方投訴或訴訟索賠的,您應當自行承擔全部法律責任。因您的違法或違約行為導致““巨魔錄音機”軟體開發者向任何第三方賠償或遭受國家機關處罰的,您還應足額賠償“巨魔錄音機”軟體開發者囙此遭受的全部損失。 150 | 3. 因您違反本協定或其他服務條款規定,造成“巨魔錄音機”軟體開發者向任何第三方承擔責任的,“巨魔錄音機”軟體開發者在承擔所述責任後有權向用戶足額追償,由此產生的合理費用包括但不限於,律師費、公證費、鑒定費、差旅費、訴訟費等亦由用戶承擔。 151 | 4. “巨魔錄音機”軟體開發者尊重並保護法人、公民的知識產權、名譽權、姓名權、隱私權等合法權益。您保證,在使用相關服務時上傳的文字、圖片、視頻、音訊、連結等不侵犯任何第三方的知識產權、名譽權、姓名權、隱私權等權利及合法權益。針對第三方提出的全部權利主張,您應自行承擔全部法律責任;如因您的侵權行為導致“巨魔錄音機”軟體開發者遭受損失的(包括經濟、商譽等損失),您還應足額賠償“巨魔錄音機”軟體開發者遭受的全部損失。 152 | 153 | ## 不可抗力及其他免責事由 154 | 155 | 1. 您理解並同意,在使用本服務的過程中,可能會遇到不可抗力等風險因素,使本服務發生中斷。不可抗力是指不能預見、不能克服並不能避免且對一方或雙方造成重大影響的客觀事件,包括但不限於自然灾害如洪水、地震、瘟疫流行和風暴等以及社會事件如戰爭、動亂、政府行為等。出現上述情况時,“巨魔錄音機”軟體開發者將努力在第一時間與相關組織配合,及時進行修復,但是由此給您造成的損失“巨魔錄音機”軟體開發者在法律允許的範圍內免責。 156 | 2. 在法律允許的範圍內,“巨魔錄音機”軟體開發者對以下情形導致的服務中斷或受阻不承擔責任: 157 | 1. 受到電腦病毒、木馬或其他惡意程式、駭客攻擊的破壞; 158 | 2. 用戶或“巨魔錄音機”軟體開發者的電腦軟體、系統、硬體和通信線路出現故障; 159 | 3. 用戶操作不當; 160 | 4. 用戶通過非“巨魔錄音機”軟體開發者授權的方式使用本服務; 161 | 5. 其他“巨魔錄音機”軟體開發者無法控制或合理預見的情形。 162 | 3. 您理解並同意,“巨魔錄音機”軟體及相關服務可能會受多種因素的影響或干擾,“巨魔錄音機”軟體開發者不保證(包括但不限於): 163 | 1. “巨魔錄音機”軟體開發者完全適合用戶的使用要求; 164 | 2. “巨魔錄音機”軟體開發者不受干擾,及時、安全、可靠或不出現錯誤;您經由“巨魔錄音機”軟體開發者取得的任何軟體、服務或其他資料符合用戶的期望; 165 | 3. 軟體中任何錯誤都將能得到更正。 166 | 4. 您理解並同意,在使用本服務的過程中,可能會遇到網絡資訊或其他用戶行為帶來的風險,“巨魔錄音機”軟體開發者不對任何資訊的真實性、適用性、合法性承擔責任,也不對因侵權行為給您造成的損害負責。這些風險包括但不限於: 167 | 1. 來自他人匿名或冒名的含有威脅、誹謗、令人反感或非法內容的資訊; 168 | 2. 因使用本協定項下的服務,遭受他人誤導、欺騙或其他導致或可能導致的任何心理、生理上的傷害以及經濟上的損失; 169 | 3. 其他因網絡資訊或用戶行為引起的風險。 170 | 5. 對於涉嫌借款、理財、證券、保險以及其他涉財產的網絡資訊、帳戶密碼、廣告或推廣等資訊的,請您謹慎對待並自行進行判斷,基於前述原因您因此遭受的利潤、商業信譽、資料損失或其他有形或無形損失,“巨魔錄音機”軟體開發者不承擔任何直接、間接、附帶、特別、衍生性或懲罰性的賠償責任。 171 | 6. 您理解並同意,本服務並非為某些特定目的而設計,包括但不限於核設施、軍事用途、醫療設施、交通通訊等重要領域。如果因為軟體或服務的原因導致上述操作失敗而帶來的人員傷亡、財產損失和環境破壞等,“巨魔錄音機”軟體開發者不承擔法律責任。 172 | 7.“巨魔錄音機”軟體開發者依據本協議約定獲得處理違法違規內容的權利,該權利不構成“巨魔錄音機”軟體開發者的義務或承諾,“巨魔錄音機”軟體開發者不能保證及時發現違法行為或進行相應處理。 173 | 174 | ## 服務的變更、中斷、終止 175 | 176 | 1. 您理解並同意,“巨魔錄音機”軟體開發者提供的服務是按照現有科技和條件所能達到的現狀提供的。“巨魔錄音機”軟體開發者會盡最大努力向您提供服務,確保服務的連貫性和安全性。您理解,“巨魔錄音機”軟體開發者不能隨時預見和防範科技以及其他風險,包括但不限於不可抗力、病毒、木馬、駭客攻擊、系統不穩定、第三方服務瑕疵及其他各種安全問題的侵擾等原因可能導致的服務中斷、資訊丟失以及其他的損失和風險。 177 | 2. 您理解並同意,“巨魔錄音機”軟體開發者為了服務整體運營的需要,有權在公告通知後修改、中斷、中止或終止相關服務,而無須向用戶負責或承擔任何賠償責任。 178 | 179 | ## 管轄與法律適用 180 | 181 | 1. 本協定的成立、生效、履行、解釋及糾紛解决,適用中華人民共和國大陸地區法律(不包括衝突法)。 182 | 2. 本協定簽訂地為中華人民共和國江蘇省南京市江寧區。 183 | 3. 若您和“巨魔錄音機”軟體開發者之間發生任何糾紛或爭議,首先應友好協商解决;協商不成的,您同意將糾紛或爭議提交本協定簽訂地(即中華人民共和國江蘇省南京市江寧區)有管轄權的人民法院管轄。 184 | 4. 本協定所有條款的標題僅為閱讀方便,本身並無實際涵義,不能作為本協定涵義解釋的依據。 185 | 5. 本協定條款無論因何種原因部分無效或不可執行,其餘條款仍有效,對雙方具有約束力。 186 | 187 | **譯自簡體中文版本。如需本協議的原始副本,請與我們聯繫。對於原始版本與翻譯版本之間的差異,應以原始版本為準。** 188 | -------------------------------------------------------------------------------- /res/zh-Hant-HK.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "巨魔錄音機"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "“巨魔錄音機” 需要訪問你的聯絡人,以便在錄音中顯示聯絡人姓名。"; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "“巨魔錄音機” 需要使用 Face ID 以驗證你的身份。"; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "“巨魔錄音機” 需要訪問你的位置,以便在錄音中保存位置資訊。"; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "“巨魔錄音機” 需要訪問你的位置,以便在錄音中保存位置資訊。"; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "“巨魔錄音機” 需要訪問你的位置,以便在錄音中保存位置資訊。"; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "“巨魔錄音機” 需要訪問你的位置,以便在錄音中保存位置資訊。"; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "“巨魔錄音機” 需要訪問你的位置,以便在錄音中保存位置資訊。"; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "“巨魔錄音機” 需要訪問你的麥克風,以便實現最基本的錄音功能。"; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "“巨魔錄音機” 需要使用語音辨識技術為你的錄音製作文字副本。"; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "巨魔錄音機"; 33 | 34 | /* TODO */ 35 | "Toggle Call Recording" = "Toggle Call Recording"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "打開/關閉懸浮球"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "開始/停止錄音"; 42 | 43 | /* TODO */ 44 | "Toggle System Audio Recording" = "Toggle System Audio Recording"; 45 | 46 | /* TODO */ 47 | "Toggle Voice Memo Recording" = "Toggle Voice Memo Recording"; 48 | -------------------------------------------------------------------------------- /res/zh-Hant-HK.lproj/PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | # 隱私政策 2 | 3 | “巨魔錄音機” 承諾保護您的隱私。本隱私政策概述了我們如何處理您的個人資訊。 4 | 5 | ## 個人數據處理 6 | 7 | “巨魔錄音機” 不會收集或上傳任何個人數據和錄音。應用程式請求的所有權限都是爲了提供應用程式功能所需的服務。大多數數據處理在您的裝置本地執行。 8 | 9 | 然而,與技術錯誤有關的數據可能會被上載到伺服器,以幫助改進應用程式功能,同時可能會使用 Cookie 和類似技術來識別您的購買記錄。 10 | 11 | ## 技術錯誤報告 12 | 13 | 我們收集使用應用程式時遇到技術錯誤時的數據,以幫助改善功能。這些數據將被安全上載到 Bugsnag 伺服器中,並按照其數據安全協議處理。有關如何使用這些數據的更多資訊,請參閲 [Bugsnag Security](https://www.bugsnag.com/product/security/) 上有關數據安全的資訊。 14 | 15 | ## Cookie 和類似技術 16 | 17 | 我們使用 Cookie 或類似技術識別您的購買記錄,以便啟動付費功能。 18 | 19 | ## 第三方 20 | 21 | 您的個人資訊保密,我們不會與第三方共享。 22 | 23 | ## 與其他網站的連結 24 | 25 | 雖然我們可能會在應用程式中包含連結,以方便您使用和參考,但我們不對這些外部網站的隱私政策負責。我們建議您注意這些網站的隱私政策可能與我們的不同。 26 | 27 | ## 數據安全 28 | 29 | 我們優先考慮您個人資訊的安全。然而,儘管我們採用商業上可接受的方式來保護您的個人資訊,但請注意沒有任何電子存儲或互聯網傳輸方式是完全安全的。我們不能保證絕對安全,但我們會盡力提供最高級別的保護。 30 | 31 | ## 本隱私政策的變更 32 | 33 | 本隱私政策自 2024/2/29 起生效。我們保留隨時更新或更改隱私政策的權利。任何變更在本頁面發佈後立即生效。 34 | 35 | 我們建議您定期查看本隱私政策。如果我們對本隱私政策作出任何重大變更,我們將通過您提供給我們的電子郵寄地址通知您,或在我們的應用程式上發佈醒目通知。 36 | 37 | ## 聯繫我們 38 | 39 | 如果您對本隱私政策有任何問題或疑慮,請通過 [82flex@gmail.com](mailto:82flex@gmail.com) 聯繫我們。 40 | 41 | **譯自簡體中文版本。如需本協議的原始副本,請與我們聯繫。對於原始版本與翻譯版本之間的差異,應以原始版本為準。** 42 | -------------------------------------------------------------------------------- /res/zh-Hant-TW.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "CFBundleDisplayName" = "巨魔錄音機"; 3 | 4 | /* No comment provided by engineer. */ 5 | "NSContactsUsageDescription" = "巨魔錄音機需要訪問你的聯繫人,以便在錄音中顯示聯繫人姓名。"; 6 | 7 | /* No comment provided by engineer. */ 8 | "NSFaceIDUsageDescription" = "巨魔錄音機需要使用面容 ID 以驗證你的身份。"; 9 | 10 | /* No comment provided by engineer. */ 11 | "NSLocationAlwaysAndWhenInUseUsageDescription" = "巨魔錄音機需要訪問你的位置,以便在錄音中保存位置信息。"; 12 | 13 | /* No comment provided by engineer. */ 14 | "NSLocationAlwaysUsageDescription" = "巨魔錄音機需要訪問你的位置,以便在錄音中保存位置信息。"; 15 | 16 | /* No comment provided by engineer. */ 17 | "NSLocationTemporaryUsageDescriptionDictionary" = "巨魔錄音機需要訪問你的位置,以便在錄音中保存位置信息。"; 18 | 19 | /* No comment provided by engineer. */ 20 | "NSLocationUsageDescription" = "巨魔錄音機需要訪問你的位置,以便在錄音中保存位置信息。"; 21 | 22 | /* No comment provided by engineer. */ 23 | "NSLocationWhenInUseUsageDescription" = "巨魔錄音機需要訪問你的位置,以便在錄音中保存位置信息。"; 24 | 25 | /* No comment provided by engineer. */ 26 | "NSMicrophoneUsageDescription" = "巨魔錄音機需要訪問你的麥克風,以便實現最基本的錄音功能。"; 27 | 28 | /* No comment provided by engineer. */ 29 | "NSSpeechRecognitionUsageDescription" = "巨魔錄音機需要使用語音識別技術為你的錄音製作文字副本。"; 30 | 31 | /* No comment provided by engineer. */ 32 | "NSUbiquitousContainerName" = "巨魔錄音機"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Toggle Call Recording" = "啓停通話錄音"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Toggle Hoverball" = "切換懸浮球"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Toggle Recording" = "啓停錄音"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Toggle System Audio Recording" = "啓停系統音頻錄制"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Toggle Voice Memo Recording" = "啓停語音備忘錄"; 48 | --------------------------------------------------------------------------------