diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..78be535
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,21 @@
+name: build
+
+on:
+ push:
+ branches: [ main, "feature/**" ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ name: 빌드 · 단위 테스트
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ - name: 빌드
+ run: dotnet build SoundVisualizer.slnx -c Release
+ - name: 단위 테스트
+ run: dotnet test SoundVisualizer.Tests/SoundVisualizer.Tests.csproj -c Release --no-build
diff --git a/.gitignore b/.gitignore
index dca8637..e73bbb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ obj/
*.suo
*.sln.docstates
.agents/mcp_config.json
+*.lscache
diff --git a/README.ko.md b/README.ko.md
index 076ff1d..824a941 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -4,7 +4,7 @@
-# Sound Visualizer
+# SoundVisualizer
@@ -20,9 +20,11 @@
-> **Sound Visualizer**는 컴퓨터의 시스템 오디오를 실시간으로 캡처하여 방향·크기·의미를 그래픽 오버레이로 변환하는 데스크톱 애플리케이션입니다.
+> **SoundVisualizer**는 컴퓨터의 시스템 오디오를 실시간으로 캡처하여 방향·크기·의미를 그래픽 오버레이로 변환하는 데스크톱 애플리케이션입니다.
단순한 파형 시각화를 넘어, **YAMNet**과 **ONNX Runtime** 기반의 AI 사운드 분류기를 내장하여 현재 재생 중인 소리의 종류(배경음, 대화음, 위협음 등)를 실시간으로 인지하고 피드백을 제공합니다.
+> 📱 안드로이드 버전도 있습니다: [SoundVisualizer for Android](https://github.com/bibibic76/SoundVisualizer)
+
---
## 📖 목차
@@ -31,7 +33,6 @@
- [시스템 및 AI 아키텍처](#-시스템-및-ai-아키텍처)
- [설치 및 실행 방법](#-설치-및-실행-방법)
- [빌드 가이드 (개발자용)](#-빌드-가이드-개발자용)
-- [팀원 및 역할](#-팀원-및-역할)
- [라이선스](#-라이선스)
---
@@ -55,21 +56,40 @@
- **원형 모드 (Circle)**: 화면 중앙을 기점으로 바깥쪽으로 퍼져나가는 원형 파동을 렌더링합니다.
- **패드 모드 (Pad)**: 지정된 공간 그리드(방향)에 고정된 패드 형태로 소리를 표현합니다.
- **외곽선 모드 (Outline)**: 파도 모드를 기반으로, 파도의 상한 외곽선 테두리만 표현하여 화면 가림을 최소화합니다.
+- 30 FPS부터 모니터 주사율까지, 최대 프레임을 얼마로 두어도 그래픽이 같은 빠르기로 움직입니다.
### 2. 온스크린 실시간 제어 및 에디터
전체 화면 게임을 내리지 않고도 오버레이 상태에서 즉각적인 수정이 가능합니다.
- **F2 / F3 단축키**: 사운드 모드 및 시각화 모드 실시간 변경. 핫키는 변경 가능합니다.
-- **에디터 모드 (기본 F4키)**: 화면에 나타나는 가이드라인 경계를 직접 드래그하여 시각화 영역의 크기와 한계를 자유롭게 리사이징합니다. 색상, 투명도, AI 감지 민감도 등 모든 설정 또한 패널에서 조정 가능합니다.
+- **에디터 모드 (기본 F4키)**: 화면에 나타나는 가이드라인 경계를 직접 드래그하여 시각화 영역의 크기와 한계를 자유롭게 리사이징합니다. 색상, 투명도 등 모든 설정 또한 패널에서 조정 가능합니다.
### 3. 하드웨어 인식 7.1 다중 채널 지원
- 시스템의 오디오 채널 구성(Stereo, 5.1, 7.1 Surround)을 자동 인식하고, 소리의 발생 방향(각 채널)을 정확하게 매핑하여 입체적인 시각 효과를 렌더링합니다.
+- 사운드 모드는 실행할 때와 출력 장치가 바뀔 때 그 장치에 맞춰지며, 직접 바꿀 수도 있습니다.
+- 스테레오 출력에서는 좌우에 함께 있는 소리를 화면 위쪽에, 좌우 차이만 옆쪽에 그립니다. 그래서 가운데에서 나는 소리가 양쪽에서 나는 소리처럼 보이지 않습니다.
### 4. AI 실시간 사운드 분류 (ONNX & YAMNet)
- **3-Class 분류 체계**: 모든 소리를 `Ambient(환경음)`, `Speech(대화음)`, `Danger(위협음)` 3가지로 분석하여 카테고리별로 지정된 UI 색상과 투명도로 동적 시각 피드백을 제공합니다. 색상과 투명도는 커스터마이징이 가능합니다.
-- **Gunshot Booster**: 게임 및 영상 내의 총소리를 놓치지 않도록 특수 학습된 부스터 모델이 가동되어 위협음 탐지 민감도를 향상합니다.
-
-### 5. 글로벌 다국어 지원
-- 한국어, 영어, 일본어, 중국어, 스페인어, 프랑스어, 독일어, 러시아어 등 8개 국어를 지원합니다.
+- **위협 단서**: 사이렌, 경보, 폭발음, 폭죽 같은 경고음은 모델의 상위 후보 다섯 개 안에(5% 이상) 들기만 해도 `위협음`으로 봅니다. 음악이나 게임 소리에 묻혀도 놓치지 않기 위해서입니다. 총소리 이름은 오경보를 막기 위해 이렇게 끌어올리지 않습니다.
+- **흔들리지 않는 색**: `위협음`은 조건을 넘는 첫 결과에 바로 바뀌고, `대화음`과 `환경음`은 두 번 연달아 같아야 바뀌어 색이 깜빡이지 않습니다. 조용한 동안에는 AI 가 쉬고, 소리가 나면 바로 다시 듣습니다.
+- **모든 채널을 듣습니다**: 5.1/7.1 환경에서는 앞쪽뿐 아니라 뒤쪽·옆쪽 채널의 소리도 AI 가 듣습니다.
+- **늦게 온 위협음**: `위협음` 판정이 소리보다 조금 늦게 와도, 그 소리를 나던 때의 크기로 그립니다. 앞서 숨겨 둔 종류였던 소리도 그렇습니다.
+
+### 5. 소리마다 종류 정하기 (분류 탭)
+- AI 가 알아듣는 소리 521개를 안드로이드 버전과 같은 묶음으로 보여 주고, 묶음마다(고급 모드에서는 소리마다) `환경음`, `대화음`, `위협음` 가운데 무엇으로 볼지 선택합니다.
+- 이름으로 찾고, 종류나 바꾼 소리로 거르고, 한 번에 모두 되돌릴 수 있습니다. 바꾼 것은 오버레이의 AI 에 바로 반영됩니다.
+
+### 6. 도움말, 안내, 트레이 아이콘
+- **도움말 탭**: 시작하기, 표현 모드, 소리 종류, 단축키, 자주 묻는 질문, 정보가 미리 채워진 GitHub 이슈나 메일로 보내는 제보 버튼, 오픈소스 라이선스를 담았습니다.
+- **안내**: 캡처할 출력 장치가 없을 때, 소리 종류 구분을 쓸 수 없을 때, 시각화가 멈춘 까닭을 홈 탭에 알려 줍니다.
+- **트레이 아이콘**: 알림 영역에서 시각화를 켜고 끄거나, 런처로 돌아오거나, 종료할 수 있습니다.
+- **튜토리얼**: 처음 열면 실제 그리기 엔진으로 소리, 방향, 소리 종류가 어떻게 그려지는지 짧게 보여 줍니다. 홈 탭에서 다시 볼 수 있습니다.
+- **개발자 모드**: AI 가 들은 소리(상위 다섯 개, 확신도, 걸린 시간)를 오버레이 왼쪽 위에 띄우고, 모든 결과를 앱 옆 `ai-log` 폴더의 CSV 파일로 기록할 수 있습니다.
+- **키보드와 화면 읽어주기**: 런처의 모든 칸에 Tab 키로 닿습니다. 탭 줄에서는 화살표로 탭을 옮기고, 어디서나 `Ctrl + Tab` 으로 다음 탭으로 갑니다. 대화 창과 오버레이 편집 패널은 `Esc` 로 닫습니다. 언어 목록은 화살표로 열어 둘러보고 `Enter` 로 선택할 때만 언어가 바뀝니다. 아이콘 버튼, 단축키 버튼, 색 버튼, 소리 목록은 내레이터·NVDA 같은 화면 읽어주기가 이름으로 읽고, 실행 상태와 안내, 바꾼 소리 수는 바뀔 때 바로 읽힙니다. 편집 화면에서 마우스로 끄는 값은 모두 패널의 슬라이더로도 바꿀 수 있습니다.
+
+### 7. 글로벌 다국어 지원
+- 한국어, 영어, 일본어, 중국어(간체·번체), 베트남어, 태국어, 인도네시아어, 스페인어, 포르투갈어(브라질), 프랑스어, 이탈리아어, 독일어, 러시아어, 폴란드어, 터키어, 힌디어, 아랍어(오른쪽에서 왼쪽) 등 18개 언어를 지원합니다. 기본값은 Windows 표시 언어를 따릅니다.
+- 화면 글꼴은 Pretendard 이고, Pretendard 에 없는 문자는 Windows 글꼴로 보입니다.
---
@@ -78,20 +98,23 @@
본 프로젝트는 고성능 실시간 처리를 위해 철저한 메모리 관리 및 백그라운드 최적화로 설계되었습니다.
* **Core Audio API (WASAPI)**: Windows 시스템 전체 오디오를 딜레이 없이 루프백 캡처.
-* **DSP & FFT 연산**: 백그라운드 오디오 스레드에서 초당 수십 번의 고속 주파수 변환 연산 수행.
+* **채널별 크기 측정**: 캡처 스레드가 버퍼마다 채널별 최고 크기를 재고, 렌더 루프는 지난 프레임 뒤로 가장 큰 값을 가져갑니다.
* **Zero-Allocation 렌더링**: C# / WPF 환경에서 GC(가비지 컬렉터)의 개입을 최소화하여 프레임 드랍 방지.
-* **ONNX 추론 파이프라인**: 16kHz 모노 변환 및 Log-mel 스펙트로그램 전처리를 거친 후, `Microsoft.ML.OnnxRuntime` 엔진으로 전이 학습된 YAMNet 가중치 기반 실시간 분류 수행.
+* **프레임과 무관한 움직임, 쉬는 오버레이**: 스무딩은 60 FPS 기준으로 맞춥니다. 1초 동안 보이는 것이 없으면 프레임마다 그리지 않고 쉬다가, 소리가 나면 바로 깨어납니다.
+* **ONNX 추론 파이프라인**: 250ms 마다 최근 0.975초의 소리를 모노로 섞고, 앨리어싱 방지 필터를 거쳐 16kHz 로 바꾼 뒤, 모델이 만들어질 때와 같은 방식의 Log-mel 스펙트로그램으로 바꿔 `Microsoft.ML.OnnxRuntime` 의 YAMNet 으로 분류합니다. 모델 파일은 불러올 때 고정된 SHA-256 으로 확인하고, 각 단계는 기준 출력과 비교하는 단위 테스트로 검증합니다.
---
## 🚀 설치 및 실행 방법
-Sound Visualizer는 설치가 필요 없는 가벼운 **포터블(Portable)** 패키지로 제공됩니다.
+SoundVisualizer는 설치가 필요 없는 가벼운 **포터블(Portable)** 패키지로 제공됩니다. Windows 10/11 (64비트)에서 돌아가며, .NET 을 따로 설치하지 않아도 됩니다.
1. **[GitHub Releases](https://github.com/amophi/SoundVisualizer/releases)** 페이지에 접속합니다.
-2. 최신 버전의 `SoundVisualizer.zip` 파일을 다운로드 및 압축 해제합니다.
+2. 최신 버전의 `SoundVisualizer_vX.Y.zip` 파일을 다운로드해 원하는 곳에 압축 해제합니다.
3. 폴더 내의 **`SoundVisualizer.exe`**를 실행합니다.
-4. 런처에서 언어 및 초기 설정을 마친 후, **Start**를 클릭하여 오버레이를 활성화합니다.
+4. 런처에서 언어 및 초기 설정을 마친 후, **실행**을 눌러 오버레이를 켭니다.
+
+설정은 `SoundVisualizer.exe` 옆의 `settings.json` 에 저장되므로, 폴더를 그대로 옮기거나 복사해도 됩니다.
---
@@ -99,13 +122,19 @@ Sound Visualizer는 설치가 필요 없는 가벼운 **포터블(Portable)**
코드 수정 및 기여를 위해 소스에서 직접 빌드하는 방법입니다.
-1. **요구 사항**: Windows 10/11, Visual Studio 2022 (.NET 데스크톱 개발 워크로드), .NET 10.0 SDK
+1. **요구 사항**: Windows 10/11, .NET 10 SDK. Visual Studio 2026 (.NET 데스크톱 개발 워크로드)은 있어도 되고 없어도 됩니다.
2. **리포지토리 클론**:
```bash
git clone https://github.com/amophi/SoundVisualizer.git
```
-3. Visual Studio에서 `SoundVisualizer.slnx` 솔루션을 엽니다.
-4. `Release` 또는 `Debug` 구성 선택 후 솔루션을 빌드(`Ctrl + Shift + B`)하고 실행(`F5`)합니다.
+3. 리포지토리 폴더에서 빌드하고 테스트합니다.
+ ```bash
+ dotnet build SoundVisualizer.slnx -c Release
+ dotnet test SoundVisualizer.Tests/SoundVisualizer.Tests.csproj
+ ```
+ 또는 Visual Studio 에서 `SoundVisualizer.slnx` 를 열어 빌드(`Ctrl + Shift + B`)하고 실행(`F5`)합니다.
+
+솔루션은 세 프로젝트로 나뉩니다. `SoundVisualizer`(WPF 앱), `SoundVisualizer.Core`(오디오, AI, 렌더링 계산, 설정, 18개 언어 문자열 표. 화면에 의존하지 않음), `SoundVisualizer.Tests`(xUnit 테스트. AI 각 단계의 기준 출력 비교 포함).
---
diff --git a/README.md b/README.md
index 8452cfa..ae2b19b 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-# Sound Visualizer
+# SoundVisualizer
@@ -20,9 +20,11 @@
-> **Sound Visualizer** is a desktop application that captures real-time system audio and translates its direction, intensity, and meaning into a graphical overlay.
+> **SoundVisualizer** is a desktop application that captures real-time system audio and translates its direction, intensity, and meaning into a graphical overlay.
Going beyond simple waveform rendering, it integrates an AI sound classifier powered by **YAMNet** and **ONNX Runtime** to instantly identify the type of sound playing (ambient, speech, danger) and provide adaptive visual feedback.
+> 📱 There is also an Android version: [SoundVisualizer for Android](https://github.com/bibibic76/SoundVisualizer).
+
---
## 📖 Table of Contents
@@ -31,7 +33,6 @@ Going beyond simple waveform rendering, it integrates an AI sound classifier pow
- [System & AI Architecture](#-system--ai-architecture)
- [Installation & Quick Start](#-installation--quick-start)
- [Build Guide (For Developers)](#-build-guide-for-developers)
-- [Team Members](#-team-members)
- [License](#-license)
---
@@ -40,7 +41,7 @@ Going beyond simple waveform rendering, it integrates an AI sound classifier pow
### Enhancing Media Accessibility for the Deaf and Hard of Hearing
Modern media content, including YouTube, OTT, and gaming, relies heavily on spatial audio to maximize immersion. However, this creates a severe barrier for the deaf and hard of hearing. Traditional subtitle systems only deliver dialogue, completely omitting crucial acoustic information such as **sound direction, footsteps, or urgent sound effects (e.g., gunshots).**
-Sound Visualizer was created to break down these barriers by **visualizing the intensity, classification, and spatial direction of invisible sounds.** Our primary goal is to resolve this information asymmetry and guarantee an equal, fully accessible media consumption environment for everyone.
+SoundVisualizer was created to break down these barriers by **visualizing the intensity, classification, and spatial direction of invisible sounds.** Our primary goal is to resolve this information asymmetry and guarantee an equal, fully accessible media consumption environment for everyone.
### Expanding Utility for All Users
This overlay technology is also highly beneficial for gamers, providing a **tactical visual indicator (situational awareness)** in competitive environments. Additionally, it serves as a perfect alternative for users consuming media in public or silent environments where audio output is restricted.
@@ -55,21 +56,40 @@ We provide 4 unique rendering modes that intuitively map sound intensity and dir
- **Circle Mode**: Radiates circular ripples outward from the center of the screen.
- **Pad Mode**: Displays glowing pads anchored to specific spatial grid directions.
- **Outline Mode**: A minimalist variation of Wave mode, showing only the thin borders of the wave to minimize screen occlusion.
+- Animations move at the same speed at any frame rate, from 30 FPS up to your monitor's refresh rate.
### 2. On-Screen Real-Time Editor (F4)
Modify settings instantly while in a full-screen application or game without minimizing the window.
- **F2 / F3 Hotkeys**: Switch between sound modes and visualization modes on the fly. Hotkeys can be customized.
-- **Editor Mode (Default F4)**: Drag the guideline boundaries on your screen to physically resize the rendering limits in real-time. Adjust colors, opacity, AI detection sensitivities, and all other settings directly from the pop-up control panel.
+- **Editor Mode (Default F4)**: Drag the guideline boundaries on your screen to physically resize the rendering limits in real-time. Adjust colors, opacity, and all other settings directly from the pop-up control panel.
### 3. Hardware-Aware Multi-Channel Support
- Automatically detects the system's audio configuration (Stereo, 5.1, 7.1 Surround) and accurately maps the sound's origin (Front/Back/Left/Right) to produce a 3D visual effect on a 2D screen.
+- The sound mode switches to match your output device whenever the visualizer starts or the device changes, and you can still change it yourself.
+- With stereo output, sound common to both channels is drawn at the top and only the difference between left and right goes to the sides, so a centred sound no longer looks like sound coming from both sides.
### 4. Real-Time AI Sound Classification (ONNX & YAMNet)
- **3-Class Detection**: Analyzes all incoming audio into `Ambient`, `Speech`, and `Danger` categories. Each category triggers independent, customizable UI colors and dynamic opacity changes.
-- **Gunshot Booster**: An auxiliary, highly-sensitive model designed specifically to ensure critical warning sounds (like gunshots in games/movies) are never missed.
-
-### 5. Comprehensive Localization
-- Fully supports 8 languages: English, Korean, Japanese, Chinese, Spanish, French, German, and Russian.
+- **Warning cues**: Sirens, alarms, explosions, fireworks and similar warning sounds count as `Danger` even when they are only among the model's top five candidates (at 5% or more), so music or game audio can't drown them out. Gunshot labels are not promoted this way, to avoid false alarms.
+- **Stable colors**: `Danger` appears on the first qualifying result; `Speech` and `Ambient` need two in a row, so the color doesn't flicker. The AI pauses while it's quiet and resumes as soon as sound returns.
+- **Every channel counts**: On 5.1/7.1 systems, the AI hears the rear and side channels too, not just the front.
+- **Late danger**: If a `Danger` result arrives a moment after the sound, the overlay still draws that sound at the size it had, even when its earlier type was hidden.
+
+### 5. Choose How Each Sound Counts (Sounds Tab)
+- Browse the 521 sounds the AI knows, grouped like the Android version, and choose whether each group (or, in advanced mode, each sound) counts as `Ambient`, `Speech`, or `Danger`.
+- Search by name, filter by type or by the sounds you changed, and reset everything in one step. The overlay's AI follows your changes immediately.
+
+### 6. Help, Notices, and Tray Icon
+- **Help tab**: Getting started, visual modes, sound types, hotkeys, FAQ, a report button that opens a prefilled GitHub issue or email, and the open source licenses.
+- **Notices**: The Home tab tells you when there is no playback device to capture, when sound type detection is unavailable, or why the visualizer stopped.
+- **Tray icon**: Start or stop the visualizer, return to the launcher, or exit from the notification area.
+- **Tutorial**: The first time you open the app, a short tutorial shows how sound, direction, and sound types are drawn, using the real renderer. You can open it again from the Home tab.
+- **Developer mode**: Shows what the AI heard (its top five sounds, how sure it was, and how long it took) at the top left of the overlay, and can record every result to a CSV file in the `ai-log` folder next to the app.
+- **Keyboard and screen readers**: Every control in the launcher can be reached with the Tab key. On the tab row, the arrow keys switch tabs, and `Ctrl + Tab` moves to the next tab from anywhere. Dialogs and the overlay editor close with `Esc`. The language list opens with the arrow keys so you can browse it, and the language only changes when you choose one with `Enter`. Screen readers such as Narrator and NVDA read icon buttons, hotkey buttons, color buttons, and the sound list by name, and announce the running status, notices, and the number of changed sounds as they change. Everything you can drag with the mouse in the overlay editor can also be changed with the sliders in its panel.
+
+### 7. Comprehensive Localization
+- Supports 18 languages: Korean, English, Japanese, Chinese (Simplified and Traditional), Vietnamese, Thai, Indonesian, Spanish, Portuguese (Brazil), French, Italian, German, Russian, Polish, Turkish, Hindi, and Arabic (right-to-left). By default it follows the Windows display language.
+- The interface uses the Pretendard font, with Windows fonts for scripts Pretendard doesn't cover.
---
@@ -78,40 +98,49 @@ Modify settings instantly while in a full-screen application or game without min
The project is rigorously engineered for high-performance, real-time background processing with minimal system overhead.
* **Core Audio API (WASAPI)**: Loopback captures system-wide audio with zero latency.
-* **DSP & FFT Computation**: High-speed frequency transformation calculations run entirely on a background audio thread multiple times per second.
+* **Per-Channel Levels**: The capture thread measures each channel's peak level for every buffer, and the render loop takes the loudest value since the previous frame.
* **Zero-Allocation Rendering**: The WPF/C# rendering loop is designed to minimize Garbage Collector (GC) allocation, entirely preventing frame drops.
-* **ONNX Inference Pipeline**: Audio is converted to 16kHz mono, processed into Log-mel spectrograms, and fed into a custom-trained YAMNet model via the `Microsoft.ML.OnnxRuntime` engine for instantaneous classification.
+* **Frame-Rate Independent and Idle-Aware**: Smoothing is normalized to a 60 FPS reference. When nothing has been visible for a second, the overlay stops redrawing every frame and wakes as soon as sound arrives.
+* **ONNX Inference Pipeline**: Every 250 ms, the last 0.975 seconds of audio is downmixed to mono, resampled to 16 kHz through an anti-aliasing filter, turned into the log-mel spectrogram the model was exported with, and classified by YAMNet via `Microsoft.ML.OnnxRuntime`. The model files are checked against pinned SHA-256 hashes at load. Each step is unit-tested against reference outputs.
---
## 🚀 Installation & Quick Start
-Sound Visualizer is provided as a lightweight, **portable (no-install)** open-source package.
+SoundVisualizer is provided as a lightweight, **portable (no-install)** open-source package for Windows 10/11 (64-bit). You don't need to install .NET.
1. Go to the **[GitHub Releases](https://github.com/amophi/SoundVisualizer/releases)** page.
-2. Download and extract the latest `SoundVisualizer.zip` file to any location on your PC.
+2. Download the latest `SoundVisualizer_vX.Y.zip` file and extract it to any location on your PC.
3. Run **`SoundVisualizer.exe`**.
4. Configure your initial settings and language in the launcher, then click **Start** to activate the overlay.
+Your settings are saved in `settings.json` next to `SoundVisualizer.exe`, so the folder can be moved or copied as is.
+
---
## 🛠 Build Guide (For Developers)
To build from source to modify the code or contribute to the project:
-1. **Prerequisites**: Windows 10/11, Visual Studio 2022 (with .NET Desktop Development workload), and .NET 10.0 SDK.
+1. **Prerequisites**: Windows 10/11 and the .NET 10 SDK. Visual Studio 2026 (with the .NET Desktop Development workload) is optional.
2. **Clone the repository**:
```bash
git clone https://github.com/amophi/SoundVisualizer.git
```
-3. Open the `SoundVisualizer.slnx` solution file in Visual Studio 2022.
-4. Select `Release` or `Debug` configuration, build (`Ctrl + Shift + B`), and press `F5` to run.
+3. Build and test from the repository folder:
+ ```bash
+ dotnet build SoundVisualizer.slnx -c Release
+ dotnet test SoundVisualizer.Tests/SoundVisualizer.Tests.csproj
+ ```
+ Or open `SoundVisualizer.slnx` in Visual Studio, build (`Ctrl + Shift + B`), and press `F5` to run.
+
+The solution has three projects: `SoundVisualizer` (the WPF app), `SoundVisualizer.Core` (audio, AI, rendering logic, settings, and the 18 language tables, with no UI dependency), and `SoundVisualizer.Tests` (xUnit tests, including reference outputs for each AI step).
---
## 📄 License
-Sound Visualizer is distributed under the **AGPL v3** license to encourage a virtuous cycle in the open-source community. Anyone is welcome to modify the code and share custom UI themes or streaming plugins. See the [LICENSE](LICENSE) file for more information.
+SoundVisualizer is distributed under the **AGPL v3** license to encourage a virtuous cycle in the open-source community. Anyone is welcome to modify the code and share custom UI themes or streaming plugins. See the [LICENSE](LICENSE) file for more information.
For licensing and copyright information regarding third-party libraries (NAudio, ONNX Runtime, etc.) and AI models used in this project, please refer to the [THIRD_PARTY.md](THIRD_PARTY.md) file.
diff --git a/SoundVisualizer.Core/Ai/AiAudioBuffer.cs b/SoundVisualizer.Core/Ai/AiAudioBuffer.cs
new file mode 100644
index 0000000..e9e9937
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiAudioBuffer.cs
@@ -0,0 +1,150 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Numerics;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// AI 만 쓰는 캡처 레이트 모노 링 버퍼(Android 의 AiAudioBuffer.kt). 시각화 경로와 따로 돈다.
+ ///
+ /// 캡처 스레드가 로 넣고, 추론 스레드가 로 최근 한 창을
+ /// 가져간다. 링은 잠금으로 지킨다. 섞을 때 쓰는 임시 버퍼는 할당을 피하려고 하나를 다시 쓰므로
+ /// 를 부르는 스레드는 하나여야 한다. NAudio 는 캡처 버퍼를 재사용하므로 넣는 즉시 링으로 복사한다.
+ /// 레이트와 채널 수는 만들 때 정해진다. 장치 형식이 바뀌면 새로 만든다.
+ ///
+ public sealed class AiAudioBuffer
+ {
+ public const int DefaultCaptureSampleRate = 44100;
+ public const int RingSeconds = 2;
+
+ /// Android 가 받는 레이트(16·44.1·48kHz)의 링 크기. 인덱스를 비트 마스크로 돌리려고 2의 거듭제곱이다.
+ public const int MaxRingSize = 131072;
+
+ private readonly object _lock = new();
+ private readonly int _captureSampleRate;
+ private readonly int _channels;
+ private readonly float[] _ring;
+ private readonly int _ringMask;
+ private readonly int _retainedSamples;
+ private readonly int _windowSamples;
+ private int _tail;
+ private int _count;
+
+ /// 섞은 모노를 링에 넣기 전에 잠깐 두는 곳. 버퍼마다 할당하지 않으려고 다시 쓴다.
+ private readonly float[] _monoScratch = new float[MaxRingSize / 2];
+
+ /// AI 가 받지 않는 레이트().
+ public AiAudioBuffer(int captureSampleRate = DefaultCaptureSampleRate, int channels = 2)
+ {
+ if (!AiCaptureSampleRatePolicy.IsSupportedForAi(captureSampleRate))
+ throw new ArgumentOutOfRangeException(nameof(captureSampleRate), captureSampleRate, "Unsupported AI capture sample rate");
+
+ _captureSampleRate = captureSampleRate;
+ _channels = channels;
+ // 2초를 남긴다. 44.1kHz 보다 낮은 레이트도 44.1kHz 의 2초만큼 둔다(Android 와 같다).
+ _retainedSamples = Math.Max(DefaultCaptureSampleRate * RingSeconds, captureSampleRate * RingSeconds);
+ // PC 에서 더한 것: 88.2kHz 부터는 2초가 131,072 를 넘고, 176.4·192kHz 는 한 창(171,990·187,200샘플)조차 넘는다.
+ // 2초를 담는 가장 작은 2의 거듭제곱으로 늘린다(88.2·96kHz 262,144, 176.4·192kHz 524,288). Android 레이트는 그대로다.
+ int capacity = Math.Max(MaxRingSize, (int)BitOperations.RoundUpToPowerOf2((uint)_retainedSamples));
+ _ring = new float[capacity];
+ _ringMask = capacity - 1;
+ _windowSamples = CaptureAudioMath.CaptureSamplesForOneYamnetWindow(captureSampleRate);
+ }
+
+ public int SampleRate => _captureSampleRate;
+
+ public int ChannelCount => _channels;
+
+ /// 링에 담을 수 있는 모노 샘플 수(2의 거듭제곱). 한 창과 2초가 모두 들어간다.
+ public int RingCapacity => _ring.Length;
+
+ public int AvailableSamples
+ {
+ get
+ {
+ lock (_lock) return _count;
+ }
+ }
+
+ public void Reset()
+ {
+ lock (_lock)
+ {
+ _tail = 0;
+ _count = 0;
+ }
+ }
+
+ ///
+ /// 이번 캡처 버퍼에서 실제로 녹음된 인터리브 float PCM(채널 수 × 프레임)을 모노로 섞어 링에 넣는다. 캡처 스레드에서 부른다.
+ /// NAudio 의 바이트 버퍼는 MemoryMarshal.Cast<byte, float>(buffer.AsSpan(0, bytesRecorded)) 로 넘긴다.
+ ///
+ public void IngestInterleaved(ReadOnlySpan pcm)
+ {
+ if (pcm.IsEmpty || _channels <= 0) return;
+
+ // PC 에서 더한 것: 임시 버퍼(65,536프레임)보다 긴 버퍼는 프레임 단위로 나눠 넣는다. Android 는 여기서 예외를
+ // 던졌는데, 캡처 스레드에서 던지면 캡처가 멈춘다. 나눠 넣어도 링에 남는 소리는 같다.
+ long maxChunkFloats = (long)_monoScratch.Length * _channels;
+ while (!pcm.IsEmpty)
+ {
+ ReadOnlySpan chunk = pcm.Length > maxChunkFloats ? pcm[..(int)maxChunkFloats] : pcm;
+ int frames = CaptureAudioMath.DownmixInterleavedToMono(chunk, _channels, _monoScratch);
+ if (frames <= 0) return;
+ AppendMono(_monoScratch.AsSpan(0, frames));
+ pcm = pcm[chunk.Length..];
+ }
+ }
+
+ /// 이미 모노인 캡처 레이트 샘플을 넣는다(테스트·도구용). 어느 스레드에서 불러도 된다.
+ public void IngestMono(ReadOnlySpan mono)
+ {
+ if (mono.IsEmpty) return;
+ AppendMono(mono);
+ }
+
+ private void AppendMono(ReadOnlySpan monoChunk)
+ {
+ lock (_lock)
+ {
+ for (int i = 0; i < monoChunk.Length; i++)
+ {
+ _ring[_tail] = monoChunk[i];
+ _tail = (_tail + 1) & _ringMask;
+ }
+ // 링이 넘치면 가장 오래된 샘플부터 밀려나고, 2초를 넘는 앞부분은 버린다.
+ // (Android 의 head 포인터는 읽는 쪽이 쓰지 않아 남은 수만 센다. 결과는 같다.)
+ _count = (int)Math.Min((long)_count + monoChunk.Length, _retainedSamples);
+ }
+ }
+
+ ///
+ /// 최근 .Length 개를 오른쪽에 맞춰 복사하고, 모자라면 앞을 0 으로 채운다.
+ /// 복사 시점에 링에 있던 샘플 수를 돌려준다. 추론 스레드에서 부른다.
+ ///
+ public int CopyTailRightPadded(Span destination)
+ {
+ lock (_lock)
+ {
+ int take = Math.Min(_count, destination.Length);
+ int destinationStart = destination.Length - take;
+ destination[..destinationStart].Clear();
+ if (take > 0)
+ {
+ int ringStart = (_tail - take) & _ringMask;
+ int firstPart = Math.Min(take, _ring.Length - ringStart);
+ _ring.AsSpan(ringStart, firstPart).CopyTo(destination[destinationStart..]);
+ _ring.AsSpan(0, take - firstPart).CopyTo(destination[(destinationStart + firstPart)..]);
+ }
+ return _count;
+ }
+ }
+
+ /// 16kHz 한 창()을 만들 만큼 쌓였는지.
+ public bool HasEnoughForYamnetWindow()
+ {
+ lock (_lock) return _count >= _windowSamples;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/AiCaptureInferenceGate.cs b/SoundVisualizer.Core/Ai/AiCaptureInferenceGate.cs
new file mode 100644
index 0000000..741666d
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiCaptureInferenceGate.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 실시간 AI 파이프라인의 캡처 입구(Android 의 AiCaptureInferenceGate.kt).
+ ///
+ /// 소리는 게이트가 닫혀 있어도 늘 링에 넣는다. 그래야 다시 열릴 때 그 앞의 소리까지 한 창으로 본다.
+ /// 게이트는 준비된 창으로 추론을 시작해도 되는지만 정한다. Android 에서는 internal 이지만, 파이프라인을 앱 쪽에서
+ /// 엮을 수 있게 public 으로 둔다.
+ ///
+ public sealed class AiCaptureInferenceGate
+ {
+ private readonly AiAudioBuffer _audioBuffer;
+ private readonly AiSilenceGate _silenceGate;
+
+ public AiCaptureInferenceGate(AiAudioBuffer audioBuffer, AiSilenceGate? silenceGate = null)
+ {
+ _audioBuffer = audioBuffer ?? throw new ArgumentNullException(nameof(audioBuffer));
+ _silenceGate = silenceGate ?? new AiSilenceGate();
+ }
+
+ /// 캡처 버퍼 하나를 넣는다. 캡처 스레드 하나에서만 부른다().
+ public void IngestInterleaved(ReadOnlySpan pcm, long nowMs)
+ {
+ // 링에 먼저 넣고 게이트를 연다. 열린 게이트를 본 추론 스레드는 그 소리가 든 창을 반드시 가져간다.
+ _audioBuffer.IngestInterleaved(pcm);
+ _silenceGate.OnInterleavedPcm(pcm, nowMs);
+ }
+
+ public bool IsInferenceOpen(long nowMs) => _silenceGate.IsOpen(nowMs);
+
+ public void OnInferenceCompleted(long snapshotTimeMs)
+ {
+ _silenceGate.OnInferenceCompleted(snapshotTimeMs);
+ }
+
+ public void OnInferenceFailed(long snapshotTimeMs)
+ {
+ _silenceGate.OnInferenceFailed(snapshotTimeMs);
+ }
+
+ /// 게이트만 비운다. 링은 파이프라인이 따로 비운다(Android 와 같다).
+ public void Reset()
+ {
+ _silenceGate.Reset();
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/AiCaptureSampleRatePolicy.cs b/SoundVisualizer.Core/Ai/AiCaptureSampleRatePolicy.cs
new file mode 100644
index 0000000..3b6d6e6
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiCaptureSampleRatePolicy.cs
@@ -0,0 +1,56 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 어떤 캡처 레이트를 AI 에 넣을지 정한다(Android 의 AiCaptureSampleRatePolicy.kt). 링 버퍼와 리샘플러가 이 목록을 따른다.
+ ///
+ /// 16·44.1·48kHz 는 Android 와 같다. 16kHz 는 바꾸지 않고 그대로 넣는다.
+ /// PC 에서 더한 것: 루프백 캡처는 장치의 믹스 형식 레이트 그대로 받으므로 레이트가 Windows 의 장치 설정을 따른다.
+ /// 그래서 흔한 88.2·96·176.4·192kHz 도 FIR 을 레이트에 비례해 늘려()
+ /// 44.1·48kHz 와 같은 통과·차단 특성으로 받는다. 그 밖의 레이트는 Android 처럼 AI 를 끄고 화면만 돈다.
+ ///
+ public static class AiCaptureSampleRatePolicy
+ {
+ public const int DefaultCaptureSampleRate = 48000;
+
+ private static readonly int[] PreferredCaptureRates = { 48000, 44100 };
+
+ public static bool IsSupportedForAi(int sampleRate) => sampleRate switch
+ {
+ 16000 or 44100 or 48000 => true,
+ 88200 or 96000 or 176400 or 192000 => true, // PC 에서 더한 레이트
+ _ => false,
+ };
+
+ ///
+ /// 시도할 캡처 레이트의 순서. 보고된 장치 레이트는 AI 가 받을 수 있을 때만 맨 앞에 두고, 아니면 48·44.1kHz 를 먼저
+ /// 해 본 뒤 마지막에 화면만 도는 대안으로 남긴다. 0 이하나 null 은 보고되지 않은 것으로 본다.
+ ///
+ public static int[] OrderedCaptureCandidates(int? reportedSampleRate)
+ {
+ int? reported = reportedSampleRate > 0 ? reportedSampleRate : null;
+ var candidates = new List(PreferredCaptureRates.Length + 1);
+
+ if (reported is int supported && IsSupportedForAi(supported)) candidates.Add(supported);
+ foreach (int rate in PreferredCaptureRates)
+ {
+ if (!candidates.Contains(rate)) candidates.Add(rate);
+ }
+ if (reported is int fallback && !candidates.Contains(fallback)) candidates.Add(fallback);
+
+ return candidates.ToArray();
+ }
+
+ /// 후보 가운데 장치가 받아 준 첫 레이트. 하나도 없으면 .
+ public static int SelectCaptureRate(int? reportedSampleRate, Func isAccepted)
+ {
+ foreach (int rate in OrderedCaptureCandidates(reportedSampleRate))
+ {
+ if (isAccepted(rate)) return rate;
+ }
+ return DefaultCaptureSampleRate;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/AiClassificationResult.cs b/SoundVisualizer.Core/Ai/AiClassificationResult.cs
new file mode 100644
index 0000000..05fd2b9
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiClassificationResult.cs
@@ -0,0 +1,57 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 추론 한 번의 결과. 오버레이·진동·개발자 모드가 읽는 불변 스냅샷이다(모바일 AiClassificationResult.kt).
+ ///
+ /// ·· 는 히스테리시스를 거친 확정 값이라 이번 프레임보다
+ /// 오래된 것일 수 있다. 만 이번 프레임의 값이다. 필드끼리는 한 결과 안에서만 짝이 맞으므로
+ /// 여러 필드를 쓰는 쪽은 결과를 한 번만 읽어 쓴다.
+ ///
+ public sealed class AiClassificationResult
+ {
+ /// 확정한 소리 종류. 화면 색·표시 여부·진동은 이것만 보면 된다.
+ public required SoundType Coarse { get; init; }
+
+ /// 확정할 때의 영어 클래스 이름(가려진 위협음이나 안전 단서로 바뀐 이름일 수 있다). 아직 확정한 것이 없으면 빈 문자열.
+ public required string Display { get; init; }
+
+ /// 확정할 때의 확신도.
+ public required float Confidence { get; init; }
+
+ /// 이번 프레임의 모델 원출력 상위 5개. 사용자 지정으로 거르지 않는다.
+ public IReadOnlyList Top5 { get; init; } = [];
+
+ /// 이번 프레임에서 안전 단서로 위협음으로 올렸는지.
+ public bool DangerCuePromoted { get; init; }
+
+ /// 이번 프레임이 확신도 기준을 넘었는지. 못 넘었으면 확정 값은 이전 그대로다.
+ public required bool MeetsThreshold { get; init; }
+
+ /// 결과를 만든 벽시계 시각(유닉스 밀리초).
+ public required long TimestampMs { get; init; }
+
+ public double PreprocessMs { get; init; }
+
+ public double YamnetMs { get; init; }
+
+ /// 전처리 시작부터 후처리 끝까지.
+ public double TotalMs { get; init; }
+
+ /// 이 결과에 쓴 유효한 사용자 지정 수.
+ public int MappingOverrideCount { get; init; }
+
+ /// 이 결과에 쓴 매핑().
+ public string MappingSignature { get; init; } = "default";
+
+ ///
+ /// 이 결과를 만들 때 받은 사용자 지정 원본(걸러내기 전). 설정이 바뀌면 다음 추론을 기다리지 않고 옛 결과를 바로 숨기는 데 쓴다
+ /// ().
+ ///
+ internal FrozenDictionary MappingInputs { get; init; } = FrozenDictionary.Empty;
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/AiClassificationStage.cs b/SoundVisualizer.Core/Ai/AiClassificationStage.cs
new file mode 100644
index 0000000..69f28e2
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiClassificationStage.cs
@@ -0,0 +1,137 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+using System.Diagnostics;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// YAMNet 확률 521개 → 화면이 쓸 확정 결과. 모바일 RealtimeAiPipeline 에서 모델 뒤쪽(사용자 매핑 갱신, 투표, 안전 단서,
+ /// 후처리, 결과 게시)만 옮겼다. 오디오 버퍼·전처리·모델 실행·250ms 주기는 갖고 있지 않다.
+ ///
+ /// 스레드: ·Process· 은 추론 스레드에서만(또는 서로 겹치지 않게) 부른다.
+ /// 은 렌더 스레드 등 어디서 불러도 된다. 결과는 불변이고 참조 하나로 게시하므로 읽는 쪽에 락이 필요 없다.
+ ///
+ public sealed class AiClassificationStage
+ {
+ private readonly YamnetClassMap _classes;
+ private readonly YamnetCoarseClassifier _classifier;
+ private readonly AiPostProcessor _postProcessor = new();
+ private FrozenDictionary _previousOverrides = FrozenDictionary.Empty;
+ private YamnetMappingPolicy _mapping = YamnetMappingPolicy.Default;
+ private AiClassificationResult? _lastResult;
+
+ public AiClassificationStage(YamnetClassMap classes)
+ {
+ _classifier = new YamnetCoarseClassifier(classes);
+ _classes = classes;
+ }
+
+ public YamnetClassMap Classes => _classes;
+
+ /// 지금 쓰는 매핑 스냅샷.
+ public YamnetMappingPolicy Mapping => _mapping;
+
+ ///
+ /// 설정의 사용자 지정(영어 클래스 이름 → "ambient"/"speech"/"danger")을 반영한다. 추론 틱마다 먼저 부르고, 무음이라 추론을
+ /// 건너뛰는 틱에도 부른다. 그래야 이전 매핑에서 확정한 위협음이 소리가 다시 날 때 새 매핑으로 넘어오지 않는다.
+ ///
+ /// 원본이 이전과 같으면 아무것도 하지 않는다. 다르면 새 스냅샷을 만들고, 실제 매핑(서명)까지 달라졌으면 히스테리시스를
+ /// 처음부터 다시 시작하고 마지막 결과를 지운다. 그랬으면 true 를 돌려준다.
+ ///
+ public bool UpdateMapping(IReadOnlyDictionary overrides)
+ {
+ ArgumentNullException.ThrowIfNull(overrides);
+ if (SameEntries(_previousOverrides, overrides)) return false;
+
+ var next = YamnetMappingPolicy.From(overrides, _classes);
+ bool restarted = next.Signature != _mapping.Signature;
+ if (restarted)
+ {
+ _postProcessor.Reset();
+ Volatile.Write(ref _lastResult, null);
+ }
+ // 입력을 복사해 둔다. 설정 쪽 맵이 나중에 바뀌어도 이 결과가 어떤 입력으로 만들어졌는지는 그대로 남는다.
+ _previousOverrides = overrides.ToFrozenDictionary(StringComparer.Ordinal);
+ _mapping = next;
+ return restarted;
+ }
+
+ /// 소요시간을 따로 재지 않을 때. 시간 필드는 이 호출 안에서 걸린 시간만 담는다.
+ public AiClassificationResult Process(ReadOnlySpan probabilities) =>
+ Process(probabilities, Stopwatch.GetTimestamp(), preprocessMs: 0, yamnetMs: 0);
+
+ /// 모델 출력으로 결과를 만들고 게시한다.
+ /// YAMNet softmax 확률 521개. 길이가 다르면 .
+ /// 전처리를 시작할 때 로 잰 값. 전체 소요시간을 여기서부터 잰다.
+ /// 리샘플·log-mel 에 걸린 시간. 결과에 그대로 담는다.
+ /// 모델 실행에 걸린 시간. 결과에 그대로 담는다.
+ public AiClassificationResult Process(ReadOnlySpan probabilities, long startedTimestamp, double preprocessMs, double yamnetMs)
+ {
+ var mapping = _mapping;
+ var pre = _classifier.Classify(probabilities, mapping);
+ var decision = YamnetSafetyCueDecision.Decide(_classes, pre, mapping);
+ // 사용자 매핑으로 거른 치명적 단서. 기준 완화와 즉시 확정에 같은 값을 넣어, 후처리가 위협음에서 뺀 라벨을 이름 키워드로
+ // 다시 잡지 않게 한다. 그래서 모바일이 만들던 상위 후보 요약 문자열은 쓰이지 않으므로 만들지 않는다.
+ bool hasCritical = mapping.HasCriticalDangerCue(_classes, decision.PostClassIndex, decision.PostDisplay, pre.Top5);
+ var post = _postProcessor.Process(new AiPostProcessor.FrameInput(
+ Coarse: decision.PostCoarse,
+ Display: decision.PostDisplay,
+ Confidence: decision.PostConfidence,
+ DangerCuePromoted: decision.DangerCuePromoted,
+ HasStrongDangerCue: decision.HasStrongDangerCue,
+ HasCriticalDangerCue: hasCritical,
+ CriticalDangerEvent: hasCritical));
+
+ var result = new AiClassificationResult
+ {
+ Coarse = post.UiCoarse,
+ Display = post.UiDisplay,
+ Confidence = post.UiConfidence,
+ Top5 = pre.Top5,
+ DangerCuePromoted = decision.DangerCuePromoted,
+ MeetsThreshold = post.MeetsThreshold,
+ TimestampMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+ PreprocessMs = preprocessMs,
+ YamnetMs = yamnetMs,
+ TotalMs = Stopwatch.GetElapsedTime(startedTimestamp).TotalMilliseconds,
+ MappingOverrideCount = mapping.OverrideCount,
+ MappingSignature = mapping.Signature,
+ MappingInputs = _previousOverrides,
+ };
+ Volatile.Write(ref _lastResult, result);
+ return result;
+ }
+
+ ///
+ /// 마지막 결과. 아직 없거나, 결과를 만든 뒤 설정의 사용자 지정이 바뀌었으면(다음 추론 전이라도) null 이다.
+ /// 위협음에서 뺀 소리가 다음 추론까지 위협음 색으로 남지 않게 한다. 화면은 null 이면 환경음으로 그린다.
+ ///
+ public AiClassificationResult? LastClassification(IReadOnlyDictionary currentOverrides)
+ {
+ ArgumentNullException.ThrowIfNull(currentOverrides);
+ var last = Volatile.Read(ref _lastResult);
+ return last is not null && SameEntries(last.MappingInputs, currentOverrides) ? last : null;
+ }
+
+ /// 캡처를 다시 시작할 때 부른다. 확정 상태와 마지막 결과를 지운다. 매핑 스냅샷은 그대로 둔다.
+ public void Reset()
+ {
+ _postProcessor.Reset();
+ Volatile.Write(ref _lastResult, null);
+ }
+
+ // 모바일의 맵 비교(overrides != previousOverrides)와 같다. 스냅샷 쪽을 돌아서 틱마다 할당하지 않는다.
+ private static bool SameEntries(FrozenDictionary snapshot, IReadOnlyDictionary current)
+ {
+ if (ReferenceEquals(snapshot, current)) return true;
+ if (snapshot.Count != current.Count) return false;
+ foreach (var (name, label) in snapshot)
+ {
+ if (!current.TryGetValue(name, out var other) || !string.Equals(label, other, StringComparison.Ordinal)) return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/AiPostProcessor.cs b/SoundVisualizer.Core/Ai/AiPostProcessor.cs
new file mode 100644
index 0000000..8c0ca8d
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiPostProcessor.cs
@@ -0,0 +1,210 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 확신도 기준과 히스테리시스로 화면에 보낼 라벨을 확정한다. 모바일 AiPostProcessor.kt 와 같다.
+ ///
+ /// 라벨이 추론(250ms)마다 흔들리지 않게 환경음·대화음은 기준을 넘는 프레임이 두 번 이어져야 바뀐다. 위협음은 놓치면 안 되므로
+ /// 첫 프레임에 바로 확정하고, 위협음에서 벗어날 때도 확신도가 0.27 이상이면 한 프레임으로 충분하다.
+ /// 기준을 못 넘은 프레임은 아무것도 바꾸지 않으므로 이전 라벨이 그대로 남는다.
+ ///
+ /// 상태를 가지므로 한 스레드(추론 스레드)에서만 부른다.
+ ///
+ public sealed class AiPostProcessor
+ {
+ public const float DefaultBaseThreshold = 0.25f;
+ public const int CoarseHysteresisThreshold = 2;
+ public const int DangerHysteresisThreshold = 1;
+ public const float DangerImmediateSwitchConfidence = 0.28f;
+ public const float DangerExitRelaxedConfidence = 0.27f;
+
+ // 기준 완화. 안전 단서로 올린 위협음은 0.12, 상위 5개에 강한·치명적 단서가 있는 위협음은 0.20 까지 내린다.
+ // 대화음은 기본 기준을 낮게 줘도 0.25 아래로 내리지 않는다.
+ private const float PromotedCueThreshold = 0.12f;
+ private const float DangerCueThreshold = 0.20f;
+ private const float SpeechMinThreshold = 0.25f;
+
+ /// 한 프레임의 입력. 안전 단서 판단()을 거친 라벨이다.
+ /// 이번 프레임의 종류.
+ /// 이번 프레임의 영어 클래스 이름. 확정되면 화면에 간다.
+ /// 이번 프레임의 확신도.
+ /// 안전 단서로 위협음으로 올렸는지(기준을 0.12 까지 내린다).
+ /// 상위 5개에 강한 위협 단서가 있는지(기준 완화용).
+ /// 표시 이름이나 상위 5개에 치명적 단서(총기, 폭발, 폭죽)가 있는지(기준 완화용).
+ /// 상위 후보 이름을 이은 문자열. 가 없을 때만 치명적 사건 판단에 쓴다.
+ ///
+ /// 사용자 매핑을 반영해 미리 정한 치명적 사건 여부. 파이프라인은 늘 넣는다. 없으면(null) 이름 키워드로 판단하는데,
+ /// 그러면 위협음에서 뺀 라벨도 다시 걸리므로 매핑을 아는 쪽은 꼭 넣는다.
+ ///
+ public readonly record struct FrameInput(
+ SoundType Coarse,
+ string Display,
+ float Confidence,
+ bool DangerCuePromoted = false,
+ bool HasStrongDangerCue = false,
+ bool HasCriticalDangerCue = false,
+ string TopKSummary = "",
+ bool? CriticalDangerEvent = null);
+
+ ///
+ /// 처리 뒤 상태. 만 이번 프레임의 값이고, Confirmed 는 이전 프레임에서 확정한 값일 수 있다.
+ /// 는 바뀌려고 기다리는 종류이고, 없으면 null 이다(모바일은 빈 문자열).
+ ///
+ public readonly record struct FrameResult(
+ float EffectiveThreshold,
+ bool MeetsThreshold,
+ SoundType? CandidateCoarse,
+ int CandidateStreak,
+ SoundType ConfirmedCoarse,
+ string ConfirmedDisplay,
+ float ConfirmedConfidence)
+ {
+ // 모바일은 옛 Booster 미리보기 때문에 화면용 값을 따로 두었다. 미리보기를 없앤 지금은 늘 확정 값과 같다.
+ public SoundType UiCoarse => ConfirmedCoarse;
+
+ public string UiDisplay => ConfirmedDisplay;
+
+ public float UiConfidence => ConfirmedConfidence;
+ }
+
+ private readonly float _baseConfidenceThreshold;
+ private SoundType _confirmedCoarse = SoundType.Ambient;
+ private string _confirmedDisplay = "";
+ private float _confirmedConfidence;
+ private SoundType? _candidateCoarse;
+ private int _candidateStreak;
+
+ public AiPostProcessor(float baseConfidenceThreshold = DefaultBaseThreshold)
+ {
+ _baseConfidenceThreshold = baseConfidenceThreshold;
+ }
+
+ ///
+ /// 이번 프레임의 기준. 안전 단서로 올린 위협음 0.12, 강한·치명적 단서가 있는 위협음 0.20 까지 내리고, 대화음은 0.25 아래로 내리지 않는다.
+ ///
+ public static float ComputeEffectiveThreshold(
+ SoundType coarse,
+ float baseThreshold,
+ bool hasStrongDangerCue,
+ bool hasCriticalDangerCue,
+ bool dangerCuePromoted = false)
+ {
+ float effective = baseThreshold;
+ if (coarse == SoundType.Danger && dangerCuePromoted)
+ {
+ effective = Math.Min(effective, PromotedCueThreshold);
+ }
+ else if (coarse == SoundType.Danger && (hasStrongDangerCue || hasCriticalDangerCue))
+ {
+ effective = Math.Min(effective, DangerCueThreshold);
+ }
+ else if (coarse == SoundType.Speech)
+ {
+ effective = Math.Max(effective, SpeechMinThreshold);
+ }
+ return effective;
+ }
+
+ /// 치명적 단서: 총기, 폭발, 폭죽. 확신도가 낮아도 위협음으로 바로 확정한다.
+ public static bool IsCriticalDangerKeyword(string? name)
+ {
+ if (string.IsNullOrEmpty(name)) return false;
+ if (YamnetSafetyCueDecision.IsGunshotKeyword(name)) return true;
+ string s = name.ToLowerInvariant();
+ return s.Contains("explosion") || s.Contains("fireworks") || s.Contains("firecracker");
+ }
+
+ public static bool IsCriticalDangerEvent(string? display, string? topKSummary) =>
+ IsCriticalDangerKeyword(display) || IsCriticalDangerKeyword(topKSummary);
+
+ /// 처음 상태(환경음, 이름 없음)로 돌린다. 캡처를 다시 시작하거나 사용자 매핑이 바뀌면 부른다.
+ public void Reset()
+ {
+ _confirmedCoarse = SoundType.Ambient;
+ _confirmedDisplay = "";
+ _confirmedConfidence = 0f;
+ _candidateCoarse = null;
+ _candidateStreak = 0;
+ }
+
+ public FrameResult Process(FrameInput frame)
+ {
+ float effective = ComputeEffectiveThreshold(
+ frame.Coarse,
+ _baseConfidenceThreshold,
+ frame.HasStrongDangerCue,
+ frame.HasCriticalDangerCue,
+ frame.DangerCuePromoted);
+ bool meets = frame.Confidence >= effective;
+ bool critical = frame.CriticalDangerEvent ?? IsCriticalDangerEvent(frame.Display, frame.TopKSummary);
+
+ // 매개변수 없는 생성자로 만든 입력은 이름이 null 이다. 확정 이름은 늘 문자열로 둔다.
+ ApplyCoarseHysteresis(meets, frame.Coarse, frame.Display ?? "", frame.Confidence, critical);
+
+ return new FrameResult(
+ EffectiveThreshold: effective,
+ MeetsThreshold: meets,
+ CandidateCoarse: _candidateCoarse,
+ CandidateStreak: _candidateStreak,
+ ConfirmedCoarse: _confirmedCoarse,
+ ConfirmedDisplay: _confirmedDisplay,
+ ConfirmedConfidence: _confirmedConfidence);
+ }
+
+ private void ApplyCoarseHysteresis(bool meetsThreshold, SoundType newCoarse, string display, float confidence, bool criticalDangerEvent)
+ {
+ if (!meetsThreshold) return;
+
+ if (newCoarse == _confirmedCoarse)
+ {
+ _candidateStreak = 0;
+ _candidateCoarse = null;
+ _confirmedDisplay = display;
+ _confirmedConfidence = confidence;
+ return;
+ }
+
+ if (newCoarse == SoundType.Danger &&
+ (confidence >= DangerImmediateSwitchConfidence || criticalDangerEvent))
+ {
+ Confirm(newCoarse, display, confidence);
+ return;
+ }
+
+ if (newCoarse == _candidateCoarse)
+ {
+ _candidateStreak++;
+ }
+ else
+ {
+ _candidateCoarse = newCoarse;
+ _candidateStreak = 1;
+ }
+
+ int required = newCoarse == SoundType.Danger ? DangerHysteresisThreshold : CoarseHysteresisThreshold;
+ // 위협음이 끝났다는 확신이 충분하면 한 프레임 만에 놓아 준다. 위협음 색이 소리보다 오래 남지 않게 한다.
+ if (_confirmedCoarse == SoundType.Danger &&
+ newCoarse != SoundType.Danger &&
+ confidence >= DangerExitRelaxedConfidence)
+ {
+ required = 1;
+ }
+
+ if (_candidateStreak >= required)
+ {
+ Confirm(newCoarse, display, confidence);
+ }
+ }
+
+ private void Confirm(SoundType coarse, string display, float confidence)
+ {
+ _confirmedCoarse = coarse;
+ _confirmedDisplay = display;
+ _confirmedConfidence = confidence;
+ _candidateStreak = 0;
+ _candidateCoarse = null;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/AiSilenceGate.cs b/SoundVisualizer.Core/Ai/AiSilenceGate.cs
new file mode 100644
index 0000000..259db8c
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/AiSilenceGate.cs
@@ -0,0 +1,114 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 최근 캡처에 소리가 있는 동안만 실시간 추론을 열어 둔다(Android 의 AiSilenceGate.kt). 조용할 때 CPU 를 쓰지 않으려는 것이다.
+ ///
+ /// 마지막 소리가 YAMNet 창을 빠져나간 뒤에도, 완전히 조용한 창으로 추론이 두 번 끝나 후처리의 히스테리시스가 정리될
+ /// 때까지는 열어 둔다. 그래야 소리가 그친 뒤 마지막 라벨이 남지 않는다. 캡처 스레드가 소리를 알리고 추론 스레드가 결과를
+ /// 알리므로 상태는 한 잠금 안에서 함께 바꾼다. 시각은 단조 증가하는 밀리초( 등)다.
+ ///
+ public sealed class AiSilenceGate
+ {
+ /// 소리가 있다고 보는 원래 크기. Android 에서 진동과 시각화가 깨어나는 기준(0.01)과 같다.
+ public const float ActiveThreshold = 0.01f;
+
+ /// 실시간으로 계속 들어오는 소리가 YAMNet 창(15,600샘플 = 0.975초)을 다 빠져나가는 데 걸리는 시간.
+ public const long YamnetWindowMs = CaptureAudioMath.RequiredMono16kSamples * 1000L / CaptureAudioMath.TargetSampleRate;
+
+ ///
+ /// 가장 긴 종류 전환 히스테리시스를 정리하는 데 필요한 완전히 조용한 추론 수.
+ /// Android 의 AiPostProcessor.COARSE_HYSTERESIS_THRESHOLD(2) 와 같아야 한다.
+ ///
+ public const int RequiredSilentInferenceCompletions = 2;
+
+ ///
+ /// 추론이 계속 실패하면 창 네 개만큼 지난 뒤 다시 시도하지 않는다. 성공하면 이 대비를 지우고 정상 횟수를 그대로 요구하므로
+ /// 잠깐의 실패로 정리가 짧아지지 않는다.
+ ///
+ public const long FailureFallbackMs = YamnetWindowMs * 4;
+
+ private const long NoTimestamp = long.MinValue;
+
+ private readonly object _stateLock = new();
+ private readonly float _activeThreshold;
+ private long _lastActiveInputMs = NoTimestamp;
+ private int _silentInferenceCompletions;
+ private long _firstConsecutiveFailureMs = NoTimestamp;
+
+ public AiSilenceGate(float activeThreshold = ActiveThreshold)
+ {
+ _activeThreshold = activeThreshold;
+ }
+
+ ///
+ /// 캡처 버퍼 하나를 본다. 섞기 전 인터리브 PCM 의 최고값으로 재므로 채널 수와 상관없이 어느 한 채널(LFE 포함)만 커도 열린다.
+ /// 캡처 스레드에서 부른다.
+ ///
+ public void OnInterleavedPcm(ReadOnlySpan interleaved, long nowMs)
+ {
+ if (CaptureAudioMath.MaxAbsoluteInterleavedPeak(interleaved) > _activeThreshold)
+ {
+ lock (_stateLock)
+ {
+ _lastActiveInputMs = nowMs;
+ _silentInferenceCompletions = 0;
+ _firstConsecutiveFailureMs = NoTimestamp;
+ }
+ }
+ }
+
+ public bool IsOpen(long nowMs)
+ {
+ lock (_stateLock)
+ {
+ if (_lastActiveInputMs == NoTimestamp) return false;
+ if (nowMs - _lastActiveInputMs < YamnetWindowMs) return true;
+ if (_silentInferenceCompletions >= RequiredSilentInferenceCompletions) return false;
+ return _firstConsecutiveFailureMs == NoTimestamp ||
+ nowMs - _firstConsecutiveFailureMs < FailureFallbackMs;
+ }
+ }
+
+ /// 에 가져간 창의 추론이 성공했다.
+ public void OnInferenceCompleted(long snapshotTimeMs)
+ {
+ lock (_stateLock)
+ {
+ if (_lastActiveInputMs == NoTimestamp) return;
+ if (snapshotTimeMs - _lastActiveInputMs < YamnetWindowMs) return;
+ _firstConsecutiveFailureMs = NoTimestamp;
+ if (_silentInferenceCompletions < RequiredSilentInferenceCompletions)
+ {
+ _silentInferenceCompletions++;
+ }
+ }
+ }
+
+ /// 소리가 다 빠져나간 창에서 연달아 실패할 때만 제한 시간을 잰다.
+ public void OnInferenceFailed(long snapshotTimeMs)
+ {
+ lock (_stateLock)
+ {
+ if (_lastActiveInputMs == NoTimestamp) return;
+ if (snapshotTimeMs - _lastActiveInputMs < YamnetWindowMs) return;
+ if (_firstConsecutiveFailureMs == NoTimestamp)
+ {
+ _firstConsecutiveFailureMs = snapshotTimeMs;
+ }
+ }
+ }
+
+ public void Reset()
+ {
+ lock (_stateLock)
+ {
+ _lastActiveInputMs = NoTimestamp;
+ _silentInferenceCompletions = 0;
+ _firstConsecutiveFailureMs = NoTimestamp;
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/CaptureAudioMath.cs b/SoundVisualizer.Core/Ai/CaptureAudioMath.cs
new file mode 100644
index 0000000..8959651
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/CaptureAudioMath.cs
@@ -0,0 +1,338 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 캡처한 소리를 YAMNet 입력(16kHz 모노)으로 바꾸는 계산. Android 의 CaptureAudioMath.kt 를 옮겼다.
+ ///
+ /// 1·2채널과 16·44.1·48kHz 는 Android 와 같은 순서로 같은 float·double 계산을 한다. Python 기준 구현과 1e-5 안으로
+ /// 맞추는 기준 데이터 테스트가 두 앱을 함께 묶는다.
+ /// PC 에서 더한 것은 두 가지다. 5.1·7.1 채널을 섞는 법()과
+ /// 88.2·96·176.4·192kHz 에서 FIR 을 레이트에 비례해 늘리는 것()이다.
+ ///
+ public static class CaptureAudioMath
+ {
+ public const int TargetSampleRate = 16000;
+
+ /// YAMNet 한 창(0.975초)의 16kHz 샘플 수. log-mel 의 창 400 + 홉 160 × (96프레임 − 1) = 15,600 이다.
+ public const int RequiredMono16kSamples = 400 + 160 * (96 - 1);
+
+ // 44.1·48kHz 설계(차단 7.8kHz, β 8.6). YAMNet 멜 대역의 끝(7,500Hz)은 −3dB 안으로 남는다. 8~8.5kHz 는 덜 막히지만
+ // 16kHz 로 줄이면 7.5~8kHz 로 접혀 멜 대역 밖에 떨어지고, 9kHz 부터는 45dB 넘게 막힌다.
+ private const int BaseFirTaps = 96;
+ private const double FirCutoffHz = 7800.0;
+ private const double FirKaiserBeta = 8.6;
+ private const int MaxCachedPhases = 1024;
+
+ // 5.1·7.1 → 스테레오 계수. 스피커로 다시 내보낼 때 쓰는 ChannelDownmix 와 같은 ITU-R BS.775 값이다.
+ // 둘이 어긋나지 않는지는 테스트가 ChannelDownmix 의 결과와 비교해 지킨다.
+ private const float KCenter = 0.707f; // -3dB
+ private const float KSide = 0.707f; // -3dB
+ private const float KBack = 0.500f; // -6dB
+
+ /// 레이트마다 한 번 만들어 250ms 마다 다시 쓴다. 만든 뒤에는 바뀌지 않아 잠금 밖에서 읽어도 된다.
+ private sealed class FirTable
+ {
+ public FirTable(int taps, int phaseCount, float[] coefficients)
+ {
+ Taps = taps;
+ PhaseCount = phaseCount;
+ Coefficients = coefficients;
+ }
+
+ public int Taps { get; }
+
+ public int PhaseCount { get; }
+
+ /// 위상 p 의 계수는 [p × Taps, (p + 1) × Taps) 에 있다.
+ public float[] Coefficients { get; }
+ }
+
+ private static readonly object FirTablesLock = new();
+ private static readonly Dictionary FirTables = new();
+
+ /// 16kHz 15,600샘플을 만드는 데 필요한 캡처 레이트의 샘플 수(올림).
+ public static int CaptureSamplesForOneYamnetWindow(int captureSampleRate)
+ {
+ return (int)Math.Ceiling(RequiredMono16kSamples * (double)captureSampleRate / TargetSampleRate);
+ }
+
+ ///
+ /// 섞기 전 캡처 PCM 의 가장 큰 절댓값. 무음 게이트가 쓴다.
+ /// 섞은 뒤에 재면 좌우 위상이 반대인 짧은 소리가 서로 지워져 게이트가 열리지 않는다.
+ /// PC 의 5.1·7.1 도 LFE 를 포함한 모든 채널을 그대로 본다. NaN 은 건너뛴다.
+ ///
+ public static float MaxAbsoluteInterleavedPeak(ReadOnlySpan interleaved)
+ {
+ float peak = 0f;
+ foreach (float value in interleaved)
+ {
+ float sample = Math.Abs(value);
+ if (sample > peak) peak = sample;
+ }
+ return peak;
+ }
+
+ ///
+ /// 인터리브 PCM 을 모노 프레임으로 섞어 앞쪽에 쓰고 프레임 수를 돌려준다.
+ /// 채널 수로 나눠 떨어지지 않는 끝의 샘플은 버린다.
+ ///
+ /// 1채널은 그대로, 2채널은 (L+R)/2 로 Android 와 같다.
+ /// PC 에서 더한 것: 6채널(FL FR FC LFE SL SR)과 8채널(FL FR FC LFE BL BR SL SR)은 ChannelDownmix 와 같은
+ /// ITU-R BS.775 계수로 스테레오를 만든 뒤(LFE 는 뺀다) (L+R)/2 를 한다. 예전 PC 코드는 FL·FR·FC 만 AI 에 넣어
+ /// 뒤·옆 스피커로만 나는 소리를 AI 가 듣지 못했다. 스피커용과 달리 ±1 로 자르지 않는다. 자르면 큰 소리의 파형이
+ /// 바뀌어 AI 가 다른 소리로 듣는다(AI 입력은 소리로 내보내지 않으므로 1 을 넘어도 된다).
+ /// 그 밖의 채널 수는 Android 처럼 모든 채널의 평균이다.
+ ///
+ /// 이 프레임 수보다 짧다.
+ public static int DownmixInterleavedToMono(ReadOnlySpan interleaved, int channels, Span destination)
+ {
+ if (interleaved.Length <= 0 || channels <= 0) return 0;
+ int frames = interleaved.Length / channels;
+ if (frames <= 0) return 0;
+ if (frames > destination.Length)
+ throw new ArgumentException($"destination must hold {frames} frames", nameof(destination));
+
+ switch (channels)
+ {
+ case 1:
+ interleaved[..frames].CopyTo(destination);
+ return frames;
+ case 6:
+ Downmix51ToMono(interleaved, frames, destination);
+ return frames;
+ case 8:
+ Downmix71ToMono(interleaved, frames, destination);
+ return frames;
+ }
+
+ int si = 0;
+ for (int f = 0; f < frames; f++)
+ {
+ float sum = 0f;
+ for (int c = 0; c < channels; c++)
+ {
+ sum += interleaved[si++];
+ }
+ destination[f] = sum / channels;
+ }
+ return frames;
+ }
+
+ /// L = FL + 0.707·FC + 0.707·SL, R 도 같은 꼴(ChannelDownmix 와 같은 식). LFE 는 뺀다.
+ private static void Downmix51ToMono(ReadOnlySpan src, int frames, Span dst)
+ {
+ for (int f = 0; f < frames; f++)
+ {
+ int o = f * 6;
+ float fl = src[o + 0];
+ float fr = src[o + 1];
+ float fc = src[o + 2];
+ // o + 3 은 LFE
+ float sl = src[o + 4];
+ float sr = src[o + 5];
+
+ float left = fl + KCenter * fc + KSide * sl;
+ float right = fr + KCenter * fc + KSide * sr;
+ dst[f] = (left + right) / 2f;
+ }
+ }
+
+ /// L = FL + 0.707·FC + 0.707·SL + 0.5·BL, R 도 같은 꼴(ChannelDownmix 와 같은 식). LFE 는 뺀다.
+ private static void Downmix71ToMono(ReadOnlySpan src, int frames, Span dst)
+ {
+ for (int f = 0; f < frames; f++)
+ {
+ int o = f * 8;
+ float fl = src[o + 0];
+ float fr = src[o + 1];
+ float fc = src[o + 2];
+ // o + 3 은 LFE
+ float bl = src[o + 4];
+ float br = src[o + 5];
+ float sl = src[o + 6];
+ float sr = src[o + 7];
+
+ float left = fl + KCenter * fc + KSide * sl + KBack * bl;
+ float right = fr + KCenter * fc + KSide * sr + KBack * br;
+ dst[f] = (left + right) / 2f;
+ }
+ }
+
+ ///
+ /// 이 레이트에서 쓰는 FIR 탭 수. Android 가 받는 44.1·48kHz 와 그 밖의 레이트는 96탭이다.
+ ///
+ /// PC 에서 더한 것: 88.2·176.4kHz 는 96 × 레이트 ÷ 44,100, 96·192kHz 는 96 × 레이트 ÷ 48,000 (192·384탭)으로 늘린다.
+ /// Kaiser 창 FIR 의 전이 대역 폭(Hz)은 레이트 ÷ 탭 수에 비례하므로 이렇게 해야 7.8kHz 언저리의 통과·차단이
+ /// 44.1·48kHz 와 같다. 96탭 그대로면 96kHz 에서 전이 대역이 두 배로 넓어져 7kHz 가 깎이고 9kHz 가 덜 막힌다.
+ ///
+ public static int FirTapsFor(int sourceSampleRate) => sourceSampleRate switch
+ {
+ 88200 or 176400 => BaseFirTaps * sourceSampleRate / 44100,
+ 96000 or 192000 => BaseFirTaps * sourceSampleRate / 48000,
+ _ => BaseFirTaps,
+ };
+
+ ///
+ /// 모노 캡처를 16kHz 로 바꿔 전체를 채운다(보통 개).
+ ///
+ /// 16kHz 는 그대로 복사한다. 16kHz 보다 낮으면 예전 선형 보간을 그대로 둔다(AI 는 이 레이트를 받지 않는다).
+ /// 더 높으면 Kaiser 창(β 8.6) sinc 저역통과 FIR(차단 7.8kHz)을 출력 샘플마다 원본 위치에서 계산한다.
+ /// 위치는 정수(i × 레이트 ÷ 16,000 의 몫과 나머지)로 세어 15,600샘플 동안 어긋나지 않고, 나머지로 고르는 위상별
+ /// 계수 표는 레이트마다 한 번만 만든다(위상 수: 44.1kHz 160개, 88.2kHz 80개, 176.4kHz 40개, 48·96·192kHz 1개).
+ /// 원본 범위 밖은 0 으로 보므로 창 가장자리는 FIR 의 일부만 더한다. 곱과 합은 double, 계수와 결과는 float 로 Android 와 같다.
+ ///
+ public static void ResampleMonoFloatTo16kCustom(ReadOnlySpan source, int sourceSampleRate, Span destination)
+ {
+ destination.Clear();
+ if (source.Length <= 0) return;
+
+ if (sourceSampleRate == TargetSampleRate)
+ {
+ int copyLength = Math.Min(source.Length, destination.Length);
+ source[..copyLength].CopyTo(destination);
+ return;
+ }
+
+ if (sourceSampleRate < TargetSampleRate)
+ {
+ LinearResample(source, sourceSampleRate, destination);
+ return;
+ }
+
+ int sourceAvailable = source.Length;
+ FirTable table = FirTableFor(sourceSampleRate);
+ int taps = table.Taps;
+ int halfTaps = taps / 2;
+ float[] coefficients = table.Coefficients;
+ for (int i = 0; i < destination.Length; i++)
+ {
+ long positionNumerator = (long)i * sourceSampleRate;
+ int center = (int)(positionNumerator / TargetSampleRate);
+ int phase = PhaseIndex(positionNumerator % TargetSampleRate, table.PhaseCount);
+ int phaseStart = phase * taps;
+ int @base = center - halfTaps + 1;
+ int firstTap = Math.Max(0, -@base);
+ int lastTapExclusive = Math.Min(taps, sourceAvailable - @base);
+ double sum = 0.0;
+ for (int tap = firstTap; tap < lastTapExclusive; tap++)
+ {
+ sum += (double)coefficients[phaseStart + tap] * source[@base + tap];
+ }
+ destination[i] = (float)sum;
+ }
+ }
+
+ /// 16kHz 보다 낮은 레이트(AI 는 받지 않는다)에서 예전 보간을 그대로 둔다.
+ private static void LinearResample(ReadOnlySpan source, int sourceSampleRate, Span destination)
+ {
+ int sourceAvailable = source.Length;
+ double factor = (double)sourceSampleRate / TargetSampleRate;
+ for (int i = 0; i < destination.Length; i++)
+ {
+ double sourcePosition = i * factor;
+ int index1 = (int)sourcePosition;
+ int index2 = index1 + 1;
+ if (index1 >= sourceAvailable) continue;
+ float alpha = (float)(sourcePosition - index1);
+ float value1 = source[index1];
+ float value2 = index2 < sourceAvailable ? source[index2] : value1;
+ destination[i] = (1f - alpha) * value1 + alpha * value2;
+ }
+ }
+
+ private static FirTable FirTableFor(int sourceSampleRate)
+ {
+ if (sourceSampleRate <= 0)
+ throw new ArgumentOutOfRangeException(nameof(sourceSampleRate), sourceSampleRate, "sample rate must be positive");
+
+ lock (FirTablesLock)
+ {
+ if (FirTables.TryGetValue(sourceSampleRate, out FirTable? cached)) return cached;
+
+ int taps = FirTapsFor(sourceSampleRate);
+ int exactPhaseCount = TargetSampleRate / GreatestCommonDivisor(sourceSampleRate, TargetSampleRate);
+ int phaseCount = Math.Min(exactPhaseCount, MaxCachedPhases);
+ var coefficients = new float[phaseCount * taps];
+ for (int phase = 0; phase < phaseCount; phase++)
+ {
+ CreateFirCoefficients(
+ sourceSampleRate,
+ (double)phase / phaseCount,
+ coefficients.AsSpan(phase * taps, taps));
+ }
+
+ var table = new FirTable(taps, phaseCount, coefficients);
+ FirTables[sourceSampleRate] = table;
+ return table;
+ }
+ }
+
+ private static int PhaseIndex(long remainder, int phaseCount)
+ {
+ if (phaseCount == 1) return 0;
+ long rounded = (remainder * phaseCount + TargetSampleRate / 2) / TargetSampleRate;
+ return (int)(rounded % phaseCount);
+ }
+
+ ///
+ /// 원본 위치가 정수에서 만큼 지난 출력 샘플의 계수. 탭 수는 넘겨받은 칸 수이고,
+ /// 합이 1 이 되게 맞춰 상수 입력이 그대로 나온다. 계수를 float 로 한 번 줄인 뒤 double 합으로 나누는 순서까지 Android 와 같다.
+ ///
+ private static void CreateFirCoefficients(int sourceSampleRate, double fractionalPosition, Span coefficients)
+ {
+ int taps = coefficients.Length;
+ int halfTaps = taps / 2;
+ double windowScale = BesselI0(FirKaiserBeta);
+ double normalizedCutoff = 2.0 * FirCutoffHz / sourceSampleRate;
+ double sum = 0.0;
+ for (int tap = 0; tap < taps; tap++)
+ {
+ int sampleOffset = tap - halfTaps + 1;
+ double time = sampleOffset - fractionalPosition;
+ double normalizedRadius = Math.Min(1.0, Math.Abs(time) / halfTaps);
+ double window = BesselI0(FirKaiserBeta * Math.Sqrt(1.0 - normalizedRadius * normalizedRadius)) / windowScale;
+ double value = normalizedCutoff * Sinc(normalizedCutoff * time) * window;
+ coefficients[tap] = (float)value;
+ sum += value;
+ }
+ for (int tap = 0; tap < taps; tap++)
+ {
+ coefficients[tap] = (float)(coefficients[tap] / sum);
+ }
+ }
+
+ private static double Sinc(double value)
+ {
+ return Math.Abs(value) < 1e-12 ? 1.0 : Math.Sin(Math.PI * value) / (Math.PI * value);
+ }
+
+ private static double BesselI0(double value)
+ {
+ double term = 1.0;
+ double sum = 1.0;
+ int k = 1;
+ while (true)
+ {
+ term *= (value * value / 4.0) / ((double)k * k);
+ sum += term;
+ if (term < sum * 1e-15) return sum;
+ k++;
+ }
+ }
+
+ private static int GreatestCommonDivisor(int a, int b)
+ {
+ int x = Math.Abs(a);
+ int y = Math.Abs(b);
+ while (y != 0)
+ {
+ int remainder = x % y;
+ x = y;
+ y = remainder;
+ }
+ return x;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/InternalsVisibleToTests.cs b/SoundVisualizer.Core/Ai/InternalsVisibleToTests.cs
new file mode 100644
index 0000000..74cd261
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/InternalsVisibleToTests.cs
@@ -0,0 +1,7 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Runtime.CompilerServices;
+
+// 라벨 목록을 공개하지 않고도, 목록의 이름이 실제 YAMNet 클래스 이름과 맞는지 테스트가 직접 확인할 수 있게 한다.
+[assembly: InternalsVisibleTo("SoundVisualizer.Tests")]
diff --git a/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs b/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs
new file mode 100644
index 0000000..160d9d6
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs
@@ -0,0 +1,62 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Security.Cryptography;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 앱과 함께 배포한 모델 파일이 고정해 둔 바로 그 파일인지 확인한다. Android 의 ModelCacheIntegrity.kt 에서 검사 부분만 옮겼다.
+ ///
+ /// Android 는 assets 를 앱 저장소로 복사하면서 크기와 SHA-256 을 맞춰 보고, 어긋난 사본은 지우고 다시 복사한다.
+ /// PC 는 실행 파일 옆의 파일을 그대로 쓰므로 다시 복사할 원본이 없다. 그래서 불러오기 전에 확인만 하고, 어긋나면
+ /// 예외를 던져 앱이 AI 없이 화면만 돌게 한다. 잘린 파일, 크기는 같은데 깨진 파일, 다른 버전의 모델이 모두 여기서 걸린다.
+ ///
+ internal static class ModelFileIntegrity
+ {
+ /// 파일 하나의 기대값. 은 소문자 16진수 64자다.
+ internal sealed record ExpectedFile(string Name, long ByteCount, string Sha256);
+
+ ///
+ /// 파일이 있고 크기와 SHA-256 이 모두 맞는지. 크기가 다르면 해시를 계산하지 않는다.
+ /// 읽다가 난 I/O 오류는 false 로 바꾸지 않고 그대로 던진다(Android 와 같다).
+ ///
+ public static bool IsValid(string path, ExpectedFile expected)
+ {
+ ArgumentNullException.ThrowIfNull(expected);
+ var file = new FileInfo(path);
+ return file.Exists && file.Length == expected.ByteCount && Sha256Hex(path) == expected.Sha256;
+ }
+
+ ///
+ /// 의 파일을 모두 확인한다. 무엇이 어떻게 다른지를 메시지에 담는다.
+ ///
+ /// 파일이 없다.
+ /// 크기나 SHA-256 이 배포한 파일과 다르다.
+ public static void Verify(string directory, IReadOnlyList files)
+ {
+ ArgumentNullException.ThrowIfNull(directory);
+ ArgumentNullException.ThrowIfNull(files);
+ foreach (var expected in files)
+ {
+ string path = Path.Combine(directory, expected.Name);
+ var file = new FileInfo(path);
+ if (!file.Exists)
+ throw new FileNotFoundException($"Model file '{expected.Name}' is missing", path);
+ if (file.Length != expected.ByteCount)
+ throw new InvalidDataException(
+ $"Model file '{expected.Name}' is {file.Length} bytes, expected {expected.ByteCount} (truncated or a different model): {path}");
+ string actual = Sha256Hex(path);
+ if (actual != expected.Sha256)
+ throw new InvalidDataException(
+ $"Model file '{expected.Name}' has SHA-256 {actual}, expected {expected.Sha256} (damaged or a different model): {path}");
+ }
+ }
+
+ internal static string Sha256Hex(string path)
+ {
+ using var stream = File.OpenRead(path);
+ return Convert.ToHexStringLower(SHA256.HashData(stream));
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/QualcommSourceAudioPreprocessor.cs b/SoundVisualizer.Core/Ai/QualcommSourceAudioPreprocessor.cs
new file mode 100644
index 0000000..113b637
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/QualcommSourceAudioPreprocessor.cs
@@ -0,0 +1,231 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// Qualcomm 의 YAMNet 레시피가 쓰는 원본 전처리를 옮겨, 16kHz 모노 15,600샘플(0.975초)을 YAMNet 입력
+ /// [1, 1, 96, 64] 로그멜로 바꾼다. 결과는 시간 우선(time * 64 + mel)으로 편 6,144칸이다.
+ ///
+ /// 출처(고정한 버전):
+ /// - qai-hub-models commit 7925bdd04586a87b09914164903f51b3f889c4aa
+ /// - yamnet/app.py::preprocessing_yamnet_from_source
+ /// - yamnet/model.py 가 torch_audioset commit e8852c5 를 고정한다
+ /// - w-hc/torch_audioset commit e8852c53becef811784754a2de9c4617d8db2156
+ /// - torch_input_processing.py::WaveformToInput
+ /// - torch_input_processing.py::VGGishLogMelSpectrogram
+ ///
+ /// torch.stft(center=True, pad_mode="reflect") 처럼 양쪽을 256샘플씩 반사로 늘려 98프레임을 만들고 앞 96프레임만 쓴다.
+ /// 400점 periodic Hann 창을 512점 FFT 가운데에 놓고, 파워가 아닌 크기(magnitude)에 torchaudio 기본 HTK 멜 필터
+ /// (연속 주파수 위의 삼각형, 125~7,500Hz, 64대역, 정규화 없음)를 곱한 뒤 ln(x + 0.001) 을 취한다.
+ ///
+ /// 모바일 판 QualcommSourceAudioPreprocessor.kt 를 계산 순서와 자료형(double)까지 그대로 옮겼다. Python 기준
+ /// 데이터와의 오차를 Kotlin 과 같은 한도(최대 1e-5, 평균 1e-7) 안에 두려면 순서를 바꾸거나 float 로 줄이면 안 된다.
+ /// 다만 창과 멜 경계를 만드는 cos·expm1 은 플랫폼마다 마지막 자리(1ulp)가 달라 표가 Kotlin 과 비트까지 같지는 않다.
+ /// 그 차이는 1e-15 수준이라 float 로 줄인 출력에는 드물게 1ulp 로만 드러날 수 있다.
+ ///
+ /// 창·멜 필터·FFT 표는 생성할 때 한 번만 만든다. 작업 버퍼를 인스턴스가 들고 있으므로 스레드 안전하지 않다.
+ /// 추론 스레드 하나가 인스턴스 하나를 쓴다.
+ ///
+ public sealed class QualcommSourceAudioPreprocessor
+ {
+ public const int SampleRate = 16000;
+ public const int MelBins = 64;
+ public const int TimeFrames = 96;
+ public const int WindowLength = 400;
+ public const int HopLength = 160;
+ public const int FftSize = 512;
+ public const float MelFMin = 125f;
+ public const float MelFMax = 7500f;
+
+ ///
+ /// 로그 앞에 더하는 값. Kotlin 은 0.001f 를 double 로 넓혀(0.0010000000474974513) 더하므로 float 로 둔다.
+ /// 0.001(double) 로 바꾸면 기준 데이터와의 오차가 Kotlin 과 달라진다.
+ ///
+ public const float LogEps = 0.001f;
+
+ /// 한 번에 쓰는 입력 길이. 400 + 160 × 95 = 15,600샘플.
+ public const int RequiredMono16kSamples = WindowLength + HopLength * (TimeFrames - 1);
+
+ /// 출력 길이. 96 × 64 = 6,144칸.
+ public const int LogMelSize = TimeFrames * MelBins;
+
+ private const int FreqBins = FftSize / 2 + 1;
+ private const int ReflectPadding = FftSize / 2;
+ private const int WindowOffset = (FftSize - WindowLength) / 2;
+
+ private readonly double[] _hannWindow = CreateHannWindow(WindowLength);
+ private readonly double[][] _melFilterBank =
+ CreateTorchaudioHtkMelFilterBank(MelBins, FftSize, SampleRate, MelFMin, MelFMax);
+ private readonly int[] _melStartBins = new int[MelBins];
+ private readonly int[] _melEndBins = new int[MelBins];
+ private readonly Radix2Fft _fft = new(FftSize);
+
+ private readonly float[] _fitted = new float[RequiredMono16kSamples];
+ private readonly double[] _fftReal = new double[FftSize];
+ private readonly double[] _fftImag = new double[FftSize];
+ private readonly double[] _magnitude = new double[FreqBins];
+
+ public QualcommSourceAudioPreprocessor()
+ {
+ // 필터마다 0 이 아닌 구간만 더한다. 0 을 더해도 합은 그대로이므로 결과는 같고 계산만 준다.
+ for (int mel = 0; mel < MelBins; mel++)
+ {
+ double[] weights = _melFilterBank[mel];
+ int start = 0;
+ while (start < FreqBins && weights[start] == 0.0) start++;
+ int last = FreqBins - 1;
+ while (last >= start && weights[last] == 0.0) last--;
+ _melStartBins[mel] = start;
+ _melEndBins[mel] = Math.Min(FreqBins, last + 1);
+ }
+ }
+
+ ///
+ /// 의 로그멜을 새 배열에 담아 돌려준다(Kotlin 과 같은 꼴). 실시간 경로에서는 매번 새 배열을
+ /// 만들지 않도록 버퍼를 받는 쪽을 쓴다.
+ ///
+ public float[] ComputeLogMelSpectrogram(ReadOnlySpan mono16k)
+ {
+ var logMel = new float[LogMelSize];
+ ComputeLogMelSpectrogram(mono16k, logMel);
+ return logMel;
+ }
+
+ ///
+ /// 16kHz 모노 소리의 [1, 1, 96, 64] 로그멜을 앞쪽 6,144칸에 시간 우선으로 쓴다.
+ /// 입력이 15,600샘플보다 길면 앞 15,600샘플만 쓰고, 짧으면 뒤를 0 으로 채운 다음 반사 패딩을 한다(고정한 원본과 같다).
+ /// 메모리를 새로 잡지 않는다.
+ ///
+ public void ComputeLogMelSpectrogram(ReadOnlySpan mono16k, Span logMel)
+ {
+ if (logMel.Length < LogMelSize)
+ throw new ArgumentException($"logMel must hold {LogMelSize} values", nameof(logMel));
+
+ int copyLength = Math.Min(mono16k.Length, RequiredMono16kSamples);
+ mono16k[..copyLength].CopyTo(_fitted);
+ Array.Clear(_fitted, copyLength, RequiredMono16kSamples - copyLength);
+
+ for (int time = 0; time < TimeFrames; time++)
+ {
+ Array.Clear(_fftReal);
+ Array.Clear(_fftImag);
+
+ // torch.stft(center=True, pad_mode="reflect") 는 양쪽을 256샘플씩 늘린 뒤,
+ // 400점 창을 512점 FFT 가운데(56번째 칸부터)에 놓는다.
+ int unpaddedFrameStart = time * HopLength - ReflectPadding;
+ for (int windowIndex = 0; windowIndex < WindowLength; windowIndex++)
+ {
+ int fftIndex = WindowOffset + windowIndex;
+ int sourceIndex = ReflectIndex(unpaddedFrameStart + fftIndex, RequiredMono16kSamples);
+ _fftReal[fftIndex] = (double)_fitted[sourceIndex] * _hannWindow[windowIndex];
+ }
+
+ _fft.Transform(_fftReal, _fftImag);
+ for (int frequency = 0; frequency < FreqBins; frequency++)
+ {
+ double real = _fftReal[frequency];
+ double imaginary = _fftImag[frequency];
+ _magnitude[frequency] = Math.Sqrt(real * real + imaginary * imaginary);
+ }
+
+ for (int mel = 0; mel < MelBins; mel++)
+ {
+ double sum = 0.0;
+ double[] weights = _melFilterBank[mel];
+ for (int frequency = _melStartBins[mel]; frequency < _melEndBins[mel]; frequency++)
+ {
+ sum += weights[frequency] * _magnitude[frequency];
+ }
+ logMel[time * MelBins + mel] = (float)Math.Log(sum + (double)LogEps);
+ }
+ }
+ }
+
+ /// numpy·torch 의 reflect: 가장자리 샘플을 되풀이하지 않고 접는다(-1 → 1, length → length - 2).
+ private static int ReflectIndex(int index, int length)
+ {
+ System.Diagnostics.Debug.Assert(length > 1, "reflect padding requires at least two samples");
+ int reflected = index;
+ while (reflected < 0 || reflected >= length)
+ {
+ reflected = reflected < 0 ? -reflected : 2 * length - 2 - reflected;
+ }
+ return reflected;
+ }
+
+ /// periodic Hann(torch.hann_window 기본값). 분모가 length - 1 이 아니라 length 다.
+ private static double[] CreateHannWindow(int length)
+ {
+ var window = new double[length];
+ for (int index = 0; index < length; index++)
+ {
+ window[index] = 0.5 - 0.5 * Math.Cos(2.0 * Math.PI * index / length);
+ }
+ return window;
+ }
+
+ ///
+ /// torchaudio.functional.melscale_fbanks 의 기본값(HTK 척도, norm=None). 삼각형을 FFT 빈 번호로 반올림하지 않고
+ /// 각 빈의 연속 주파수 위에서 잰다. 결과는 [멜 대역][FFT 빈] 이다.
+ ///
+ private static double[][] CreateTorchaudioHtkMelFilterBank(
+ int melBins,
+ int fftSize,
+ int sampleRate,
+ double minimumFrequency,
+ double maximumFrequency)
+ {
+ int frequencyBins = fftSize / 2 + 1;
+ double minimumMel = HertzToHtkMel(minimumFrequency);
+ double maximumMel = HertzToHtkMel(maximumFrequency);
+ var frequencyEdges = new double[melBins + 2];
+ for (int edge = 0; edge < frequencyEdges.Length; edge++)
+ {
+ double mel = minimumMel + (maximumMel - minimumMel) * edge / (melBins + 1.0);
+ frequencyEdges[edge] = HtkMelToHertz(mel);
+ }
+
+ var weights = new double[melBins][];
+ for (int mel = 0; mel < melBins; mel++)
+ {
+ weights[mel] = new double[frequencyBins];
+ double lower = frequencyEdges[mel];
+ double center = frequencyEdges[mel + 1];
+ double upper = frequencyEdges[mel + 2];
+ for (int frequencyBin = 0; frequencyBin < frequencyBins; frequencyBin++)
+ {
+ double frequency = frequencyBin * (sampleRate / 2.0) / (frequencyBins - 1);
+ double downSlope = (frequency - lower) / (center - lower);
+ double upSlope = (upper - frequency) / (upper - center);
+ weights[mel][frequencyBin] = Math.Max(0.0, Math.Min(downSlope, upSlope));
+ }
+ }
+ return weights;
+ }
+
+ private static double HertzToHtkMel(double hertz) => 1127.0 * Log1P(hertz / 700.0);
+
+ private static double HtkMelToHertz(double mel) => 700.0 * ExpM1(mel / 1127.0);
+
+ // double.LogP1·ExpM1 은 Log(x + 1)·Exp(x) − 1 을 그대로 계산해 x 가 0 에 가까우면 자릿수를 잃는다.
+ // Kotlin 의 ln1p·expm1(fdlibm) 처럼 어디서나 정확한 값을 내도록 Kahan 의 보정을 쓴다.
+ private static double Log1P(double x)
+ {
+ double u = 1.0 + x;
+ if (u == 1.0) return x;
+ if (double.IsPositiveInfinity(u)) return u;
+ return Math.Log(u) * x / (u - 1.0);
+ }
+
+ private static double ExpM1(double x)
+ {
+ double u = Math.Exp(x);
+ if (u == 1.0) return x;
+ if (double.IsPositiveInfinity(u)) return u;
+ double uMinusOne = u - 1.0;
+ if (uMinusOne == -1.0) return -1.0;
+ return uMinusOne * x / Math.Log(u);
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/Radix2Fft.cs b/SoundVisualizer.Core/Ai/Radix2Fft.cs
new file mode 100644
index 0000000..407e2e9
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/Radix2Fft.cs
@@ -0,0 +1,118 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Numerics;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 기수 2 Cooley–Tukey FFT(제자리 계산). 모바일 판 AudioPreprocessor.kt 의 fftInPlace 를 곱셈·덧셈 순서까지
+ /// 그대로 옮겼다. 로그멜 기준 데이터와의 오차를 Kotlin 과 같게 두려면 계산 순서가 같아야 한다.
+ ///
+ /// Kotlin 은 단계마다 회전 인자를 w ← w·w_len 점화식으로 블록마다 처음부터 다시 만든다. 여기서는 같은 점화식으로
+ /// 생성할 때 한 번만 표를 만든다. 같은 곱셈을 같은 순서로 하므로 블록마다 다시 만든 값과 비트까지 같고,
+ /// 호출마다 삼각함수와 점화식 곱셈을 하지 않아도 된다.
+ ///
+ /// 표는 만든 뒤 읽기만 하므로 여러 스레드가 함께 써도 된다. 버퍼는 부르는 쪽이 준다.
+ ///
+ internal sealed class Radix2Fft
+ {
+ private readonly int[] _bitReversed;
+
+ // 단계(len = 2·half)마다 w_0..w_{half-1} 을 [half - 1, 2·half - 1) 에 둔다. 모두 합쳐 Size - 1 칸이다.
+ private readonly double[] _twiddleRe;
+ private readonly double[] _twiddleIm;
+
+ public Radix2Fft(int size)
+ {
+ if (!BitOperations.IsPow2(size))
+ throw new ArgumentException("FFT size must be a power of two", nameof(size));
+
+ Size = size;
+ _bitReversed = CreateBitReversedIndices(size);
+ _twiddleRe = new double[size - 1];
+ _twiddleIm = new double[size - 1];
+
+ for (int len = 2; len <= size; len <<= 1)
+ {
+ double ang = -2.0 * Math.PI / len;
+ double wLenRe = Math.Cos(ang);
+ double wLenIm = Math.Sin(ang);
+ int half = len >> 1;
+ double wRe = 1.0;
+ double wIm = 0.0;
+ for (int j = 0; j < half; j++)
+ {
+ _twiddleRe[half - 1 + j] = wRe;
+ _twiddleIm[half - 1 + j] = wIm;
+ double nextWRe = wRe * wLenRe - wIm * wLenIm;
+ double nextWIm = wRe * wLenIm + wIm * wLenRe;
+ wRe = nextWRe;
+ wIm = nextWIm;
+ }
+ }
+ }
+
+ public int Size { get; }
+
+ /// · 를 제자리에서 이산 푸리에 변환한다(지수 부호 −).
+ public void Transform(Span real, Span imag)
+ {
+ int n = Size;
+ if (real.Length != n || imag.Length != n)
+ throw new ArgumentException($"FFT buffers must hold exactly {n} values");
+
+ for (int i = 0; i < n; i++)
+ {
+ int j = _bitReversed[i];
+ if (j > i)
+ {
+ double tr = real[i]; real[i] = real[j]; real[j] = tr;
+ double ti = imag[i]; imag[i] = imag[j]; imag[j] = ti;
+ }
+ }
+
+ for (int len = 2; len <= n; len <<= 1)
+ {
+ int half = len >> 1;
+ int twiddleStart = half - 1;
+ for (int i = 0; i < n; i += len)
+ {
+ for (int j = 0; j < half; j++)
+ {
+ double wRe = _twiddleRe[twiddleStart + j];
+ double wIm = _twiddleIm[twiddleStart + j];
+ int top = i + j;
+ int bottom = top + half;
+ double uRe = real[top];
+ double uIm = imag[top];
+ double vRe = real[bottom] * wRe - imag[bottom] * wIm;
+ double vIm = real[bottom] * wIm + imag[bottom] * wRe;
+ real[top] = uRe + vRe;
+ imag[top] = uIm + vIm;
+ real[bottom] = uRe - vRe;
+ imag[bottom] = uIm - vIm;
+ }
+ }
+ }
+ }
+
+ private static int[] CreateBitReversedIndices(int n)
+ {
+ int bits = BitOperations.TrailingZeroCount(n);
+ var indices = new int[n];
+ for (int i = 0; i < n; i++)
+ {
+ int v = i;
+ int r = 0;
+ for (int b = 0; b < bits; b++)
+ {
+ r = (r << 1) | (v & 1);
+ v >>= 1;
+ }
+ indices[i] = r;
+ }
+ return indices;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs b/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs
new file mode 100644
index 0000000..3f5ea49
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs
@@ -0,0 +1,517 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+using System.Diagnostics;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 실시간 AI 분류(Android 의 RealtimeAiPipeline.kt). 캡처 스레드가 넣은 소리를 배경 스레드가 250ms 마다 꺼내
+ /// 링 → 16kHz → Qualcomm 로그멜 → YAMNet → 투표·안전 단서·후처리() 를 거쳐 확정 결과를 게시한다.
+ ///
+ /// 스레드:
+ /// - 는 캡처 스레드 하나에서만 부른다. 링에 복사만 하고 바로 돌아온다.
+ /// - 은 어느 스레드에서나 부른다. 불변 결과의 참조 하나를 잠금 없이 읽는다.
+ /// - ·· 는 UI 스레드 등 어디서나 부른다. 돌고 있는 추론을 최대 1초 기다린다.
+ /// - 틱은 이 클래스가 띄우는 배경 스레드 "SV-AiTick" 에서 돈다(Android 는 Dispatchers.Default 코루틴).
+ /// 시각은 모두 단조 증가하는 밀리초 하나()로 잰다.
+ ///
+ /// PC 에서 바꾼 것:
+ /// - 이 돌고 있는 틱을 기다리고 마지막 결과를 바로 숨기며, 는 앞 틱이 끝난 뒤에 비운다.
+ /// Android 서비스는 늦게 끝난 추론이 비운 뒤의 결과를 덮지 않도록 읽는 쪽을 떼고 AI 재시작을 미뤘는데, 여기서는 그럴 필요가 없다.
+ /// - 가 1초 안에 추론을 기다리지 못하면 Android 는 세션을 버리지만(누수), 여기서는 그 추론이 끝나면서 닫는다.
+ ///
+ public sealed class RealtimeAiPipeline : IDisposable
+ {
+ /// 틱 시작에서 다음 시작까지(#379).
+ public const long PredictIntervalMs = 250;
+
+ /// 추론이 150ms 넘게 걸리는 느린 PC 에서도 쉬지 않고 연달아 돌지 않게 하는 최소 휴식.
+ internal const long MinTickRestMs = 100;
+
+ /// Stop·Dispose 가 돌고 있는 추론을 기다리는 한도. UI 스레드를 오래 붙잡지 않는다(Android 는 메인 스레드 ANR).
+ internal const int CloseWaitMs = 1000;
+
+ private const string Tag = nameof(RealtimeAiPipeline);
+ private const long FailureLogIntervalMs = 2000;
+ private const long NoTimestamp = long.MinValue;
+
+ private static readonly IReadOnlyDictionary NoOverrides = FrozenDictionary.Empty;
+
+ private readonly AiAudioBuffer _buffer;
+ private readonly AiCaptureInferenceGate _gate;
+ private readonly AiClassificationStage _stage;
+ private readonly QualcommSourceAudioPreprocessor _frontend = new();
+ private readonly IYamnetModel _model;
+ private readonly Func> _mappingOverrides;
+ private readonly Action? _tickFailed;
+ private readonly Func _clock;
+ private readonly Func _waitForNextTick;
+ private readonly bool _runScheduler;
+
+ // 추론을 한 번에 하나로 줄 세운다. 모델·후처리·작업 버퍼는 이 잠금 안에서만 쓴다.
+ private readonly object _inferLock = new();
+ // Start·Stop·Dispose 끼리의 순서를 지킨다. 추론 잠금을 기다리는 동안에도 잡고 있으므로 틱은 이 잠금을 잡지 않는다.
+ private readonly object _lifecycleLock = new();
+ private Thread? _schedulerThread;
+ private CancellationTokenSource? _schedulerStop;
+ private bool _running;
+ private int _closed;
+ private bool _modelDisposed;
+
+ private long _inferenceExecutions;
+ private long _inferenceCompletions;
+ private long _silenceSkippedTicks;
+ private long _lastFailureLogMs = NoTimestamp;
+
+ private readonly float[] _captureScratch;
+ private readonly float[] _mono16k = new float[CaptureAudioMath.RequiredMono16kSamples];
+ private readonly float[] _logMel = new float[QualcommSourceAudioPreprocessor.LogMelSize];
+ private readonly float[] _logits = new float[YamnetInference.NumClasses];
+ private readonly float[] _probabilities = new float[YamnetInference.NumClasses];
+
+ /// 캡처 레이트. 링·리샘플러가 이 레이트로 만들어지므로 장치 형식이 바뀌면 새로 만든다.
+ /// 인터리브 채널 수(1 이상).
+ /// 파이프라인이 넘겨받아 에서 닫는다.
+ /// YAMNet 클래스 521개.
+ /// 지금의 사용자 지정. null 이면 늘 비어 있다.
+ /// 틱 실패를 받을 곳. null 이면 으로 2초에 한 번까지 남긴다.
+ /// 단조 밀리초. 테스트가 시각을 정한다. 기본은 .
+ /// 다음 틱까지 쉰다. 멈추라는 신호를 받으면 false. 테스트가 기다리지 않고 한 걸음씩 돌린다.
+ /// false 면 가 상태만 비우고 스레드를 띄우지 않는다. 테스트가 를 직접 부른다.
+ internal RealtimeAiPipeline(
+ int captureSampleRate,
+ int channels,
+ IYamnetModel model,
+ YamnetClassMap classes,
+ Func>? mappingOverrides = null,
+ Action? tickFailed = null,
+ Func? clock = null,
+ Func? waitForNextTick = null,
+ bool runScheduler = true)
+ {
+ ValidateArguments(captureSampleRate, channels, classes);
+ ArgumentNullException.ThrowIfNull(model);
+ _buffer = new AiAudioBuffer(captureSampleRate, channels);
+ _gate = new AiCaptureInferenceGate(_buffer);
+ _stage = new AiClassificationStage(classes);
+ _model = model;
+ _mappingOverrides = mappingOverrides ?? (static () => NoOverrides);
+ _tickFailed = tickFailed;
+ _clock = clock ?? (static () => Environment.TickCount64);
+ _waitForNextTick = waitForNextTick ?? WaitForNextTick;
+ _runScheduler = runScheduler;
+ _captureScratch = new float[CaptureAudioMath.CaptureSamplesForOneYamnetWindow(captureSampleRate)];
+ }
+
+ ///
+ /// 모델을 불러와 파이프라인을 만든다. 아직 돌지 않으므로 를 부른다. 모델 확인과 세션 생성에 수십 ms 가
+ /// 걸리므로 UI 스레드가 아닌 곳에서 부른다(Android 의 SV-AiInit). 실패하면 앱은 AI 없이 화면만 돌고 "AI 를 쓸 수 없음" 을 알린다.
+ ///
+ /// 캡처 레이트().
+ /// 캡처 버퍼의 인터리브 채널 수.
+ /// yamnet.onnx 와 yamnet.data 가 있는 폴더.
+ /// yamnet_class_map.csv 에서 읽은 클래스 521개().
+ ///
+ /// 지금의 사용자 지정(영어 클래스 이름 → "ambient"/"speech"/"danger"). 틱마다, 그리고 을 부를 때마다
+ /// 부르므로 여러 스레드에서 불려도 안전하고 가벼워야 한다(설정이 바뀔 때 통째로 바꿔 끼우는 불변 맵의 참조를 돌려주는 식).
+ /// 예외를 던지면 안 된다. null 을 돌려주면 사용자 지정이 없는 것으로 본다.
+ ///
+ /// 틱 실패를 틱 스레드에서 받는다. null 이면 으로 2초에 한 번까지 남긴다.
+ /// AI 가 받지 않는 캡처 레이트이거나 채널 수가 1 보다 작다.
+ /// 모델 파일이 없다.
+ /// 모델 파일이 배포한 것과 다르거나 입출력이 YAMNet 계약과 다르다.
+ /// ONNX Runtime 이 세션을 만들지 못했다.
+ public static RealtimeAiPipeline Create(
+ int captureSampleRate,
+ int channels,
+ string modelDirectory,
+ YamnetClassMap classes,
+ Func>? mappingOverrides = null,
+ Action? tickFailed = null)
+ {
+ // 모델을 읽기 전에 거른다. 화면만 돌 레이트라면 15MB 를 해시하고 세션을 만들 까닭이 없다.
+ ValidateArguments(captureSampleRate, channels, classes);
+ var model = YamnetInference.Create(modelDirectory);
+ try
+ {
+ return new RealtimeAiPipeline(captureSampleRate, channels, model, classes, mappingOverrides, tickFailed);
+ }
+ catch
+ {
+ model.Dispose();
+ throw;
+ }
+ }
+
+ public int SampleRate => _buffer.SampleRate;
+
+ public int ChannelCount => _buffer.ChannelCount;
+
+ /// 뒤, · 전.
+ public bool IsRunning => Volatile.Read(ref _running) && !IsClosed;
+
+ private bool IsClosed => Volatile.Read(ref _closed) != 0;
+
+ ///
+ /// 캡처 버퍼 하나(인터리브 float PCM, 채널 수 × 프레임)를 링에 복사한다. 추론은 하지 않고 바로 돌아온다.
+ /// 캡처 스레드 하나에서만 부른다. 돌고 있지 않을 때( 전, 뒤)와 닫힌 뒤에는 버린다.
+ /// NAudio 의 바이트 버퍼는 MemoryMarshal.Cast<byte, float>(e.Buffer.AsSpan(0, e.BytesRecorded)) 로 넘긴다.
+ ///
+ public void Ingest(ReadOnlySpan interleaved)
+ {
+ if (!Volatile.Read(ref _running) || IsClosed) return;
+ // 섞기 전의 원래 채널로 무음을 판단하고, 게이트가 닫혀 있어도 늘 링에 남긴다. 다시 열릴 때 그 앞의 소리까지 한 창으로 본다.
+ _gate.IngestInterleaved(interleaved, _clock());
+ }
+
+ ///
+ /// 마지막 확정 결과. 화면은 null 이면 환경음으로 그린다. 어느 스레드에서나 부를 수 있고 잠그지 않는다.
+ /// 돌고 있지 않거나(시작 전, 멈춘 뒤, 닫힌 뒤), 첫 추론이 아직 끝나지 않았거나, 결과를 만든 뒤 사용자 지정이 바뀌었으면
+ /// (다음 추론 전이라도) null 이다. 필드끼리는 한 결과 안에서만 짝이 맞으므로 여러 필드를 쓰는 쪽은 한 번만 읽는다.
+ ///
+ public AiClassificationResult? LastClassification()
+ {
+ if (!Volatile.Read(ref _running) || IsClosed) return null;
+ return _stage.LastClassification(CurrentOverrides());
+ }
+
+ ///
+ /// 추론을 시작한다. 링·무음 게이트·히스테리시스·마지막 결과를 비우고 틱 스레드를 띄운다. 이미 돌고 있거나 닫혔으면 아무것도
+ /// 하지 않는다. 앞선 실행의 추론이 아직 돌고 있으면 그 추론이 끝난 뒤에 비운다. 그래야 그 결과가 비운 뒤에 되살아나지 않는다.
+ ///
+ /// 앞선 실행의 추론이 1초 안에 끝나지 않았다.
+ public void Start()
+ {
+ lock (_lifecycleLock)
+ {
+ if (IsClosed || Volatile.Read(ref _running)) return;
+
+ if (!Monitor.TryEnter(_inferLock, CloseWaitMs))
+ throw new TimeoutException("The previous AI inference did not finish within 1 s");
+ try
+ {
+ ResetStateLocked();
+ }
+ finally
+ {
+ Monitor.Exit(_inferLock);
+ }
+
+ if (_runScheduler)
+ {
+ var stop = new CancellationTokenSource();
+ CancellationToken token = stop.Token;
+ var thread = new Thread(() => RunScheduler(token))
+ {
+ IsBackground = true,
+ Name = "SV-AiTick",
+ };
+ try
+ {
+ thread.Start();
+ }
+ catch
+ {
+ stop.Dispose();
+ throw;
+ }
+ _schedulerThread = thread;
+ _schedulerStop = stop;
+ }
+ Volatile.Write(ref _running, true);
+ }
+ }
+
+ ///
+ /// 추론을 멈춘다. 마지막 결과는 바로 숨기고( 이 null), 다음 틱을 막고, 돌고 있는 틱이 끝나기를
+ /// 최대 1초 기다린다. 다시 할 수 있다. 여러 번 불러도 된다.
+ ///
+ public void Stop() => StopScheduler(Environment.TickCount64 + CloseWaitMs);
+
+ ///
+ /// 멈추고 모델을 닫는다. 여러 번 불러도 된다. 닫은 뒤에는 추론하지 않고, 는 버리고,
+ /// 은 null 이다. 돌고 있는 추론은 끝나기를 기다리되 1초까지만 기다린다. 그 안에 끝나지 않으면
+ /// 기다리지 않고 돌아오고, 그 추론이 끝나면서 모델을 닫는다. 어느 경우에도 쓰는 중인 세션을 닫지 않는다.
+ ///
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _closed, 1) != 0) return;
+ long deadlineMs = Environment.TickCount64 + CloseWaitMs;
+ StopScheduler(deadlineMs);
+
+ // 멈추기는 다음 틱만 막는다. 이미 모델 안에 들어간 추론이 있으면 끝나기를 기다린 뒤에 세션을 닫아야 해제한 메모리를 쓰지 않는다.
+ if (Monitor.TryEnter(_inferLock, RemainingMs(deadlineMs)))
+ {
+ try
+ {
+ DisposeModelLocked();
+ }
+ finally
+ {
+ Monitor.Exit(_inferLock);
+ }
+ }
+ else
+ {
+ AppLog.Warn(Tag, "Inference still running after 1 s; the running tick releases the YAMNet session when it finishes");
+ }
+ }
+
+ ///
+ /// 다음 틱까지 쉴 시간(Kotlin remainingTickDelayMs, #379). 틱 시작에서 다음 시작까지를 로 맞추려고
+ /// 틱에 걸린 시간을 빼되, 오래 걸린 틱 뒤에도 는 쉰다. 끝이 시작보다 앞이면 걸린 시간을 0 으로 본다.
+ ///
+ internal static long RemainingTickDelayMs(long intervalMs, long tickStartedAtMs, long tickFinishedAtMs, long minimumRestMs)
+ {
+ long tickDurationMs = Math.Max(0L, tickFinishedAtMs - tickStartedAtMs);
+ return Math.Max(intervalMs - tickDurationMs, minimumRestMs);
+ }
+
+ ///
+ /// 틱 한 번(Kotlin runTickInternal). 스케줄러가 마다 부르고, 테스트는 시각을 정해 직접 부른다.
+ /// 예외를 던지지 않는다. 실패는 알리고 무음 게이트에 넘긴다.
+ ///
+ internal AiTickOutcome RunTickOnce(long nowMs)
+ {
+ if (IsClosed) return AiTickOutcome.Closed;
+ if (!_buffer.HasEnoughForYamnetWindow()) return AiTickOutcome.NotEnoughAudio;
+ // 앞 틱이 아직 돌고 있으면 기다리지 않고 건너뛴다(Kotlin 의 tryLock).
+ if (!Monitor.TryEnter(_inferLock)) return AiTickOutcome.Busy;
+
+ (AiTickOutcome Outcome, Exception? Failure) tick;
+ try
+ {
+ tick = RunTickLocked(nowMs);
+ }
+ finally
+ {
+ // Dispose 가 기다리다 포기한 뒤에 끝난 틱이면 여기서 닫는다. 잠금 안이라 다른 추론과 겹치지 않는다.
+ if (IsClosed) DisposeModelLocked();
+ Monitor.Exit(_inferLock);
+ }
+
+ if (tick.Failure is not null) ReportTickFailure(tick.Failure);
+ return tick.Outcome;
+ }
+
+ /// 추론 횟수(테스트와 진단용). 가 0 으로 되돌린다.
+ internal InferenceStats Stats => new(
+ Interlocked.Read(ref _inferenceExecutions),
+ Interlocked.Read(ref _inferenceCompletions),
+ Interlocked.Read(ref _silenceSkippedTicks));
+
+ /// 마지막 추론의 16kHz 입력. 다음 틱이 덮어쓰므로 스케줄러를 돌리지 않는 테스트만 읽는다.
+ internal ReadOnlySpan Mono16kForTest => _mono16k;
+
+ /// 마지막 추론의 로그멜.
+ internal ReadOnlySpan LogMelForTest => _logMel;
+
+ /// 마지막 추론의 softmax 확률.
+ internal ReadOnlySpan ProbabilitiesForTest => _probabilities;
+
+ /// 닫기 경합 테스트가 틱을 링 검사에 세워 두는 데 쓴다.
+ internal AiAudioBuffer AudioBufferForTest => _buffer;
+
+ private (AiTickOutcome Outcome, Exception? Failure) RunTickLocked(long nowMs)
+ {
+ // Dispose 가 잠금을 잡기 직전에 위의 검사를 지났을 수 있으므로 잠금 안에서 다시 본다.
+ if (IsClosed) return (AiTickOutcome.Closed, null);
+
+ try
+ {
+ // 무음이라 건너뛰는 틱에도 먼저 반영한다. 이전 매핑에서 확정한 위협음이 새 매핑으로 넘어가지 않게 한다.
+ _stage.UpdateMapping(CurrentOverrides());
+ }
+ catch (Exception ex)
+ {
+ return (AiTickOutcome.Failed, ex);
+ }
+
+ if (!_gate.IsInferenceOpen(nowMs))
+ {
+ Interlocked.Increment(ref _silenceSkippedTicks);
+ return (AiTickOutcome.SkippedForSilence, null);
+ }
+
+ try
+ {
+ InferLocked();
+ }
+ catch (Exception ex)
+ {
+ _gate.OnInferenceFailed(nowMs);
+ return (AiTickOutcome.Failed, ex);
+ }
+ // 결과를 게시한 뒤에만 완료로 센다. 닫기 경합 테스트가 잠깐 보이는 결과 대신 완료 수로 확인한다(Kotlin 과 같다).
+ Interlocked.Increment(ref _inferenceCompletions);
+ _gate.OnInferenceCompleted(nowMs);
+ return (AiTickOutcome.Completed, null);
+ }
+
+ private void InferLocked()
+ {
+ Interlocked.Increment(ref _inferenceExecutions);
+ long started = Stopwatch.GetTimestamp();
+ _buffer.CopyTailRightPadded(_captureScratch);
+ CaptureAudioMath.ResampleMonoFloatTo16kCustom(_captureScratch, _buffer.SampleRate, _mono16k);
+ _frontend.ComputeLogMelSpectrogram(_mono16k, _logMel);
+ long preprocessed = Stopwatch.GetTimestamp();
+ _model.Infer(_logMel, _logits, _probabilities);
+ long inferred = Stopwatch.GetTimestamp();
+ _stage.Process(
+ _probabilities,
+ started,
+ preprocessMs: Stopwatch.GetElapsedTime(started, preprocessed).TotalMilliseconds,
+ yamnetMs: Stopwatch.GetElapsedTime(preprocessed, inferred).TotalMilliseconds);
+ }
+
+ private void RunScheduler(CancellationToken stop)
+ {
+ try
+ {
+ while (!stop.IsCancellationRequested)
+ {
+ long startedMs = _clock();
+ try
+ {
+ RunTickOnce(startedMs);
+ }
+ catch (Exception ex)
+ {
+ // RunTickOnce 는 실패를 스스로 받아 알린다. 그래도 새어 나온 예외가 루프를 죽이지 않게 한 번 더 막는다.
+ AppLog.Warn(Tag, "AI tick failed", ex);
+ }
+ long delayMs = RemainingTickDelayMs(PredictIntervalMs, startedMs, _clock(), MinTickRestMs);
+ if (!_waitForNextTick(delayMs, stop)) return;
+ }
+ }
+ catch (Exception ex)
+ {
+ // 여기까지 오는 것은 시계나 대기의 결함뿐이다. 앱을 죽이지 않고 AI 만 멈춘다.
+ AppLog.Warn(Tag, "AI scheduler stopped unexpectedly", ex);
+ }
+ }
+
+ private static bool WaitForNextTick(long delayMs, CancellationToken stop) =>
+ !stop.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(delayMs));
+
+ private void StopScheduler(long deadlineMs)
+ {
+ Thread? thread;
+ CancellationTokenSource? stop;
+ lock (_lifecycleLock)
+ {
+ Volatile.Write(ref _running, false);
+ thread = _schedulerThread;
+ stop = _schedulerStop;
+ _schedulerThread = null;
+ _schedulerStop = null;
+ stop?.Cancel();
+ }
+ if (thread is null) return;
+ // 틱 스레드 안(실패 알림 등)에서 불렸으면 자기 자신은 기다릴 수 없다. 스레드는 지금 틱을 마치고 스스로 빠져나간다.
+ if (thread == Thread.CurrentThread) return;
+
+ if (thread.Join(RemainingMs(deadlineMs)))
+ {
+ stop?.Dispose();
+ }
+ else
+ {
+ // 대기 핸들을 아직 쓰고 있을 수 있으므로 토큰은 버리지 않고 GC 에 맡긴다.
+ AppLog.Warn(Tag, "AI tick is still running after 1 s; it stops after the current inference");
+ }
+ }
+
+ private void ResetStateLocked()
+ {
+ _buffer.Reset();
+ _gate.Reset();
+ _stage.Reset();
+ Interlocked.Exchange(ref _inferenceExecutions, 0);
+ Interlocked.Exchange(ref _inferenceCompletions, 0);
+ Interlocked.Exchange(ref _silenceSkippedTicks, 0);
+ }
+
+ /// 추론 잠금 안에서만 부른다. 그래서 추론과 겹치지 않고 두 번 닫지도 않는다.
+ private void DisposeModelLocked()
+ {
+ if (_modelDisposed) return;
+ _modelDisposed = true;
+ try
+ {
+ _model.Dispose();
+ }
+ catch (Exception ex)
+ {
+ AppLog.Warn(Tag, "Failed to release the YAMNet session", ex);
+ }
+ }
+
+ private void ReportTickFailure(Exception failure)
+ {
+ if (_tickFailed is not null)
+ {
+ try
+ {
+ _tickFailed(failure);
+ }
+ catch (Exception callbackFailure)
+ {
+ AppLog.Warn(Tag, "AI tick failure callback threw", callbackFailure);
+ }
+ return;
+ }
+
+ // 모델이 계속 실패하면 틱마다(초당 4번) 같은 줄이 쌓이므로 2초에 한 번만 남긴다.
+ long now = Environment.TickCount64;
+ long last = Interlocked.Read(ref _lastFailureLogMs);
+ if (last != NoTimestamp && now - last < FailureLogIntervalMs) return;
+ Interlocked.Exchange(ref _lastFailureLogMs, now);
+ AppLog.Warn(Tag, "AI tick failed", failure);
+ }
+
+ private IReadOnlyDictionary CurrentOverrides() => _mappingOverrides() ?? NoOverrides;
+
+ private static int RemainingMs(long deadlineMs) =>
+ (int)Math.Clamp(deadlineMs - Environment.TickCount64, 0L, CloseWaitMs);
+
+ private static void ValidateArguments(int captureSampleRate, int channels, YamnetClassMap classes)
+ {
+ if (!AiCaptureSampleRatePolicy.IsSupportedForAi(captureSampleRate))
+ throw new ArgumentOutOfRangeException(nameof(captureSampleRate), captureSampleRate, "Unsupported AI capture sample rate; run visual-only");
+ if (channels < 1)
+ throw new ArgumentOutOfRangeException(nameof(channels), channels, "Channel count must be at least 1");
+ ArgumentNullException.ThrowIfNull(classes);
+ if (classes.Count != YamnetCoarseClassifier.NumClasses)
+ throw new ArgumentException($"Expected {YamnetCoarseClassifier.NumClasses} class names, got {classes.Count}", nameof(classes));
+ }
+
+ /// 추론 횟수. Executed 는 시작한 추론, Completed 는 결과를 게시하고 끝난 추론이다.
+ internal readonly record struct InferenceStats(long Executed, long Completed, long SkippedForSilence);
+ }
+
+ /// 틱 한 번이 어떻게 끝났는지. 테스트와 진단용이다.
+ internal enum AiTickOutcome
+ {
+ /// 닫힌 뒤라 아무것도 하지 않았다.
+ Closed,
+
+ /// 링에 한 창(0.975초)이 아직 모이지 않았다. 사용자 지정도 반영하지 않았다(Kotlin 과 같다).
+ NotEnoughAudio,
+
+ /// 앞 틱이 아직 돌고 있어 건너뛰었다.
+ Busy,
+
+ /// 무음 게이트가 닫혀 추론을 건너뛰었다. 사용자 지정은 반영했다.
+ SkippedForSilence,
+
+ /// 추론하고 결과를 게시했다.
+ Completed,
+
+ /// 사용자 지정을 읽거나 추론하다 실패했다. 알렸고, 추론 실패는 무음 게이트에도 넘겼다.
+ Failed,
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/SoundType.cs b/SoundVisualizer.Core/Ai/SoundType.cs
new file mode 100644
index 0000000..f86249e
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/SoundType.cs
@@ -0,0 +1,68 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// AI 가 확정하는 소리 종류. 화면 색·표시 여부·진동 설정이 이 값으로 갈린다.
+ ///
+ /// 를 0 으로 둔다. 모바일 판은 결과가 없을 때와 모르는 라벨을 모두 환경음으로 그리므로,
+ /// 초기화하지 않은 값(default)도 같은 뜻이 되게 한다.
+ ///
+ public enum SoundType
+ {
+ /// 환경음.
+ Ambient = 0,
+
+ /// 대화음. 알아듣는 말만이 아니라 사람이 내는 소리 전반(웃음, 울음, 고함 등)이다.
+ Speech = 1,
+
+ /// 위협음.
+ Danger = 2,
+ }
+
+ ///
+ /// 과 저장용 문자열 사이의 변환.
+ ///
+ /// 문자열은 모바일 판(AiClassification.AMBIENT·SPEECH·DANGER, 설정의 sound_types JSON)과 글자까지 같다.
+ /// 사용자가 바꾼 소리 종류를 "영어 클래스 이름 → 이 문자열" 맵으로 저장하므로, 두 판이 같은 설정을 읽고 쓸 수 있게 맞춘다.
+ ///
+ public static class SoundTypeLabels
+ {
+ public const string Ambient = "ambient";
+ public const string Speech = "speech";
+ public const string Danger = "danger";
+
+ /// 저장할 문자열. 정의되지 않은 값은 프로그램 오류라 예외를 던진다.
+ public static string ToLabel(this SoundType type) => type switch
+ {
+ SoundType.Ambient => Ambient,
+ SoundType.Speech => Speech,
+ SoundType.Danger => Danger,
+ _ => throw new ArgumentOutOfRangeException(nameof(type), type, "알 수 없는 소리 종류"),
+ };
+
+ ///
+ /// 저장된 문자열을 읽는다. 세 문자열과 대소문자까지 같을 때만 true 다. 모바일 판도 "Danger" 처럼
+ /// 조금이라도 다른 값은 사용자 지정에서 버리므로 여기서 너그럽게 읽지 않는다.
+ ///
+ public static bool TryParse(string? label, out SoundType type)
+ {
+ switch (label)
+ {
+ case Ambient:
+ type = SoundType.Ambient;
+ return true;
+ case Speech:
+ type = SoundType.Speech;
+ return true;
+ case Danger:
+ type = SoundType.Danger;
+ return true;
+ default:
+ type = SoundType.Ambient;
+ return false;
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/YamnetClassMap.cs b/SoundVisualizer.Core/Ai/YamnetClassMap.cs
new file mode 100644
index 0000000..a90346f
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/YamnetClassMap.cs
@@ -0,0 +1,196 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections;
+using System.Globalization;
+using System.Text;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// yamnet_class_map.csv 의 클래스 이름 목록(모델 출력 번호 → 영어 display_name). 사용자 지정 종류도 이 이름을 키로 쓴다.
+ ///
+ /// 모바일 판은 이름 목록만 들고 추론 때마다 이름을 소문자로 바꿔 키워드를 찾는다(효과음에 가려진 위협음을 찾을 때는 521개 전부).
+ /// 여기서는 만들 때 이름마다 기본 종류와 키워드 판정을 한 번 계산해 두고, 추론 중에는 번호로 꺼내 쓴다.
+ /// 규칙은 문자열을 받는 함수( 등) 한 곳에만 있고 이 표는 그 결과를 담기만 하므로,
+ /// 같은 이름이면 결과도 같다.
+ ///
+ public sealed class YamnetClassMap : IReadOnlyList
+ {
+ private readonly string[] _names;
+ private readonly SoundType[] _defaultCoarse;
+ private readonly YamnetLabelTraits[] _traits;
+ private readonly int[] _defaultDangerIndices;
+ private readonly Dictionary _indexByName;
+
+ /// 이름 목록을 복사해 판정을 미리 계산한다. 분류기는 521개만 받지만 목록 자체는 길이를 따지지 않는다.
+ public YamnetClassMap(IReadOnlyList names)
+ {
+ ArgumentNullException.ThrowIfNull(names);
+ int count = names.Count;
+ _names = new string[count];
+ _defaultCoarse = new SoundType[count];
+ _traits = new YamnetLabelTraits[count];
+ _indexByName = new Dictionary(count, StringComparer.Ordinal);
+ var dangers = new List();
+ for (int i = 0; i < count; i++)
+ {
+ string name = names[i] ?? throw new ArgumentException($"Class name at {i} is null", nameof(names));
+ _names[i] = name;
+ _defaultCoarse[i] = YamnetThreeClassMapper.MapDisplayNameToCoarse(name);
+ _traits[i] = ComputeTraits(name);
+ _indexByName.TryAdd(name, i); // 같은 이름이 또 있으면 앞 번호를 남긴다(List.indexOf 와 같다).
+ if (_defaultCoarse[i] == SoundType.Danger) dangers.Add(i);
+ }
+ _defaultDangerIndices = dangers.ToArray();
+ }
+
+ public int Count => _names.Length;
+
+ public string this[int index] => _names[index];
+
+ /// 이름과 정확히 같은(대소문자 포함) 클래스의 번호. 없으면 -1.
+ public int IndexOf(string? displayName) =>
+ displayName is not null && _indexByName.TryGetValue(displayName, out int index) ? index : -1;
+
+ /// 사용자 지정을 반영하지 않은 기본 종류. 와 같은 값이다.
+ public SoundType DefaultCoarse(int index) => _defaultCoarse[index];
+
+ public IEnumerator GetEnumerator() => ((IEnumerable)_names).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// 파일에서 읽는다. 앱과 테스트는 Models/yamnet_class_map.csv 를 쓴다.
+ public static YamnetClassMap Load(string csvPath)
+ {
+ using var stream = File.OpenRead(csvPath);
+ return Load(stream);
+ }
+
+ public static YamnetClassMap Load(Stream csv) => new(LoadClassNames(csv));
+
+ ///
+ /// CSV(index, mid, display_name — 따옴표로 감싼 칸 가능)에서 521개 이름을 읽는다. 모바일 YamnetCoarseClassifier.loadClassNames 와 같다.
+ /// 번호가 숫자가 아닌 줄(머리글)과 범위를 벗어난 번호는 건너뛰고, 빠진 번호는 "class_{번호}" 로 남긴다.
+ /// 받은 스트림은 닫지 않는다.
+ ///
+ public static IReadOnlyList LoadClassNames(Stream csv)
+ {
+ ArgumentNullException.ThrowIfNull(csv);
+ var names = new string[YamnetCoarseClassifier.NumClasses];
+ for (int i = 0; i < names.Length; i++) names[i] = "class_" + i.ToString(CultureInfo.InvariantCulture);
+
+ using var reader = new StreamReader(csv, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
+ string? line;
+ while ((line = reader.ReadLine()) is not null)
+ {
+ var parts = ParseCsvLine(line);
+ if (parts.Count == 0) continue;
+ if (!int.TryParse(parts[0].Trim().Trim('"'), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out int index)) continue;
+ if (index < 0 || index >= names.Length) continue;
+
+ string name;
+ if (parts.Count >= 3) name = parts[2].Trim();
+ else if (parts.Count >= 2) name = parts[1].Trim();
+ else continue;
+
+ if (name.Length >= 2 && name[0] == '"' && name[^1] == '"')
+ {
+ name = name[1..^1].Replace("\"\"", "\"");
+ }
+ if (name.Length > 0) names[index] = name;
+ }
+ return names;
+ }
+
+ /// yamnet_class_map.csv 에 맞춘 최소한의 RFC 4180 식 분리. 따옴표 안의 쉼표와 두 번 쓴 따옴표("")를 처리한다.
+ internal static List ParseCsvLine(string line)
+ {
+ var output = new List(3);
+ var sb = new StringBuilder();
+ bool inQuotes = false;
+ for (int i = 0; i < line.Length; i++)
+ {
+ char c = line[i];
+ if (c == '"')
+ {
+ if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
+ {
+ sb.Append('"');
+ i++;
+ }
+ else
+ {
+ inQuotes = !inQuotes;
+ }
+ }
+ else if (c == ',' && !inQuotes)
+ {
+ output.Add(sb.ToString());
+ sb.Clear();
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ output.Add(sb.ToString());
+ return output;
+ }
+
+ /// 기본 매핑이 위협음인 클래스 번호. 오름차순이라 확률이 같으면 앞 번호가 남는 것까지 모바일의 전체 순회와 같다.
+ internal ReadOnlySpan DefaultDangerIndices => _defaultDangerIndices;
+
+ internal YamnetLabelTraits TraitsAt(int index) => _traits[index];
+
+ ///
+ /// 의 판정. 그 번호의 이름과 같으면 미리 계산한 값을 쓰고, 다르면(직접 만든 결과 등) 이름으로 다시 계산한다.
+ /// 결과 안의 표시 이름은 늘 그 번호의 클래스 이름이라 추론 중에는 계산하는 일이 없다.
+ ///
+ internal YamnetLabelTraits TraitsOf(int index, string? name) =>
+ (uint)index < (uint)_names.Length && string.Equals(_names[index], name, StringComparison.Ordinal)
+ ? _traits[index]
+ : ComputeTraits(name);
+
+ /// 목록이 없으면 이름으로 계산한다. 문자열만 받는 공개 함수와 번호를 아는 추론 경로가 같은 구현을 쓰게 한다.
+ internal static YamnetLabelTraits TraitsOf(YamnetClassMap? classes, int index, string? name) =>
+ classes is null ? ComputeTraits(name) : classes.TraitsOf(index, name);
+
+ internal static YamnetLabelTraits ComputeTraits(string? name)
+ {
+ var traits = YamnetLabelTraits.None;
+ if (YamnetCoarseClassifier.IsGameMixMaskDisplay(name)) traits |= YamnetLabelTraits.GameMixMask;
+ if (YamnetSafetyCueDecision.IsSpeechLikeDisplay(name)) traits |= YamnetLabelTraits.SpeechLikeDisplay;
+ if (YamnetSafetyCueDecision.IsSilenceLikeDisplay(name)) traits |= YamnetLabelTraits.SilenceLikeDisplay;
+ if (YamnetSafetyCueDecision.IsGunshotKeyword(name)) traits |= YamnetLabelTraits.Gunshot;
+ if (YamnetSafetyCueDecision.IsStrongDangerKeyword(name)) traits |= YamnetLabelTraits.StrongDangerCue;
+ if (AiPostProcessor.IsCriticalDangerKeyword(name)) traits |= YamnetLabelTraits.CriticalDanger;
+ return traits;
+ }
+ }
+
+ /// 클래스 이름에서 미리 계산해 두는 키워드 판정. 각 값은 같은 이름의 공개 문자열 함수 결과다.
+ [Flags]
+ internal enum YamnetLabelTraits : byte
+ {
+ None = 0,
+
+ /// : 음악·게임·효과음이라 위협음을 가릴 수 있는 1위.
+ GameMixMask = 1 << 0,
+
+ /// 말소리 계열 표시 이름. 안전 단서 승격과 가려진 위협음 찾기를 막는다.
+ SpeechLikeDisplay = 1 << 1,
+
+ /// 무음 계열 표시 이름. 안전 단서 승격과 가려진 위협음 찾기를 막는다.
+ SilenceLikeDisplay = 1 << 2,
+
+ ///
+ Gunshot = 1 << 3,
+
+ ///
+ StrongDangerCue = 1 << 4,
+
+ ///
+ CriticalDanger = 1 << 5,
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/YamnetCoarseClassifier.cs b/SoundVisualizer.Core/Ai/YamnetCoarseClassifier.cs
new file mode 100644
index 0000000..de08963
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/YamnetCoarseClassifier.cs
@@ -0,0 +1,273 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// YAMNet 확률 521개 → 상위 5개 → (효과음·음악에 가려진 위협음이면 표시 이름을 그쪽으로) → 상위 3개 확률 투표.
+ /// 모바일 YamnetCoarseClassifier.kt 와 같다. 안전 단서는 , 확신도 기준과
+ /// 히스테리시스는 가 맡는다. 옛 PC 판의 Gunshot Booster 모델은 모바일처럼 없앴다.
+ ///
+ public sealed class YamnetCoarseClassifier
+ {
+ public const int NumClasses = 521;
+ public const int TopK = 5;
+ public const int VoteK = 3;
+
+ // 가려진 위협음을 표시 이름으로 고르는 문턱: max(0.06, 1위 확률 × 0.22).
+ private const float GameMixMinDangerProbability = 0.06f;
+ private const float GameMixRelativeToTop = 0.22f;
+
+ private readonly YamnetClassMap _classes;
+
+ public YamnetCoarseClassifier(YamnetClassMap classes)
+ {
+ ArgumentNullException.ThrowIfNull(classes);
+ if (classes.Count != NumClasses)
+ throw new ArgumentException($"Expected {NumClasses} class names, got {classes.Count}", nameof(classes));
+ _classes = classes;
+ }
+
+ /// 이름 목록에서 바로 만든다. 판정을 다시 계산하므로 같은 목록이면 하나를 나눠 쓴다.
+ public YamnetCoarseClassifier(IReadOnlyList classNames)
+ : this(classNames as YamnetClassMap ?? new YamnetClassMap(classNames))
+ {
+ }
+
+ public YamnetClassMap Classes => _classes;
+
+ /// 상위 후보 하나. 은 영어 클래스 이름이다.
+ public readonly record struct TopClassHit(int Index, string Name, float Probability);
+
+ /// 상위 3개를 종류별로 더한 확률.
+ public readonly record struct VoteScores(float Ambient, float Speech, float Danger);
+
+ ///
+ /// 한 프레임의 투표 결과.
+ ///
+ /// 사용자 지정까지 반영한 상위 3개 투표.
+ ///
+ /// 같은 상위 3개를 기본 매핑으로만 투표한 결과. 사용자가 어떤 소리를 대화음으로 바꿔도 다른 독립 경보까지
+ /// 막지 않도록, 안전 단서의 "대화음이면 올리지 않음" 은 이 값으로 판단한다(#283 B).
+ ///
+ /// 표시 이름. 1위이거나, 가려진 위협음을 고른 경우 그 위협음 이름이다.
+ /// 의 확률.
+ /// 의 클래스 번호. 후보가 없으면 -1.
+ /// 걸러내지 않은 모델 원출력 상위 5개. 사용자 지정과 상관없다.
+ /// 사용자 지정을 반영한 투표 점수.
+ /// 사용자 지정을 반영한 투표 점수.
+ /// 사용자 지정을 반영한 투표 점수.
+ /// 가려진 위협음을 표시 이름으로 골랐는지. 투표는 그대로 원래 상위 3개다.
+ public sealed record Result(
+ SoundType Coarse,
+ SoundType DefaultCoarse,
+ string DisplayName,
+ float Confidence,
+ int YamnetClassIndex,
+ IReadOnlyList Top5,
+ float AmbientScore,
+ float SpeechScore,
+ float DangerScore,
+ bool GameMixPreferenceApplied)
+ {
+ // 모바일 data class 처럼 상위 5개도 내용으로 비교한다. record 기본 비교는 목록을 참조로만 본다.
+ public bool Equals(Result? other) =>
+ other is not null &&
+ Coarse == other.Coarse &&
+ DefaultCoarse == other.DefaultCoarse &&
+ DisplayName == other.DisplayName &&
+ Confidence.Equals(other.Confidence) &&
+ YamnetClassIndex == other.YamnetClassIndex &&
+ Top5.SequenceEqual(other.Top5) &&
+ AmbientScore.Equals(other.AmbientScore) &&
+ SpeechScore.Equals(other.SpeechScore) &&
+ DangerScore.Equals(other.DangerScore) &&
+ GameMixPreferenceApplied == other.GameMixPreferenceApplied;
+
+ public override int GetHashCode() =>
+ HashCode.Combine(Coarse, DefaultCoarse, DisplayName, Confidence, YamnetClassIndex, Top5.Count, GameMixPreferenceApplied);
+ }
+
+ ///
+ /// 확률 521개를 투표한다. 은 추론 한 번 동안 바뀌지 않는 사용자 지정 스냅샷이고, 없으면 기본 매핑이다.
+ /// 상위 5개 배열은 스택에 두고(모바일은 추론마다 새로 만든다) 힙에는 결과와 상위 5개 목록만 남긴다.
+ ///
+ public Result Classify(ReadOnlySpan probabilities, YamnetMappingPolicy? mapping = null)
+ {
+ mapping ??= YamnetMappingPolicy.Default;
+ if (probabilities.Length != NumClasses)
+ throw new ArgumentException($"Expected {NumClasses} probabilities, got {probabilities.Length}", nameof(probabilities));
+
+ Span topIdx = stackalloc int[TopK];
+ Span topProbs = stackalloc float[TopK];
+ ComputeTop5(probabilities, topIdx, topProbs);
+
+ int maxIndex = topIdx[0];
+ float conf = maxIndex >= 0 ? topProbs[0] : 0f;
+ string display = maxIndex >= 0 ? _classes[maxIndex] : "";
+
+ var (shownIndex, shownConf, shownDisplay) =
+ PreferDangerWhenTopIsMaskedByGameMix(probabilities, maxIndex, conf, display, mapping);
+ bool gameMixApplied = shownIndex != maxIndex || shownConf != conf || shownDisplay != display;
+
+ var scores = CoarseVoteScores(topIdx, topProbs, VoteK, mapping);
+ var coarse = VoteCoarseFromScores(scores);
+
+ int count = 0;
+ for (int i = 0; i < TopK; i++)
+ {
+ if (topIdx[i] >= 0) count++;
+ }
+ var top5 = new TopClassHit[count];
+ for (int i = 0, j = 0; i < TopK; i++)
+ {
+ int idx = topIdx[i];
+ if (idx < 0) continue;
+ top5[j++] = new TopClassHit(idx, _classes[idx], topProbs[i]);
+ }
+
+ return new Result(
+ Coarse: coarse,
+ DefaultCoarse: mapping.OverrideCount == 0 ? coarse : VoteCoarseFromTop5(topIdx, topProbs, VoteK),
+ DisplayName: shownDisplay,
+ Confidence: shownConf,
+ YamnetClassIndex: shownIndex,
+ Top5: top5,
+ AmbientScore: scores.Ambient,
+ SpeechScore: scores.Speech,
+ DangerScore: scores.Danger,
+ GameMixPreferenceApplied: gameMixApplied);
+ }
+
+ ///
+ /// 확률이 큰 5개. 앞선 값보다 "커야" 자리를 차지하므로 같은 확률이면 앞 번호가 남는다. 남는 자리는 번호 -1, 확률 -1 이다.
+ ///
+ public void ComputeTop5(ReadOnlySpan probs, Span outIndices, Span outProbs)
+ {
+ if (outIndices.Length < TopK || outProbs.Length < TopK)
+ throw new ArgumentException($"Output spans must hold {TopK} entries");
+ for (int i = 0; i < TopK; i++)
+ {
+ outIndices[i] = -1;
+ outProbs[i] = -1f;
+ }
+ int n = Math.Min(probs.Length, _classes.Count);
+ for (int i = 0; i < n; i++)
+ {
+ float p = probs[i];
+ for (int j = 0; j < TopK; j++)
+ {
+ if (p > outProbs[j])
+ {
+ for (int k = TopK - 1; k > j; k--)
+ {
+ outProbs[k] = outProbs[k - 1];
+ outIndices[k] = outIndices[k - 1];
+ }
+ outProbs[j] = p;
+ outIndices[j] = i;
+ break;
+ }
+ }
+ }
+ }
+
+ /// 앞의 개(최대 5개) 후보 확률을 의 종류별로 더한다.
+ public VoteScores CoarseVoteScores(ReadOnlySpan topIndices, ReadOnlySpan topProbs, int k, YamnetMappingPolicy? mapping = null)
+ {
+ mapping ??= YamnetMappingPolicy.Default;
+ float danger = 0f;
+ float speech = 0f;
+ float ambient = 0f;
+ int limit = Math.Min(TopK, k);
+ for (int idx = 0; idx < limit; idx++)
+ {
+ int i = topIndices[idx];
+ if (i < 0) continue;
+ float p = topProbs[idx];
+ switch (mapping.CoarseAt(_classes, i))
+ {
+ case SoundType.Danger:
+ danger += p;
+ break;
+ case SoundType.Speech:
+ speech += p;
+ break;
+ default:
+ ambient += p;
+ break;
+ }
+ }
+ return new VoteScores(Ambient: ambient, Speech: speech, Danger: danger);
+ }
+
+ /// 기본 매핑으로 투표한다(사용자 지정 무시).
+ public SoundType VoteCoarseFromTop5(ReadOnlySpan topIndices, ReadOnlySpan topProbs, int k) =>
+ VoteCoarseFromScores(CoarseVoteScores(topIndices, topProbs, k));
+
+ /// 점수가 가장 큰 종류. 같으면 위협음 → 대화음 → 환경음 순으로 고른다. 놓치면 더 위험한 쪽이 이긴다.
+ public static SoundType VoteCoarseFromScores(VoteScores scores)
+ {
+ if (scores.Danger >= scores.Speech && scores.Danger >= scores.Ambient) return SoundType.Danger;
+ if (scores.Speech >= scores.Ambient) return SoundType.Speech;
+ return SoundType.Ambient;
+ }
+
+ ///
+ /// 1위가 위협음을 가릴 수 있는 소리(효과음, 음악, 비디오 게임)인지. 게임·영상 속 총성은 효과음이나 음악이 1위를 차지하기 쉽다.
+ /// 말소리·무음 계열 이름은 제외한다.
+ ///
+ public static bool IsGameMixMaskDisplay(string? display)
+ {
+ if (string.IsNullOrEmpty(display)) return false;
+ // 모바일은 이 두 판정을 분류기와 안전 단서에 같은 내용으로 따로 두는데, 여기서는 한 벌을 같이 쓴다.
+ if (YamnetSafetyCueDecision.IsSpeechLikeDisplay(display) || YamnetSafetyCueDecision.IsSilenceLikeDisplay(display)) return false;
+ if (YamnetThreeClassMapper.IsGenericSoundEffectLabel(display)) return true;
+ string s = display.ToLowerInvariant();
+ return s.Contains("music") || s.Contains("video game");
+ }
+
+ ///
+ /// 1위가 효과음·음악이면, 기본 매핑이 위협음인 클래스 중 확률이 가장 큰 것을 찾아 max(0.06, 1위 × 0.22) 이상이면
+ /// 표시 이름과 확신도를 그쪽으로 바꾼다. 투표에는 영향이 없다.
+ /// 사용자가 위협음에서 뺀 라벨은 고르지 않고, 새로 위협음으로 지정한 라벨은 투표에만 들어가므로 여기서 찾지 않는다.
+ ///
+ public (int Index, float Confidence, string Display) PreferDangerWhenTopIsMaskedByGameMix(
+ ReadOnlySpan probs,
+ int maxIndex,
+ float conf,
+ string display,
+ YamnetMappingPolicy? mapping = null)
+ {
+ mapping ??= YamnetMappingPolicy.Default;
+ if (maxIndex < 0 || maxIndex >= _classes.Count || probs.Length != _classes.Count)
+ {
+ return (maxIndex, conf, display);
+ }
+ if ((_classes.TraitsOf(maxIndex, display) & YamnetLabelTraits.GameMixMask) == 0)
+ {
+ return (maxIndex, conf, display);
+ }
+
+ // 모바일은 521개 이름을 매번 소문자로 바꿔 기본 종류를 다시 구한다. 미리 모아 둔 위협음 번호만 돈다.
+ int bestDangerIdx = -1;
+ float bestDangerProb = 0f;
+ foreach (int i in _classes.DefaultDangerIndices)
+ {
+ if (!mapping.AllowsSafetyCue(_classes[i])) continue;
+ if (probs[i] > bestDangerProb)
+ {
+ bestDangerProb = probs[i];
+ bestDangerIdx = i;
+ }
+ }
+ if (bestDangerIdx < 0) return (maxIndex, conf, display);
+
+ float topProb = probs[maxIndex];
+ float bar = Math.Max(GameMixMinDangerProbability, topProb * GameMixRelativeToTop);
+ if (bestDangerProb < bar) return (maxIndex, conf, display);
+
+ return (bestDangerIdx, bestDangerProb, _classes[bestDangerIdx]);
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/YamnetInference.cs b/SoundVisualizer.Core/Ai/YamnetInference.cs
new file mode 100644
index 0000000..1873e66
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/YamnetInference.cs
@@ -0,0 +1,247 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using Microsoft.ML.OnnxRuntime;
+using Microsoft.ML.OnnxRuntime.Tensors;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// YAMNet ONNX 실행(Android 의 YamnetInference.kt). 로그멜 [1, 1, 96, 64] → 로짓 521개 → softmax 확률.
+ ///
+ /// 가중치는 yamnet.data 에 따로 있고 ONNX Runtime 은 그 파일을 yamnet.onnx 가 있는 폴더에서 찾는다. 그래서 두 파일을 한 폴더에 두고
+ /// 경로로 세션을 만든다. 만들기 전에 두 파일의 크기와 SHA-256 을 Android 와 같은 고정값으로 확인한다().
+ /// 세션 설정도 Android 와 같다. 연산 스레드를 하나로 묶어 캡처·화면과 CPU 를 다투지 않게 한다.
+ ///
+ /// PC 에서 바꾼 것: 입력·출력 버퍼를 한 번만 잡아 IoBinding 으로 묶어 두고, 로짓과 확률은 부르는 쪽 칸에 쓴다. 그래서 추론마다
+ /// 관리 힙에 할당하지 않는다(Android 는 추론마다 텐서와 결과 배열을 새로 만든다). 대신 스레드 안전하지 않으므로 한 번에 한 스레드만
+ /// 부른다. 이 추론 잠금으로 줄 세운다.
+ ///
+ public sealed class YamnetInference : IYamnetModel
+ {
+ public const string InputName = "audio";
+ public const string OutputName = "class_scores";
+ public const int NumClasses = 521;
+ public const int TimeFrames = 96;
+ public const int MelBins = 64;
+ public const int LogMelSize = TimeFrames * MelBins;
+
+ private static readonly long[] InputShape = { 1, 1, TimeFrames, MelBins };
+ private static readonly long[] OutputShape = { 1, NumClasses };
+
+ private readonly InferenceSession _session;
+ private readonly float[] _input;
+ private readonly float[] _output;
+ private readonly OrtValue _inputValue;
+ private readonly OrtValue _outputValue;
+ private readonly RunOptions _runOptions;
+ private readonly OrtIoBinding _binding;
+ private bool _disposed;
+
+ private YamnetInference(InferenceSession session)
+ {
+ ValidateContract(session);
+ _session = session;
+ // 고정 힙에 둔다. ONNX Runtime 이 두 배열의 주소를 들고 있는 동안 GC 가 옮기거나 힙을 조각내지 않게 한다.
+ _input = GC.AllocateArray(LogMelSize, pinned: true);
+ _output = GC.AllocateArray(NumClasses, pinned: true);
+ try
+ {
+ _inputValue = OrtValue.CreateTensorValueFromMemory(_input, InputShape);
+ _outputValue = OrtValue.CreateTensorValueFromMemory(_output, OutputShape);
+ _runOptions = new RunOptions();
+ // 묶어 둔 입력은 복사하지 않고 주소로 읽으므로, _input 을 고쳐 쓰면 다음 실행이 새 값을 본다.
+ _binding = session.CreateIoBinding();
+ _binding.BindInput(InputName, _inputValue);
+ _binding.BindOutput(OutputName, _outputValue);
+ }
+ catch
+ {
+ ReleaseBuffers();
+ throw;
+ }
+ }
+
+ ///
+ /// 의 yamnet.onnx·yamnet.data 로 세션을 만든다. 실패하면 앱은 AI 없이 화면만 돈다.
+ ///
+ /// 모델 파일이 없다.
+ /// 모델 파일이 배포한 것과 다르거나(크기·SHA-256), 입출력이 YAMNet 계약과 다르다.
+ /// ONNX Runtime 이 세션을 만들지 못했다.
+ public static YamnetInference Create(string modelDirectory)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(modelDirectory);
+ string directory = Path.GetFullPath(modelDirectory);
+ ModelFileIntegrity.Verify(directory, YamnetModelFiles.All);
+
+ InferenceSession session;
+ using (var options = new SessionOptions
+ {
+ IntraOpNumThreads = 1,
+ InterOpNumThreads = 1,
+ ExecutionMode = ExecutionMode.ORT_SEQUENTIAL,
+ GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
+ })
+ {
+ session = new InferenceSession(Path.Combine(directory, YamnetModelFiles.Onnx.Name), options);
+ }
+
+ try
+ {
+ return new YamnetInference(session);
+ }
+ catch
+ {
+ // 계약 확인이나 버퍼 준비에 실패했다. 세션을 닫되 닫다가 난 오류가 원래 예외를 덮지 않게 한다(Kotlin closeOnFailure).
+ try
+ {
+ session.Dispose();
+ }
+ catch (Exception cleanupFailure)
+ {
+ AppLog.Warn(nameof(YamnetInference), "Failed to release a rejected YAMNet session", cleanupFailure);
+ }
+ throw;
+ }
+ }
+
+ ///
+ /// 로그멜 6,144칸(시간 우선 time × 64 + mel, 의 출력)으로 추론해 로짓 521개를
+ /// 에, softmax 확률을 에 쓴다. 할당하지 않는다.
+ ///
+ /// 닫은 뒤에 불렀다. 닫힌 네이티브 세션에 들어가기 전에 여기서 막는다.
+ public void Infer(ReadOnlySpan logMel, Span logits, Span probabilities)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (logMel.Length != LogMelSize)
+ throw new ArgumentException($"Expected log-mel size {LogMelSize}, got {logMel.Length}", nameof(logMel));
+ if (logits.Length != NumClasses)
+ throw new ArgumentException($"Expected {NumClasses} logits, got {logits.Length}", nameof(logits));
+ if (probabilities.Length != NumClasses)
+ throw new ArgumentException($"Expected {NumClasses} probabilities, got {probabilities.Length}", nameof(probabilities));
+
+ logMel.CopyTo(_input);
+ _session.RunWithBinding(_runOptions, _binding);
+ _output.AsSpan().CopyTo(logits);
+ Softmax(logits, probabilities);
+ }
+
+ /// 진단용 한 줄. 예: in=audio[1,1,96,64] out=class_scores[1,521]
+ public string InputOutputSummary()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ return $"in={InputName}[{string.Join(",", _session.InputMetadata[InputName].Dimensions)}] " +
+ $"out={OutputName}[{string.Join(",", _session.OutputMetadata[OutputName].Dimensions)}]";
+ }
+
+ ///
+ /// 세션과 버퍼를 놓는다. 여러 번 불러도 된다. 다른 스레드의 와 겹쳐 부르면 안 된다
+ /// (파이프라인은 추론 잠금 안에서만 부른다).
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ ReleaseBuffers();
+ _session.Dispose();
+ }
+
+ ///
+ /// Kotlin 의 softmax 와 같은 순서로 계산한다. 최댓값을 뺀 차를 float 로 구한 뒤 double 로 넓혀 exp 하고, 합은 double 로 더하고,
+ /// float 로 줄인 exp 값을 그 합으로 나눈다. 합이 0 이하이거나 유한하지 않으면(NaN·무한 로짓) 모두 1/n 이다.
+ /// Python 기준 구현은 차를 double 로 구하므로 Python 과는 float 마지막 자리에서만 다를 수 있다.
+ ///
+ public static void Softmax(ReadOnlySpan logits, Span probabilities)
+ {
+ if (probabilities.Length < logits.Length)
+ throw new ArgumentException($"probabilities must hold {logits.Length} values", nameof(probabilities));
+ if (logits.IsEmpty) return;
+
+ float max = logits[0];
+ for (int i = 1; i < logits.Length; i++)
+ {
+ if (logits[i] > max) max = logits[i];
+ }
+
+ double sum = 0.0;
+ for (int i = 0; i < logits.Length; i++)
+ {
+ // (float) 로 차를 float 에서 반올림한다. Kotlin 은 Float 끼리 뺀 뒤 toDouble() 한다.
+ double e = Math.Exp((float)(logits[i] - max));
+ probabilities[i] = (float)e;
+ sum += e;
+ }
+
+ if (sum <= 0.0 || !double.IsFinite(sum))
+ {
+ probabilities[..logits.Length].Fill(1f / logits.Length);
+ return;
+ }
+ for (int i = 0; i < logits.Length; i++)
+ {
+ probabilities[i] = (float)(probabilities[i] / sum);
+ }
+ }
+
+ /// Android 처럼 첫 입력·출력의 이름과 입력 모양을 보고, PC 는 IoBinding 이 기대는 개수·자료형·출력 모양까지 본다.
+ private static void ValidateContract(InferenceSession session)
+ {
+ if (session.InputNames.Count != 1 || session.InputNames[0] != InputName)
+ throw new InvalidDataException($"Expected a single input '{InputName}', got [{string.Join(", ", session.InputNames)}]");
+ if (session.OutputNames.Count != 1 || session.OutputNames[0] != OutputName)
+ throw new InvalidDataException($"Expected a single output '{OutputName}', got [{string.Join(", ", session.OutputNames)}]");
+ RequireFloatTensor(session.InputMetadata[InputName], InputName, InputShape);
+ RequireFloatTensor(session.OutputMetadata[OutputName], OutputName, OutputShape);
+ }
+
+ private static void RequireFloatTensor(NodeMetadata metadata, string name, long[] shape)
+ {
+ bool matches = metadata.IsTensor &&
+ metadata.ElementDataType == TensorElementType.Float &&
+ metadata.Dimensions.Length == shape.Length &&
+ metadata.Dimensions.Select(dimension => (long)dimension).SequenceEqual(shape);
+ if (!matches)
+ {
+ throw new InvalidDataException(
+ $"Expected '{name}' as float[{string.Join(",", shape)}], got {metadata.ElementDataType}[{string.Join(",", metadata.Dimensions)}]");
+ }
+ }
+
+ private void ReleaseBuffers()
+ {
+ // 생성자 도중에 실패하면 일부는 아직 없다.
+ _binding?.Dispose();
+ _runOptions?.Dispose();
+ _outputValue?.Dispose();
+ _inputValue?.Dispose();
+ }
+ }
+
+ ///
+ /// 파이프라인이 쓰는 모델 단계. 실제로는 이고, 테스트는 오래 걸리거나 실패하는 모델로 바꿔 끼워
+ /// 닫기 경합과 실패 경로를 순서대로 재현한다.
+ ///
+ internal interface IYamnetModel : IDisposable
+ {
+ ///
+ void Infer(ReadOnlySpan logMel, Span logits, Span probabilities);
+ }
+
+ ///
+ /// 배포하는 YAMNet 두 파일의 크기와 SHA-256. Android 의 YamnetModelFiles 와 글자까지 같아야 하고, 모델을 바꾸면 두 앱에서 함께 고친다.
+ ///
+ internal static class YamnetModelFiles
+ {
+ public static readonly ModelFileIntegrity.ExpectedFile Onnx = new(
+ "yamnet.onnx",
+ 25_592L,
+ "290369c40886a4ae948f77671fff901023eec81484ac393465dfb1b342b1ca85");
+
+ public static readonly ModelFileIntegrity.ExpectedFile Data = new(
+ "yamnet.data",
+ 14_915_108L,
+ "aa05b5b196bdfd74fb59ae4cbba22578c5ab25b9e792e52867678844bcecc839");
+
+ public static readonly IReadOnlyList All = [Onnx, Data];
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/YamnetMappingPolicy.cs b/SoundVisualizer.Core/Ai/YamnetMappingPolicy.cs
new file mode 100644
index 0000000..f44dd34
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/YamnetMappingPolicy.cs
@@ -0,0 +1,103 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 사용자가 분류 탭에서 바꾼 소리 종류의 불변 스냅샷. 추론 한 번의 모든 단계(투표, 가려진 위협음, 안전 단서, 치명적 단서)가
+ /// 같은 스냅샷을 본다. 모바일 YamnetMappingPolicy.kt 와 같다.
+ ///
+ /// 위협음에서 뺀 라벨은 투표뿐 아니라 키워드로 다시 들어오는 길(가려진 위협음, 안전 단서, 치명적 단서와 그에 따른 기준 완화)에서도
+ /// 빠진다. 새로 위협음으로 지정한 라벨은 투표에만 들어간다.
+ ///
+ public sealed class YamnetMappingPolicy
+ {
+ /// 사용자 지정이 없는 기본 매핑.
+ public static YamnetMappingPolicy Default { get; } = new(FrozenDictionary.Empty);
+
+ private readonly FrozenDictionary _overrides;
+
+ private YamnetMappingPolicy(FrozenDictionary overrides)
+ {
+ _overrides = overrides;
+ // 해시가 아니라 내용 그대로다. 평가 CSV 에 남긴 값만으로 그때의 매핑을 되살릴 수 있다.
+ // 순서는 모바일의 toSortedMap 과 같은 서수(UTF-16) 순이다. 문화권 정렬을 쓰면 대소문자가 섞인 이름에서 어긋난다.
+ Signature = overrides.Count == 0
+ ? "default"
+ : string.Join(";", overrides
+ .OrderBy(entry => entry.Key, StringComparer.Ordinal)
+ .Select(entry => entry.Key + "=" + entry.Value.ToLabel()));
+ }
+
+ /// 기본 종류와 다른, 유효한 사용자 지정 수.
+ public int OverrideCount => _overrides.Count;
+
+ /// "default", 또는 영어 이름 순으로 "이름=종류;이름=종류". 개발자 모드 기록과 매핑이 바뀌었는지 판단하는 데 쓴다.
+ public string Signature { get; }
+
+ /// 이 매핑에서 의 종류. 사용자 지정이 없으면 기본 종류다.
+ public SoundType Coarse(string name) =>
+ _overrides.TryGetValue(name, out var coarse) ? coarse : YamnetThreeClassMapper.MapDisplayNameToCoarse(name);
+
+ /// 추론 경로용. 기본 종류를 이름에서 다시 구하지 않고 미리 계산한 값을 쓴다.
+ internal SoundType CoarseAt(YamnetClassMap classes, int index) =>
+ _overrides.TryGetValue(classes[index], out var coarse) ? coarse : classes.DefaultCoarse(index);
+
+ ///
+ /// 이 라벨을 키워드 기반 안전 단서로 써도 되는지. 위협음에서 뺀 라벨은 키워드로 다시 들어오지 못한다.
+ /// 사용자 지정이 위협음인 라벨은 true 지만, 그런 라벨이 단서 키워드에 걸리지 않으면 결국 투표에만 쓰인다.
+ ///
+ public bool AllowsSafetyCue(string name) =>
+ !_overrides.TryGetValue(name, out var coarse) || coarse == SoundType.Danger;
+
+ /// 표시 이름이나 상위 5개 중 하나가, 위협음에서 빼지 않은 치명적 단서(총기, 폭발, 폭죽)인지.
+ public bool HasCriticalDangerCue(string display, IReadOnlyList top5) =>
+ HasCriticalDangerCue(null, -1, display, top5);
+
+ /// 추론 경로용. 같은 판정을 미리 계산한 값으로 한다( 가 null 이면 이름으로 계산한다).
+ internal bool HasCriticalDangerCue(
+ YamnetClassMap? classes,
+ int displayIndex,
+ string display,
+ IReadOnlyList top5)
+ {
+ if (AllowsSafetyCue(display) &&
+ (YamnetClassMap.TraitsOf(classes, displayIndex, display) & YamnetLabelTraits.CriticalDanger) != 0)
+ {
+ return true;
+ }
+ for (int i = 0; i < top5.Count; i++)
+ {
+ var hit = top5[i];
+ if (AllowsSafetyCue(hit.Name) &&
+ (YamnetClassMap.TraitsOf(classes, hit.Index, hit.Name) & YamnetLabelTraits.CriticalDanger) != 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// 저장된 사용자 지정(영어 클래스 이름 → "ambient"/"speech"/"danger")에서 스냅샷을 만든다. 입력은 복사하므로 나중에 바뀌어도 상관없다.
+ /// 목록에 정확히(대소문자 포함) 있는 이름, 세 문자열 중 하나인 값, 기본 종류와 다른 값만 남긴다. 남는 것이 없으면 다.
+ ///
+ public static YamnetMappingPolicy From(IReadOnlyDictionary overrides, IReadOnlyList classNames)
+ {
+ ArgumentNullException.ThrowIfNull(overrides);
+ ArgumentNullException.ThrowIfNull(classNames);
+ var names = new HashSet(classNames, StringComparer.Ordinal);
+ var valid = new Dictionary(StringComparer.Ordinal);
+ foreach (var (name, label) in overrides)
+ {
+ if (name is null || !names.Contains(name)) continue;
+ if (!SoundTypeLabels.TryParse(label, out var coarse)) continue;
+ if (coarse == YamnetThreeClassMapper.MapDisplayNameToCoarse(name)) continue;
+ valid[name] = coarse;
+ }
+ return valid.Count == 0 ? Default : new YamnetMappingPolicy(valid.ToFrozenDictionary(StringComparer.Ordinal));
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/YamnetSafetyCueDecision.cs b/SoundVisualizer.Core/Ai/YamnetSafetyCueDecision.cs
new file mode 100644
index 0000000..90101b8
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/YamnetSafetyCueDecision.cs
@@ -0,0 +1,186 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// 상위 5개 안에 분명한 경보·사이렌·폭발 단서가 있으면 투표가 환경음이어도 위협음으로 올린다. 모바일 YamnetSafetyCueDecision.kt 와 같다.
+ ///
+ /// 옛 Gunshot Booster 경로는 보조 모델 점수 채택과 이 안전 정책을 함께 했다. 모델 점수 쪽은 없앴고 이 정책만 남긴다.
+ /// 총기 라벨은 이 길로 올리지 않는다(총소리는 투표로만 위협음이 된다).
+ ///
+ public static class YamnetSafetyCueDecision
+ {
+ /// 단서가 상위 5개 안에서 이 확률 이상이어야 올린다(#353, #358).
+ public const float StrongDangerCueMinProbability = 0.05f;
+
+ /// 표시 이름의 확신도가 이보다 낮으면 올리지 않는다.
+ public const float MinDisplayConfidence = 0.12f;
+
+ ///
+ /// 안전 단서 판단 결과. Pre 는 투표 결과, Post 는 승격을 반영한 결과다. 승격해도 는 단서의 확률이
+ /// 아니라 원래 표시 이름의 확신도다(모바일과 같다).
+ ///
+ public readonly record struct Result(
+ SoundType PreCoarse,
+ string PreDisplay,
+ int PreClassIndex,
+ float PreConfidence,
+ SoundType PostCoarse,
+ string PostDisplay,
+ int PostClassIndex,
+ float PostConfidence,
+ bool HasGunshotCue,
+ bool HasStrongDangerCue,
+ bool DangerCuePromoted);
+
+ ///
+ /// 를 만든 분류기와 같은 , 같은 으로 부른다.
+ ///
+ public static Result Decide(YamnetClassMap classNames, YamnetCoarseClassifier.Result pre, YamnetMappingPolicy? mapping = null)
+ {
+ ArgumentNullException.ThrowIfNull(classNames);
+ ArgumentNullException.ThrowIfNull(pre);
+ mapping ??= YamnetMappingPolicy.Default;
+
+ // 사용자가 위협음에서 뺀 라벨은 번호를 -1 로 지워 단서로 쓰지 않는다. 확률 자리는 남겨 순위는 그대로 둔다.
+ Span topIndices = stackalloc int[5];
+ Span topProbabilities = stackalloc float[5];
+ topIndices.Fill(-1);
+ topProbabilities.Fill(-1f);
+ for (int i = 0; i < pre.Top5.Count && i < 5; i++)
+ {
+ var hit = pre.Top5[i];
+ topIndices[i] = mapping.AllowsSafetyCue(hit.Name) ? hit.Index : -1;
+ topProbabilities[i] = hit.Probability;
+ }
+
+ bool hasGunshotCue = HasGunshotCueInTop5(topIndices, classNames);
+ bool hasStrongDangerCue = HasStrongDangerCueInTop5(topIndices, topProbabilities, classNames);
+ var displayTraits = classNames.TraitsOf(pre.YamnetClassIndex, pre.DisplayName);
+ bool blockPromotion =
+ // 사용자가 대화음으로 지정한 라벨 때문에 다른 독립 단서까지 막지 않게, 기본 매핑의 투표로 본다(#283 B).
+ // 단서에서 빼는 것은 위의 사용자 지정을 그대로 따른다.
+ pre.DefaultCoarse == SoundType.Speech ||
+ (displayTraits & (YamnetLabelTraits.SpeechLikeDisplay | YamnetLabelTraits.SilenceLikeDisplay)) != 0 ||
+ pre.Confidence < MinDisplayConfidence;
+ int cueIndex = TryPickBestStrongDangerDisplay(topIndices, topProbabilities, classNames);
+ // 총기 후보가 함께 있어도 충분히 강한 다른 단서는 막지 않는다(#351). 총기 라벨 자체는 여기서 올리지 않는다.
+ bool promote = !blockPromotion && hasStrongDangerCue && cueIndex >= 0;
+
+ return new Result(
+ PreCoarse: pre.Coarse,
+ PreDisplay: pre.DisplayName,
+ PreClassIndex: pre.YamnetClassIndex,
+ PreConfidence: pre.Confidence,
+ PostCoarse: promote ? SoundType.Danger : pre.Coarse,
+ PostDisplay: promote ? classNames[cueIndex] : pre.DisplayName,
+ PostClassIndex: promote ? cueIndex : pre.YamnetClassIndex,
+ PostConfidence: pre.Confidence,
+ HasGunshotCue: hasGunshotCue,
+ HasStrongDangerCue: hasStrongDangerCue,
+ DangerCuePromoted: promote);
+ }
+
+ public static bool IsGunshotKeyword(string? name)
+ {
+ if (string.IsNullOrEmpty(name)) return false;
+ string s = name.ToLowerInvariant();
+ return s.Contains("gunshot") || s.Contains("gunfire") || s.Contains("machine gun") ||
+ s.Contains("artillery") || s.Contains("fusillade") || s.Contains("cap gun");
+ }
+
+ /// 안전 단서가 되는 이름. 총기, 폭발·폭죽, siren·alarm(alarm clock 제외), 승인한 경적·깨짐 라벨이다.
+ public static bool IsStrongDangerKeyword(string? name)
+ {
+ if (string.IsNullOrEmpty(name)) return false;
+ if (IsGunshotKeyword(name)) return true;
+ string s = name.ToLowerInvariant();
+ if (s.Contains("alarm clock")) return false;
+ if (ApprovedAttentionCueLabels.Contains(s)) return true;
+ return s.Contains("explosion") || s.Contains("fireworks") || s.Contains("firecracker") ||
+ s.Contains("siren") || s.Contains("alarm");
+ }
+
+ // 모바일은 이 두 함수를 분류기(YamnetCoarseClassifier)에도 같은 내용으로 따로 둔다. 여기서는 한 벌만 두고 같이 쓴다.
+ internal static bool IsSpeechLikeDisplay(string? display)
+ {
+ if (string.IsNullOrEmpty(display)) return false;
+ string s = display.ToLowerInvariant();
+ return s.Contains("speech") || s.Contains("conversation") || s.Contains("narration") ||
+ s.Contains("speaking") || s.Contains("babbling") || s.Contains("whisper") ||
+ s.Contains("singing") || s.Contains("choir") || s.Contains("laughter") ||
+ s.Contains("crying") || s.Contains("sobbing") || s.Contains("shout");
+ }
+
+ internal static bool IsSilenceLikeDisplay(string? display)
+ {
+ if (string.IsNullOrEmpty(display)) return false;
+ string s = display.ToLowerInvariant();
+ return s.Contains("silence") || s.Contains("quiet") || s.Contains("background noise");
+ }
+
+ private static bool HasGunshotCueInTop5(ReadOnlySpan topIndices, YamnetClassMap classNames)
+ {
+ foreach (int index in topIndices)
+ {
+ if (index >= 0 && (classNames.TraitsAt(index) & YamnetLabelTraits.Gunshot) != 0) return true;
+ }
+ return false;
+ }
+
+ private static bool HasStrongDangerCueInTop5(ReadOnlySpan topIndices, ReadOnlySpan topProbabilities, YamnetClassMap classNames)
+ {
+ for (int position = 0; position < topIndices.Length; position++)
+ {
+ int index = topIndices[position];
+ if (index >= 0 && topProbabilities[position] >= StrongDangerCueMinProbability &&
+ (classNames.TraitsAt(index) & YamnetLabelTraits.StrongDangerCue) != 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// 총기가 아닌 단서 중 확률이 가장 큰 것의 번호. 없으면 -1. 총기 후보의 확률을 다른 약한 단서에 빌려주지 않는다.
+ private static int TryPickBestStrongDangerDisplay(ReadOnlySpan topIndices, ReadOnlySpan topProbabilities, YamnetClassMap classNames)
+ {
+ int bestIndex = -1;
+ float bestProbability = 0f;
+ for (int position = 0; position < topIndices.Length; position++)
+ {
+ int index = topIndices[position];
+ if (index < 0) continue;
+ var traits = classNames.TraitsAt(index);
+ if ((traits & YamnetLabelTraits.Gunshot) != 0) continue;
+ float probability = topProbabilities[position];
+ if (probability >= StrongDangerCueMinProbability &&
+ (traits & YamnetLabelTraits.StrongDangerCue) != 0 &&
+ probability > bestProbability)
+ {
+ bestIndex = index;
+ bestProbability = probability;
+ }
+ }
+ return bestIndex;
+ }
+
+ ///
+ /// 3분류 매퍼가 이미 위협음으로 보는 정확한 라벨 중 안전 단서로도 승인한 것(소문자). 비슷한 이름이 따라오지 않게 정확히 같은 이름만 본다.
+ ///
+ internal static readonly FrozenSet ApprovedAttentionCueLabels = new[]
+ {
+ "vehicle horn, car horn, honking",
+ "air horn, truck horn",
+ "burst, pop",
+ "boom",
+ "bang",
+ "smash, crash",
+ "breaking",
+ "shatter",
+ }.ToFrozenSet(StringComparer.Ordinal);
+ }
+}
diff --git a/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs b/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs
new file mode 100644
index 0000000..a3eb31e
--- /dev/null
+++ b/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs
@@ -0,0 +1,115 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+
+namespace SoundVisualizer.Ai
+{
+ ///
+ /// YAMNet 클래스 이름(영어 display_name) → 기본 소리 종류. 모바일 YamnetThreeClassMapper.kt 와 규칙이 같다.
+ ///
+ /// 위협음 → 대화음 → 환경음 순서로 보고 어디에도 걸리지 않으면 환경음이다. 실제 521개 클래스는 위협음 37개,
+ /// 대화음 33개, 환경음 451개로 나뉜다. 옛 PC 판이 위협음으로 보던 총소리 대리 라벨(Plop, Gargling, 비·폭포 소리)과
+ /// 발소리는 모바일에서 원래 뜻대로 환경음으로 돌렸으므로(#319) 여기에도 없다.
+ ///
+ /// 이름을 소문자로 바꿔 비교하므로 부를 때마다 문자열을 하나 만든다. 추론 중에는 부르지 않고,
+ /// 이 클래스마다 한 번 계산해 둔 값을 쓴다.
+ ///
+ public static class YamnetThreeClassMapper
+ {
+ /// 이름에 "sound effect" 가 든 범용 효과음 라벨인지. 효과음에 가려진 위협음을 찾을 때 쓴다.
+ public static bool IsGenericSoundEffectLabel(string? displayName)
+ {
+ if (string.IsNullOrEmpty(displayName)) return false;
+ return displayName.ToLowerInvariant().Contains("sound effect");
+ }
+
+ /// 사용자 지정을 반영하지 않은 기본 종류. 이름이 없으면 환경음이고, 목록에 없는 이름도 같은 키워드 규칙으로 본다.
+ public static SoundType MapDisplayNameToCoarse(string? displayName)
+ {
+ if (string.IsNullOrEmpty(displayName)) return SoundType.Ambient;
+ string s = displayName.ToLowerInvariant();
+ if (MatchesDanger(s)) return SoundType.Danger;
+ if (MatchesSpeech(s)) return SoundType.Speech;
+ if (MatchesAmbient(s)) return SoundType.Ambient;
+ return SoundType.Ambient;
+ }
+
+ private static bool MatchesDanger(string s)
+ {
+ // 주의를 끌어야 하는 신호·갑작스러운 사건으로 승인한 라벨(#343). 정확히 같은 이름만 본다.
+ // 투표만 바꾸고, 안전 단서 목록(YamnetSafetyCueDecision)에는 넣지 않는다.
+ if (AttentionLabels.Contains(s)) return true;
+ // 발소리는 환경음이다. 경적은 이름에 horn 이 든 소리 전부가 아니라 차량 경적 두 라벨만 위협음이다.
+ if (s == "vehicle horn, car horn, honking") return true;
+ if (s == "air horn, truck horn") return true;
+ // 바로 알아야 하는 소리. Rain·Wind 처럼 비슷한 이름이 따라오지 않게 정확히 같은 이름만 본다.
+ if (s == "chainsaw" || s == "thunder" || s == "thunderstorm") return true;
+ if (s.Contains("gunshot") || s.Contains("gunfire") || s.Contains("machine gun") ||
+ s.Contains("artillery") || s.Contains("fusillade") || s.Contains("cap gun"))
+ {
+ return true;
+ }
+ if (s.Contains("explosion") || s.Contains("fireworks") || s.Contains("firecracker")) return true;
+ // 터지거나 부서지는 소리는 YAMNet 이 원인(총성인지 풍선인지)을 가리지 못해도 위협음으로 본다.
+ if (s == "burst, pop" || s == "boom" || s == "bang" ||
+ s == "smash, crash" || s == "breaking" || s == "shatter")
+ {
+ return true;
+ }
+ if (s.Contains("civil defense siren")) return true;
+ if (s.Contains("police car") && s.Contains("siren")) return true;
+ if (s.Contains("ambulance") && s.Contains("siren")) return true;
+ if (s.Contains("fire engine") || s.Contains("fire truck")) return true;
+ if (s.Contains("siren") && !s.Contains("telephone")) return true;
+ if (s.Contains("smoke detector") || s.Contains("fire alarm")) return true;
+ if (s == "alarm" || s.Contains("car alarm")) return true;
+ return false;
+ }
+
+ private static bool MatchesSpeech(string s)
+ {
+ // 악기 이름이다. 사람 목소리가 아니다.
+ if (s == "singing bowl") return false;
+ // 대화음은 알아듣는 말만이 아니라 사람이 내는 소리 전체다. 개의 낑낑거림 같은 비슷한 이름이 따라오지 않게
+ // 정확히 같은 이름만 넣는다(#345).
+ if (HumanVocalizationLabels.Contains(s)) return true;
+ if (s.Contains("speech") || s.Contains("conversation") || s.Contains("narration")) return true;
+ if (s.Contains("speaking") || s.Contains("babbling")) return true;
+ if (s.Contains("shout") || s.Contains("whisper") || s.Contains("screaming")) return true;
+ if (s.Contains("laughter") || s.Contains("crying") || s.Contains("sobbing")) return true;
+ if (s.Contains("singing") || s.Contains("choir") || s.Contains("rapping")) return true;
+ if (s.Contains("crowd") || s.Contains("chatter") || s.Contains("hubbub")) return true;
+ if (s.Contains("children playing")) return true;
+ return false;
+ }
+
+ // 어디에도 안 걸린 소리도 환경음이라 결과는 바뀌지 않는다. 일부러 환경음으로 둔 소리를 적어 두고,
+ // 모바일 코드와 줄마다 맞춰 보기 쉽게 그대로 옮긴다.
+ private static bool MatchesAmbient(string s)
+ {
+ if (s.Contains("wind") || s.Contains("rustling leaves")) return true;
+ if (s.Contains("traffic")) return true;
+ if (s.Contains("music") || s.EndsWith(" music", StringComparison.Ordinal)) return true;
+ if (s.Contains("environmental noise") || s.Contains("field recording")) return true;
+ if (s.Contains("ocean") || s.Contains("waves") || s.Contains("rain") || s.Contains("thunder")) return true;
+ if (s.Contains("white noise") || s.Contains("pink noise") || s.Contains("static")) return true;
+ return false;
+ }
+
+ /// 위협음으로 투표하는 주의 신호 9개(#343). 소문자. 테스트가 실제 클래스 이름과 맞는지 확인한다.
+ internal static readonly FrozenSet AttentionLabels = new[]
+ {
+ "reversing beeps", "train horn", "train whistle", "foghorn", "bicycle bell",
+ "emergency vehicle", "skidding", "tire squeal", "slam",
+ }.ToFrozenSet(StringComparer.Ordinal);
+
+ /// 대화음으로 옮긴 사람 소리 11개(#345). 소문자. 테스트가 실제 클래스 이름과 맞는지 확인한다.
+ internal static readonly FrozenSet HumanVocalizationLabels = new[]
+ {
+ "bellow", "whoop", "yell", "cheering",
+ "giggle", "snicker", "belly laugh", "chuckle, chortle",
+ "baby cry, infant cry", "whimper", "wail, moan",
+ }.ToFrozenSet(StringComparer.Ordinal);
+ }
+}
diff --git a/SoundVisualizer.Core/AppLog.cs b/SoundVisualizer.Core/AppLog.cs
new file mode 100644
index 0000000..9463b74
--- /dev/null
+++ b/SoundVisualizer.Core/AppLog.cs
@@ -0,0 +1,24 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Diagnostics;
+
+namespace SoundVisualizer
+{
+ ///
+ /// 조용히 삼키던 실패를 남긴다. 사용자에게 보이지는 않고, DebugView 나 디버거 출력 창에서 볼 수 있다.
+ /// 설정 파일, 캡처 장치, AI 모델처럼 실패해도 앱이 계속 돌아야 하는 곳에서 쓴다.
+ ///
+ public static class AppLog
+ {
+ /// 정상 동작의 이정표(예: AI 를 어떤 형식으로 시작했는지). 문제를 되짚을 때 쓴다.
+ public static void Info(string tag, string message) => Trace.WriteLine($"[SoundVisualizer:{tag}] {message}");
+
+ public static void Warn(string tag, string message, Exception? error = null)
+ {
+ Trace.WriteLine(error == null
+ ? $"[SoundVisualizer:{tag}] {message}"
+ : $"[SoundVisualizer:{tag}] {message} ({error.GetType().Name}: {error.Message})");
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/AppSettings.cs b/SoundVisualizer.Core/AppSettings.cs
new file mode 100644
index 0000000..c60e1ec
--- /dev/null
+++ b/SoundVisualizer.Core/AppSettings.cs
@@ -0,0 +1,531 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using SoundVisualizer.Localization;
+using SoundVisualizer.Sounds;
+
+namespace SoundVisualizer
+{
+ public class VisualModeSettings
+ {
+ public double Intensity { get; set; } = 50.0;
+ public double PositionSpeed { get; set; } = 20.0;
+ public double Sensitivity { get; set; } = 3.75;
+ public double VisualOpacity { get; set; } = 50.0;
+ public bool IsGlowMode { get; set; } = false;
+ public double GlowIntensity { get; set; } = 0.0;
+ public double CircleRadius { get; set; } = 40.0;
+ public bool IntensityAsOpacity { get; set; } = false;
+ public double OpacityFixedSize { get; set; } = 30.0;
+ public double OpacityFixedMaxOpacity { get; set; } = 100.0;
+ }
+
+ public static class AppSettings
+ {
+ // 모드별 개별 설정 인스턴스. 기본값은 VisualModeSettings 의 초기값 한 곳에만 둔다.
+ public static VisualModeSettings WaveMode { get; set; } = new VisualModeSettings();
+ public static VisualModeSettings PadMode { get; set; } = new VisualModeSettings();
+ public static VisualModeSettings CircleMode { get; set; } = new VisualModeSettings();
+ public static VisualModeSettings OutlineMode { get; set; } = new VisualModeSettings();
+
+ // 단축키 기본값(가상 키 코드). F2 사운드 모드, F3 표현 모드, F4 오버레이 설정.
+ public const int DefaultSoundModeKey = 0x71;
+ public const int DefaultVisualModeKey = 0x72;
+ public const int DefaultEditModeKey = 0x73;
+ public const double DefaultTargetFps = 144.0;
+
+ // 1. 파도의 크기 (현재 모드값 반환/설정)
+ public static double WaveIntensity
+ {
+ get => GetCurrentModeSettings().Intensity;
+ set => GetCurrentModeSettings().Intensity = value;
+ }
+
+ // 2. 파도 위치 변화 속도
+ public static double WavePositionSpeed
+ {
+ get => GetCurrentModeSettings().PositionSpeed;
+ set => GetCurrentModeSettings().PositionSpeed = value;
+ }
+
+ // 3. 파도의 민감성/떨림
+ public static double WaveSensitivity
+ {
+ get => GetCurrentModeSettings().Sensitivity;
+ set => GetCurrentModeSettings().Sensitivity = value;
+ }
+
+ // 4. 파도/그래픽 투명도 (0 ~ 100)
+ public static double VisualOpacity
+ {
+ get => GetCurrentModeSettings().VisualOpacity;
+ set => GetCurrentModeSettings().VisualOpacity = value;
+ }
+
+ // 5. 시각화 모드 (0 = Wave 모드, 1 = Pad 모드, 2 = Circle 모드)
+ public static int VisualMode { get; set; } = 0;
+
+ // 5.5. 원형 모드 반지름 (10 ~ 100)
+ public static double CircleRadius
+ {
+ get => GetCurrentModeSettings().CircleRadius;
+ set => GetCurrentModeSettings().CircleRadius = value;
+ }
+
+ // 6. 투명도로 세기 표현 (크기 고정)
+ public static bool IntensityAsOpacity
+ {
+ get => GetCurrentModeSettings().IntensityAsOpacity;
+ set => GetCurrentModeSettings().IntensityAsOpacity = value;
+ }
+
+ public static double OpacityFixedSize
+ {
+ get => GetCurrentModeSettings().OpacityFixedSize;
+ set => GetCurrentModeSettings().OpacityFixedSize = value;
+ }
+
+ public static double OpacityFixedMaxOpacity
+ {
+ get => GetCurrentModeSettings().OpacityFixedMaxOpacity;
+ set => GetCurrentModeSettings().OpacityFixedMaxOpacity = value;
+ }
+
+ // 6. 스테레오 확장 모드
+ public static int SoundMode { get; set; } = 2;
+
+ // 6.5. 광원(Glow) 효과 모드
+ public static bool IsGlowMode
+ {
+ get => GetCurrentModeSettings().IsGlowMode;
+ set => GetCurrentModeSettings().IsGlowMode = value;
+ }
+
+ public static double GlowIntensity
+ {
+ get => GetCurrentModeSettings().GlowIntensity;
+ set => GetCurrentModeSettings().GlowIntensity = value;
+ }
+
+ // 7. 단축키 설정
+ public static List StereoUpmixKeyBind { get; set; } = new List { DefaultSoundModeKey }; // F2
+ public static List VisualModeKeyBind { get; set; } = new List { DefaultVisualModeKey }; // F3
+ public static List EditModeKeyBind { get; set; } = new List { DefaultEditModeKey }; // F4
+
+ // 구버전 호환용
+ public static int StereoUpmixHotkey { get; set; } = DefaultSoundModeKey;
+ public static int VisualModeHotkey { get; set; } = DefaultVisualModeKey;
+ public static int EditModeHotkey { get; set; } = DefaultEditModeKey;
+
+ // 7. 화면 언어 설정. "system"(Windows 화면 언어를 따름) 또는 언어 코드("ko", "zh-Hans" 등). 새로 설치하면 system 이다.
+ public const string DefaultLanguage = UiLanguages.SystemCode;
+ public static string Language { get; set; } = DefaultLanguage;
+
+ // 8.5. 관리자 모드
+ public static bool IsAdminMode { get; set; } = false;
+
+ // 8.5.1. 개발자 모드의 결과 기록(모바일 developer_record, #193). 개발자 모드를 꺼도 값은 남고, 오버레이가 떠 있고 개발자 모드가
+ // 켜져 있을 때만 기록한다(Diagnostics.AiDebugHud). 켜 두고 잊으면 디스크를 먹으므로 기본은 꺼짐이다.
+ public static bool DeveloperRecord { get; set; } = false;
+
+ public static bool AutoMinimizeOnLaunch { get; set; } = false;
+
+ // 8.6. 렌더링 최대 프레임
+ public static double TargetFps { get; set; } = DefaultTargetFps;
+
+ // 9. 화면 표시 여부 (환경음, 말소리, 강조음)
+ public static bool ShowAmbient { get; set; } = true;
+ public static bool ShowSpeech { get; set; } = true;
+ public static bool ShowDanger { get; set; } = true;
+
+ // 10. 화면 표시 색상 (환경음, 말소리, 강조음)
+ public const string DefaultColorAmbient = "#FFFFFFFF";
+ public const string DefaultColorSpeech = "#FFFFFF00";
+ public const string DefaultColorDanger = "#FFFF0000";
+ public static string ColorAmbient { get; set; } = DefaultColorAmbient;
+ public static string ColorSpeech { get; set; } = DefaultColorSpeech;
+ public static string ColorDanger { get; set; } = DefaultColorDanger;
+
+ // 11. 소리 탭에서 바꾼 소리 종류(모바일 sound_types). 키는 모델의 영어 소리 이름, 값은 SoundTypeLabels 의 문자열이고
+ // 기본 종류와 같은 값은 두지 않는다(SoundTypeOverrides). 오버레이의 AI 가 틱마다 다른 스레드에서 읽으므로 고치지 않는 맵을
+ // 통째로 바꿔 끼운다. 넣을 때 얼려 두어, 넘긴 쪽이 나중에 맵을 고쳐도 AI 가 반쯤 바뀐 맵을 읽는 일이 없다.
+ private static readonly IReadOnlyDictionary NoSoundTypes = FrozenDictionary.Empty;
+ private static IReadOnlyDictionary _soundTypes = NoSoundTypes;
+
+ public static IReadOnlyDictionary SoundTypes
+ {
+ get => Volatile.Read(ref _soundTypes);
+ set => Volatile.Write(ref _soundTypes, Freeze(value ?? throw new ArgumentNullException(nameof(value))));
+ }
+
+ // 12. 소리 탭을 고급 모드(소리를 하나씩 바꾸는 화면)로 볼지(모바일 classify_advanced). 바꾼 종류와 따로 두는 화면 설정이다.
+ public static bool ClassifyAdvanced { get; set; } = false;
+
+ // 13. 튜토리얼을 본 적이 있는지(모바일 tutorial_seen). 아직이면 런처를 열 때 튜토리얼부터 띄운다. 튜토리얼을 닫으면(끝까지 넘기거나
+ // 건너뛰면) true 가 되고, 그 뒤로는 홈의 "튜토리얼 보기"로만 연다. 저장한 적이 없으면 읽을 때 정한다(Load).
+ // 보는 사람에 대한 값이라 전체 초기화(ResetToDefaults)가 건드리지 않는다. 모양 설정을 되돌렸다고 튜토리얼이 다시 뜨면 안 된다.
+ public static bool TutorialSeen { get; set; } = false;
+
+ /// 설정 파일 위치. 포터블 배포라 실행 파일 옆에 둔다. 테스트는 임시 파일로 바꾼다.
+ public static string SettingsFilePath { get; set; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
+
+ ///
+ /// 저장을 이만큼 미뤄 모은다. 슬라이더를 끌면 값이 바뀔 때마다 저장을 부르는데, 그때마다 파일을 쓰면
+ /// UI 스레드가 디스크를 기다린다. 마지막 변경 뒤 이만큼 조용하면 한 번 쓴다.
+ ///
+ public const int SaveDelayMs = 400;
+
+ private static readonly object SaveGate = new();
+ private static System.Threading.Timer? _saveTimer;
+ private static bool _savePending;
+
+ /// 지금 표현 모드()의 설정. 오버레이 엔진이 프레임마다 읽는다.
+ public static VisualModeSettings CurrentModeSettings => GetCurrentModeSettings();
+
+ private static VisualModeSettings GetCurrentModeSettings()
+ {
+ if (VisualMode == 0) return WaveMode;
+ if (VisualMode == 1) return PadMode;
+ if (VisualMode == 2) return CircleMode;
+ return OutlineMode;
+ }
+
+ public static void Load()
+ {
+ // 튜토리얼(모바일 loadTutorialSeen): 설정 파일이 없으면 새로 설치한 것이라 아직 보지 않았다. 파일이 있으면 이미 앱을 써 본
+ // 사람이라, 아래에서 저장한 값을 읽지 못하면(이 기능 전의 파일, 깨진 파일) 본 것으로 둔다.
+ // 모바일은 정한 값을 곧바로 저장한다(기기 표시가 먼저 저장되면 새 설치를 쓰던 사람으로 잘못 친다). PC 는 저장할 때마다 모든
+ // 항목을 쓰므로, 무엇을 먼저 저장하든 정한 값이 함께 남는다. 파일이 없는 동안은 늘 새 설치로 읽힌다.
+ TutorialSeen = File.Exists(SettingsFilePath);
+ try
+ {
+ if (File.Exists(SettingsFilePath))
+ {
+ var json = File.ReadAllText(SettingsFilePath);
+ var data = JsonSerializer.Deserialize(json);
+ if (data != null)
+ {
+ VisualMode = data.VisualMode;
+ SoundMode = data.SoundMode;
+ if (data.StereoUpmixKeyBind != null && data.StereoUpmixKeyBind.Count > 0) StereoUpmixKeyBind = data.StereoUpmixKeyBind;
+ else StereoUpmixKeyBind = new List { data.StereoUpmixHotkey != 0 ? data.StereoUpmixHotkey : DefaultSoundModeKey };
+
+ if (data.VisualModeKeyBind != null && data.VisualModeKeyBind.Count > 0) VisualModeKeyBind = data.VisualModeKeyBind;
+ else VisualModeKeyBind = new List { data.VisualModeHotkey != 0 ? data.VisualModeHotkey : DefaultVisualModeKey };
+
+ if (data.EditModeKeyBind != null && data.EditModeKeyBind.Count > 0) EditModeKeyBind = data.EditModeKeyBind;
+ else EditModeKeyBind = new List { data.EditModeHotkey != 0 ? data.EditModeHotkey : DefaultEditModeKey };
+
+ // 예전 판은 언어 이름("日本語")이나 약어("KOR")를 적었다. 읽을 때 코드로 옮기고, 다음 저장부터 코드로 쓴다.
+ Language = UiLanguages.NormalizeSetting(data.Language);
+
+ IsAdminMode = data.IsAdminMode;
+ DeveloperRecord = data.DeveloperRecord;
+ AutoMinimizeOnLaunch = data.AutoMinimizeOnLaunch;
+ if (data.TargetFps >= 30.0) TargetFps = data.TargetFps;
+ ShowAmbient = data.ShowAmbient;
+ ShowSpeech = data.ShowSpeech;
+ ShowDanger = data.ShowDanger;
+ ColorAmbient = data.ColorAmbient ?? DefaultColorAmbient;
+ ColorSpeech = data.ColorSpeech ?? DefaultColorSpeech;
+ ColorDanger = data.ColorDanger ?? DefaultColorDanger;
+
+ // 소리 탭이 생기기 전의 설정 파일에는 둘 다 없다. 그때는 바꾼 소리가 없고 기본 모드로 연다.
+ SoundTypes = ReadSoundTypes(data.SoundTypes);
+ ClassifyAdvanced = data.ClassifyAdvanced ?? DefaultClassifyAdvanced(SoundTypes);
+
+ // 튜토리얼 항목이 없는 파일은 이 기능 전부터 쓰던 사람의 것이다. 이미 앱을 알므로 튜토리얼을 띄우지 않는다.
+ TutorialSeen = data.TutorialSeen ?? true;
+
+ // 하이브리드 모드 로드 (모드별 설정이 없는 구버전 호환)
+ if (data.WaveMode != null) WaveMode = data.WaveMode;
+ else WaveMode = new VisualModeSettings { Intensity = data.WaveIntensity, PositionSpeed = data.WavePositionSpeed, Sensitivity = data.WaveSensitivity, VisualOpacity = data.VisualOpacity, IsGlowMode = data.IsGlowMode, GlowIntensity = data.GlowIntensity, CircleRadius = data.CircleRadius };
+
+ if (data.PadMode != null) PadMode = data.PadMode;
+ else PadMode = new VisualModeSettings { Intensity = data.WaveIntensity, PositionSpeed = data.WavePositionSpeed, Sensitivity = data.WaveSensitivity, VisualOpacity = data.VisualOpacity, IsGlowMode = data.IsGlowMode, GlowIntensity = data.GlowIntensity, CircleRadius = data.CircleRadius };
+
+ if (data.CircleMode != null) CircleMode = data.CircleMode;
+ else CircleMode = new VisualModeSettings { Intensity = data.WaveIntensity, PositionSpeed = data.WavePositionSpeed, Sensitivity = data.WaveSensitivity, VisualOpacity = data.VisualOpacity, IsGlowMode = data.IsGlowMode, GlowIntensity = data.GlowIntensity, CircleRadius = data.CircleRadius };
+
+ if (data.OutlineMode != null) OutlineMode = data.OutlineMode;
+ else OutlineMode = new VisualModeSettings { Intensity = data.WaveIntensity, PositionSpeed = data.WavePositionSpeed, Sensitivity = data.WaveSensitivity, VisualOpacity = data.VisualOpacity, IsGlowMode = data.IsGlowMode, GlowIntensity = data.GlowIntensity, CircleRadius = data.CircleRadius };
+
+ int maxRate = GetMonitorRefreshRate();
+ if (TargetFps > maxRate) TargetFps = maxRate;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ AppLog.Warn("settings", "설정 파일을 읽지 못해 기본값으로 시작합니다.", ex);
+ }
+ }
+
+ ///
+ /// 저장을 예약한다. 안에 다시 부르면 마지막 것 한 번만 쓴다.
+ /// 부른 스레드(보통 UI 스레드)에서 쓰므로 설정을 고치는 쪽과 엇갈리지 않는다. 앱을 닫을 때는 를 부른다.
+ ///
+ public static void Save()
+ {
+ lock (SaveGate)
+ {
+ _savePending = true;
+ _saveContext = SynchronizationContext.Current;
+ _saveTimer ??= new System.Threading.Timer(OnSaveTimer, null, Timeout.Infinite, Timeout.Infinite);
+ _saveTimer.Change(SaveDelayMs, Timeout.Infinite);
+ }
+ }
+
+ /// 예약된 저장이 있으면 지금 쓴다. 앱을 닫을 때와 오버레이 편집을 마칠 때 부른다.
+ public static void Flush()
+ {
+ lock (SaveGate)
+ {
+ if (!_savePending) return;
+ _savePending = false;
+ _saveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ WriteFile();
+ }
+ }
+
+ /// 미루지 않고 바로 쓴다. 예약된 저장도 이것으로 끝난다.
+ public static void SaveNow()
+ {
+ lock (SaveGate)
+ {
+ _savePending = false;
+ _saveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ WriteFile();
+ }
+ }
+
+ /// 예약된 저장이 남아 있는지. 테스트가 쓴다.
+ public static bool HasPendingSave
+ {
+ get { lock (SaveGate) return _savePending; }
+ }
+
+ private static SynchronizationContext? _saveContext;
+
+ private static void OnSaveTimer(object? state)
+ {
+ SynchronizationContext? context;
+ lock (SaveGate) context = _saveContext;
+ if (context != null) context.Post(_ => Flush(), null);
+ else Flush();
+ }
+
+ ///
+ /// 언어를 뺀 모든 설정을 기본값으로 되돌린다(전체 초기화 버튼). 단축키는 실제로 쓰는 키 목록까지 되돌린다.
+ /// 저장은 부르는 쪽이 한다.
+ ///
+ /// 소리 탭에서 바꾼 소리 종류와 고급 모드도 건드리지 않는다(모바일과 같다). 모바일에서 바꾼 종류를 지우는 것은 소리 탭의
+ /// 모두 되돌리기뿐이고(바꾼 수를 보이고 한 번 더 묻는다), 고급 모드는 어느 되돌리기도 건드리지 않는 화면 설정이다.
+ /// 소리를 하나씩 골라 둔 것을 모양 설정을 되돌리다 함께 잃지 않게 한다.
+ ///
+ public static void ResetToDefaults()
+ {
+ WaveMode = new VisualModeSettings();
+ PadMode = new VisualModeSettings();
+ CircleMode = new VisualModeSettings();
+ OutlineMode = new VisualModeSettings();
+
+ VisualMode = 0;
+ SoundMode = 2; // 실행 중이면 장치의 채널 수에 맞춰 다시 정해진다
+
+ StereoUpmixKeyBind = new List { DefaultSoundModeKey };
+ VisualModeKeyBind = new List { DefaultVisualModeKey };
+ EditModeKeyBind = new List { DefaultEditModeKey };
+ StereoUpmixHotkey = DefaultSoundModeKey;
+ VisualModeHotkey = DefaultVisualModeKey;
+ EditModeHotkey = DefaultEditModeKey;
+
+ IsAdminMode = false;
+ DeveloperRecord = false;
+ AutoMinimizeOnLaunch = false;
+ TargetFps = Math.Min(DefaultTargetFps, GetMonitorRefreshRate());
+
+ ShowAmbient = true;
+ ShowSpeech = true;
+ ShowDanger = true;
+ ColorAmbient = DefaultColorAmbient;
+ ColorSpeech = DefaultColorSpeech;
+ ColorDanger = DefaultColorDanger;
+ }
+
+ private static void WriteFile()
+ {
+ try
+ {
+ var data = new SettingsData
+ {
+ VisualMode = VisualMode,
+ SoundMode = SoundMode,
+ StereoUpmixKeyBind = StereoUpmixKeyBind,
+ VisualModeKeyBind = VisualModeKeyBind,
+ EditModeKeyBind = EditModeKeyBind,
+ Language = Language,
+
+ IsAdminMode = IsAdminMode,
+ DeveloperRecord = DeveloperRecord,
+ AutoMinimizeOnLaunch = AutoMinimizeOnLaunch,
+ TargetFps = TargetFps,
+ ShowAmbient = ShowAmbient,
+ ShowSpeech = ShowSpeech,
+ ShowDanger = ShowDanger,
+ ColorAmbient = ColorAmbient,
+ ColorSpeech = ColorSpeech,
+ ColorDanger = ColorDanger,
+ SoundTypes = WriteSoundTypes(SoundTypes),
+ ClassifyAdvanced = ClassifyAdvanced,
+ TutorialSeen = TutorialSeen,
+
+ WaveMode = WaveMode,
+ PadMode = PadMode,
+ CircleMode = CircleMode,
+ OutlineMode = OutlineMode,
+
+ // 구버전 호환성 저장을 위해 현재 모드의 최종 설정값도 루트 필드에 동시 저장
+ WaveIntensity = WaveIntensity,
+ WavePositionSpeed = WavePositionSpeed,
+ WaveSensitivity = WaveSensitivity,
+ VisualOpacity = VisualOpacity,
+ CircleRadius = CircleRadius,
+ IsGlowMode = IsGlowMode,
+ GlowIntensity = GlowIntensity,
+ OpacityFixedMaxOpacity = OpacityFixedMaxOpacity
+ };
+ var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(SettingsFilePath, json);
+ }
+ catch (Exception ex)
+ {
+ // 쓰기 금지 폴더(Program Files 등)에 풀어 둔 경우 여기로 온다. 설정은 이번 실행 동안만 남는다.
+ AppLog.Warn("settings", $"설정을 저장하지 못했습니다: {SettingsFilePath}", ex);
+ }
+ }
+
+ /// 바꿔 끼울 맵. 이미 얼린 맵(글자 그대로 비교)이면 그대로 쓰고, 아니면 얼린 복사본을 만든다.
+ private static IReadOnlyDictionary Freeze(IReadOnlyDictionary types)
+ {
+ if (types.Count == 0) return NoSoundTypes;
+ if (types is FrozenDictionary frozen && ReferenceEquals(frozen.Comparer, StringComparer.Ordinal)) return frozen;
+ return types.ToFrozenDictionary(StringComparer.Ordinal);
+ }
+
+ ///
+ /// 저장된 소리 종류(이름 → 라벨 JSON 객체). 문자열이 아닌 값은 버리고, 남은 것은 로
+ /// 거른다(세 라벨이 아닌 값과 지금 기본 종류와 같은 값을 버린다). 객체가 아니면 바꾼 것이 없는 것으로 본다(모바일 loadSoundTypes).
+ /// 항목을 JSON 그대로 받는 것은 이 값 하나가 깨졌다고 다른 설정까지 기본값으로 시작하지 않게 하려는 것이다.
+ ///
+ private static IReadOnlyDictionary ReadSoundTypes(JsonElement? stored)
+ {
+ if (stored is not { ValueKind: JsonValueKind.Object } json) return NoSoundTypes;
+ var pairs = new List>();
+ foreach (var property in json.EnumerateObject())
+ {
+ if (property.Value.ValueKind == JsonValueKind.String) pairs.Add(new(property.Name, property.Value.GetString()!));
+ }
+ return SoundTypeOverrides.Sanitize(pairs);
+ }
+
+ /// 이름 순서로 적는다. 얼린 맵은 순서가 정해져 있지 않아, 그대로 적으면 같은 값도 저장할 때마다 줄이 뒤섞인다.
+ private static JsonElement WriteSoundTypes(IReadOnlyDictionary types)
+ {
+ var sorted = new SortedDictionary(StringComparer.Ordinal);
+ foreach (var (name, label) in types) sorted[name] = label;
+ return JsonSerializer.SerializeToElement(sorted);
+ }
+
+ ///
+ /// 고급 모드를 저장한 적이 없을 때의 값(모바일 loadClassifyAdvanced). 이미 소리를 하나씩 바꿔 둔 사람이면 고급 모드로 연다.
+ /// 기본 모드에서 묶음을 고르면 하나씩 바꾼 것이 먼저 묻지 않고 덮어써질 수 있어서다. 기본 모드로 돌아갈 때는 탭이 먼저 알린다.
+ /// 모바일은 정한 값을 곧바로 저장하지만 PC 는 저장할 때마다 모든 항목을 쓰므로, 다음 저장에서 함께 남는다.
+ ///
+ private static bool DefaultClassifyAdvanced(IReadOnlyDictionary soundTypes) => soundTypes.Count > 0;
+
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
+ private static extern IntPtr GetDC(IntPtr hWnd);
+
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
+ private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
+
+ [System.Runtime.InteropServices.DllImport("gdi32.dll")]
+ private static extern int GetDeviceCaps(IntPtr hDC, int nIndex);
+
+ private const int VREFRESH = 116;
+
+ public static int GetMonitorRefreshRate()
+ {
+ IntPtr hDC = GetDC(IntPtr.Zero);
+ try
+ {
+ int rate = GetDeviceCaps(hDC, VREFRESH);
+ return rate > 0 ? rate : 60; // 디바이스 주사율 질의 실패 시 안전하게 60Hz 리턴
+ }
+ catch
+ {
+ return 60;
+ }
+ finally
+ {
+ ReleaseDC(IntPtr.Zero, hDC);
+ }
+ }
+
+ private class SettingsData
+ {
+ public double WaveIntensity { get; set; } = 50.0;
+ public double WavePositionSpeed { get; set; } = 20.0;
+ public double WaveSensitivity { get; set; } = 3.75;
+ public double VisualOpacity { get; set; } = 50.0;
+ public int VisualMode { get; set; } = 0;
+ public double CircleRadius { get; set; } = 40.0;
+ public int SoundMode { get; set; } = 2;
+ public bool IsGlowMode { get; set; } = false;
+ public double GlowIntensity { get; set; } = 0.0;
+ public double OpacityFixedMaxOpacity { get; set; } = 100.0;
+ public List StereoUpmixKeyBind { get; set; } = new List();
+ public List VisualModeKeyBind { get; set; } = new List();
+ public List EditModeKeyBind { get; set; } = new List();
+
+ // 구버전 호환
+ public int StereoUpmixHotkey { get; set; } = 0x71;
+ public int VisualModeHotkey { get; set; } = 0x72;
+ public int EditModeHotkey { get; set; } = 0x73;
+ // JSON 이름은 예전 그대로 Language 다. 언어 항목이 없던 아주 예전 설정 파일은 한국어로 읽던 그대로 둔다.
+ public string? Language { get; set; } = "KOR";
+
+ public bool IsAdminMode { get; set; } = false;
+ // 결과 기록이 생기기 전의 설정 파일에는 없어 꺼진 채로 읽힌다.
+ public bool DeveloperRecord { get; set; } = false;
+ public bool AutoMinimizeOnLaunch { get; set; } = false;
+ public double TargetFps { get; set; } = 144.0;
+ public bool ShowAmbient { get; set; } = true;
+ public bool ShowSpeech { get; set; } = true;
+ public bool ShowDanger { get; set; } = true;
+ public string ColorAmbient { get; set; } = "#FFFFFFFF";
+ public string ColorSpeech { get; set; } = "#FFFFFF00";
+ public string ColorDanger { get; set; } = "#FFFF0000";
+
+ // 소리 탭. 예전 설정 파일에는 없어 null 로 읽힌다. 소리 종류는 JSON 그대로 받아 ReadSoundTypes 가 거른다.
+ public JsonElement? SoundTypes { get; set; }
+ public bool? ClassifyAdvanced { get; set; }
+
+ // 튜토리얼을 본 적이 있는지. 예전 설정 파일에는 없어 null 로 읽힌다(그때는 본 것으로 친다).
+ public bool? TutorialSeen { get; set; }
+
+ public VisualModeSettings WaveMode { get; set; } = new VisualModeSettings();
+ public VisualModeSettings PadMode { get; set; } = new VisualModeSettings();
+ public VisualModeSettings CircleMode { get; set; } = new VisualModeSettings();
+ public VisualModeSettings OutlineMode { get; set; } = new VisualModeSettings();
+ }
+ }
+}
+
+
diff --git a/SoundVisualizer.Core/Audio/CaptureLevel.cs b/SoundVisualizer.Core/Audio/CaptureLevel.cs
new file mode 100644
index 0000000..cd2b624
--- /dev/null
+++ b/SoundVisualizer.Core/Audio/CaptureLevel.cs
@@ -0,0 +1,67 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Audio
+{
+ ///
+ /// 지금 소리 크기(0..1). 가장 최근 캡처 버퍼의 채널 가운데 가장 큰 피크다(모바일 AudioEngine.currentLevel). 개발자 모드 HUD 와
+ /// 결과 기록의 lvl 칸이 이 값이고, "왜 아무것도 그려지지 않았나" 를 조용해서인지 AI 탓인지 가르는 데 쓴다.
+ ///
+ /// 와 달리 읽어도 비우지 않고 버퍼끼리 모으지도 않는다. 버퍼 하나의 값이라 "그사이 소리가 났는가" 는
+ /// 이것으로 묻지 않는다(모바일도 같다).
+ ///
+ /// 채널은 캡처한 것을 모두 본다(모바일 캡처는 좌우 둘뿐이다). 그리는 쪽()은 사운드
+ /// 모드가 그리지 못하는 채널을 빼지만, AI 무음 게이트는 버퍼의 모든 채널에서 가장 큰 값을 본다. 이 값은 AI 쪽에 맞춘다. 그래야
+ /// 이 값이 아래에 머물 때 "AI 가 추론을 건너뛰었다" 로 읽을 수 있다.
+ ///
+ /// PC 에서 더한 것: WASAPI 루프백은 아무것도 재생하지 않으면 버퍼를 보내지 않는다. 그대로 두면 마지막 버퍼의 값이 남아 조용한데도
+ /// 소리가 나는 것처럼 읽힌다. 그래서 마지막 버퍼가 보다 오래되면 0 으로 읽는다. 모바일 캡처는 조용해도
+ /// 버퍼를 계속 보내 이런 일이 없다.
+ ///
+ /// 캡처 스레드가 버퍼마다 하고, UI 스레드가 한다. 할당은 없다.
+ ///
+ public sealed class CaptureLevel
+ {
+ ///
+ /// 마지막 버퍼가 이보다 오래되면(밀리초) 소리가 없는 것으로 읽는다. 재생하는 동안 버퍼는 10~50ms 마다 오므로, 그 다섯 배가 넘게
+ /// 오지 않으면 재생이 멈춘 것이다.
+ ///
+ public const long StaleAfterMs = 250;
+
+ private readonly object _gate = new();
+ private float _level;
+ private long _receivedMs;
+ private bool _hasBuffer;
+
+ ///
+ /// 버퍼 하나의 채널별 최고값을 받는다. 앞 버퍼의 값은 버린다. NaN 인 채널은 건너뛴다(장치가 깨진 값을 보내도 HUD 가 NaN 이 되지 않게).
+ ///
+ /// 이 버퍼의 채널별 최고값(0..1). 순서는 상관없다.
+ /// 받은 시각(단조 밀리초, ). 와 같은 시계다.
+ public void Offer(ReadOnlySpan channelPeaks, long nowMs)
+ {
+ float level = 0f;
+ foreach (float peak in channelPeaks)
+ {
+ if (peak > level) level = peak; // NaN 은 비교가 거짓이라 건너뛴다
+ }
+ lock (_gate)
+ {
+ _level = level;
+ _receivedMs = nowMs;
+ _hasBuffer = true;
+ }
+ }
+
+ /// 지금 소리 크기. 버퍼를 받은 적이 없거나 마지막 버퍼가 보다 오래되면 0 이다.
+ /// 읽는 시각(단조 밀리초, 와 같은 시계).
+ public float Read(long nowMs)
+ {
+ lock (_gate)
+ {
+ if (!_hasBuffer || nowMs - _receivedMs > StaleAfterMs) return 0f;
+ return _level;
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Audio/ChannelDownmix.cs b/SoundVisualizer.Core/Audio/ChannelDownmix.cs
new file mode 100644
index 0000000..092315e
--- /dev/null
+++ b/SoundVisualizer.Core/Audio/ChannelDownmix.cs
@@ -0,0 +1,106 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Runtime.InteropServices;
+
+namespace SoundVisualizer.Audio
+{
+ ///
+ /// 캡처한 소리의 채널 수를 출력 장치에 맞춘다(VB-CABLE 로 받은 소리를 실제 스피커로 다시 내보낼 때).
+ ///
+ /// 입력은 이번 버퍼에서 **실제로 녹음된 바이트만** 받는다. NAudio 는 캡처 버퍼를 재사용하므로 배열 전체를 넘기면
+ /// 지난 버퍼의 남은 소리까지 스피커로 나간다.
+ ///
+ /// 7.1·5.1 → 2ch 는 ITU-R BS.775 계수로 섞고 LFE 는 뺀다. 채널 순서는 WASAPI 표준을 따른다.
+ /// 7.1 은 FL, FR, FC, LFE, BL, BR, SL, SR 이고, 5.1 은 FL, FR, FC, LFE 뒤에 서라운드 한 쌍이다.
+ ///
+ public static class ChannelDownmix
+ {
+ private const float KCenter = 0.707f; // -3dB
+ private const float KSide = 0.707f; // -3dB
+ private const float KBack = 0.500f; // -6dB
+
+ ///
+ /// 의 채널 수를 로 바꿔 앞쪽에 쓴다.
+ /// 버퍼가 모자라면 넉넉하게 새로 만든다. 쓴 바이트 수를 돌려준다.
+ /// 7.1·5.1 float → 2ch 는 섞고, 그 밖에는 앞쪽 채널을 골라 쓰거나 첫 채널을 복제한다.
+ ///
+ public static int Convert(ReadOnlySpan input, int inChannels, int bytesPerSample, int outChannels, ref byte[] outputBuffer)
+ {
+ if (inChannels <= 0 || outChannels <= 0 || bytesPerSample <= 0) return 0;
+
+ int frames = input.Length / (inChannels * bytesPerSample);
+ int neededSize = frames * outChannels * bytesPerSample;
+ if (outputBuffer.Length < neededSize)
+ {
+ outputBuffer = new byte[neededSize * 2]; // 다음 버퍼가 조금 커도 다시 만들지 않게 넉넉하게
+ }
+
+ var output = outputBuffer.AsSpan(0, neededSize);
+ var usedInput = input[..(frames * inChannels * bytesPerSample)];
+
+ if (bytesPerSample == 4 && outChannels == 2 && inChannels == 8)
+ {
+ Downmix8To2(MemoryMarshal.Cast(usedInput), MemoryMarshal.Cast(output));
+ return neededSize;
+ }
+ if (bytesPerSample == 4 && outChannels == 2 && inChannels == 6)
+ {
+ Downmix6To2(MemoryMarshal.Cast(usedInput), MemoryMarshal.Cast(output));
+ return neededSize;
+ }
+
+ // 샘플 단위로 바이트를 옮긴다. float 가 아닌 형식(16·24bit PCM)도 그대로 맞는다.
+ for (int f = 0; f < frames; f++)
+ {
+ for (int ch = 0; ch < outChannels; ch++)
+ {
+ int srcCh = ch < inChannels ? ch : 0;
+ usedInput.Slice((f * inChannels + srcCh) * bytesPerSample, bytesPerSample)
+ .CopyTo(output.Slice((f * outChannels + ch) * bytesPerSample, bytesPerSample));
+ }
+ }
+ return neededSize;
+ }
+
+ /// L = FL + 0.707·FC + 0.707·SL + 0.5·BL, R 도 같은 꼴. LFE 는 뺀다.
+ private static void Downmix8To2(ReadOnlySpan src, Span dst)
+ {
+ int frames = src.Length / 8;
+ for (int i = 0; i < frames; i++)
+ {
+ int o = i * 8;
+ float fl = src[o + 0];
+ float fr = src[o + 1];
+ float fc = src[o + 2];
+ // o + 3 은 LFE
+ float bl = src[o + 4];
+ float br = src[o + 5];
+ float sl = src[o + 6];
+ float sr = src[o + 7];
+
+ dst[i * 2 + 0] = Math.Clamp(fl + KCenter * fc + KSide * sl + KBack * bl, -1f, 1f);
+ dst[i * 2 + 1] = Math.Clamp(fr + KCenter * fc + KSide * sr + KBack * br, -1f, 1f);
+ }
+ }
+
+ /// L = FL + 0.707·FC + 0.707·서라운드 L, R 도 같은 꼴. LFE 는 뺀다.
+ private static void Downmix6To2(ReadOnlySpan src, Span dst)
+ {
+ int frames = src.Length / 6;
+ for (int i = 0; i < frames; i++)
+ {
+ int o = i * 6;
+ float fl = src[o + 0];
+ float fr = src[o + 1];
+ float fc = src[o + 2];
+ // o + 3 은 LFE
+ float sl = src[o + 4];
+ float sr = src[o + 5];
+
+ dst[i * 2 + 0] = Math.Clamp(fl + KCenter * fc + KSide * sl, -1f, 1f);
+ dst[i * 2 + 1] = Math.Clamp(fr + KCenter * fc + KSide * sr, -1f, 1f);
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Audio/PeakAccumulator.cs b/SoundVisualizer.Core/Audio/PeakAccumulator.cs
new file mode 100644
index 0000000..a207efa
--- /dev/null
+++ b/SoundVisualizer.Core/Audio/PeakAccumulator.cs
@@ -0,0 +1,69 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Audio
+{
+ ///
+ /// 캡처 스레드가 버퍼마다 잰 채널별 최고값을, 렌더 스레드가 프레임마다 가져갈 때까지 모아 둔다.
+ ///
+ /// 프레임 사이에 버퍼가 여러 개 오면 마지막 값만 남기지 않고 가장 큰 값을 남긴다. 그래야 30~60ms 짜리 총성이
+ /// 다음 버퍼의 조용한 값에 덮여 사라지지 않는다. 캡처 스레드와 렌더 스레드가 같은 필드를 따로 고치던 경합도 없앤다
+ /// (렌더 스레드의 감쇠가 막 들어온 값을 덮어쓸 수 있었다).
+ ///
+ /// 채널 순서는 FL, FR, FC, BL, BR, SL, SR, LFE 이다.
+ ///
+ public sealed class PeakAccumulator
+ {
+ public const int ChannelCount = 8;
+
+ private readonly object _gate = new();
+ private readonly float[] _peaks = new float[ChannelCount];
+ private bool _hasData;
+
+ /// 버퍼 하나의 채널별 최고값을 더한다. 이미 모인 값보다 큰 것만 남는다. 캡처 스레드에서 부른다.
+ public void Offer(float fl, float fr, float fc, float bl, float br, float sl, float sr, float lfe)
+ {
+ lock (_gate)
+ {
+ Max(0, fl); Max(1, fr); Max(2, fc); Max(3, bl);
+ Max(4, br); Max(5, sl); Max(6, sr); Max(7, lfe);
+ _hasData = true;
+ }
+ }
+
+ ///
+ /// 지난번에 가져간 뒤로 모인 값을 에 쓰고 비운다. 그사이 버퍼가 하나도 오지 않았으면
+ /// false 를 돌려주고 아무것도 쓰지 않는다(렌더 쪽은 그때 이전 값을 줄여 나간다). 렌더 스레드에서 부른다.
+ ///
+ public bool TryTake(Span destination)
+ {
+ if (destination.Length < ChannelCount)
+ throw new ArgumentException($"destination must hold {ChannelCount} channels", nameof(destination));
+
+ lock (_gate)
+ {
+ if (!_hasData) return false;
+ _peaks.AsSpan().CopyTo(destination);
+ Array.Clear(_peaks);
+ _hasData = false;
+ return true;
+ }
+ }
+
+ /// 모인 값을 버린다. 캡처를 다시 시작할 때 부른다.
+ public void Clear()
+ {
+ lock (_gate)
+ {
+ Array.Clear(_peaks);
+ _hasData = false;
+ }
+ }
+
+ private void Max(int index, float value)
+ {
+ // NaN 은 버린다. 장치가 깨진 값을 보내도 그래픽이 굳지 않게 한다.
+ if (value > _peaks[index]) _peaks[index] = value;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/ChoiceRows.cs b/SoundVisualizer.Core/ChoiceRows.cs
new file mode 100644
index 0000000..0016216
--- /dev/null
+++ b/SoundVisualizer.Core/ChoiceRows.cs
@@ -0,0 +1,36 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer
+{
+ ///
+ /// 같은 너비의 선택지 칸(표현 모드, 사운드 모드)을 한 줄에 몇 개씩 둘지. WPF 의 EqualChoicePanel 이 쓴다.
+ /// 안드로이드 판 choicesPerRow(#304)와 같은 규칙이다.
+ ///
+ public static class ChoiceRows
+ {
+ ///
+ /// 모두 한 줄부터 하나씩 줄이며, 가장 넓은 칸이 줄을 바꾸지 않고 들어가는 첫 수를 고른다.
+ /// 칸을 좁힌 채 글자를 접으면 WPF 는 단어 가운데서도 끊고("Outli/ne"), 접지 않으면 글자가 잘리기 때문이다.
+ /// 마지막 줄에 한 칸만 남는 나눔(네 칸의 3+1, 다섯 칸의 4+1)은 건너뛴다. 어느 수에도 안 들어가면 한 칸씩이다.
+ ///
+ /// 칸 수
+ /// 줄 전체 너비
+ /// 칸 사이 간격
+ /// 가장 넓은 칸이 줄을 바꾸지 않을 때의 너비(안쪽 여백 포함)
+ public static int PerRow(int count, double rowWidth, double gap, double widestCell)
+ {
+ if (count <= 1) return 1;
+ for (int perRow = count; perRow >= 1; perRow--)
+ {
+ bool lonelyLastRow = perRow != count && count % perRow != 0 && count % perRow < perRow - 1;
+ if (lonelyLastRow) continue;
+ if (widestCell <= CellWidth(rowWidth, perRow, gap)) return perRow;
+ }
+ return 1;
+ }
+
+ /// 한 줄에 perRow 칸을 둘 때 칸 하나의 너비.
+ public static double CellWidth(double rowWidth, int perRow, double gap) => Math.Max(0, (rowWidth - gap * (perRow - 1)) / Math.Max(1, perRow));
+ }
+}
diff --git a/SoundVisualizer.Core/ComboListKeys.cs b/SoundVisualizer.Core/ComboListKeys.cs
new file mode 100644
index 0000000..9e965bf
--- /dev/null
+++ b/SoundVisualizer.Core/ComboListKeys.cs
@@ -0,0 +1,59 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer
+{
+ /// 콤보에 누른 키. WPF 의 Key 를 Core 에서 쓰려고 필요한 것만 옮겼다.
+ public enum ComboKey
+ {
+ Other,
+ Up,
+ Down,
+ Left,
+ Right,
+ Home,
+ End,
+ PageUp,
+ PageDown,
+ Enter,
+ Space,
+ Tab,
+ }
+
+ /// 키를 콤보에 넘기기 전에 할 일.
+ public enum ComboKeyAction
+ {
+ /// 콤보가 늘 하던 대로 한다.
+ Default,
+
+ /// 값을 바꾸지 않고 목록을 연다(키는 콤보에 넘기지 않는다).
+ OpenList,
+
+ /// 고르지 않고 목록을 닫은 뒤 초점을 다음(Shift 면 앞) 칸으로 옮긴다.
+ CloseAndMoveFocus,
+ }
+
+ ///
+ /// 고르는 즉시 앱 전체가 바뀌는 콤보(언어)의 키 규칙. WPF 콤보는 닫혀 있을 때 화살표·Home/End·글자 키로 바로 다음 항목을 골라서,
+ /// 키를 누를 때마다 언어(글꼴, 쓰는 방향, 모든 글자)가 바뀌었다. 그래서 닫혀 있을 때는 이 키들로 목록을 열기만 하고,
+ /// 열린 목록에서 둘러본 뒤 Enter(또는 Alt+↑, F4, 마우스 클릭)로 고를 때만 바꾼다. Esc 는 고르지 않고 닫는다.
+ /// WAI-ARIA 의 "select-only combobox" 와 같은 동작이다. WPF 의 ui:CommitOnCloseComboBox 가 쓴다.
+ ///
+ public static class ComboListKeys
+ {
+ ///
+ /// 키를 콤보에 넘기기 전에 할 일. 닫힌 콤보의 이동 키와 Enter·스페이스는 목록을 연다. 열린 목록의 Tab 은 고르지 않고 닫은 뒤
+ /// 다음 칸으로 간다(WPF 는 목록 안의 다음 항목으로만 돌아 목록을 빠져나가지 못한다). Alt 와 함께 누른 키(Alt+↓ 는 WPF 가 이미
+ /// 연다)와 그 밖의 키(F4, Esc)는 콤보가 하던 대로 둔다. 열린 목록의 이동 키는 WPF 가 항목을 옮겨 보이기만 하므로 손대지 않는다.
+ ///
+ public static ComboKeyAction OnKeyDown(ComboKey key, bool alt, bool listOpen)
+ {
+ if (alt || key == ComboKey.Other) return ComboKeyAction.Default;
+ if (listOpen) return key == ComboKey.Tab ? ComboKeyAction.CloseAndMoveFocus : ComboKeyAction.Default;
+ return key == ComboKey.Tab ? ComboKeyAction.Default : ComboKeyAction.OpenList;
+ }
+
+ /// 닫힌 콤보에 친 글자도 목록부터 연다. 닫힌 채로 글자를 찾으면 찾은 항목이 바로 골라지기 때문이다.
+ public static bool TextOpensList(string? text, bool listOpen) => !listOpen && !string.IsNullOrEmpty(text);
+ }
+}
diff --git a/SoundVisualizer.Core/Diagnostics/AiDebugCsv.cs b/SoundVisualizer.Core/Diagnostics/AiDebugCsv.cs
new file mode 100644
index 0000000..b62ae69
--- /dev/null
+++ b/SoundVisualizer.Core/Diagnostics/AiDebugCsv.cs
@@ -0,0 +1,120 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Globalization;
+using System.Text;
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Diagnostics
+{
+ ///
+ /// AI 판정 하나를 CSV 한 줄로 바꾼다(모바일 AiDebugCsv.kt, #193). 파일로 쓰는 일은 가 한다.
+ ///
+ /// 머리글, 칸 순서, 따옴표, 숫자 서식이 모바일과 글자까지 같다. 두 판의 기록을 같은 스크립트로 읽고 나란히 비교한다.
+ /// HUD 는 지금 순간만 보여주므로, 총소리 한 발처럼 짧은 판정은 이 기록으로 되짚는다.
+ ///
+ public static class AiDebugCsv
+ {
+ /// top-5 를 몇 칸까지 적는지. 모델이 덜 내놓으면 남은 칸은 비워 둔다.
+ public const int TopSlots = 5;
+
+ /// 값이 없을 때 넣는 것.
+ public const string None = "";
+
+ ///
+ /// capture_source 칸. PC 는 시스템 소리(WASAPI 루프백)만 받으므로 늘 이 값이다. 모바일은 실행마다 고른
+ /// InternalPlayback·Microphone 을 적는다.
+ ///
+ public const string CaptureSource = "Loopback";
+
+ public static IReadOnlyList Columns { get; } = BuildColumns();
+
+ /// 머리글 한 줄. 파일을 새로 열 때 한 번만 쓴다. 칸 이름에는 쉼표가 없어 따옴표로 감싸지 않는다.
+ public static string Header { get; } = string.Join(",", Columns);
+
+ /// 결과 하나를 한 줄로. 줄 끝 문자는 붙이지 않는다.
+ /// 적을 결과.
+ ///
+ /// 읽은 시각(유닉스 밀리초). 와의 차이가 age 다. 무음이면 추론을 건너뛰고
+ /// 마지막 결과를 들고 있으므로, 나이가 자라는 것이 보여야 "추론이 멈췄다" 를 알 수 있다.
+ ///
+ /// 그때의 소리 크기. "왜 아무것도 안 그려졌나" 를 조용해서인지 AI 탓인지 가른다.
+ ///
+ /// 그 종류 표시가 켜져 있었는지. 꺼 두면 오버레이가 아무것도 그리지 않으므로, 이것 없이는 설정 상태를 "AI 가 못 잡았다" 로 오독한다.
+ ///
+ public static string Row(AiClassificationResult result, long nowMs, float level, bool shown)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ var row = new StringBuilder(384);
+ Cell(row, Integer(nowMs));
+ Cell(row, Integer(result.TimestampMs));
+ // 벽시계는 뒤로 튈 수 있다. 음수 나이는 뜻이 없으니 0 으로 붙인다.
+ Cell(row, Integer(Math.Max(nowMs - result.TimestampMs, 0L)));
+ Cell(row, CaptureSource);
+ Cell(row, result.Coarse.ToLabel());
+ Cell(row, result.Display);
+ Cell(row, Prob(result.Confidence));
+ Cell(row, Bool(result.MeetsThreshold));
+ Cell(row, Bool(result.DangerCuePromoted));
+ for (int slot = 0; slot < TopSlots; slot++)
+ {
+ bool has = slot < result.Top5.Count;
+ Cell(row, has ? result.Top5[slot].Name : None);
+ Cell(row, has ? Prob(result.Top5[slot].Probability) : None);
+ }
+ Cell(row, Prob(level));
+ Cell(row, Bool(shown));
+ Cell(row, Millis(result.PreprocessMs));
+ Cell(row, Millis(result.YamnetMs));
+ Cell(row, Millis(result.TotalMs));
+ Cell(row, result.MappingOverrideCount.ToString(CultureInfo.InvariantCulture));
+ Cell(row, result.MappingSignature);
+ return row.ToString();
+ }
+
+ private static IReadOnlyList BuildColumns()
+ {
+ var columns = new List
+ {
+ "time_ms",
+ "result_ms",
+ "age_ms",
+ "capture_source",
+ "coarse",
+ "display",
+ "confidence",
+ // 임계값 자체는 결과에 실리지 않는다(후처리 안에만 있다). 넘었는지만 적는다.
+ "meets_threshold",
+ "danger_cue_promoted",
+ };
+ for (int slot = 1; slot <= TopSlots; slot++)
+ {
+ columns.Add(string.Create(CultureInfo.InvariantCulture, $"top{slot}_name"));
+ columns.Add(string.Create(CultureInfo.InvariantCulture, $"top{slot}_prob"));
+ }
+ columns.AddRange(["level", "shown", "preprocess_ms", "yamnet_ms", "total_ms", "mapping_override_count", "mapping_signature"]);
+ return columns.AsReadOnly();
+ }
+
+ ///
+ /// 칸 하나를 따옴표로 감싸 붙인다. YAMNet 이름에는 쉼표가 들어간다("Gunshot, gunfire"). 감싸지 않으면 그 줄만 칸이 하나 밀려
+ /// 표 전체가 어긋난다. 이름에 든 따옴표는 두 번 적는다. 모바일처럼 숫자 칸도 모두 감싼다.
+ ///
+ private static void Cell(StringBuilder row, string value)
+ {
+ if (row.Length > 0) row.Append(',');
+ row.Append('"').Append(value.Replace("\"", "\"\"", StringComparison.Ordinal)).Append('"');
+ }
+
+ private static string Integer(long value) => value.ToString(CultureInfo.InvariantCulture);
+
+ /// Kotlin Boolean.toString 과 같은 소문자. .NET 의 bool.ToString 은 "True" 다.
+ private static string Bool(bool value) => value ? "true" : "false";
+
+ /// 확률·소리 크기는 다섯 자리로. 로캘을 고정한다(아랍어 로캘에서 소수점이 아랍 숫자로 나가면 표가 읽히지 않는다).
+ private static string Prob(float value) => float.IsNaN(value) ? None : JavaNumberFormat.Fixed(value, 5);
+
+ /// 소요 시간은 한 자리면 된다.
+ private static string Millis(double value) => double.IsNaN(value) ? None : JavaNumberFormat.Fixed(value, 1);
+ }
+}
diff --git a/SoundVisualizer.Core/Diagnostics/AiDebugHud.cs b/SoundVisualizer.Core/Diagnostics/AiDebugHud.cs
new file mode 100644
index 0000000..ca13557
--- /dev/null
+++ b/SoundVisualizer.Core/Diagnostics/AiDebugHud.cs
@@ -0,0 +1,109 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Diagnostics
+{
+ ///
+ /// 오버레이 하나의 개발자 모드 HUD 와 결과 기록을 언제 돌리고 멈출지 정한다(모바일 AiDebugOverlay 의 produceState 와 DisposableEffect).
+ ///
+ /// - HUD 는 오버레이가 열려 있고 개발자 모드가 켜져 있는 동안만 마다 읽는다().
+ /// - 기록은 거기에 기록 스위치까지 켜져 있을 때만 돈다(). 셋 가운데 하나라도 바뀌면 그 자리에서 켜고 끈다.
+ /// 개발자 모드를 꺼도 기록 스위치의 값은 남아, 다시 켜면 새 파일에 기록한다(모바일과 같다).
+ /// - 읽을 때마다 결과를 기록기에 넘긴다. 같은 결과를 거듭 넘겨도 기록기가 한 줄만 남긴다().
+ /// - 오버레이를 닫으면() 기록기를 닫는다. 그 뒤에는 무엇을 넘겨도 다시 켜지 않는다.
+ ///
+ /// 화면과 타이머는 오버레이가 맡고, 여기는 무엇을 할 차례인지만 정해 기기 없이 검사한다(AiDebugHudTests). UI 스레드에서만 쓴다.
+ /// 기록기는 디스크를 기다리지 않으므로() 읽을 때마다 불러도 된다. 닫을 때만 남은 줄을 쓰기를 1초까지 기다린다.
+ ///
+ public sealed class AiDebugHud : IDisposable
+ {
+ /// HUD 를 다시 읽는 간격(밀리초, 모바일 DEBUG_POLL_MS). 추론이 250ms 마다라 그보다 촘촘해야 짧은 판정 하나를 건너뛰지 않는다.
+ public const int PollIntervalMs = 100;
+
+ private readonly IAiDebugRecorder _recorder;
+ private bool _developerMode;
+ private bool _record;
+ private bool _closed;
+
+ /// 앱이 쓰는 HUD. 기록은 실행 파일 폴더(쓸 수 없으면 LocalAppData)의 ai-log 에 남는다().
+ public AiDebugHud()
+ : this(new AiDebugRecorder())
+ {
+ }
+
+ /// 켜고 끌 기록기. 이 HUD 가 넘겨받아 에서 닫는다. 테스트가 가짜나 임시 폴더의 기록기를 준다.
+ internal AiDebugHud(IAiDebugRecorder recorder)
+ {
+ ArgumentNullException.ThrowIfNull(recorder);
+ _recorder = recorder;
+ }
+
+ /// HUD 를 보이고 마다 읽을 차례인지. 오버레이가 열려 있고 개발자 모드가 켜져 있다.
+ public bool IsPolling => !_closed && _developerMode;
+
+ ///
+ /// 결과를 기록할 차례인지. 읽는 중이고 기록 스위치도 켜져 있다. 기록기가 상한이나 쓰기 실패로 스스로 멈춘 것은 여기에 나타나지
+ /// 않는다(모바일과 같다). 스위치를 껐다 켜면 새 파일로 다시 시작한다.
+ ///
+ public bool IsRecording => IsPolling && _record;
+
+ ///
+ /// 지금 설정을 알린다. 오버레이가 연 직후와 프레임마다(쉬는 동안에는 확인할 때마다) 부르므로, 런처·편집 패널·초기화 어디서 바꿔도
+ /// 곧바로 따른다. 기록할 차례가 되면 기록기를 켜고, 아니게 되면 끈다. 같은 값이면 아무것도 하지 않는다. 닫은 뒤에는 무시한다.
+ ///
+ /// 이 바뀌었으면 true. 부르는 쪽이 HUD 를 보이거나 숨기고 타이머를 켜고 끈다.
+ public bool Update(bool developerMode, bool record)
+ {
+ if (_closed) return false;
+ bool wasPolling = IsPolling;
+ bool wasRecording = IsRecording;
+ _developerMode = developerMode;
+ _record = record;
+
+ if (IsRecording != wasRecording)
+ {
+ if (IsRecording) _recorder.Start();
+ else _recorder.Stop();
+ }
+ return IsPolling != wasPolling;
+ }
+
+ ///
+ /// HUD 를 한 번 읽어 그 글자를 돌려준다. 기록할 차례이고 결과가 있으면 같은 값으로 기록기에도 넘긴다. 값은 부르는 쪽이 이 순간
+ /// 한 번씩 읽어 넘긴다. 쓰는 스레드가 나중에 상태를 읽으면 그사이 바뀐 값이 적힌다.
+ ///
+ /// 마지막 결과. 아직 없거나 AI 가 돌지 않으면 null.
+ /// 지금 벽시계 시각(유닉스 밀리초, 와 같은 시계).
+ /// 지금 소리 크기(0..1, ).
+ /// 그 결과의 종류를 표시하도록 켜 두었는지. 결과가 없으면 false.
+ /// AI 를 만들지 못해 분류가 아예 돌지 않으면 false. 만드는 중에는 true 다.
+ public AiDebugLines Poll(AiClassificationResult? result, long nowMs, float level, bool shown, bool aiAvailable)
+ {
+ if (IsRecording && result is not null) _recorder.Offer(result, nowMs, level, shown);
+ return AiDebugText.Format(result, nowMs, level, shown, aiAvailable);
+ }
+
+ /// 오버레이를 닫았다. 기록기를 닫는다(남은 줄을 쓰고 파일을 닫기를 1초까지 기다린다). 여러 번 불러도 된다.
+ public void Dispose()
+ {
+ if (_closed) return;
+ _closed = true;
+ _recorder.Dispose();
+ }
+ }
+
+ /// 결과 기록기(). 가 켜고 끄고, 테스트는 가짜로 바꿔 끼운다.
+ internal interface IAiDebugRecorder : IDisposable
+ {
+ ///
+ void Start();
+
+ ///
+ void Stop();
+
+ ///
+ void Offer(AiClassificationResult result, long nowMs, float level, bool shown);
+ }
+}
diff --git a/SoundVisualizer.Core/Diagnostics/AiDebugLogWriter.cs b/SoundVisualizer.Core/Diagnostics/AiDebugLogWriter.cs
new file mode 100644
index 0000000..2d5be4b
--- /dev/null
+++ b/SoundVisualizer.Core/Diagnostics/AiDebugLogWriter.cs
@@ -0,0 +1,73 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Diagnostics
+{
+ ///
+ /// AI 판정을 받아 CSV 줄로 쌓는다(모바일 AiDebugLogWriter.kt, #193). 어디에 쌓느냐는 sink 가 정한다. 파일이든 테스트의 문자열이든.
+ ///
+ /// 세 가지를 여기서 지킨다.
+ /// - 머리글은 한 번만. 첫 줄을 쓸 때 같이 나간다. 결과가 한 번도 없으면 아무것도 쓰지 않는다.
+ /// - 같은 결과는 한 번만. HUD 는 추론(250ms)보다 자주 읽어 같은 결과를 여러 번 본다.
+ /// 가 앞서 쓴 것과 같으면 건너뛴다.
+ /// - 상한에 닿으면 멈춘다. 초당 네 줄이 하루면 수십 MB 가 된다. 켜 둔 것을 잊은 PC 의 디스크를 먹지 않게 한다.
+ /// 멈춘 뒤에는 조용히 아무것도 쓰지 않는다( 로 알 수 있다).
+ ///
+ /// 한 스레드에서만 쓴다( 의 쓰는 스레드).
+ ///
+ public sealed class AiDebugLogWriter
+ {
+ /// 기본 상한 20MB. 초당 네 줄(한 줄 약 350바이트)이면 하루쯤 담긴다.
+ public const long DefaultMaxBytes = 20L * 1024 * 1024;
+
+ private readonly TextWriter _sink;
+ private readonly long _maxBytes;
+ private bool _wroteHeader;
+ private long _written;
+ private long? _lastResultMs;
+
+ public AiDebugLogWriter(TextWriter sink, long maxBytes = DefaultMaxBytes)
+ {
+ ArgumentNullException.ThrowIfNull(sink);
+ _sink = sink;
+ _maxBytes = maxBytes;
+ }
+
+ /// 쓴 줄 수(머리글은 세지 않는다).
+ public int Rows { get; private set; }
+
+ /// 상한에 닿아 그만 쓰기로 했는지.
+ public bool Stopped { get; private set; }
+
+ /// 결과 하나를 쌓는다. 실제로 한 줄을 썼으면 true, 같은 결과라 건너뛰었거나 상한에 닿았으면 false.
+ /// 상한은 쓰고 나서 본다. 상한을 넘기는 그 줄까지는 쓰고 멈춘다(모바일과 같다).
+ public bool Write(AiClassificationResult result, long nowMs, float level, bool shown)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ if (Stopped) return false;
+ if (result.TimestampMs == _lastResultMs) return false;
+
+ if (!_wroteHeader)
+ {
+ Append(AiDebugCsv.Header);
+ _wroteHeader = true;
+ }
+ Append(AiDebugCsv.Row(result, nowMs, level, shown));
+ _lastResultMs = result.TimestampMs;
+ Rows++;
+ if (_written >= _maxBytes) Stopped = true;
+ return true;
+ }
+
+ private void Append(string line)
+ {
+ // 줄 끝은 모바일 파일과 같은 \n 하나다. WriteLine 은 Windows 에서 \r\n 을 쓴다.
+ _sink.Write(line);
+ _sink.Write('\n');
+ // 한 글자를 한 바이트로 센다. 이름이 아스키라 거의 맞고, 상한은 정확할 필요가 없다.
+ _written += line.Length + 1;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Diagnostics/AiDebugRecorder.cs b/SoundVisualizer.Core/Diagnostics/AiDebugRecorder.cs
new file mode 100644
index 0000000..0bdf804
--- /dev/null
+++ b/SoundVisualizer.Core/Diagnostics/AiDebugRecorder.cs
@@ -0,0 +1,410 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Security;
+using System.Text;
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Diagnostics
+{
+ ///
+ /// 개발자 모드의 결과 기록을 파일로 남긴다(모바일 AiDebugRecorder.kt, #193). 줄을 만드는 규칙은 에 있다.
+ ///
+ /// 켜면 <실행 파일 폴더>/ai-log/ai-yyyyMMdd-HHmmss.csv 를 새로 열고, 끄면 닫는다. 앱이 설정 파일도 실행 파일 옆에 두는
+ /// 포터블 배포라 기록도 옆에 둔다. 그 폴더에 쓸 수 없으면(Program Files 에 풀어 둔 경우 등) %LOCALAPPDATA%/SoundVisualizer/ai-log/ 에 둔다.
+ ///
+ /// 디스크는 쓰는 스레드에서만 만진다. 부르는 곳은 오버레이의 개발자 모드 HUD(UI 스레드, )라, 거기서 폴더를
+ /// 확인하거나 파일을 열거나 쓰면 그리는 프레임이 밀린다. 그래서 는 큐에 넣고 바로 돌아오고, 폴더를 고르는 일까지 쓰는 스레드가 한다(#199, #208).
+ ///
+ /// 켠 회차마다 큐와 쓰는 스레드를 따로 둔다. 은 쓰는 스레드를 기다리지 않으므로 껐다 곧바로 켜면 옛 스레드가 아직
+ /// 살아 있다. 큐가 하나면 두 스레드가 나눠 먹어 줄이 두 파일로 갈렸다(#208). 회차마다 따로면 옛 스레드는 자기 줄만 비우고 나가고,
+ /// 새 줄은 새 스레드만 본다.
+ ///
+ /// 스레드: ·· 는 UI 스레드에서 부른다(어느 스레드여도 되고, 디스크를 기다리지 않는다).
+ /// 는 오버레이를 닫을 때 부르며, 남은 줄을 쓰고 파일을 닫기를 1초까지 기다린다.
+ ///
+ /// 모바일과 다른 점:
+ /// - 폴더를 고르려면 실제로 만들어 봐야 해서 폴더와 파일을 첫 줄이 올 때 함께 정한다. 그래서 은 파일이
+ /// 생긴 뒤에 채워진다(모바일은 쓰는 스레드가 시작할 때 경로만 정한다).
+ /// - 같은 초에 다시 켜면 모바일은 같은 이름의 파일을 덮어쓴다. 여기서는 앞 회차를 지우지 않게 ai-…-2.csv 처럼 번호를 붙인다.
+ /// Windows 에서는 옛 스레드가 아직 연 파일을 다시 열 수도 없다.
+ /// - 가 같은 결과를 큐에 거듭 넣지 않는다. HUD 는 추론(250ms)보다 자주(100ms) 읽어 같은 결과를 두세 번 넘기는데,
+ /// 모바일은 그것을 모두 큐에 넣고 쓰는 쪽에서 거른다. 남는 줄은 모바일과 같다(그 결과를 처음 본 때의 값이 남는다).
+ /// - 끈 뒤에 들어온 줄은 받지 않고, 끄기 전에 받은 줄은 모두 쓴다. 모바일은 끄는 순간과 겹친 줄이 아무도 비우지 않는 큐에 남을 수 있다.
+ /// - 쓰다 실패해도 회차를 닫는다. 모바일은 상한과 열기 실패에서만 닫아, 쓰기 실패 뒤에도 오버레이가 아무도 비우지 않는 큐에 줄을 넣는다.
+ ///
+ public sealed class AiDebugRecorder : IAiDebugRecorder
+ {
+ /// 기록을 두는 폴더 이름. 모바일과 같다.
+ public const string DirectoryName = "ai-log";
+
+ ///
+ /// 한 회차의 큐에 담아 두는 줄 수. 넘치면 오래된 것을 버린다. 초당 네 줄이니 50초 분량이다. 디스크가 그만큼 밀리는 일은 없고,
+ /// 있다면 그 PC 에서는 기록 자체가 뜻이 없다.
+ ///
+ internal const int QueueCapacity = 200;
+
+ /// 가 쓰는 스레드를 기다리는 한도. 앱을 닫는 UI 스레드를 오래 잡지 않는다.
+ internal const int CloseWaitMs = 1000;
+
+ /// 같은 초에 다시 켠 회차가 붙이는 번호의 끝. 사람이 스위치를 누르는 간격이라 실제로는 2 를 넘지 않는다.
+ private const int MaxNameAttempts = 100;
+
+ private const string Tag = nameof(AiDebugRecorder);
+
+ private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
+
+ private readonly string? _portableBase;
+ private readonly string? _fallbackBase;
+ private readonly long _maxBytes;
+ private readonly Func _clock;
+ private readonly Func _createFile;
+
+ // 켜기·끄기·닫기와, 쓰는 스레드가 스스로 회차를 닫는 일의 순서를 지킨다. 디스크를 기다리는 동안에는 잡지 않는다.
+ private readonly object _lifecycleLock = new();
+ // 아직 끝나지 않은 쓰는 스레드들. 끈 회차가 남은 줄을 쓰는 중이면 그 스레드도 여기 있다.
+ private readonly List _writers = [];
+ private Session? _session;
+ private string? _currentFile;
+ private bool _disposed;
+
+ /// 앱이 쓰는 기록기. 폴더는 실행 파일 폴더, 거기에 쓸 수 없으면 %LOCALAPPDATA%/SoundVisualizer 다.
+ public AiDebugRecorder()
+ : this(portableBase: null, fallbackBase: null)
+ {
+ }
+
+ /// 먼저 쓰는 폴더의 부모. null 이면 실행 파일 폴더().
+ /// 거기에 쓸 수 없을 때의 부모. null 이면 %LOCALAPPDATA%/SoundVisualizer.
+ /// 파일 하나의 상한. 테스트가 작게 준다.
+ /// 파일 이름에 쓰는 지역 시각. 테스트가 정한다.
+ ///
+ /// 새 파일을 만든다. 같은 이름이 있으면 을 던진다. 테스트가 쓰는 스레드를 세워 두거나 권한 없음을 흉내 낸다.
+ ///
+ internal AiDebugRecorder(
+ string? portableBase,
+ string? fallbackBase,
+ long maxBytes = AiDebugLogWriter.DefaultMaxBytes,
+ Func? clock = null,
+ Func? createFile = null)
+ {
+ _portableBase = portableBase;
+ _fallbackBase = fallbackBase;
+ _maxBytes = maxBytes;
+ _clock = clock ?? (static () => DateTime.Now);
+ _createFile = createFile ?? CreateNewFile;
+ }
+
+ /// 켜져 있는지. 상한에 닿았거나 파일을 만들거나 쓰지 못해 쓰는 쪽이 스스로 닫으면 false 가 된다.
+ public bool IsRecording => Volatile.Read(ref _session) is not null;
+
+ /// 지금 쓰고 있는 파일의 전체 경로. 꺼져 있거나, 켰지만 첫 줄이 와서 파일이 생기기 전이면 null.
+ public string? CurrentFile => Volatile.Read(ref _currentFile);
+
+ /// 지금 회차에서 큐가 차서 버린 줄 수. 0 이 아니면 디스크가 따라오지 못한 것이다. 꺼져 있으면 0.
+ public int Dropped => Volatile.Read(ref _session)?.Dropped ?? 0;
+
+ ///
+ /// 기록을 시작한다. 이미 켜져 있거나 닫은 뒤면 아무것도 하지 않는다(설정을 두 번 켜도 파일이 갈리지 않게).
+ ///
+ /// 여기서는 디스크를 만지지 않는다. 파일 이름에 쓸 시각만 정하고 쓰는 스레드를 띄운다. 줄이 한 번도 오지 않으면 폴더도 파일도
+ /// 만들지 않는다. 켜 보고 바로 끈 PC 에 빈 파일이 남지 않는 편이 낫다. 파일을 만들지 못하면 쓰는 스레드가 로그를 남기고 그 회차를
+ /// 닫는다. 기록이 안 되는 것이 앱이 멈추는 것보다 낫다.
+ ///
+ public void Start()
+ {
+ string stamp;
+ lock (_lifecycleLock)
+ {
+ if (_disposed || _session is not null) return;
+ stamp = _clock().ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
+ var mine = new Session(stamp);
+ var writer = new Thread(() => Run(mine))
+ {
+ IsBackground = true,
+ Name = "SV-AiDebugRecord",
+ };
+ writer.Start();
+ // 쓰는 스레드는 끝날 때 이 잠금 안에서 자기를 뺀다. 잠금을 쥔 채 넣으므로 빼기보다 넣기가 먼저다.
+ _writers.Add(writer);
+ Volatile.Write(ref _currentFile, null);
+ Volatile.Write(ref _session, mine);
+ }
+ AppLog.Info(Tag, $"기록 시작: ai-{stamp}.csv (폴더는 첫 줄이 올 때 쓰는 스레드가 정한다)");
+ }
+
+ ///
+ /// 기록을 멈춘다. 꺼져 있으면 아무것도 하지 않는다. 쓰는 스레드를 기다리지 않는다. 그 스레드는 끄기 전에 받은 줄을 모두 쓰고
+ /// 파일을 닫은 뒤 스스로 나간다.
+ ///
+ public void Stop()
+ {
+ Session? leaving;
+ lock (_lifecycleLock)
+ {
+ leaving = _session;
+ if (leaving is null) return;
+ Volatile.Write(ref _session, null);
+ // 지금 쓰고 있는 파일이다. 끈 뒤에도 남겨 두면 켠 상태와 끈 상태를 가릴 수 없다.
+ Volatile.Write(ref _currentFile, null);
+ }
+ leaving.Deactivate();
+ }
+
+ ///
+ /// 결과 하나를 지금 회차의 큐에 넣고 바로 돌아온다. 꺼져 있으면 아무것도 하지 않는다. HUD 가 읽을 때마다(100ms) 부르며,
+ /// 렌더 루프에서 프레임마다 불러도 될 만큼 가볍다.
+ /// 같은 결과( 가 같은 것)를 거듭 넘기면 처음 넘긴 것만 남는다.
+ ///
+ /// 마지막 결과.
+ /// 지금 벽시계 시각(유닉스 밀리초).
+ /// 지금 소리 크기(0..1).
+ /// 그 종류의 표시가 켜져 있는지.
+ public void Offer(AiClassificationResult result, long nowMs, float level, bool shown)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ Volatile.Read(ref _session)?.Offer(new Row(result, nowMs, level, shown));
+ }
+
+ ///
+ /// 기록을 멈추고, 쓰는 스레드들이 남은 줄을 쓰고 파일을 닫기를 1초까지 기다린다. 앱을 닫을 때 부른다. 쓰는 스레드는 배경 스레드라
+ /// 기다리지 않으면 프로세스가 끝나면서 받아 둔 줄을 잃는다. 닫은 뒤에는 가 아무것도 하지 않는다. 여러 번 불러도 된다.
+ ///
+ public void Dispose()
+ {
+ lock (_lifecycleLock)
+ {
+ if (_disposed) return;
+ _disposed = true;
+ }
+ Stop();
+ if (!WaitForWriters(CloseWaitMs))
+ AppLog.Warn(Tag, "쓰는 스레드가 1초 안에 끝나지 않았다. 프로세스가 끝나면 남은 줄을 잃을 수 있다");
+ }
+
+ /// 지금 살아 있는 쓰는 스레드가 모두 끝나기를 기다린다. 모두 끝났으면 true. 끄지 않은 회차의 스레드는 끝나지 않는다.
+ internal bool WaitForWriters(int timeoutMs)
+ {
+ Thread[] writers;
+ lock (_lifecycleLock) writers = [.. _writers];
+ long deadline = Environment.TickCount64 + timeoutMs;
+ foreach (var writer in writers)
+ {
+ if (!writer.Join((int)Math.Max(0L, deadline - Environment.TickCount64))) return false;
+ }
+ return true;
+ }
+
+ /// 파일 이름. 같은 초에 다시 켠 회차만 번호가 붙는다.
+ internal static string FileName(string stamp, int attempt) =>
+ attempt == 1 ? $"ai-{stamp}.csv" : string.Create(CultureInfo.InvariantCulture, $"ai-{stamp}-{attempt}.csv");
+
+ /// 쓰는 스레드의 본체. 자기 회차의 큐만 본다.
+ private void Run(Session mine)
+ {
+ TextWriter? writer = null;
+ AiDebugLogWriter? log = null;
+ string? path = null;
+ try
+ {
+ while (mine.TryTake(out var row))
+ {
+ if (writer is null || log is null)
+ {
+ // 파일은 첫 줄이 올 때 만든다. 어느 폴더에 둘지도 이때 실제로 만들어 보고 정한다.
+ if (!TryOpen(mine.Stamp, out writer, out path))
+ {
+ // 회차를 닫아, 오버레이가 아무도 비우지 않는 큐에 줄을 넣지 않게 한다.
+ CloseSession(mine);
+ return;
+ }
+ log = new AiDebugLogWriter(writer, _maxBytes);
+ PublishCurrentFile(mine, path);
+ }
+
+ // 줄마다 디스크에 밀어 넣는다. 모아 두면 기록 중에 연 파일에 최근 몇 초가 비어, 그 구간을 "결과가 없었다" 로 읽는다.
+ // 초당 네 번 쓰는 비용은 프레임마다 그리는 것 옆에서 없는 셈이다.
+ if (log.Write(row.Result, row.NowMs, row.Level, row.Shown)) writer.Flush();
+ if (log.Stopped)
+ {
+ // 상태를 사실에 맞춘다. 닫지 않으면 오버레이가 아무도 비우지 않는 큐에 계속 줄을 넣는다.
+ AppLog.Warn(Tag, $"기록 상한에 닿아 멈춘다: {log.Rows}줄, {Path.GetFileName(path)}");
+ CloseSession(mine);
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // 이 스레드에서 새어 나간 예외는 앱을 끝낸다. 디스크가 가득 찼거나 쓰던 파일에 쓸 수 없게 된 경우가 여기로 온다.
+ AppLog.Warn(Tag, "기록을 쓰다 실패해 멈춘다", ex);
+ CloseSession(mine);
+ }
+ finally
+ {
+ try
+ {
+ writer?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ AppLog.Warn(Tag, "기록 파일을 닫다 실패했다", ex);
+ }
+ string what = path is null ? "파일을 만들지 않았다" : $"{log?.Rows ?? 0}줄, {Path.GetFileName(path)}";
+ AppLog.Info(Tag, $"기록 끝: {what}, 버린 줄 {mine.Dropped}");
+ lock (_lifecycleLock) _writers.Remove(Thread.CurrentThread);
+ }
+ }
+
+ /// 폴더를 고르고 새 파일을 만든다. 쓰는 스레드에서만 부른다. 실행 파일 폴더가 먼저고, 거기에 쓸 수 없으면 LocalAppData 다.
+ private bool TryOpen(string stamp, [NotNullWhen(true)] out TextWriter? writer, [NotNullWhen(true)] out string? path)
+ {
+ foreach (string directory in CandidateDirectories())
+ {
+ try
+ {
+ Directory.CreateDirectory(directory);
+ for (int attempt = 1; attempt <= MaxNameAttempts; attempt++)
+ {
+ string candidate = Path.Combine(directory, FileName(stamp, attempt));
+ Stream stream;
+ try
+ {
+ stream = _createFile(candidate);
+ }
+ catch (IOException) when (File.Exists(candidate))
+ {
+ // 같은 초에 켠 앞 회차의 파일이다. 덮어쓰지 않고 다음 번호로 간다.
+ continue;
+ }
+ writer = new StreamWriter(stream, Utf8NoBom);
+ path = candidate;
+ AppLog.Info(Tag, $"기록 파일: {candidate}");
+ return true;
+ }
+ AppLog.Warn(Tag, $"같은 시각의 기록 파일이 너무 많다: {directory}");
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or NotSupportedException or ArgumentException)
+ {
+ // 쓸 수 없는 폴더다(Program Files, 읽기 전용 매체, 폴더 자리에 같은 이름의 파일 등). 다음 후보로 간다.
+ AppLog.Warn(Tag, $"기록 폴더에 쓸 수 없다: {directory}", ex);
+ }
+ }
+ writer = null;
+ path = null;
+ return false;
+ }
+
+ /// 기록 폴더 후보. 포터블 배포라 실행 파일 옆이 먼저다(설정 파일도 그 옆에 둔다).
+ private IEnumerable CandidateDirectories()
+ {
+ yield return Path.Combine(_portableBase ?? AppContext.BaseDirectory, DirectoryName);
+ // 알려진 폴더를 찾는 일도 쓰는 스레드에서 한다.
+ string fallbackBase = _fallbackBase ?? DefaultFallbackBase();
+ if (fallbackBase.Length > 0) yield return Path.Combine(fallbackBase, DirectoryName);
+ }
+
+ private static string DefaultFallbackBase()
+ {
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ return localAppData.Length == 0 ? "" : Path.Combine(localAppData, "SoundVisualizer");
+ }
+
+ /// 새 파일을 만든다. 같은 이름이 있으면 . 쓰는 동안에도 다른 프로그램이 읽을 수 있게 연다.
+ private static Stream CreateNewFile(string path) =>
+ new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
+
+ private void PublishCurrentFile(Session mine, string path)
+ {
+ lock (_lifecycleLock)
+ {
+ // 그사이 꺼졌으면 남기지 않는다. 끈 상태에서 파일이 보이면 켜져 있는 것으로 읽힌다.
+ if (_session == mine) Volatile.Write(ref _currentFile, path);
+ }
+ }
+
+ ///
+ /// 쓰는 쪽이 스스로 그만둘 때(상한, 열기·쓰기 실패) 자기 회차를 닫는다. 이미 다른 회차가 켜져 있으면 건드리지 않는다.
+ /// 남의 회차를 끄면 켜 둔 사람의 기록이 조용히 멈춘다.
+ ///
+ private void CloseSession(Session mine)
+ {
+ mine.Deactivate();
+ lock (_lifecycleLock)
+ {
+ if (_session != mine) return;
+ Volatile.Write(ref _session, null);
+ Volatile.Write(ref _currentFile, null);
+ }
+ }
+
+ /// 오버레이가 결과를 본 순간의 값. 쓰는 스레드가 나중에 상태를 읽지 않도록 그때 고정한다.
+ private readonly record struct Row(AiClassificationResult Result, long NowMs, float Level, bool Shown);
+
+ /// 켠 한 회차. 큐와 "아직 받는지" 를 함께 들고 있어 회차끼리 섞이지 않는다.
+ private sealed class Session(string stamp)
+ {
+ private readonly object _gate = new();
+ private readonly Queue _queue = new(QueueCapacity);
+ private bool _active = true;
+ private bool _hasOffered;
+ private long _lastOfferedMs;
+ private int _dropped;
+
+ /// 파일 이름에 쓰는 켠 시각.
+ public string Stamp { get; } = stamp;
+
+ public int Dropped
+ {
+ get
+ {
+ lock (_gate) return _dropped;
+ }
+ }
+
+ public void Offer(in Row row)
+ {
+ lock (_gate)
+ {
+ // 끈 뒤에 들어온 줄은 받지 않는다. 받은 줄은 쓰는 스레드가 모두 쓴다.
+ if (!_active) return;
+ // HUD 는 추론보다 자주 읽어 같은 결과를 거듭 넘긴다. 큐가 같은 결과로 차서 새 결과를 밀어내지 않게 여기서 거른다.
+ long timestampMs = row.Result.TimestampMs;
+ if (_hasOffered && _lastOfferedMs == timestampMs) return;
+ _hasOffered = true;
+ _lastOfferedMs = timestampMs;
+ if (_queue.Count >= QueueCapacity)
+ {
+ // 가장 오래된 것을 버리고 새것을 넣는다. 최근 것이 궁금한 도구라 뒤를 살린다.
+ _queue.Dequeue();
+ _dropped++;
+ }
+ _queue.Enqueue(row);
+ Monitor.Pulse(_gate);
+ }
+ }
+
+ /// 다음 줄을 기다려 꺼낸다. 꺼졌고 남은 줄도 없으면 false.
+ public bool TryTake(out Row row)
+ {
+ lock (_gate)
+ {
+ while (_queue.Count == 0 && _active) Monitor.Wait(_gate);
+ return _queue.TryDequeue(out row);
+ }
+ }
+
+ /// 더 받지 않는다. 기다리던 쓰는 스레드를 깨워 남은 줄을 쓰고 나가게 한다.
+ public void Deactivate()
+ {
+ lock (_gate)
+ {
+ _active = false;
+ Monitor.PulseAll(_gate);
+ }
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Diagnostics/AiDebugText.cs b/SoundVisualizer.Core/Diagnostics/AiDebugText.cs
new file mode 100644
index 0000000..dcd4f60
--- /dev/null
+++ b/SoundVisualizer.Core/Diagnostics/AiDebugText.cs
@@ -0,0 +1,156 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Globalization;
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Diagnostics
+{
+ ///
+ /// 개발자 모드 HUD 에 뿌릴 글자들(모바일 AiDebugLines). 화면 배치는 오버레이가 한다. 모바일 HUD 의 첫 줄은
+ /// Coarse ● Label Confidence 이고(점은 의 색), 그 아래로 Detail, Verdict, Timing, top-5 가 한 줄씩이다.
+ ///
+ /// 점을 칠할 색의 소리 종류(사용자가 그 종류에 고른 색). 결과가 없으면 null.
+ /// 소리 종류를 대문자로. 결과가 없으면 왜 없는지를 대신 넣는다.
+ /// 모델이 말한 이름(번역하지 않은 YAMNet 클래스 이름).
+ /// 확신도 두 자리.
+ /// 임계값·나이.
+ /// YAMNet 위험 단서로 올렸는지.
+ /// 소리 크기·표시 여부·단계별 소요시간.
+ /// 모델이 가장 높게 본 이름들. 한 줄에 하나씩 순위·이름·확률.
+ public sealed record AiDebugLines(
+ SoundType? ColorType,
+ string Coarse,
+ string Label,
+ string Confidence,
+ string Detail,
+ string Verdict,
+ string Timing,
+ IReadOnlyList Top5)
+ {
+ // 모바일 data class 처럼 top-5 도 내용으로 비교한다. 오버레이는 지난번과 같으면 글자를 다시 넣지 않는다.
+ public bool Equals(AiDebugLines? other) =>
+ other is not null &&
+ ColorType == other.ColorType &&
+ Coarse == other.Coarse &&
+ Label == other.Label &&
+ Confidence == other.Confidence &&
+ Detail == other.Detail &&
+ Verdict == other.Verdict &&
+ Timing == other.Timing &&
+ Top5.SequenceEqual(other.Top5);
+
+ public override int GetHashCode() =>
+ HashCode.Combine(ColorType, Coarse, Label, Confidence, Detail, Verdict, Timing, Top5.Count);
+ }
+
+ ///
+ /// AI 분류 결과를 개발자 모드 HUD 의 글자로 바꾼다(모바일 AiDebugText.kt).
+ ///
+ /// 이 도구의 목적은 분류가 맞는지 사람이 눈으로 채점하는 것이라, 모델이 실제로 말한 것을 손대지 않고 보여준다.
+ /// 라벨을 번역하지 않는 이유도 같다. 확신도가 낮을 때도 숨기지 않는다. 무엇을 왜 보여주는지는 모바일 docs/ARCHITECTURE.md 4장에 있다.
+ /// 화면과 상관없는 순수 함수라 어느 스레드에서 불러도 된다. 부를 때마다 문자열을 여럿 만들므로 렌더 프레임마다가 아니라 HUD 를 고칠 때
+ /// (모바일은 100ms 마다. 추론 주기 250ms 보다 촘촘해야 짧은 위협음 판정 하나를 건너뛰지 않는다) 부른다.
+ ///
+ public static class AiDebugText
+ {
+ /// 값이 없거나 뜻이 없을 때. 숫자 자리에 NaN 이 그대로 뜨는 것을 막는다.
+ public const string None = "-";
+
+ /// 모델을 못 불러와 분류가 아예 돌지 않는 상태.
+ public const string CoarseAiOff = "AI OFF";
+
+ /// 분류는 돌지만 첫 결과가 아직 없는 상태. 캡처 직후와 장치를 바꿔 AI 를 다시 만든 직후가 여기다.
+ public const string CoarseWaiting = "WAIT";
+
+ ///
+ /// top-5 이름 칸의 폭(글자 수). 고정폭 글꼴에서 순위·확률까지 한 줄이 폰 폭 안에 들어간다.
+ /// 더 긴 이름("Vehicle horn, car horn, honking" 등)은 줄여서 끝에 를 붙인다.
+ ///
+ public const int Top5NameWidth = 26;
+
+ public const string Ellipsis = "…";
+
+ ///
+ /// HUD 한 줄에 들어가는 글자 수. 이보다 길면 폰에서 줄이 두 줄로 꺾인다(#165). 폭 411dp 폰에서 고정폭 11sp 글자(6.6dp)가
+ /// 383dp 에 58자다. PC 화면은 훨씬 넓지만 두 판이 같은 줄을 만들도록 같은 한도를 지킨다(나이를 짧게 쓰는 규칙이 여기서 나온다).
+ ///
+ public const int HudMaxColumns = 58;
+
+ /// 마지막 결과. 아직 없거나 AI 가 돌지 않으면 null.
+ /// 지금 벽시계 시각(유닉스 밀리초, 와 같은 시계).
+ /// 지금 소리 크기(0..1). 모바일은 가장 최근 버퍼의 좌우 중 큰 피크를 쓴다.
+ /// 그 종류의 표시가 켜져 있는지. 결과가 없으면 false.
+ /// AI 를 만들지 못해 분류가 아예 돌지 않으면 false. 만드는 중에는 true 다.
+ public static AiDebugLines Format(AiClassificationResult? result, long nowMs, float level, bool shown, bool aiAvailable)
+ {
+ string levelText = "lvl " + TwoDecimals(level);
+
+ if (result is null)
+ {
+ // 분류기가 없거나 첫 추론이 끝나지 않았다. 낡은 줄을 남겨 두면 그것을 지금 결과로 착각하므로,
+ // 값이 없다는 것을 그대로 보여준다.
+ return new AiDebugLines(
+ ColorType: null,
+ Coarse: aiAvailable ? CoarseWaiting : CoarseAiOff,
+ Label: aiAvailable ? "no result yet" : "model load failed",
+ Confidence: None,
+ Detail: None,
+ Verdict: None,
+ Timing: levelText,
+ Top5: []);
+ }
+
+ var top5 = new string[Math.Min(result.Top5.Count, 5)];
+ for (int i = 0; i < top5.Length; i++)
+ {
+ var hit = result.Top5[i];
+ top5[i] = string.Create(CultureInfo.InvariantCulture, $"{i + 1} {FitName(hit.Name)} {TwoDecimals(hit.Probability)}");
+ }
+
+ return new AiDebugLines(
+ ColorType: result.Coarse,
+ Coarse: result.Coarse.ToLabel().ToUpperInvariant(),
+ // 확정된 이름이 아직 없으면 빈 문자열이 온다. 임계값을 한 번도 넘지 못한 상태다.
+ Label: result.Display.Length == 0 ? "(none)" : result.Display,
+ Confidence: TwoDecimals(result.Confidence),
+ Detail: $"thr {YesNo(result.MeetsThreshold)} age {AgeText(result.TimestampMs, nowMs)}",
+ // 투표와 따로, YAMNet 의 강한 위험 단서로 위협음을 지켰는지.
+ Verdict: $"cue {YesNo(result.DangerCuePromoted)}",
+ Timing: $"{levelText} shown {YesNo(shown)} {TimingOf(result)}",
+ Top5: top5);
+ }
+
+ /// 고정폭 칸에 맞춘 이름. 짧으면 공백으로 채워 확률이 세로 한 열에 선다.
+ private static string FitName(string name) =>
+ name.Length > Top5NameWidth
+ ? string.Concat(name.AsSpan(0, Top5NameWidth - Ellipsis.Length), Ellipsis)
+ : name.PadRight(Top5NameWidth);
+
+ private static string TimingOf(AiClassificationResult result) =>
+ $"{WholeMillis(result.TotalMs)}ms ({WholeMillis(result.PreprocessMs)}/{WholeMillis(result.YamnetMs)})";
+
+ ///
+ /// 결과가 나온 뒤 흐른 시간. 무음이면 추론을 건너뛰고 마지막 결과를 그대로 들고 있으므로, 이 값이 계속 자라는 것이
+ /// "지금 추론이 돌지 않는다" 는 신호다. 벽시계라 시계가 뒤로 조정되면 음수가 될 수 있다. 그때는 0 으로 본다.
+ ///
+ /// 오래될수록 짧게 쓴다. 100초가 넘으면 소수점은 뜻이 없고, 그대로 두면 123.4s 처럼 길어져 detail 줄이
+ /// 를 넘는다(#165). 값 자리는 늘 5글자 이하다.
+ ///
+ private static string AgeText(long timestampMs, long nowMs)
+ {
+ long elapsed = Math.Max(nowMs - timestampMs, 0L);
+ if (elapsed < 99_950L) return JavaNumberFormat.Fixed(elapsed / 1000.0, 1) + "s"; // 0.0s ~ 99.9s
+ if (elapsed < 999_500L) return string.Create(CultureInfo.InvariantCulture, $"{(elapsed + 500L) / 1000L}s"); // 100s ~ 999s
+ return string.Create(CultureInfo.InvariantCulture, $"{(elapsed + 30_000L) / 60_000L}m"); // 17m ~
+ }
+
+ private static string TwoDecimals(float value) =>
+ float.IsNaN(value) ? None : JavaNumberFormat.Fixed(value, 2);
+
+ private static string WholeMillis(double value) =>
+ double.IsNaN(value) ? None : JavaNumberFormat.Fixed(value, 0);
+
+ private static string YesNo(bool value) => value ? "Y" : "N";
+ }
+}
diff --git a/SoundVisualizer.Core/Diagnostics/JavaNumberFormat.cs b/SoundVisualizer.Core/Diagnostics/JavaNumberFormat.cs
new file mode 100644
index 0000000..4410afd
--- /dev/null
+++ b/SoundVisualizer.Core/Diagnostics/JavaNumberFormat.cs
@@ -0,0 +1,40 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Globalization;
+
+namespace SoundVisualizer.Diagnostics
+{
+ ///
+ /// 소수를 모바일 판(Kotlin String.format(Locale.US, "%.Nf", value))과 같은 글자로 적는다.
+ ///
+ /// .NET 의 "F" 서식은 이진값 그대로를 반올림하고 딱 절반이면 짝수 쪽으로 보낸다(0.15 → "0.1", 0.25 → "0.2", 12.5 → "12").
+ /// Java 는 값을 가장 짧은 십진수(Double.toString 의 자릿수)로 적은 뒤 5 에서 올린다(0.15 → "0.2", 0.25 → "0.3", 12.5 → "13").
+ /// HUD 의 나이는 밀리초를 1000 으로 나눈 값이라 이 경계에 자주 걸린다. 그대로 두면 같은 결과를 두 판이 다르게 적어,
+ /// 두 기기의 기록을 나란히 놓고 볼 때 헷갈린다. 로캘은 늘 고정이다(아랍어 로캘에서는 숫자가 아랍 숫자로 나간다).
+ ///
+ internal static class JavaNumberFormat
+ {
+ ///
+ /// 소수점 아래 자리로 적는다. float 는 double 로 넓혀서 넘긴다. Java 의 Formatter 도
+ /// float 를 double 로 바꾼 뒤 적으므로, 0.145f 는 0.14499999582767487 로 보고 "0.14" 가 된다.
+ ///
+ public static string Fixed(double value, int decimals)
+ {
+ // NaN·Infinity·-Infinity 는 Java 와 글자가 같다. 값이 없다는 표시("-" 등)는 부르는 쪽이 정한다.
+ if (!double.IsFinite(value)) return value.ToString(CultureInfo.InvariantCulture);
+
+ string format = "F" + decimals.ToString(CultureInfo.InvariantCulture);
+ // Java 는 크기만 적고 부호를 앞에 붙인다. 그래서 -0.0 과 -0.001 이 "-0.00" 이 된다.
+ string sign = double.IsNegative(value) ? "-" : "";
+ double magnitude = Math.Abs(value);
+ // 가장 짧은 왕복 표현은 17자리 이하라 decimal 에 그대로 담긴다. decimal 범위(약 7.9e28)를 넘는 값만
+ // .NET 서식으로 적는다. 여기서 적는 확률·소리 크기·밀리초는 그 근처에도 가지 않는다.
+ string shortest = magnitude.ToString("R", CultureInfo.InvariantCulture);
+ string text = decimal.TryParse(shortest, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal exact)
+ ? Math.Round(exact, decimals, MidpointRounding.AwayFromZero).ToString(format, CultureInfo.InvariantCulture)
+ : magnitude.ToString(format, CultureInfo.InvariantCulture);
+ return sign + text;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Help/AppVersionText.cs b/SoundVisualizer.Core/Help/AppVersionText.cs
new file mode 100644
index 0000000..e07d455
--- /dev/null
+++ b/SoundVisualizer.Core/Help/AppVersionText.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Help
+{
+ ///
+ /// 실행 파일의 버전(AssemblyInformationalVersion)을 화면과 제보에 맞게 적는다.
+ ///
+ /// .NET SDK 는 이 값 뒤에 빌드한 커밋을 붙인다("1.0.0+a8d9c377c4a4..."). 앱 정보에는 사람이 읽는 앞부분만 보이고,
+ /// 제보에는 커밋을 짧게(7자) 남긴다. 같은 버전 번호로 여러 번 빌드해도 어느 빌드에서 난 문제인지 가릴 수 있다.
+ ///
+ public static class AppVersionText
+ {
+ private const int ShortCommitLength = 7;
+
+ /// 앱 정보에 보일 버전. 빌드 정보(+ 뒤)를 뗀다. 값이 없으면 빈 문자열.
+ public static string Display(string? informationalVersion)
+ {
+ string value = (informationalVersion ?? "").Trim();
+ int plus = value.IndexOf('+', StringComparison.Ordinal);
+ return plus < 0 ? value : value[..plus];
+ }
+
+ /// 제보에 넣을 버전. 빌드 정보가 있으면 7자로 줄여 붙인다("1.0.0+a8d9c37").
+ public static string ForReport(string? informationalVersion)
+ {
+ string value = (informationalVersion ?? "").Trim();
+ int plus = value.IndexOf('+', StringComparison.Ordinal);
+ if (plus < 0) return value;
+ string build = value[(plus + 1)..];
+ if (build.Length == 0) return value[..plus];
+ return value[..plus] + "+" + (build.Length > ShortCommitLength ? build[..ShortCommitLength] : build);
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Help/LicenseNotices.cs b/SoundVisualizer.Core/Help/LicenseNotices.cs
new file mode 100644
index 0000000..6d819e0
--- /dev/null
+++ b/SoundVisualizer.Core/Help/LicenseNotices.cs
@@ -0,0 +1,99 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Reflection;
+using System.Text;
+
+namespace SoundVisualizer.Help
+{
+ ///
+ /// 도움말의 "오픈소스 라이선스" 창에 보이는 고지. 안드로이드 LicenseAssets.kt 를 옮겼다.
+ ///
+ /// 저장소의 고지 파일을 복사하지 않고 제자리에서 Core 에 넣는다(csproj 의 EmbeddedResource). 안드로이드는 assets 에 복사본을
+ /// 두고 원본과 같은지 테스트로 지켰는데, 여기서는 복사본이 없어 원본을 고치면 다음 빌드에 그대로 들어간다.
+ ///
+ public static class LicenseNotices
+ {
+ /// 창에 보일 고지 하나. 는 저장소 뿌리에서의 경로이고 창에서 고지의 머리가 된다.
+ public sealed record Document(string Path, string ResourceName);
+
+ ///
+ /// 창에 보이는 순서. 무엇을 넣었는지(라이브러리·모델·글꼴 목록) → 앱에 넣은 글꼴의 라이선스(OFL) → 분류 탭 소리 이름의 고지 →
+ /// 이 앱의 라이선스(AGPL). csproj 의 LogicalName 과 맞춘다.
+ ///
+ public static IReadOnlyList Documents { get; } = new[]
+ {
+ new Document("THIRD_PARTY.md", "SoundVisualizer.Licenses.THIRD_PARTY.md"),
+ new Document("SoundVisualizer/Fonts/Pretendard-LICENSE.txt", "SoundVisualizer.Licenses.Pretendard-LICENSE.txt"),
+ new Document("SoundVisualizer.Core/Sounds/Data/NOTICE.txt", "SoundVisualizer.Licenses.NOTICE.txt"),
+ new Document("LICENSE", "SoundVisualizer.Licenses.LICENSE"),
+ };
+
+ /// 이보다 짧은 '=' '-' 줄은 구분선이 아니라 내용으로 본다.
+ private const int MinRulerLength = 10;
+
+ private static readonly Assembly Owner = typeof(LicenseNotices).Assembly;
+
+ /// 고지 하나의 전문. 넣어 둔 리소스가 없으면 (빌드 설정이 틀렸다).
+ public static string Read(Document document)
+ {
+ using var stream = Owner.GetManifestResourceStream(document.ResourceName)
+ ?? throw new InvalidOperationException("고지 리소스가 없습니다: " + document.ResourceName);
+ using var reader = new StreamReader(stream, Encoding.UTF8); // BOM 이 있어도 떼고 읽는다
+ return reader.ReadToEnd();
+ }
+
+ ///
+ /// 창에 보일 전문. 고지마다 저장소 경로를 머리로 달고 빈 줄 둘로 띄운다. 줄 끝은 \n 이다.
+ /// Pretendard 의 OFL 처럼 첫 줄만 봐서는 무엇인지 모르는 고지가 있어 머리를 단다.
+ ///
+ public static string ReadAll()
+ {
+ var text = new StringBuilder();
+ foreach (var document in Documents)
+ {
+ if (text.Length > 0) text.Append("\n\n\n");
+ text.Append("==== ").Append(document.Path).Append(" ====\n\n");
+ text.Append(Read(document).Replace("\r\n", "\n", StringComparison.Ordinal).TrimEnd());
+ }
+ return text.Append('\n').ToString();
+ }
+
+ ///
+ /// 고지를 창에 보일 글로 바꾼다. 줄 끝을 \n 으로 맞추고 구분선을 창 폭에 맞춘다.
+ /// 저장소에는 LF 로 들어 있지만 Windows 에서 받으면(core.autocrlf) 작업 폴더가 CRLF 가 되고, 그 폴더에서 빌드하면
+ /// 리소스에도 \r 이 들어간다.
+ ///
+ public static string DisplayText(string raw, int columns) =>
+ FitRulers(raw.Replace("\r\n", "\n", StringComparison.Ordinal), columns);
+
+ ///
+ /// 줄 전체가 한 글자('=' 또는 '-')로 된 구분선을 글자까지 줄인다. 다른 줄은 손대지 않는다.
+ /// 줄 끝은 \n 이어야 한다.
+ ///
+ /// 고지는 80칸에 맞춰 적혀 있어, 창이 그보다 좁으면 구분선 하나가 '=' 로 가득 찬 두세 줄로 꺾인다(안드로이드 #167).
+ /// 창이 더 넓으면 원래 길이 그대로 둔다. 파일 자체는 고치지 않는다. 저장소에서 읽는 사람에게는 80칸이 맞다.
+ /// 안드로이드는 '=' 만 보았지만 PC 판 고지에는 '-' 구분선(NOTICE 80칸, OFL 59칸)도 있어 함께 맞춘다.
+ ///
+ public static string FitRulers(string text, int columns)
+ {
+ if (columns < 1) return text;
+ var lines = text.Split('\n');
+ for (int i = 0; i < lines.Length; i++)
+ {
+ if (IsRuler(lines[i]) && lines[i].Length > columns) lines[i] = new string(lines[i][0], columns);
+ }
+ return string.Join("\n", lines);
+ }
+
+ private static bool IsRuler(string line)
+ {
+ if (line.Length < MinRulerLength || (line[0] != '=' && line[0] != '-')) return false;
+ foreach (char c in line)
+ {
+ if (c != line[0]) return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Help/ReportLink.cs b/SoundVisualizer.Core/Help/ReportLink.cs
new file mode 100644
index 0000000..c8325b4
--- /dev/null
+++ b/SoundVisualizer.Core/Help/ReportLink.cs
@@ -0,0 +1,55 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Help
+{
+ ///
+ /// 제보용 GitHub 이슈 주소, 메일(mailto) 주소, 환경 정보 문구를 만든다. 안드로이드 ReportLink.kt 를 옮겼다.
+ ///
+ /// 화면이나 운영체제 API 없이 문자열만 다루므로 단위 테스트로 고정한다. 넣는 값은 의 인자뿐이라
+ /// 소리, 장치 이름, 사용자 경로가 섞일 수 없다. 환경 정보의 이름표는 번역하지 않는다. 제보를 읽는 쪽이 어느 언어로 온 제보든
+ /// 같은 꼴로 읽을 수 있어야 한다.
+ ///
+ public static class ReportLink
+ {
+ /// 새 이슈 화면. PC 판 저장소다(안드로이드 판과 다르다).
+ public const string IssuesUrl = "https://github.com/amophi/SoundVisualizer/issues/new";
+
+ /// 메일 제보를 받는 곳. 안드로이드 판과 같은 주소다.
+ public const string ReportEmail = "nawwhod@gmail.com";
+
+ /// 제보 본문 끝에 붙는 네 줄. 줄은 \n 으로 나눈다.
+ public static string EnvironmentBlock(string appVersion, string windows, string dotnet, string language) =>
+ string.Join("\n",
+ "App version: " + appVersion,
+ "Windows: " + windows,
+ ".NET: " + dotnet,
+ "App language: " + language);
+
+ ///
+ /// 제목과 본문을 미리 채운 새 이슈 주소. GitHub 은 title·body 쿼리로 새 이슈 화면을 채워 준다.
+ /// 값은 모두 인코딩하므로 사용자가 쓸 자리에 줄바꿈이나 한글이 있어도 주소가 깨지지 않는다.
+ ///
+ public static string IssueUrl(string title, string bodyPrompt, string environment) =>
+ IssuesUrl + "?title=" + Encode(title) + "&body=" + Encode(Body(bodyPrompt, environment));
+
+ /// 사용자가 쓸 자리와 환경 정보를 합친 제보 본문. 이슈와 메일이 같은 본문을 쓴다.
+ public static string Body(string bodyPrompt, string environment) =>
+ bodyPrompt.Length == 0 ? environment : bodyPrompt + "\n\n" + environment;
+
+ ///
+ /// 제목과 본문을 채운 mailto 주소. 메일 본문의 줄바꿈은 RFC 6068 대로 CRLF(%0D%0A)로 적는다.
+ /// 안드로이드는 Gmail 이 받는 \n 그대로 넣었지만, Windows 의 메일 앱은 여러 가지라 규격을 따른다.
+ ///
+ public static string MailtoUri(string address, string subject, string bodyPrompt, string environment) =>
+ "mailto:" + address + "?subject=" + Encode(subject) + "&body=" + Encode(MailBody(bodyPrompt, environment));
+
+ /// 메일에 들어가는 본문. 와 같고 줄바꿈만 CRLF 다.
+ public static string MailBody(string bodyPrompt, string environment) =>
+ Body(bodyPrompt, environment).Replace("\r\n", "\n", StringComparison.Ordinal).Replace("\n", "\r\n", StringComparison.Ordinal);
+
+ // RFC 3986 의 비예약 문자만 남기고 모두 %XX 로 적는다. 공백은 %20 이다. 안드로이드 URLEncoder 는 공백을 + 로 적었는데,
+ // mailto 쿼리에서 + 는 공백이 아니라 글자 그대로라 그쪽은 따로 %20 으로 바꿨다. 여기서는 처음부터 %20 이라 둘이 같다.
+ private static string Encode(string value) => Uri.EscapeDataString(value);
+ }
+}
diff --git a/SoundVisualizer.Core/KeyedList.cs b/SoundVisualizer.Core/KeyedList.cs
new file mode 100644
index 0000000..ed2eeab
--- /dev/null
+++ b/SoundVisualizer.Core/KeyedList.cs
@@ -0,0 +1,85 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer
+{
+ ///
+ /// 화면 목록의 줄(보기 객체)을 새 항목 목록에 맞춘다. 키가 같은 줄은 버리지 않고 고쳐 쓰고, 없어진 줄만 지우고 새 줄만 넣는다.
+ /// 모바일 LazyColumn 의 키와 같은 일을 WPF 의 ObservableCollection 에 한다.
+ ///
+ /// 통째로 바꿔 끼우면 목록이 줄을 모두 새로 만들어, 종류를 고른 카드가 키보드 초점을 잃고 고급 모드 스위치를 누른 줄도 초점을
+ /// 잃는다. 남는 줄끼리의 앞뒤 순서가 그대로이면(소리 탭은 늘 그렇다) 지우기와 넣기만 하고 줄을 옮기지 않는다.
+ ///
+ public static class KeyedList
+ {
+ ///
+ /// 를 의 차례로 맞춘다. 끝나면 rows[i] 의 키는 items[i] 의 키와 같다.
+ ///
+ /// 화면 목록. 키가 겹치지 않아야 한다.
+ /// 새 항목. 키가 겹치지 않아야 한다.
+ /// 항목의 키.
+ /// 줄의 키.
+ /// 새 키의 줄을 만든다. 만든 줄에 항목을 넣는 것까지 한다.
+ /// 남는 줄에 새 항목을 넣는다. 바뀐 것이 없으면 아무것도 알리지 않게 하는 것은 줄의 몫이다.
+ public static void Sync(
+ IList rows,
+ IReadOnlyList items,
+ Func itemKey,
+ Func rowKey,
+ Func create,
+ Action update)
+ {
+ ArgumentNullException.ThrowIfNull(rows);
+ ArgumentNullException.ThrowIfNull(items);
+ ArgumentNullException.ThrowIfNull(itemKey);
+ ArgumentNullException.ThrowIfNull(rowKey);
+ ArgumentNullException.ThrowIfNull(create);
+ ArgumentNullException.ThrowIfNull(update);
+
+ var wanted = new HashSet(StringComparer.Ordinal);
+ foreach (TItem item in items)
+ {
+ if (!wanted.Add(itemKey(item))) throw new ArgumentException("키가 겹칩니다: " + itemKey(item), nameof(items));
+ }
+
+ // 1. 없어진 줄을 뒤에서부터 지운다.
+ for (int i = rows.Count - 1; i >= 0; i--)
+ {
+ if (!wanted.Contains(rowKey(rows[i]))) rows.RemoveAt(i);
+ }
+
+ var remaining = new Dictionary(StringComparer.Ordinal);
+ foreach (TRow row in rows) remaining.Add(rowKey(row), row);
+
+ // 2. 앞에서부터 맞춘다. 제자리의 줄은 고쳐 쓰고, 새 키는 그 자리에 넣는다. 남는 줄의 순서가 바뀐 때만(소리 탭에는 없다) 옮긴다.
+ for (int i = 0; i < items.Count; i++)
+ {
+ TItem item = items[i];
+ string key = itemKey(item);
+ if (i < rows.Count && string.Equals(rowKey(rows[i]), key, StringComparison.Ordinal))
+ {
+ update(rows[i], item);
+ }
+ else if (remaining.TryGetValue(key, out TRow? row))
+ {
+ rows.RemoveAt(IndexOf(rows, key, rowKey, i + 1));
+ rows.Insert(i, row);
+ update(row, item);
+ }
+ else
+ {
+ rows.Insert(i, create(item));
+ }
+ }
+ }
+
+ private static int IndexOf(IList rows, string key, Func rowKey, int start)
+ {
+ for (int i = start; i < rows.Count; i++)
+ {
+ if (string.Equals(rowKey(rows[i]), key, StringComparison.Ordinal)) return i;
+ }
+ throw new InvalidOperationException("줄을 찾지 못했습니다: " + key);
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Localization/Localizer.cs b/SoundVisualizer.Core/Localization/Localizer.cs
new file mode 100644
index 0000000..27ad2bc
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Localizer.cs
@@ -0,0 +1,107 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Concurrent;
+using System.Globalization;
+
+namespace SoundVisualizer.Localization
+{
+ ///
+ /// 화면 문자열을 지금 언어의 표에서 찾는다. 그 언어 표에 없는 키는 영어 표에서 찾고, 영어에도 없으면 키를 그대로 돌려준다
+ /// (빈칸보다 키가 보여야 빠진 문자열을 바로 알아챈다).
+ ///
+ /// 언어를 바꿀 때는 (코드, 그 언어 표, 영어 표) 묶음을 새로 만들어 참조 하나로 바꿔 끼운다. 그래서 다른 스레드(캡처 스레드의
+ /// 오류 문구 등)가 읽는 도중에 언어가 바뀌어도 두 언어가 섞인 결과를 보지 않는다.
+ ///
+ public sealed class Localizer
+ {
+ private sealed record Snapshot(string Code, StringTable Current, StringTable Fallback);
+
+ private readonly Func _tables;
+ private readonly ConcurrentDictionary _reportedMissing = new(StringComparer.Ordinal);
+ private Snapshot _snapshot;
+
+ /// Core 에 넣어 둔 표를 쓴다. 영어로 시작한다.
+ public Localizer() : this(StringTables.Get)
+ {
+ }
+
+ /// 표를 주는 함수를 바꿔 끼운다(테스트용).
+ internal Localizer(Func tables)
+ {
+ _tables = tables;
+ _snapshot = Build(UiLanguages.DefaultCode);
+ }
+
+ /// 지금 화면 언어의 코드. 지원하지 않는 코드를 받았으면 영어다.
+ public string Code => Volatile.Read(ref _snapshot).Code;
+
+ public bool IsRightToLeft => UiLanguages.IsRightToLeft(Code);
+
+ /// 화면 언어를 바꾼다. "system" 같은 설정값이 아니라 로 푼 코드를 받는다.
+ public void SetLanguage(string code) => Volatile.Write(ref _snapshot, Build(code));
+
+ public string Get(string key) => Lookup(Volatile.Read(ref _snapshot), key);
+
+ ///
+ /// 문자열의 {0}, {1} 자리에 값을 넣는다. 숫자는 InvariantCulture 로 적는다(FPS, % 같은 기술 표기가 Windows 지역 설정을 따라
+ /// 바뀌지 않게). 번역문의 자리표시자가 깨졌으면 영어 문장으로 채운다. 테스트가 막는 경우지만 화면이 멈추지는 않게 한다.
+ ///
+ public string Format(string key, params object?[] args)
+ {
+ var snapshot = Volatile.Read(ref _snapshot);
+ string template = Lookup(snapshot, key);
+ try
+ {
+ return string.Format(CultureInfo.InvariantCulture, template, args);
+ }
+ catch (FormatException ex)
+ {
+ AppLog.Warn("l10n", $"{snapshot.Code} 문자열 '{key}' 의 자리표시자가 맞지 않아 영어로 보여 줍니다.", ex);
+ if (snapshot.Fallback.TryGet(key, out var english))
+ {
+ try
+ {
+ return string.Format(CultureInfo.InvariantCulture, english, args);
+ }
+ catch (FormatException)
+ {
+ }
+ }
+ return template;
+ }
+ }
+
+ private string Lookup(Snapshot snapshot, string key)
+ {
+ if (snapshot.Current.TryGet(key, out var text)) return text;
+ if (snapshot.Fallback.TryGet(key, out text)) return text;
+
+ // 화면 갱신마다 부르는 곳이 있어, 같은 키는 한 번만 남긴다.
+ if (_reportedMissing.TryAdd(key, 0)) AppLog.Warn("l10n", $"문자열 키 '{key}' 가 영어 표에도 없습니다.");
+ return key;
+ }
+
+ private Snapshot Build(string code)
+ {
+ string resolved = UiLanguages.Find(code)?.Code ?? UiLanguages.DefaultCode;
+ var fallback = Load(UiLanguages.DefaultCode) ?? StringTable.Empty(UiLanguages.DefaultCode);
+ var current = resolved == UiLanguages.DefaultCode ? fallback : Load(resolved) ?? StringTable.Empty(resolved);
+ return new Snapshot(resolved, current, fallback);
+ }
+
+ private StringTable? Load(string code)
+ {
+ try
+ {
+ return _tables(code);
+ }
+ catch (FormatException ex)
+ {
+ // 깨진 번역 파일 하나 때문에 앱이 뜨지 않으면 안 된다. 그 언어는 영어로 보인다.
+ AppLog.Warn("l10n", $"{code} 문자열 표를 읽지 못해 영어로 보여 줍니다.", ex);
+ return null;
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Localization/StringTable.cs b/SoundVisualizer.Core/Localization/StringTable.cs
new file mode 100644
index 0000000..d3c8ae5
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/StringTable.cs
@@ -0,0 +1,67 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+
+namespace SoundVisualizer.Localization
+{
+ ///
+ /// 한 언어의 화면 문자열 표. 만든 뒤에는 바뀌지 않으므로 여러 스레드가 잠금 없이 읽는다.
+ ///
+ /// 파일은 평평한 JSON 객체 하나다({ "launcher.tab.home": "홈" }). 번역 도구가 그대로 열 수 있게 중첩과 주석은 쓰지 않고,
+ /// 키는 화면별로 점을 찍어 묶는다. 값이 문자열이 아니거나 같은 키가 두 번 나오면 읽지 않는다(뒤엣것이 조용히 이기면 번역이 사라진다).
+ ///
+ public sealed class StringTable
+ {
+ private readonly FrozenDictionary _entries;
+
+ private StringTable(string code, FrozenDictionary entries)
+ {
+ Code = code;
+ _entries = entries;
+ }
+
+ /// 표의 언어 코드().
+ public string Code { get; }
+
+ /// 키와 문자열. 읽기 전용이다.
+ public IReadOnlyDictionary Entries => _entries;
+
+ public bool TryGet(string key, [MaybeNullWhen(false)] out string value) => _entries.TryGetValue(key, out value);
+
+ /// 아직 번역 파일이 없는 언어의 표. 모든 키가 영어로 넘어간다.
+ public static StringTable Empty(string code) => new(code, FrozenDictionary.Empty);
+
+ /// JSON 을 읽는다. 형식이 맞지 않으면 을 던진다.
+ public static StringTable Parse(string code, string json)
+ {
+ JsonDocument document;
+ try
+ {
+ document = JsonDocument.Parse(json);
+ }
+ catch (JsonException ex)
+ {
+ throw new FormatException($"{code}: 문자열 표가 올바른 JSON 이 아닙니다({ex.Message}).", ex);
+ }
+
+ using (document)
+ {
+ if (document.RootElement.ValueKind != JsonValueKind.Object)
+ throw new FormatException($"{code}: 문자열 표는 JSON 객체 하나여야 합니다.");
+
+ var entries = new Dictionary(StringComparer.Ordinal);
+ foreach (var property in document.RootElement.EnumerateObject())
+ {
+ if (property.Value.ValueKind != JsonValueKind.String)
+ throw new FormatException($"{code}: '{property.Name}' 의 값이 문자열이 아닙니다.");
+ if (!entries.TryAdd(property.Name, property.Value.GetString()!))
+ throw new FormatException($"{code}: '{property.Name}' 키가 두 번 나옵니다.");
+ }
+ return new StringTable(code, entries.ToFrozenDictionary(StringComparer.Ordinal));
+ }
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Localization/StringTables.cs b/SoundVisualizer.Core/Localization/StringTables.cs
new file mode 100644
index 0000000..a677438
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/StringTables.cs
@@ -0,0 +1,42 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Concurrent;
+using System.Reflection;
+
+namespace SoundVisualizer.Localization
+{
+ ///
+ /// Core 에 넣어 둔 언어별 문자열 표(Localization/Strings/{코드}.json)를 읽는다. 언어마다 처음 쓸 때 한 번만 읽는다.
+ /// 번역 파일이 아직 없는 언어는 null 이고, 그 언어의 화면은 키마다 영어로 나온다.
+ ///
+ public static class StringTables
+ {
+ // csproj 의 EmbeddedResource LogicalName 과 맞춘다.
+ private const string ResourcePrefix = "SoundVisualizer.Localization.Strings.";
+ private const string ResourceSuffix = ".json";
+
+ private static readonly Assembly Owner = typeof(StringTables).Assembly;
+ private static readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal);
+
+ /// 표 파일이 들어 있는 언어 코드(파일 이름 그대로).
+ public static IReadOnlyList AvailableCodes { get; } = Owner.GetManifestResourceNames()
+ .Where(name => name.StartsWith(ResourcePrefix, StringComparison.Ordinal) && name.EndsWith(ResourceSuffix, StringComparison.Ordinal))
+ .Select(name => name[ResourcePrefix.Length..^ResourceSuffix.Length])
+ .OrderBy(code => code, StringComparer.Ordinal)
+ .ToArray();
+
+ /// 표 파일의 내용. 파일이 없으면 null.
+ public static string? ReadJson(string code)
+ {
+ using var stream = Owner.GetManifestResourceStream(ResourcePrefix + code + ResourceSuffix);
+ if (stream == null) return null;
+ using var reader = new StreamReader(stream); // BOM 이 있어도 떼고 읽는다
+ return reader.ReadToEnd();
+ }
+
+ /// 언어의 표. 파일이 없으면 null, 파일이 깨졌으면 .
+ public static StringTable? Get(string code) =>
+ Cache.GetOrAdd(code, c => ReadJson(c) is { } json ? StringTable.Parse(c, json) : null);
+ }
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/ar.json b/SoundVisualizer.Core/Localization/Strings/ar.json
new file mode 100644
index 0000000..7ed2d1e
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/ar.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "اللغة",
+ "common.reset_defaults": "استعادة الإعدادات التلقائية",
+ "common.minimize": "تصغير",
+ "common.close": "إغلاق",
+ "language.system": "الإعداد التلقائي للنظام",
+
+ "launcher.window_title": "نافذة تشغيل SoundVisualizer",
+ "launcher.tab.home": "الرئيسية",
+ "launcher.tab.settings": "الإعدادات",
+ "launcher.tab.sounds": "الأصوات",
+ "launcher.tab.help": "المساعدة",
+ "launcher.tab.more_info": "معرفة المزيد",
+ "launcher.home.desc": "يرسم على شاشتك الأصوات التي لا تراها.\nمن الألعاب إلى الأفلام، عِش تجربة جديدة كليًا.",
+ "launcher.home.start": "تشغيل",
+ "launcher.home.stop": "إيقاف",
+ "launcher.home.tutorial": "عرض البرنامج التعليمي",
+ "launcher.status.running": "الحالة: قيد التشغيل",
+ "launcher.status.idle": "الحالة: جاهز للتشغيل",
+
+ "settings.section.mode": "إعدادات الوضع",
+ "settings.visual_mode.title": "وضع العرض",
+ "settings.visual_mode.desc": "اختر الشكل الأساسي للرسومات التي تظهر على الشاشة.",
+ "settings.mode.wave": "الموجة",
+ "settings.mode.pad": "الأشرطة",
+ "settings.mode.circle": "الدائرة",
+ "settings.mode.outline": "الإطار",
+ "settings.sound_mode.title": "وضع الصوت",
+ "settings.sound_mode.desc": "اختر قنوات الصوت التي تناسب مكبرات الصوت لديك.\nلمكبرات الصوت ثنائية القناة اختر «قناتان»، ولأنظمة الصوت المحيطي اختر «5.1 قناة» أو «7.1 قناة».",
+ "settings.channels.2": "قناتان",
+ "settings.channels.5_1": "5.1 قناة",
+ "settings.channels.7_1": "7.1 قناة",
+ "settings.max_fps.title": "أقصى معدل للإطارات",
+ "settings.max_fps.desc": "يضبط أعلى معدل إطارات تُرسم به الرسومات.",
+ "settings.section.mode_detail": "إعدادات كل وضع",
+ "settings.mode_card.wave": "وضع الموجة",
+ "settings.mode_card.pad": "وضع الأشرطة",
+ "settings.mode_card.circle": "وضع الدائرة",
+ "settings.mode_card.outline": "وضع الإطار",
+ "settings.size_lock.title": "تثبيت الحجم",
+ "settings.size_lock.desc": "يُبقي الرسومات بحجم ثابت، ويُظهر شدة الصوت من خلال الشفافية بدلًا من الحجم.",
+ "settings.fixed_size.wave": "حجم الموجة",
+ "settings.fixed_size.pad": "حجم الأشرطة",
+ "settings.fixed_size.circle": "حجم الدائرة",
+ "settings.fixed_size.outline": "سُمك الإطار",
+ "settings.fixed_size.desc": "يضبط الحجم الثابت الذي تبقى عليه الرسومات بينما تتغير شفافيتها.",
+ "settings.max_opacity.title": "أقصى عتامة",
+ "settings.max_opacity.desc": "مدى عتامة الرسومات عند أعلى مستوى للصوت.",
+ "settings.size.title": "الحجم",
+ "settings.size.desc.wave": "يضبط ارتفاع الموجة ومدى امتلائها بشكل عام.",
+ "settings.size.desc.pad": "يضبط مدى سُمك الأشرطة وعرضها عند تفاعلها مع الصوت.",
+ "settings.size.desc.circle": "يضبط مدى بروز المعادل الدائري إلى الخارج عند تشغيل الصوت.",
+ "settings.size.desc.outline": "يضبط ارتفاع موجة الإطار ومدى امتلائها بشكل عام.",
+ "settings.opacity.title": "العتامة",
+ "settings.opacity.desc.wave": "كلما زادت القيمة بدت الرسومات أوضح، وكلما قلّت بدت أبهت وأكثر شفافية.",
+ "settings.opacity.desc.pad": "كلما زادت القيمة بدت الرسومات أوضح، وكلما قلّت بدت أبهت وأكثر شفافية.",
+ "settings.opacity.desc.circle": "كلما زادت القيمة بدت الرسومات أوضح، وكلما قلّت بدت أبهت وأكثر شفافية.",
+ "settings.opacity.desc.outline": "كلما زادت القيمة بدت الرسومات أوضح، وكلما قلّت بدت أبهت وأكثر شفافية.",
+ "settings.speed.title": "السرعة",
+ "settings.speed.desc.wave": "يضبط سرعة تدفق الموجة وتموّجها.",
+ "settings.speed.desc.pad": "يضبط سرعة توجّه الأشرطة نحو الجهة التي يأتي منها الصوت.",
+ "settings.speed.desc.circle": "يضبط سرعة تموّج المعادل الدائري ودورانه استجابةً للصوت.",
+ "settings.speed.desc.outline": "يضبط سرعة تدفق موجة الإطار وتموّجها.",
+ "settings.sensitivity.title": "الحساسية",
+ "settings.sensitivity.desc.wave": "يضبط مدى حساسية استجابة الموجة واندفاعها حتى مع الأصوات الهادئة.",
+ "settings.sensitivity.desc.pad": "يضبط الأشرطة لتلتقط الصوت بشكل أفضل وتبرز أكثر.",
+ "settings.sensitivity.desc.circle": "يضبط مدى دقة تجزّؤ سعة الدائرة عند استجابتها للصوت.",
+ "settings.sensitivity.desc.outline": "يضبط مدى حساسية استجابة موجة الإطار واندفاعها حتى مع الأصوات الهادئة.",
+ "settings.glow.title": "التوهج",
+ "settings.glow.desc.wave": "يضيف توهجًا ناعمًا يشبه ضوء النيون على طول حافة الموجة، ويضبط قوته.\nملاحظة: يستهلك المزيد من موارد GPU.",
+ "settings.glow.desc.pad": "يضيف حول الأشرطة توهجًا ناعمًا يشبه الهالة.\nملاحظة: يستهلك المزيد من موارد GPU.",
+ "settings.glow.desc.circle": "يضيف هالة متلألئة على طول حافة المعادل الدائري.\nملاحظة: يستهلك المزيد من موارد GPU.",
+ "settings.glow.desc.outline": "يضيف توهجًا ناعمًا يشبه ضوء النيون إلى الإطار، ويضبط قوته.\nملاحظة: يستهلك المزيد من موارد GPU.",
+ "settings.radius.title": "نصف القطر",
+ "settings.radius.desc": "يضبط نصف قطر الحلقة التي تتوسط الشاشة في وضع الدائرة.",
+ "settings.section.sound_types": "أنواع الأصوات (AI)",
+ "settings.show_ambient": "إظهار الأصوات المحيطة",
+ "settings.show_speech": "إظهار الكلام",
+ "settings.show_danger": "إظهار أصوات الخطر",
+ "settings.set_color": "تعيين اللون",
+ "settings.set_color_for": "تعيين اللون: {0}",
+ "settings.section.hotkeys": "مفاتيح الاختصار",
+ "settings.hotkey.visual.title": "تبديل وضع العرض",
+ "settings.hotkey.visual.desc": "مفتاح اختصار يغيّر الشكل فورًا أثناء التشغيل.",
+ "settings.hotkey.sound.title": "تبديل وضع الصوت",
+ "settings.hotkey.sound.desc": "مفتاح اختصار يغيّر وضع الصوت فورًا أثناء التشغيل.",
+ "settings.hotkey.edit.title": "إعدادات التراكب",
+ "settings.hotkey.edit.desc": "مفتاح اختصار يفتح محرر التراكب أو يغلقه.",
+ "settings.hotkey.set": "تعيين مفتاح",
+ "settings.section.advanced": "الإعدادات المتقدّمة",
+ "settings.developer_mode.title": "وضع المطوّر",
+ "settings.developer_mode.desc": "يعرض في الزاوية العلوية اليسرى من التراكب ما سمعه الذكاء الاصطناعي (اسم الصوت الذي أبلغ عنه ومدى ثقته والوقت المستغرق)، ومعدل الإطارات (FPS) في الزاوية العلوية اليمنى. تظهر هذه المعلومات بالإنجليزية وليست مخصّصة للاستخدام اليومي.",
+ "settings.developer_record.title": "تسجيل النتائج في ملف",
+ "settings.developer_record.desc": "يكتب كل نتيجة للذكاء الاصطناعي (أسماء الأصوات الأعلى تقييمًا ومدى ثقته والأوقات المستغرقة) في ملف CSV جديد داخل مجلد ai-log بجوار SoundVisualizer.exe، لتتمكن لاحقًا من مراجعة ما سمعه. وإذا تعذّرت الكتابة في ذلك المجلد، يُحفظ الملف في %LOCALAPPDATA%\\SoundVisualizer\\ai-log. قد تكبر الملفات كثيرًا، لذا أوقف التسجيل عند الانتهاء.",
+ "settings.auto_minimize.title": "تصغير نافذة التشغيل عند البدء",
+ "settings.auto_minimize.desc": "يصغّر نافذة التشغيل هذه تلقائيًا عند بدء التراكب.",
+
+ "sounds.title": "اختيار أنواع الأصوات",
+ "sounds.desc": "تُجمع الأصوات التي يتعرّف عليها الذكاء الاصطناعي مع الأصوات المشابهة لها في مجموعات، وتُعرض حسب النوع الذي تُصنَّف ضمنه تلقائيًا. إذا غيّرت نوع مجموعة، فستأخذ أصواتها فورًا لون النوع الجديد.",
+ "sounds.sibling_note": "كثيرًا ما يسمع الذكاء الاصطناعي الصوت الواحد بعدة أسماء معًا. على سبيل المثال، تُسمع «صفارة إنذار» أيضًا على أنها «إنذار»، وهو في مجموعة أخرى. إذا ظل صوت يظهر بنوعه القديم بعد التغيير، فغيّر أيضًا المجموعات أو الأصوات المرتبطة به.",
+ "sounds.advanced.title": "الوضع المتقدّم",
+ "sounds.advanced.desc": "يعرض كل صوت ضمن مجموعته، لتتمكن من تغيير الأصوات بشكل فردي.",
+ "sounds.search.hint": "البحث عن صوت",
+ "sounds.search.clear": "محو نص البحث",
+ "sounds.filter.title": "تصفية",
+ "sounds.filter.all": "الكل",
+ "sounds.filter.changed": "المعدّلة",
+ "sounds.type.ambient": "الأصوات المحيطة",
+ "sounds.type.speech": "الكلام",
+ "sounds.type.danger": "أصوات الخطر",
+ "sounds.type.default": "{0} (تلقائي)",
+ "sounds.type.mixed": "مختلط",
+ "sounds.changed_count": "الأصوات المعدّلة: {0}",
+ "sounds.changed_from": "النوع التلقائي: {0}",
+ "sounds.reset.button": "إعادة ضبط الكل",
+ "sounds.reset.title": "هل تريد إعادة ضبط كل الأصوات؟",
+ "sounds.reset.message": "ستعود جميع الأصوات التي غيّرتها إلى نوعها التلقائي.",
+ "sounds.reset.confirm": "إعادة ضبط",
+ "sounds.reset.cancel": "إلغاء",
+ "sounds.empty": "لا توجد أصوات مطابقة.",
+ "sounds.group.mixed_note": "بعض الأصوات غُيّرت بشكل فردي",
+ "sounds.group.changed_line": "الآن: {0} · النوع التلقائي: {1}",
+ "sounds.overwrite.title": "هل تريد تغيير المجموعة كلها؟",
+ "sounds.overwrite.message": "عدد أصوات مجموعة «{0}» التي ستتحوّل إلى «{2}»: {1}. وستُستبدل أيضًا الأصوات التي غيّرتها بشكل فردي.",
+ "sounds.overwrite.confirm": "تغيير",
+ "sounds.overwrite.cancel": "إلغاء",
+ "sounds.basic_warning.title": "هل تريد التبديل إلى الوضع الأساسي؟",
+ "sounds.basic_warning.message": "في بعض المجموعات أصوات غيّرتها بشكل فردي. في الوضع الأساسي، إذا اخترت نوعًا لإحدى هذه المجموعات، فستأخذ كل أصواتها هذا النوع، وتُستبدل التغييرات الفردية التي أجريتها في تلك المجموعة. لن يتغيّر شيء قبل أن تختار.",
+ "sounds.basic_warning.confirm": "التبديل إلى الأساسي",
+ "sounds.basic_warning.cancel": "البقاء في المتقدّم",
+ "sounds.cd.sound": "{0}، النوع: {1}",
+ "sounds.cd.sound_changed": "{0}، النوع: {1}، النوع التلقائي: {2}",
+ "sounds.cd.group": "مجموعة {0}، النوع: {1}، عدد الأصوات: {2} ({3})",
+ "sounds.cd.group_changed": "مجموعة {0}، النوع: {1}، النوع التلقائي: {2}، عدد الأصوات: {3} ({4})",
+ "sounds.cd.group_header": "مجموعة {0}، عدد الأصوات: {1}",
+
+ "hotkey.prompt": "اضغط على مفتاح (Esc للإلغاء)",
+ "hotkey.none": "لا يوجد",
+ "hotkey.button_name": "{0}، مفتاح الاختصار: {1}",
+
+ "help.start.title": "بدء الاستخدام",
+ "help.start.1": "اختر اللغة في أعلى هذه النافذة، وغيّر شكل الرسومات من علامة تبويب «الإعدادات» إن أردت.",
+ "help.start.2": "في «الرئيسية»، انقر على «{0}».",
+ "help.start.3": "تُرسم الرسومات على حواف الشاشة فوق النوافذ الأخرى مثل الألعاب ومقاطع الفيديو. وتمرّ نقرات الماوس عبرها، لذا يمكنك مواصلة استخدام النافذة التي تحتها.",
+ "help.start.4": "لإيقافه، انقر على «{0}» في «الرئيسية»، أو انقر بزر الماوس الأيمن على أيقونة SoundVisualizer في منطقة الإعلامات بشريط المهام واختر «{0}».",
+ "help.modes.title": "أوضاع العرض",
+ "help.mode.wave": "تندفع حافة الشاشة بالكامل نحو الداخل بحسب شدة الصوت، ويندفع الجانب الذي يأتي منه الصوت إلى عمق أكبر.",
+ "help.mode.pad": "تضيء أشرطة في 8 مواضع حول حافة الشاشة، وهو أوضح وضع لإظهار الاتجاه.",
+ "help.mode.circle": "تنتفخ حلقة في وسط الشاشة باتجاه مصدر الصوت.",
+ "help.mode.outline": "يرسم خطًا رفيعًا يمثّل حدود الموجة فقط، لذا يغطي مساحة أقل من شاشة اللعبة.",
+ "help.modes.note": "تُحفظ إعدادات مثل «الحجم» و«العتامة» بشكل منفصل لكل وضع، ويمكنك تغييرها من علامة تبويب «الإعدادات».",
+ "help.sound.title": "أنواع الأصوات",
+ "help.sound.kinds": "يصنّف الذكاء الاصطناعي ما تسمعه إلى أصوات محيطة وكلام وأصوات خطر. الألوان التلقائية هي: الأبيض للأصوات المحيطة، والأصفر للكلام، والأحمر لأصوات الخطر. يمكنك تغيير لون كل نوع واختيار إظهاره أو إخفائه من علامة تبويب «الإعدادات».",
+ "help.sound.groups": "في علامة تبويب «الأصوات» تُجمع الأصوات المتشابهة في مجموعات، وتختار أنت النوع الذي تُحسب عليه كل مجموعة. فإذا غيّرت مجموعة «جرس الباب» مثلًا إلى أصوات الخطر، فحين يتعرّف الذكاء الاصطناعي على جرس الباب أو طرق الباب، يأخذ الصوت لون أصوات الخطر. لكن الصوت الذي تضيفه بنفسك إلى أصوات الخطر يشارك فقط في تصويت الذكاء الاصطناعي على النوع، لذلك لا يعني هذا أن كل رنين لجرس الباب سيظهر على أنه من أصوات الخطر.",
+ "help.sound.advanced": "لتغيير الأصوات بشكل فردي، فعّل الوضع المتقدّم.",
+ "help.sound.danger": "عند رصد صوت خطر (مثل طلق ناري)، يتغير اللون فورًا وليس تدريجيًا.",
+ "help.pc.title": "إعدادات الكمبيوتر واختصارات لوحة المفاتيح",
+ "help.sound_mode.title": "وضع الصوت",
+ "help.sound_mode.body": "لتمييز الأصوات من الأمام والخلف أيضًا، إلى جانب اليسار واليمين، اضبط جهاز الإخراج في إعدادات الصوت في Windows على صوت محيطي 5.1 (6 قنوات) أو 7.1 (8 قنوات). أما مع الستيريو العادي (قناتان)، فيميّز التطبيق بين اليسار واليمين، لكنه لا يميّز بين الأمام والخلف. يتوافق «وضع الصوت» في علامة تبويب «الإعدادات» مع جهاز الإخراج عند بدء التشغيل وعند تغيّر الجهاز، ويمكنك أيضًا تغييره بنفسك.",
+ "help.sound_mode.tip": "💡 إذا لم يكن لديك جهاز إخراج 7.1، فيمكنك تثبيت {0} وضبط جهاز الإخراج على 7.1 قناة لعرض التراكب بصوت محيطي 7.1.",
+ "help.sound_mode.tip_link": "برنامج التشغيل الافتراضي VB-CABLE",
+ "help.hotkeys.title": "مفاتيح اختصار مخصّصة تعمل فورًا",
+ "help.hotkeys.body": "في علامة تبويب «الإعدادات»، انقر على زر مفتاح الاختصار، ثم اضغط على مفتاح واحد أو مجموعة مفاتيح (مثل Ctrl + Shift + A) لتغيير مفتاح الاختصار كما تريد. ويؤدي الضغط على مفتاح الاختصار إلى تبديل الوضع فورًا، حتى عندما يعمل التطبيق في الخلفية.",
+ "help.hotkeys.defaults": "افتراضيًا، يبدّل F2 وضع الصوت، ويبدّل F3 وضع العرض، ويفتح F4 إعدادات التراكب.",
+ "help.size_lock.title": "وضع تثبيت الحجم",
+ "help.size_lock.body": "يُبقي وضع تثبيت الحجم الرسومات بحجم ثابت، ويستخدم شفافية تتناسب مع شدة الصوت. وهو مفيد عندما تريد إبقاء الرسومات بالحجم نفسه مع رؤية تفاعلها مع الصوت.",
+ "help.editor.title": "وضع تحرير التراكب المباشر",
+ "help.editor.body": "اضغط على مفتاح الاختصار المعيَّن (المفتاح التلقائي F4) لتشغيل وضع تحرير التراكب. يمكنك السحب على الشاشة بالماوس لضبط الحد الأقصى لحجم الرسومات، وضبط التفاصيل بدقة في لوحة الإعدادات التي تظهر.",
+ "help.developer.title": "وضع المطوّر",
+ "help.developer.body": "عند تشغيل وضع المطوّر، يظهر في الزاوية العلوية اليسرى من التراكب ما سمعه الذكاء الاصطناعي (اسم الصوت ومدى ثقته والوقت المستغرق)، ويظهر معدل الإطارات (FPS) في الزاوية العلوية اليمنى. وإذا شغّلت أيضًا «{0}»، فستُحفظ كل نتيجة في ملف CSV. وهذا مفيد عندما تريد التحقق من أن النظام يعمل بشكل صحيح.",
+ "help.notes.title": "معلومات مفيدة",
+ "help.note.direction_surround": "عند الإخراج بقنوات 5.1 أو 7.1، يعرض أعلى الشاشة الصوت القادم من الأمام، ويعرض أسفلها الصوت القادم من الخلف. ويعرض الجانبان الأيسر والأيمن الصوت القادم من اليسار واليمين، أما الرسومات التي تظهر من كل جوانب الشاشة في الوقت نفسه فهي أصوات محيطة أو أصوات بلا اتجاه.",
+ "help.note.direction_stereo": "عند الإخراج بقناتين (ستيريو)، يميّز التطبيق بين اليسار واليمين، لكنه لا يميّز بين الأمام والخلف. وعندها تُمثّل الرسومات في أعلى الشاشة وأسفلها الأصوات المسموعة بالتساوي من الجانبين، كما تعبّر عن الإحساس المكاني.",
+ "help.note.sources": "يعرض التطبيق صوت كل التطبيقات التي تعمل عبر جهاز التشغيل الافتراضي في Windows. ولا يُعرض الصوت الذي يرسله تطبيق إلى جهاز آخر، مثل سماعة رأس تستخدمها للدردشة الصوتية فقط.",
+ "help.note.volume": "إذا كان مستوى صوت تطبيق ما منخفضًا جدًا أو مكتومًا، داخل التطبيق نفسه أو في خالط مستوى الصوت في Windows، فقد لا يكاد صوته يظهر أو لا يظهر إطلاقًا. إذا كانت استجابة الرسومات ضعيفة، فارفع هذا الصوت قليلًا أو زِد «الحساسية» من علامة تبويب «الإعدادات».",
+ "help.note.ai": "قد يخطئ الذكاء الاصطناعي في التصنيف. لا تعتمد على هذا التطبيق وحده في المواقف التي تتعلق بسلامتك.",
+ "help.faq.title": "الأسئلة الشائعة",
+ "help.faq.stopped_q": "توقف الصوت المرئي دون أن أوقفه بنفسي.",
+ "help.faq.stopped_a": "يتوقف عندما يتوقف التقاط الصوت ولا يعود خلال بضع ثوانٍ، مثلًا عند فصل جهاز التشغيل وعدم توفر جهاز آخر. وعندها تعرض «الرئيسية» السبب. وصّل مكبرات صوت أو سماعات، أو عيّن جهاز تشغيل افتراضيًا في إعدادات الصوت في Windows، ثم شغّله من جديد.",
+ "help.faq.no_graphic_q": "لا تظهر أي رسومات.",
+ "help.faq.no_graphic_a": "تحقّق من أن الصوت يعمل عبر جهاز التشغيل الافتراضي في Windows وأنه غير مكتوم، ومن عدم وجود تنبيه في «الرئيسية». وإذا كانت لعبة تعمل في وضع ملء الشاشة الحصري، فجرّب وضع النافذة بلا حدود أو وضع النافذة. تأكّد أيضًا من أن «العتامة» ليست منخفضة جدًا في علامة تبويب «الإعدادات»، وأن إظهار نوع الصوت الحالي مفعّل.",
+ "help.faq.performance_q": "هل يستهلك الكثير من المعالج أو بطاقة الرسومات؟",
+ "help.faq.performance_a": "لتخفيف الحمل، خفّض «{0}» في علامة تبويب «الإعدادات» أو أوقف «{1}».",
+ "help.faq.language_q": "كيف أغيّر لغة التطبيق؟",
+ "help.faq.language_a": "اختر لغة من قائمة «{0}» في أعلى هذه النافذة. ويمكنك أثناء التشغيل تغييرها أيضًا من إعدادات التراكب. عند اختيار «{1}» يستخدم التطبيق لغة العرض في Windows.",
+ "help.report.title": "الإبلاغ عن مشكلة",
+ "help.report.desc": "أخبرنا عن خطأ برمجي، أو عن صوت عُرض على أنه من نوع غير صحيح. يُفتح GitHub في متصفحك مع ملء معلومات النظام الظاهرة أدناه مسبقًا، لذا يلزمك حساب على GitHub. إذا لم يكن لديك حساب، فأرسل إلينا رسالة بالبريد الإلكتروني بدلًا من ذلك. لا يتم إرسال أي صوت على الإطلاق.",
+ "help.report.github": "الإبلاغ عبر GitHub",
+ "help.report.mail": "الإبلاغ عبر البريد الإلكتروني",
+ "help.report.copy": "نسخ معلومات النظام",
+ "help.report.copied": "تم نسخ معلومات النظام.",
+ "help.report.copy_failed": "تعذّر النسخ إلى الحافظة. حاول مرة أخرى.",
+ "help.report.no_browser": "تعذّر فتح متصفح. انسخ معلومات النظام وأرسلها إلينا بدلًا من ذلك.",
+ "help.report.no_mail": "لا يوجد تطبيق بريد إلكتروني لإرسال الرسالة. انسخ معلومات النظام وأرسلها بطريقة أخرى.",
+ "help.report.issue_title": "بلاغ من التطبيق",
+ "help.report.issue_body": "ماذا حدث؟\n\nما الذي كنت تستمع إليه (أي تطبيق، وما نوع الصوت)؟\n\nما الذي كنت تتوقع حدوثه بدلًا من ذلك؟",
+ "help.about.title": "حول التطبيق",
+ "help.about.version": "الإصدار {0}",
+ "help.about.license": "يتم توزيع هذا التطبيق بموجب GNU Affero General Public License v3.0 (AGPL-3.0). أسماء الأصوات ومجموعاتها في علامة تبويب «الأصوات» مبنية على أنطولوجيا AudioSet ومرخّصة بموجب CC BY-SA 4.0 (راجع «تراخيص البرامج المفتوحة المصدر» أدناه).",
+ "help.licenses.button": "تراخيص البرامج المفتوحة المصدر",
+ "help.licenses.title": "تراخيص البرامج المفتوحة المصدر",
+ "help.licenses.close": "إغلاق",
+
+ "notice.ai_unavailable": "لا يمكن تحديد نوع الصوت حاليًا، لذا تظهر جميع الأصوات بلون الأصوات المحيطة. جرّب إيقاف الصوت المرئي ثم تشغيله مرة أخرى.",
+ "notice.ai_unavailable_hidden": "لا يمكن تحديد نوع الصوت حاليًا، لذا تُعدّ جميع الأصوات أصواتًا محيطة، لكن إظهار الأصوات المحيطة معطّل، فلا يظهر أي شيء. فعّل «{0}» من علامة تبويب «الإعدادات»، أو جرّب إيقاف الصوت المرئي ثم تشغيله مرة أخرى.",
+ "notice.stopped.title": "تم إيقاف الصوت المرئي",
+ "notice.stopped.no_device": "تعذّر فتح أي جهاز تشغيل، لذا لا يمكن التقاط الصوت. وصّل مكبرات صوت أو سماعات، أو عيّن جهاز تشغيل افتراضيًا في إعدادات الصوت في Windows، ثم شغّله من جديد.",
+ "notice.stopped.capture_error": "توقف التقاط الصوت بسبب خطأ، لذا توقفت الرسومات.",
+ "notice.dismiss": "إخفاء التنبيه",
+
+ "tray.open": "فتح نافذة التشغيل",
+ "tray.exit": "خروج",
+
+ "more_info.title": "مستودع GitHub والمساهمة",
+ "more_info.body": "SoundVisualizer مشروع مفتوح المصدر، وشيفرته المصدرية متاحة للجميع على GitHub. يمكنك المشاركة فيه بالإبلاغ عن الأخطاء، أو اقتراح ميزات جديدة، أو الإسهام في التطوير. انقر على الزر أدناه لزيارة مستودع GitHub الرسمي! ويمكنك أيضًا التحقق من أحدث إصدار وتنزيله من علامة تبويب Releases على GitHub.",
+ "more_info.github_button": "زيارة مستودع GitHub",
+
+ "dialog.notice_title": "تنبيه",
+ "dialog.already_running": "التراكب قيد التشغيل بالفعل.",
+ "dialog.reset_title": "إعادة ضبط الإعدادات",
+ "dialog.reset_wave": "هل تريد إعادة إعدادات وضع الموجة إلى القيم التلقائية؟",
+ "dialog.reset_pad": "هل تريد إعادة إعدادات وضع الأشرطة إلى القيم التلقائية؟",
+ "dialog.reset_circle": "هل تريد إعادة إعدادات وضع الدائرة إلى القيم التلقائية؟",
+ "dialog.reset_outline": "هل تريد إعادة إعدادات وضع الإطار إلى القيم التلقائية؟",
+ "dialog.reset_sound_types": "هل تريد إعادة إعدادات أنواع الأصوات إلى القيم التلقائية؟",
+ "dialog.reset_all_title": "استعادة الإعدادات التلقائية",
+ "dialog.reset_all": "هل تريد بالتأكيد إعادة جميع الإعدادات إلى القيم التلقائية؟",
+ "dialog.link_error": "تعذّر فتح الرابط: {0}",
+
+ "editor.title": "إعدادات التراكب",
+ "editor.drag_tip": "اسحب الخطوط الإرشادية على الشاشة بالماوس لضبط حد ارتفاع موجة التراكب وحجم مركزها في الوقت الفعلي!",
+ "editor.glow": "تأثير التوهج",
+ "editor.glow_strength": "قوة التوهج",
+ "editor.color": "اللون",
+ "editor.hotkeys": "إعدادات مفاتيح الاختصار",
+ "editor.developer_mode_desc": "يعرض ما سمعه الذكاء الاصطناعي (أعلى اليسار) ومعدل الإطارات (أعلى اليمين)",
+ "editor.save_close_hint": "للحفظ والإغلاق",
+ "editor.close_overlay": "إغلاق التراكب",
+ "editor.guide.limit": "حد {0}",
+ "editor.guide.size": "حجم {0}: {1}٪",
+ "editor.guide.base_radius": "نصف القطر الأساسي: {0}",
+ "editor.guide.circle_size": "حجم الدائرة: {0}٪",
+
+ "hud.visual_mode": "وضع العرض: [{0}] {1}",
+ "hud.sound_mode": "وضع الصوت: [{0}] {1}",
+ "hud.overlay_settings": "إعدادات التراكب: [{0}]",
+ "hud.status.ready": "النظام جاهز",
+ "hud.status.stereo": "صوت الكمبيوتر: {0} قناة (ستيريو)\nالمصدر: في الانتظار...",
+ "hud.status.surround": "صوت الكمبيوتر: 8 قنوات (7.1)\nالمصدر: في الانتظار...",
+
+ "color_picker.title": "اختيار لون",
+ "color_picker.hex": "رمز HEX",
+ "color_picker.cancel": "إلغاء",
+ "color_picker.apply": "تطبيق",
+ "color_picker.area": "التشبع والسطوع",
+ "color_picker.area_value": "التشبع {0}٪، السطوع {1}٪",
+ "color_picker.hue": "تدرّج اللون",
+ "color_picker.keys_help": "استخدم مفاتيح الأسهم لتغييره. اضغط مع الاستمرار على Shift للتغيير بخطوات أكبر.",
+ "color_picker.presets": "ألوان شائعة",
+ "color_picker.preset.white": "أبيض",
+ "color_picker.preset.red": "أحمر",
+ "color_picker.preset.orange": "برتقالي",
+ "color_picker.preset.yellow": "أصفر",
+ "color_picker.preset.green": "أخضر",
+ "color_picker.preset.cyan": "سماوي",
+ "color_picker.preset.blue": "أزرق",
+ "color_picker.preset.magenta": "ماجنتا",
+
+ "audio.error.no_device": "لم يتم العثور على جهاز إخراج.",
+ "audio.error.capture_stopped": "توقّف التقاط الصوت بشكل غير متوقع: {0}",
+ "audio.error.capture_start_failed": "تعذّر بدء محرك التقاط الصوت: {0}",
+
+ "tutorial.window_title": "البرنامج التعليمي",
+ "tutorial.skip": "تخطي",
+ "tutorial.previous": "الصفحة السابقة",
+ "tutorial.next": "التالي",
+ "tutorial.done": "حسنًا",
+ "tutorial.page_status": "الصفحة {0} من {1}",
+ "tutorial.pause": "إيقاف الرسوم المتحركة مؤقتًا",
+ "tutorial.play": "تشغيل الرسوم المتحركة",
+ "tutorial.sound_on": "تشغيل الصوت",
+ "tutorial.sound_off": "إيقاف الصوت",
+ "tutorial.sound.title": "شاهِد الصوت على شاشتك",
+ "tutorial.sound.body": "يُرسم صوت اللعبة أو الفيديو على حواف الشاشة. كلما ارتفع الصوت، رُسم بحجم أكبر. لا يُعرض إلا الصوت الذي يتم تشغيله على هذا الكمبيوتر. لا يلتقط التطبيق الأصوات من حولك.",
+ "tutorial.direction.title": "يُظهر من أين يأتي الصوت",
+ "tutorial.direction.body": "عند الإخراج بقناتين (ستيريو)، يُرسم الصوت القادم من اليسار أكبر على يسار الشاشة، والصوت القادم من اليمين أكبر على يمينها. وعند الإخراج بقنوات 5.1 أو 7.1، يميّز التطبيق أيضًا بين الأمام والخلف: يُرسم الصوت القادم من الأمام في أعلى الشاشة، والصوت القادم من الخلف في أسفلها.",
+ "tutorial.types.title": "لكل نوع من الأصوات لونه الخاص",
+ "tutorial.types.body": "يصنّف الذكاء الاصطناعي الأصوات إلى ثلاثة أنواع، ويرسم كل نوع بلونه الخاص. يمكنك تغيير الألوان من علامة تبويب «الإعدادات».",
+ "tutorial.type.ambient_examples": "الموسيقى والمطر وكل الأصوات الأخرى",
+ "tutorial.type.speech_examples": "أصوات الناس",
+ "tutorial.type.danger_examples": "طلقات نارية، انفجارات، صفارات إنذار",
+ "tutorial.start.title": "طريقة التشغيل والإيقاف",
+ "tutorial.start.hotkeys": "أثناء التشغيل، اضغط على {0} لتبديل وضع الصوت، وعلى {1} لتبديل وضع العرض، وعلى {2} لفتح إعدادات التراكب."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/de.json b/SoundVisualizer.Core/Localization/Strings/de.json
new file mode 100644
index 0000000..5a0ed4a
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/de.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Sprache",
+ "common.reset_defaults": "Auf Standardwerte zurücksetzen",
+ "common.minimize": "Minimieren",
+ "common.close": "Schließen",
+ "language.system": "Systemstandard",
+
+ "launcher.window_title": "SoundVisualizer Launcher",
+ "launcher.tab.home": "Startseite",
+ "launcher.tab.settings": "Einstellungen",
+ "launcher.tab.sounds": "Geräusche",
+ "launcher.tab.help": "Hilfe",
+ "launcher.tab.more_info": "Mehr erfahren",
+ "launcher.home.desc": "Macht Geräusche direkt auf Ihrem Bildschirm sichtbar.\nOb Gaming oder Film – erleben Sie etwas ganz Neues.",
+ "launcher.home.start": "Starten",
+ "launcher.home.stop": "Beenden",
+ "launcher.home.tutorial": "Tutorial ansehen",
+ "launcher.status.running": "Status: Aktiv",
+ "launcher.status.idle": "Status: Bereit",
+
+ "settings.section.mode": "Moduseinstellungen",
+ "settings.visual_mode.title": "Darstellungsmodus",
+ "settings.visual_mode.desc": "Wählen Sie die Grundform der Grafiken, die auf dem Bildschirm angezeigt werden.",
+ "settings.mode.wave": "Welle",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Kreis",
+ "settings.mode.outline": "Kontur",
+ "settings.sound_mode.title": "Audiomodus",
+ "settings.sound_mode.desc": "Wählen Sie die Audiokanäle, die zu Ihren Lautsprechern passen.\nBei 2-Kanal-Lautsprechern wählen Sie „2 Kanäle“, bei Surround-Lautsprechern „5.1 Kanäle“ oder „7.1 Kanäle“.",
+ "settings.channels.2": "2 Kanäle",
+ "settings.channels.5_1": "5.1 Kanäle",
+ "settings.channels.7_1": "7.1 Kanäle",
+ "settings.max_fps.title": "Max. Bildrate",
+ "settings.max_fps.desc": "Legt die höchste Bildrate fest, mit der die Grafiken gezeichnet werden.",
+ "settings.section.mode_detail": "Einstellungen je Modus",
+ "settings.mode_card.wave": "Modus „Welle“",
+ "settings.mode_card.pad": "Modus „Pad“",
+ "settings.mode_card.circle": "Modus „Kreis“",
+ "settings.mode_card.outline": "Modus „Kontur“",
+ "settings.size_lock.title": "Größe fixieren",
+ "settings.size_lock.desc": "Hält die Grafiken auf einer festen Größe und zeigt die Lautstärke stattdessen über die Transparenz an.",
+ "settings.fixed_size.wave": "Wellengröße",
+ "settings.fixed_size.pad": "Pad-Größe",
+ "settings.fixed_size.circle": "Kreisgröße",
+ "settings.fixed_size.outline": "Konturstärke",
+ "settings.fixed_size.desc": "Legt fest, welche feste Größe die Grafiken behalten, während sich ihre Transparenz ändert.",
+ "settings.max_opacity.title": "Max. Deckkraft",
+ "settings.max_opacity.desc": "Wie deckend die Grafiken beim lautesten Geräusch werden.",
+ "settings.size.title": "Größe",
+ "settings.size.desc.wave": "Legt fest, wie hoch die Welle ist und wie voll sie insgesamt wirkt.",
+ "settings.size.desc.pad": "Legt fest, wie dick und breit die Pads werden, wenn sie auf Geräusche reagieren.",
+ "settings.size.desc.circle": "Legt fest, wie weit sich der Kreis-Equalizer nach außen wölbt, wenn Ton wiedergegeben wird.",
+ "settings.size.desc.outline": "Legt fest, wie hoch die Konturwelle ist und wie voll sie insgesamt wirkt.",
+ "settings.opacity.title": "Deckkraft",
+ "settings.opacity.desc.wave": "Höhere Werte wirken kräftiger, niedrigere blasser und durchsichtiger.",
+ "settings.opacity.desc.pad": "Höhere Werte wirken kräftiger, niedrigere blasser und durchsichtiger.",
+ "settings.opacity.desc.circle": "Höhere Werte wirken kräftiger, niedrigere blasser und durchsichtiger.",
+ "settings.opacity.desc.outline": "Höhere Werte wirken kräftiger, niedrigere blasser und durchsichtiger.",
+ "settings.speed.title": "Tempo",
+ "settings.speed.desc.wave": "Legt fest, wie schnell die Welle fließt und wogt.",
+ "settings.speed.desc.pad": "Legt fest, wie schnell sich die Pads in die Richtung drehen, aus der ein Geräusch kommt.",
+ "settings.speed.desc.circle": "Legt fest, wie schnell der Kreis-Equalizer wogt und sich dreht, wenn er auf Geräusche reagiert.",
+ "settings.speed.desc.outline": "Legt fest, wie schnell die Konturwelle fließt und wogt.",
+ "settings.sensitivity.title": "Empfindlichkeit",
+ "settings.sensitivity.desc.wave": "Legt fest, wie empfindlich die Welle reagiert und ausschlägt – auch bei leisen Geräuschen.",
+ "settings.sensitivity.desc.pad": "Stellt die Pads so ein, dass sie Geräusche besser erfassen und deutlicher hervortreten.",
+ "settings.sensitivity.desc.circle": "Legt fest, wie fein sich die Amplitude des Kreises aufteilt, wenn er auf Geräusche reagiert.",
+ "settings.sensitivity.desc.outline": "Legt fest, wie empfindlich die Konturwelle reagiert und ausschlägt – auch bei leisen Geräuschen.",
+ "settings.glow.title": "Leuchten",
+ "settings.glow.desc.wave": "Fügt am Rand der Welle ein weiches Neonleuchten hinzu und regelt dessen Stärke.\nHinweis: Dies beansprucht zusätzliche GPU-Ressourcen.",
+ "settings.glow.desc.pad": "Fügt um die Pads ein weiches, auraartiges Leuchten hinzu.\nHinweis: Dies beansprucht zusätzliche GPU-Ressourcen.",
+ "settings.glow.desc.circle": "Fügt am Rand des Kreis-Equalizers einen schimmernden Lichtkranz hinzu.\nHinweis: Dies beansprucht zusätzliche GPU-Ressourcen.",
+ "settings.glow.desc.outline": "Fügt der Kontur ein weiches Neonleuchten hinzu und regelt dessen Stärke.\nHinweis: Dies beansprucht zusätzliche GPU-Ressourcen.",
+ "settings.radius.title": "Radius",
+ "settings.radius.desc": "Legt den Radius des Rings in der Bildschirmmitte im Modus „Kreis“ fest.",
+ "settings.section.sound_types": "Geräuscharten (KI)",
+ "settings.show_ambient": "Umgebungsgeräusche",
+ "settings.show_speech": "Stimmen",
+ "settings.show_danger": "Gefahrengeräusche",
+ "settings.set_color": "Farbe wählen",
+ "settings.set_color_for": "Farbe wählen: {0}",
+ "settings.section.hotkeys": "Tastenkürzel",
+ "settings.hotkey.visual.title": "Darstellungsmodus wechseln",
+ "settings.hotkey.visual.desc": "Tastenkürzel, das die Form während der Ausführung in Echtzeit ändert.",
+ "settings.hotkey.sound.title": "Audiomodus wechseln",
+ "settings.hotkey.sound.desc": "Tastenkürzel, das den Audiomodus während der Ausführung in Echtzeit ändert.",
+ "settings.hotkey.edit.title": "Overlay-Einstellungen",
+ "settings.hotkey.edit.desc": "Tastenkürzel, das den Overlay-Editor öffnet oder schließt.",
+ "settings.hotkey.set": "Tastenkürzel festlegen",
+ "settings.section.advanced": "Erweiterte Einstellungen",
+ "settings.developer_mode.title": "Entwicklermodus",
+ "settings.developer_mode.desc": "Zeigt oben links im Overlay, was die KI gehört hat (den gemeldeten Geräuschnamen, wie sicher sie war und wie lange es gedauert hat), und oben rechts die FPS. Die Anzeige ist auf Englisch und nicht für den Alltag gedacht.",
+ "settings.developer_record.title": "Ergebnisse in eine Datei schreiben",
+ "settings.developer_record.desc": "Schreibt jedes KI-Ergebnis (die am höchsten bewerteten Geräuschnamen, wie sicher sich die KI war, und die Zeiten) in eine neue CSV-Datei im Ordner ai-log neben SoundVisualizer.exe, damit Sie später nachsehen können, was sie gehört hat. Ist dieser Ordner nicht beschreibbar, landet die Datei in %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Die Dateien können groß werden. Schalten Sie die Option aus, wenn Sie fertig sind.",
+ "settings.auto_minimize.title": "Launcher beim Start minimieren",
+ "settings.auto_minimize.desc": "Minimiert dieses Launcher-Fenster automatisch, wenn das Overlay startet.",
+
+ "sounds.title": "Geräuscharten wählen",
+ "sounds.desc": "Die Geräusche, welche die KI erkennt, sind in Gruppen ähnlicher Geräusche zusammengefasst und nach der Art geordnet, zu der sie standardmäßig zählen. Ändern Sie die Art einer Gruppe, richtet sich die Farbe ihrer Geräusche sofort danach.",
+ "sounds.sibling_note": "Die KI erkennt ein Geräusch oft unter mehreren Namen zugleich. Eine Sirene wird zum Beispiel auch als „Alarm“ erkannt, und „Alarm“ gehört zu einer anderen Gruppe. Wird ein Geräusch nach einer Änderung noch in seiner alten Art angezeigt, ändern Sie auch die zugehörigen Gruppen oder Geräusche.",
+ "sounds.advanced.title": "Erweiterter Modus",
+ "sounds.advanced.desc": "Zeigt jedes Geräusch in seiner Gruppe, damit Sie Geräusche einzeln ändern können.",
+ "sounds.search.hint": "Geräusche suchen",
+ "sounds.search.clear": "Suchbegriff löschen",
+ "sounds.filter.title": "Filter",
+ "sounds.filter.all": "Alle",
+ "sounds.filter.changed": "Geändert",
+ "sounds.type.ambient": "Umgebungsgeräusche",
+ "sounds.type.speech": "Stimmen",
+ "sounds.type.danger": "Gefahrengeräusche",
+ "sounds.type.default": "{0} (Standard)",
+ "sounds.type.mixed": "Gemischt",
+ "sounds.changed_count": "Geänderte Geräusche: {0}",
+ "sounds.changed_from": "Standard: {0}",
+ "sounds.reset.button": "Alle zurücksetzen",
+ "sounds.reset.title": "Alle Geräusche zurücksetzen?",
+ "sounds.reset.message": "Alle Geräusche, die Sie geändert haben, erhalten wieder ihre standardmäßige Art.",
+ "sounds.reset.confirm": "Zurücksetzen",
+ "sounds.reset.cancel": "Abbrechen",
+ "sounds.empty": "Keine passenden Geräusche.",
+ "sounds.group.mixed_note": "Einige Geräusche einzeln geändert",
+ "sounds.group.changed_line": "Jetzt: {0} · Standard: {1}",
+ "sounds.overwrite.title": "Ganze Gruppe ändern?",
+ "sounds.overwrite.message": "Geräusche in der Gruppe „{0}“, die zur Art „{2}“ wechseln: {1}. Auch einzeln geänderte Geräusche werden überschrieben.",
+ "sounds.overwrite.confirm": "Ändern",
+ "sounds.overwrite.cancel": "Abbrechen",
+ "sounds.basic_warning.title": "Zum einfachen Modus wechseln?",
+ "sounds.basic_warning.message": "Einige Gruppen enthalten Geräusche, die Sie einzeln geändert haben. Wenn Sie im einfachen Modus die Art einer solchen Gruppe wählen, erhalten alle ihre Geräusche diese Art, und Ihre einzelnen Änderungen in dieser Gruppe werden überschrieben. Bis Sie eine Art wählen, ändert sich nichts.",
+ "sounds.basic_warning.confirm": "Zum einfachen Modus",
+ "sounds.basic_warning.cancel": "Erweitert bleiben",
+ "sounds.cd.sound": "{0}, Art: {1}",
+ "sounds.cd.sound_changed": "{0}, Art: {1}, Standard: {2}",
+ "sounds.cd.group": "Gruppe {0}, Art: {1}, Geräusche: {2} ({3})",
+ "sounds.cd.group_changed": "Gruppe {0}, Art: {1}, Standard: {2}, Geräusche: {3} ({4})",
+ "sounds.cd.group_header": "Gruppe {0}, Geräusche: {1}",
+
+ "hotkey.prompt": "Drücken Sie eine Taste (Esc zum Abbrechen)",
+ "hotkey.none": "Keine",
+ "hotkey.button_name": "{0}, Tastenkürzel: {1}",
+
+ "help.start.title": "Erste Schritte",
+ "help.start.1": "Wählen Sie oben in diesem Fenster die Sprache aus, und ändern Sie bei Bedarf im Tab „Einstellungen“ das Aussehen der Grafik.",
+ "help.start.2": "Klicken Sie im Tab „Startseite“ auf „{0}“.",
+ "help.start.3": "Die Grafik wird an den Bildschirmrändern über anderen Fenstern wie Spielen und Videos dargestellt. Mausklicks gehen durch sie hindurch, sodass Sie das Fenster darunter weiter bedienen können.",
+ "help.start.4": "Zum Ausschalten klicken Sie im Tab „Startseite“ auf „{0}“, oder Sie klicken mit der rechten Maustaste auf das Sound-Visualizer-Symbol im Infobereich der Taskleiste und wählen „{0}“.",
+ "help.modes.title": "Darstellungsmodi",
+ "help.mode.wave": "Der gesamte Bildschirmrand schiebt sich je nach Lautstärke nach innen. Auf der Seite, von der das Geräusch kommt, schiebt er sich weiter hinein.",
+ "help.mode.pad": "An 8 Stellen am Bildschirmrand leuchten Balken auf. So ist die Richtung am deutlichsten zu erkennen.",
+ "help.mode.circle": "Ein Ring in der Bildschirmmitte dehnt sich in Richtung des Geräuschs aus.",
+ "help.mode.outline": "Zeichnet nur eine dünne Kontur der Welle und verdeckt so weniger von Ihrem Spiel.",
+ "help.modes.note": "Einstellungen wie „Größe“ und „Deckkraft“ werden für jeden Modus separat gespeichert und lassen sich im Tab „Einstellungen“ ändern.",
+ "help.sound.title": "Geräuscharten",
+ "help.sound.kinds": "Die KI teilt Geräusche in drei Arten ein: Umgebungsgeräusche, Stimmen und Gefahrengeräusche. Standardmäßig sind Umgebungsgeräusche weiß, Stimmen gelb und Gefahrengeräusche rot. Im Tab „Einstellungen“ können Sie für jede Art die Farbe ändern und festlegen, ob sie angezeigt wird.",
+ "help.sound.groups": "Im Tab „Geräusche“ sind ähnliche Geräusche in Gruppen zusammengefasst, und Sie legen für jede Gruppe fest, zu welcher Art sie zählt. Ändern Sie zum Beispiel die Gruppe „Türklingel“ in Gefahrengeräusche, dann erhält ein Geräusch, das die KI als Türklingel oder Klopfen erkennt, die Farbe für Gefahrengeräusche. Ein Geräusch, das Sie neu zu den Gefahrengeräuschen zählen, fließt allerdings nur in die Abstimmung der KI über die Art ein. Das heißt also nicht, dass jede Türklingel als Gefahrengeräusch angezeigt wird.",
+ "help.sound.advanced": "Um Geräusche einzeln zu ändern, schalten Sie „Erweiterter Modus“ ein.",
+ "help.sound.danger": "Wird ein Gefahrengeräusch (z. B. ein Schuss) erkannt, wechselt die Farbe sofort statt allmählich.",
+ "help.pc.title": "PC-Einstellungen und Tastenkürzel",
+ "help.sound_mode.title": "Audiomodus",
+ "help.sound_mode.body": "Damit neben links und rechts auch vorne und hinten unterschieden werden, stellen Sie Ihr Ausgabegerät in den Windows-Soundeinstellungen auf „5.1-Surround“ (6 Kanäle) oder „7.1-Surround“ (8 Kanäle) ein. Bei normalem Stereo (2 Kanäle) werden links und rechts unterschieden, vorne und hinten jedoch nicht. Der „Audiomodus“ im Tab „Einstellungen“ passt sich beim Start und bei einem Gerätewechsel an das Ausgabegerät an; Sie können ihn auch selbst ändern.",
+ "help.sound_mode.tip": "💡 Wenn Sie kein 7.1-Ausgabegerät haben, können Sie den {0} installieren und das Ausgabegerät auf 7.1 Kanäle einstellen, um das Overlay mit 7.1-Surround-Sound anzuzeigen.",
+ "help.sound_mode.tip_link": "virtuellen Treiber VB-CABLE",
+ "help.hotkeys.title": "Eigene Tastenkürzel in Echtzeit",
+ "help.hotkeys.body": "Klicken Sie im Tab „Einstellungen“ auf eine Tastenkürzel-Schaltfläche und drücken Sie eine einzelne Taste oder eine Tastenkombination (z. B. Ctrl + Shift + A), um das Tastenkürzel nach Wunsch zu ändern. Wenn Sie das Tastenkürzel drücken, wechselt der Modus sofort – auch wenn die App im Hintergrund läuft.",
+ "help.hotkeys.defaults": "Standardmäßig wechselt F2 den Audiomodus und F3 den Darstellungsmodus, und F4 öffnet die Overlay-Einstellungen.",
+ "help.size_lock.title": "Modus „Größe fixieren“",
+ "help.size_lock.body": "Im Modus „Größe fixieren“ behalten die Grafiken eine feste Größe, und die Lautstärke wird über eine proportionale Transparenz dargestellt. Das ist praktisch, wenn die Grafiken gleich groß bleiben sollen, Sie aber trotzdem sehen möchten, wie sie auf Geräusche reagieren.",
+ "help.editor.title": "Overlay-Bearbeitungsmodus in Echtzeit",
+ "help.editor.body": "Drücken Sie das zugewiesene Tastenkürzel (standardmäßig F4), um den Overlay-Bearbeitungsmodus einzuschalten. Sie können auf dem Bildschirm mit der Maus ziehen, um die Größengrenze der Grafiken anzupassen, und im eingeblendeten Einstellungsbereich Details feinjustieren.",
+ "help.developer.title": "Entwicklermodus",
+ "help.developer.body": "Wenn Sie den Entwicklermodus einschalten, zeigt das Overlay oben links, was die KI gehört hat (den Geräuschnamen, wie sicher sie war und wie lange es gedauert hat), und oben rechts die FPS. Schalten Sie zusätzlich „{0}“ ein, wird jedes Ergebnis in einer CSV-Datei gespeichert. Das ist nützlich, um zu prüfen, ob das System richtig funktioniert.",
+ "help.notes.title": "Gut zu wissen",
+ "help.note.direction_surround": "Bei 5.1- oder 7.1-Kanal-Ausgabe zeigt der obere Bildschirmrand Geräusche von vorne, der untere Geräusche von hinten. Die linke und rechte Seite zeigen Geräusche von links und rechts. Grafiken, die gleichzeitig von allen Seiten des Bildschirms kommen, stehen für Umgebungsgeräusche oder Geräusche ohne Richtung.",
+ "help.note.direction_stereo": "Bei 2-Kanal-Ausgabe (Stereo) werden links und rechts unterschieden, vorne und hinten jedoch nicht. Grafiken am oberen und unteren Bildschirmrand zeigen dann Geräusche, die auf beiden Seiten gleich stark zu hören sind, sowie den Raumeindruck.",
+ "help.note.sources": "Angezeigt wird der Ton aller Apps, die über das Standard-Wiedergabegerät von Windows wiedergegeben werden. Ton, den eine App an ein anderes Gerät sendet, etwa an ein Headset nur für den Sprachchat, wird nicht angezeigt.",
+ "help.note.volume": "Wenn die Lautstärke einer App sehr niedrig oder stummgeschaltet ist, in der App selbst oder im Lautstärkemixer von Windows, ist ihr Ton womöglich kaum oder gar nicht zu sehen. Wenn die Grafik schwach reagiert, drehen Sie diesen Ton etwas auf oder erhöhen Sie im Tab „Einstellungen“ die „Empfindlichkeit“.",
+ "help.note.ai": "Die Einordnung durch die KI kann falsch sein. Verlassen Sie sich in Situationen, in denen es um Sicherheit geht, nicht allein auf diese App.",
+ "help.faq.title": "Häufig gestellte Fragen",
+ "help.faq.stopped_q": "Die Tonvisualisierung hat sich ausgeschaltet, ohne dass ich sie beendet habe.",
+ "help.faq.stopped_a": "Sie schaltet sich aus, wenn die Audioerfassung stoppt und sich nicht innerhalb weniger Sekunden erholt, zum Beispiel wenn das Wiedergabegerät getrennt wird und kein anderes verfügbar ist. Der Tab „Startseite“ zeigt dann den Grund an. Schließen Sie Lautsprecher oder Kopfhörer an oder legen Sie in den Windows-Soundeinstellungen ein Standard-Wiedergabegerät fest, und starten Sie sie dann erneut.",
+ "help.faq.no_graphic_q": "Ich sehe keine Grafiken.",
+ "help.faq.no_graphic_a": "Prüfen Sie, ob der Ton über das Standard-Wiedergabegerät von Windows wiedergegeben wird und nicht stummgeschaltet ist und ob im Tab „Startseite“ ein Hinweis angezeigt wird. Wenn ein Spiel im exklusiven Vollbildmodus läuft, probieren Sie den randlosen Fenstermodus oder den Fenstermodus. Achten Sie außerdem im Tab „Einstellungen“ darauf, dass die „Deckkraft“ nicht zu niedrig eingestellt und die Anzeige der aktuellen Geräuschart nicht ausgeschaltet ist.",
+ "help.faq.performance_q": "Braucht die App viel CPU- oder GPU-Leistung?",
+ "help.faq.performance_a": "Um sie zu entlasten, verringern Sie im Tab „Einstellungen“ die „{0}“ oder schalten Sie „{1}“ aus.",
+ "help.faq.language_q": "Wie ändere ich die App-Sprache?",
+ "help.faq.language_a": "Wählen Sie sie in der Liste „{0}“ oben in diesem Fenster aus. Während die Tonvisualisierung läuft, können Sie sie auch in den Overlay-Einstellungen ändern. „{1}“ übernimmt die Anzeigesprache von Windows.",
+ "help.report.title": "Problem melden",
+ "help.report.desc": "Melden Sie uns einen Fehler oder ein Geräusch, das als falsche Geräuschart angezeigt wurde. GitHub öffnet sich in Ihrem Browser mit den unten stehenden Systeminformationen bereits eingetragen. Dafür benötigen Sie ein GitHub-Konto. Wenn Sie keines haben, schicken Sie uns stattdessen eine E-Mail. Es werden niemals Audiodaten gesendet.",
+ "help.report.github": "Auf GitHub melden",
+ "help.report.mail": "Per E-Mail melden",
+ "help.report.copy": "Systeminformationen kopieren",
+ "help.report.copied": "Systeminformationen kopiert.",
+ "help.report.copy_failed": "Kopieren in die Zwischenablage fehlgeschlagen. Versuchen Sie es erneut.",
+ "help.report.no_browser": "Es konnte kein Browser geöffnet werden. Kopieren Sie stattdessen die Systeminformationen und senden Sie sie uns.",
+ "help.report.no_mail": "Keine E-Mail-App zum Senden vorhanden. Kopieren Sie die Systeminformationen und senden Sie sie auf anderem Weg.",
+ "help.report.issue_title": "Meldung aus der App",
+ "help.report.issue_body": "Was ist passiert?\n\nWas haben Sie gerade gehört (App, Art des Geräuschs)?\n\nWas hätten Sie stattdessen erwartet?",
+ "help.about.title": "Über diese App",
+ "help.about.version": "Version {0}",
+ "help.about.license": "Diese App wird unter der GNU Affero General Public License v3.0 (AGPL-3.0) bereitgestellt. Die Namen und Gruppen der Geräusche im Tab „Geräusche“ beruhen auf der AudioSet-Ontologie und stehen unter der Lizenz CC BY-SA 4.0 (siehe „Open-Source-Lizenzen“ unten).",
+ "help.licenses.button": "Open-Source-Lizenzen",
+ "help.licenses.title": "Open-Source-Lizenzen",
+ "help.licenses.close": "Schließen",
+
+ "notice.ai_unavailable": "Die Erkennung der Geräuscharten funktioniert nicht. Daher werden alle Geräusche in der Farbe für Umgebungsgeräusche angezeigt. Schalten Sie die Tonvisualisierung aus und wieder ein.",
+ "notice.ai_unavailable_hidden": "Die Erkennung der Geräuscharten funktioniert nicht. Daher zählen alle Geräusche als Umgebungsgeräusche, deren Anzeige jedoch ausgeschaltet ist, sodass nichts angezeigt wird. Schalten Sie im Tab „Einstellungen“ „{0}“ ein, oder schalten Sie die Tonvisualisierung aus und wieder ein.",
+ "notice.stopped.title": "Tonvisualisierung ausgeschaltet",
+ "notice.stopped.no_device": "Es konnte kein Wiedergabegerät geöffnet werden, daher kann kein Ton erfasst werden. Schließen Sie Lautsprecher oder Kopfhörer an oder legen Sie in den Windows-Soundeinstellungen ein Standard-Wiedergabegerät fest, und starten Sie dann erneut.",
+ "notice.stopped.capture_error": "Die Audioerfassung wurde durch einen Fehler gestoppt, daher ist die Grafik aus.",
+ "notice.dismiss": "Hinweis ausblenden",
+
+ "tray.open": "Launcher öffnen",
+ "tray.exit": "Programm beenden",
+
+ "more_info.title": "GitHub-Repository und Mitwirkung",
+ "more_info.body": "SoundVisualizer ist ein Open-Source-Projekt, dessen Quellcode öffentlich auf GitHub verfügbar ist. Sie können mitmachen, indem Sie Fehler melden, Funktionen vorschlagen oder zur Entwicklung beitragen. Klicken Sie auf die Schaltfläche unten, um das offizielle GitHub-Repository zu besuchen! Im Tab „Releases“ auf GitHub können Sie außerdem nach der neuesten Version suchen und sie herunterladen.",
+ "more_info.github_button": "GitHub-Repository besuchen",
+
+ "dialog.notice_title": "Hinweis",
+ "dialog.already_running": "Das Overlay läuft bereits.",
+ "dialog.reset_title": "Einstellungen zurücksetzen",
+ "dialog.reset_wave": "Einstellungen des Modus „Welle“ auf die Standardwerte zurücksetzen?",
+ "dialog.reset_pad": "Einstellungen des Modus „Pad“ auf die Standardwerte zurücksetzen?",
+ "dialog.reset_circle": "Einstellungen des Modus „Kreis“ auf die Standardwerte zurücksetzen?",
+ "dialog.reset_outline": "Einstellungen des Modus „Kontur“ auf die Standardwerte zurücksetzen?",
+ "dialog.reset_sound_types": "Einstellungen der Geräuscharten auf die Standardwerte zurücksetzen?",
+ "dialog.reset_all_title": "Auf Standardwerte zurücksetzen",
+ "dialog.reset_all": "Möchten Sie wirklich alle Einstellungen auf die Standardwerte zurücksetzen?",
+ "dialog.link_error": "Der Link konnte nicht geöffnet werden: {0}",
+
+ "editor.title": "Overlay-Einstellungen",
+ "editor.drag_tip": "Ziehen Sie die Hilfslinien auf dem Bildschirm mit der Maus, um die Höhengrenze der Overlay-Welle und die Größe der Mitte in Echtzeit anzupassen!",
+ "editor.glow": "Leuchteffekt",
+ "editor.glow_strength": "Leuchtstärke",
+ "editor.color": "Farbe",
+ "editor.hotkeys": "Einstellungen für Tastenkürzel",
+ "editor.developer_mode_desc": "Zeigt, was die KI gehört hat (oben links), und die FPS (oben rechts)",
+ "editor.save_close_hint": "zum Speichern und Schließen",
+ "editor.close_overlay": "Overlay beenden",
+ "editor.guide.limit": "Grenze im Modus „{0}“",
+ "editor.guide.size": "Größe im Modus „{0}“: {1} %",
+ "editor.guide.base_radius": "Grundradius: {0}",
+ "editor.guide.circle_size": "Kreisgröße: {0} %",
+
+ "hud.visual_mode": "Darstellungsmodus: [{0}] {1}",
+ "hud.sound_mode": "Audiomodus: [{0}] {1}",
+ "hud.overlay_settings": "Overlay-Einstellungen: [{0}]",
+ "hud.status.ready": "System bereit",
+ "hud.status.stereo": "PC-Audio: {0}ch (Stereo)\nQuelle: wartet...",
+ "hud.status.surround": "PC-Audio: 8ch (7.1)\nQuelle: wartet...",
+
+ "color_picker.title": "Farbe auswählen",
+ "color_picker.hex": "HEX-Code",
+ "color_picker.cancel": "Abbrechen",
+ "color_picker.apply": "Übernehmen",
+ "color_picker.area": "Sättigung und Helligkeit",
+ "color_picker.area_value": "Sättigung {0} %, Helligkeit {1} %",
+ "color_picker.hue": "Farbton",
+ "color_picker.keys_help": "Mit den Pfeiltasten ändern. Mit gedrückter Umschalttaste in größeren Schritten.",
+ "color_picker.presets": "Häufige Farben",
+ "color_picker.preset.white": "Weiß",
+ "color_picker.preset.red": "Rot",
+ "color_picker.preset.orange": "Orange",
+ "color_picker.preset.yellow": "Gelb",
+ "color_picker.preset.green": "Grün",
+ "color_picker.preset.cyan": "Türkis",
+ "color_picker.preset.blue": "Blau",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "Es wurde kein Ausgabegerät gefunden.",
+ "audio.error.capture_stopped": "Die Audioerfassung wurde unerwartet beendet: {0}",
+ "audio.error.capture_start_failed": "Die Erfassungs-Engine konnte nicht gestartet werden: {0}",
+
+ "tutorial.window_title": "Tutorial",
+ "tutorial.skip": "Überspringen",
+ "tutorial.previous": "Vorherige Seite",
+ "tutorial.next": "Weiter",
+ "tutorial.done": "Verstanden",
+ "tutorial.page_status": "Seite {0} von {1}",
+ "tutorial.pause": "Animation anhalten",
+ "tutorial.play": "Animation abspielen",
+ "tutorial.sound_on": "Ton einschalten",
+ "tutorial.sound_off": "Ton ausschalten",
+ "tutorial.sound.title": "Geräusche auf dem Bildschirm sehen",
+ "tutorial.sound.body": "Der Ton Ihres Spiels oder Videos wird an den Bildschirmrändern dargestellt. Je lauter er ist, desto größer wird er gezeichnet. Angezeigt wird nur der Ton, der auf diesem PC wiedergegeben wird. Geräusche um Sie herum werden nicht erfasst.",
+ "tutorial.direction.title": "Zeigt, woher ein Geräusch kommt",
+ "tutorial.direction.body": "Bei 2-Kanal-Ausgabe (Stereo) wird ein Geräusch von links auf der linken Seite größer dargestellt, ein Geräusch von rechts auf der rechten Seite. Bei 5.1- oder 7.1-Kanal-Ausgabe unterscheidet die App auch vorne und hinten: Geräusche von vorne erscheinen am oberen Bildschirmrand, Geräusche von hinten am unteren.",
+ "tutorial.types.title": "Jede Geräuschart hat ihre eigene Farbe",
+ "tutorial.types.body": "Die KI teilt Geräusche in drei Arten ein und stellt jede Art in einer eigenen Farbe dar. Die Farben können Sie im Tab „Einstellungen“ ändern.",
+ "tutorial.type.ambient_examples": "Musik, Regen und alles andere",
+ "tutorial.type.speech_examples": "Menschen, die sprechen",
+ "tutorial.type.danger_examples": "Schüsse, Explosionen, Sirenen",
+ "tutorial.start.title": "Ein- und ausschalten",
+ "tutorial.start.hotkeys": "Während die Tonvisualisierung läuft, wechseln Sie mit {0} den Audiomodus, mit {1} den Darstellungsmodus und öffnen mit {2} die Overlay-Einstellungen."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/en.json b/SoundVisualizer.Core/Localization/Strings/en.json
new file mode 100644
index 0000000..34b6858
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/en.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Language",
+ "common.reset_defaults": "Reset to defaults",
+ "common.minimize": "Minimize",
+ "common.close": "Close",
+ "language.system": "System default",
+
+ "launcher.window_title": "SoundVisualizer Launcher",
+ "launcher.tab.home": "Home",
+ "launcher.tab.settings": "Settings",
+ "launcher.tab.sounds": "Sounds",
+ "launcher.tab.help": "Help",
+ "launcher.tab.more_info": "Learn more",
+ "launcher.home.desc": "Draws the sounds you can't see right on your screen.\nFrom gaming to movies, try a whole new experience.",
+ "launcher.home.start": "Start",
+ "launcher.home.stop": "Stop",
+ "launcher.home.tutorial": "View tutorial",
+ "launcher.status.running": "Status: Running",
+ "launcher.status.idle": "Status: Ready",
+
+ "settings.section.mode": "Mode settings",
+ "settings.visual_mode.title": "Visual mode",
+ "settings.visual_mode.desc": "Choose the basic shape of the graphics drawn on screen.",
+ "settings.mode.wave": "Wave",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Circle",
+ "settings.mode.outline": "Outline",
+ "settings.sound_mode.title": "Sound mode",
+ "settings.sound_mode.desc": "Choose the sound channels that match your speakers.\nFor 2-channel speakers, choose '2 channels'. For surround speakers, choose '5.1 channels' or '7.1 channels'.",
+ "settings.channels.2": "2 channels",
+ "settings.channels.5_1": "5.1 channels",
+ "settings.channels.7_1": "7.1 channels",
+ "settings.max_fps.title": "Max frame rate",
+ "settings.max_fps.desc": "Sets the highest frame rate the graphics are drawn at.",
+ "settings.section.mode_detail": "Settings by mode",
+ "settings.mode_card.wave": "Wave mode",
+ "settings.mode_card.pad": "Pad mode",
+ "settings.mode_card.circle": "Circle mode",
+ "settings.mode_card.outline": "Outline mode",
+ "settings.size_lock.title": "Lock size",
+ "settings.size_lock.desc": "Keeps the graphics at a fixed size and shows loudness through their transparency instead.",
+ "settings.fixed_size.wave": "Wave size",
+ "settings.fixed_size.pad": "Pad size",
+ "settings.fixed_size.circle": "Circle size",
+ "settings.fixed_size.outline": "Outline thickness",
+ "settings.fixed_size.desc": "Sets the fixed size the graphics stay at while their transparency changes.",
+ "settings.max_opacity.title": "Max opacity",
+ "settings.max_opacity.desc": "How opaque the graphics get at the loudest sound.",
+ "settings.size.title": "Size",
+ "settings.size.desc.wave": "Adjusts the height of the wave and how full it looks overall.",
+ "settings.size.desc.pad": "Adjusts how thick and wide the pads get when they react to sound.",
+ "settings.size.desc.circle": "Adjusts how far the circle equalizer bulges out when sound plays.",
+ "settings.size.desc.outline": "Adjusts the height of the outline wave and how full it looks overall.",
+ "settings.opacity.title": "Opacity",
+ "settings.opacity.desc.wave": "Higher values draw the wave bolder; lower values let more of the game or screen behind it show through.",
+ "settings.opacity.desc.pad": "Higher values draw the pads bolder; lower values make them fainter and more transparent.",
+ "settings.opacity.desc.circle": "Higher values draw the circle bolder; lower values make it fainter and more transparent.",
+ "settings.opacity.desc.outline": "Higher values draw the outline wave bolder; lower values make it fainter and more transparent.",
+ "settings.speed.title": "Speed",
+ "settings.speed.desc.wave": "Adjusts how fast the wave flows and ripples.",
+ "settings.speed.desc.pad": "Adjusts how fast the pads turn toward the direction a sound comes from.",
+ "settings.speed.desc.circle": "Adjusts how fast the circle equalizer ripples and turns in response to sound.",
+ "settings.speed.desc.outline": "Adjusts how fast the outline wave flows and ripples.",
+ "settings.sensitivity.title": "Sensitivity",
+ "settings.sensitivity.desc.wave": "Adjusts how sensitively the wave reacts and surges, even to quiet sounds.",
+ "settings.sensitivity.desc.pad": "Adjusts the pads so they pick up sound better and stand out more.",
+ "settings.sensitivity.desc.circle": "Adjusts how finely the circle's amplitude breaks up as it reacts to sound.",
+ "settings.sensitivity.desc.outline": "Adjusts how sensitively the outline wave reacts and surges, even to quiet sounds.",
+ "settings.glow.title": "Glow",
+ "settings.glow.desc.wave": "Adds a soft neon glow along the edge of the wave and adjusts its strength.\nNote: uses extra GPU resources.",
+ "settings.glow.desc.pad": "Adds a soft, aura-like glow around the pads.\nNote: uses extra GPU resources.",
+ "settings.glow.desc.circle": "Adds a shimmering halo along the edge of the circle equalizer.\nNote: uses extra GPU resources.",
+ "settings.glow.desc.outline": "Adds a soft neon glow to the outline and adjusts its strength.\nNote: uses extra GPU resources.",
+ "settings.radius.title": "Radius",
+ "settings.radius.desc": "Sets the radius of the center ring in Circle mode.",
+ "settings.section.sound_types": "Sound types (AI)",
+ "settings.show_ambient": "Show ambient sounds",
+ "settings.show_speech": "Show speech",
+ "settings.show_danger": "Show danger sounds",
+ "settings.set_color": "Set color",
+ "settings.set_color_for": "Set color: {0}",
+ "settings.section.hotkeys": "Hotkeys",
+ "settings.hotkey.visual.title": "Switch visual mode",
+ "settings.hotkey.visual.desc": "Hotkey that changes the shape in real time while running.",
+ "settings.hotkey.sound.title": "Switch sound mode",
+ "settings.hotkey.sound.desc": "Hotkey that changes the sound mode in real time while running.",
+ "settings.hotkey.edit.title": "Overlay settings",
+ "settings.hotkey.edit.desc": "Hotkey that opens or closes the overlay editor.",
+ "settings.hotkey.set": "Set hotkey",
+ "settings.section.advanced": "Advanced settings",
+ "settings.developer_mode.title": "Developer mode",
+ "settings.developer_mode.desc": "Shows what the AI heard (the sound name it reported, how sure it was, and how long it took) in the top-left corner of the overlay, and the FPS in the top-right. The readout is in English and is not meant for everyday use.",
+ "settings.developer_record.title": "Record results to a file",
+ "settings.developer_record.desc": "Writes every AI result (the sound names it ranked highest, how sure it was, and the timings) to a new CSV file in the ai-log folder next to SoundVisualizer.exe, so you can look back at what it heard. If that folder can't be written to, the file goes to %LOCALAPPDATA%\\SoundVisualizer\\ai-log instead. Files can grow large; turn it off when you are done.",
+ "settings.auto_minimize.title": "Minimize launcher on start",
+ "settings.auto_minimize.desc": "Automatically minimizes this launcher window when the overlay starts.",
+
+ "sounds.title": "Choose sound types",
+ "sounds.desc": "Sounds the AI recognizes are grouped with similar sounds, under the type each counts as by default. Change a group’s type, and the color of its sounds follows right away.",
+ "sounds.sibling_note": "The AI often hears one sound under several names at once. A siren, for example, is also heard as “Alarm”, which is in another group. If a sound still shows its old type after a change, change the related groups or sounds too.",
+ "sounds.advanced.title": "Advanced mode",
+ "sounds.advanced.desc": "Shows every sound in its group, so you can change sounds one by one.",
+ "sounds.search.hint": "Search sounds",
+ "sounds.search.clear": "Clear search",
+ "sounds.filter.title": "Filter",
+ "sounds.filter.all": "All",
+ "sounds.filter.changed": "Changed",
+ "sounds.type.ambient": "Ambient",
+ "sounds.type.speech": "Speech",
+ "sounds.type.danger": "Danger",
+ "sounds.type.default": "{0} (default)",
+ "sounds.type.mixed": "Mixed",
+ "sounds.changed_count": "Sounds changed: {0}",
+ "sounds.changed_from": "Default: {0}",
+ "sounds.reset.button": "Reset all",
+ "sounds.reset.title": "Reset all sounds?",
+ "sounds.reset.message": "Every sound you changed goes back to its default type.",
+ "sounds.reset.confirm": "Reset",
+ "sounds.reset.cancel": "Cancel",
+ "sounds.empty": "No matching sounds.",
+ "sounds.group.mixed_note": "Some sounds changed one by one",
+ "sounds.group.changed_line": "Now: {0} · Default: {1}",
+ "sounds.overwrite.title": "Change the whole group?",
+ "sounds.overwrite.message": "Sounds in the “{0}” group that will change to {2}: {1}. Sounds you changed one by one are overwritten too.",
+ "sounds.overwrite.confirm": "Change",
+ "sounds.overwrite.cancel": "Cancel",
+ "sounds.basic_warning.title": "Switch to basic mode?",
+ "sounds.basic_warning.message": "Some groups have sounds you changed one by one. In basic mode, choosing a type for one of these groups gives every sound in it that type, and your one-by-one changes in that group are overwritten. Nothing changes until you choose.",
+ "sounds.basic_warning.confirm": "Switch to basic",
+ "sounds.basic_warning.cancel": "Stay in advanced",
+ "sounds.cd.sound": "{0}, type: {1}",
+ "sounds.cd.sound_changed": "{0}, type: {1}, default: {2}",
+ "sounds.cd.group": "{0} group, type: {1}, sounds: {2} ({3})",
+ "sounds.cd.group_changed": "{0} group, type: {1}, default: {2}, sounds: {3} ({4})",
+ "sounds.cd.group_header": "{0} group, sounds: {1}",
+
+ "hotkey.prompt": "Press a key (Esc to cancel)",
+ "hotkey.none": "None",
+ "hotkey.button_name": "{0}, hotkey: {1}",
+
+ "help.start.title": "Getting started",
+ "help.start.1": "Pick your language at the top of this window, and change how the graphics look in the Settings tab if you like.",
+ "help.start.2": "On Home, click “{0}”.",
+ "help.start.3": "The graphics are drawn along the edges of the screen, on top of other windows such as games and videos. Clicks pass through them, so you can keep using the window underneath.",
+ "help.start.4": "To turn it off, click “{0}” on Home, or right-click the SoundVisualizer icon in the notification area of the taskbar and choose “{0}”.",
+ "help.modes.title": "Visual modes",
+ "help.mode.wave": "The whole edge of the screen pushes inward with the loudness. The side the sound comes from pushes in deeper.",
+ "help.mode.pad": "Bars light up at 8 spots around the edge of the screen. Shows direction most clearly.",
+ "help.mode.circle": "A ring in the center of the screen swells toward the sound.",
+ "help.mode.outline": "Draws only a thin outline of the wave, so it covers less of your game.",
+ "help.modes.note": "Settings like size and opacity are saved separately for each mode and can be changed in the Settings tab.",
+ "help.sound.title": "Sound types",
+ "help.sound.kinds": "AI sorts what you hear into ambient, speech and danger sounds. By default, ambient is white, speech is yellow and danger is red. You can change the color of each type and whether it's shown in the Settings tab.",
+ "help.sound.groups": "In the Sounds tab, similar sounds are grouped, and you pick which type each group counts as. For example, change the “Doorbell” group to Danger, and when the AI hears a doorbell or a knock it gets the danger color. A sound you newly set to Danger only takes part in the AI's vote on the type, though, so this doesn't mean every doorbell will be shown as danger.",
+ "help.sound.advanced": "Turn on Advanced mode to change sounds one by one.",
+ "help.sound.danger": "When a danger sound (like a gunshot) is detected, the color changes instantly instead of fading.",
+ "help.pc.title": "PC settings and hotkeys",
+ "help.sound_mode.title": "Sound mode",
+ "help.sound_mode.body": "To see sounds from the front and back as well as from the left and right, set your output device to '5.1 Surround' (6 channels) or '7.1 Surround' (8 channels) in the Windows sound settings. With ordinary stereo (2 channels), the graphics can show left and right, but not front and back. 'Sound mode' in the Settings tab switches to match your output device when the visualizer starts or the device changes, and you can still change it yourself.",
+ "help.sound_mode.tip": "💡 If you don't have a 7.1 output device, you can install the {0} and set the output device to 7.1 channels to show the overlay with 7.1 surround sound.",
+ "help.sound_mode.tip_link": "VB-CABLE virtual driver",
+ "help.hotkeys.title": "Real-time custom hotkeys",
+ "help.hotkeys.body": "In the Settings tab, click a hotkey button and press a single key or a key combination (e.g. Ctrl + Shift + A) to change the hotkey as you like. Pressing the hotkey, even while the app is in the background, switches the mode instantly.",
+ "help.hotkeys.defaults": "By default, F2 switches the sound mode, F3 switches the visual mode and F4 opens the overlay settings.",
+ "help.size_lock.title": "Lock size mode",
+ "help.size_lock.body": "Lock size mode keeps the graphics at a fixed size by using transparency in proportion to the loudness of the sound. It's useful when you want to keep the graphics the same size while still seeing them react to sound.",
+ "help.editor.title": "Real-time overlay edit mode",
+ "help.editor.body": "Press the assigned hotkey (F4 by default) to turn on the overlay edit mode. You can drag on the screen with your mouse to adjust the size limit of the graphics, and fine-tune the details in the settings panel that pops up.",
+ "help.developer.title": "Developer mode",
+ "help.developer.body": "Turning on Developer mode shows what the AI heard (the sound name, how sure it was, and how long it took) in the top-left corner of the overlay, and the FPS in the top-right. If you also turn on “{0}”, every result is saved to a CSV file. It's useful when you want to check that the system is working properly.",
+ "help.notes.title": "Good to know",
+ "help.note.direction_surround": "With 5.1 or 7.1 channel output, the top of the screen shows sound from the front and the bottom shows sound from behind. The left and right sides show sound from the left and right, and graphics coming from every side at once are ambient or non-directional sounds.",
+ "help.note.direction_stereo": "With 2-channel (stereo) output, it can tell left from right, but not front from back. Graphics at the top and bottom of the screen then show sounds heard equally on both sides and a sense of space.",
+ "help.note.sources": "It shows the sound of every app that plays through the default playback device in Windows. Sound that an app sends to another device, such as a headset used only for voice chat, isn't shown.",
+ "help.note.volume": "If an app's volume is turned down very low or muted, in the app itself or in the Windows volume mixer, its sound may barely show or not show at all. If the graphics don't respond well, turn that sound up a little or raise Sensitivity in the Settings tab.",
+ "help.note.ai": "AI classification can be wrong. Don't rely on this app as your only safeguard when safety is at stake.",
+ "help.faq.title": "FAQ",
+ "help.faq.stopped_q": "It turned off without me stopping it.",
+ "help.faq.stopped_a": "It turns off when sound capture stops and doesn't recover within a few seconds, for example when the playback device is disconnected and no other device is available. Home then shows why. Connect speakers or headphones, or set a default playback device in the Windows sound settings, then start it again.",
+ "help.faq.no_graphic_q": "I don't see any graphics.",
+ "help.faq.no_graphic_a": "Check that the sound is playing through the default playback device in Windows and isn't muted, and whether Home shows a notice. If a game runs in exclusive full-screen mode, try its borderless or windowed mode. Also make sure opacity isn't too low in Settings, and that showing the current sound type isn't turned off.",
+ "help.faq.performance_q": "Does it use a lot of CPU or GPU?",
+ "help.faq.performance_a": "To make it lighter, lower “{0}” in the Settings tab or turn off “{1}”.",
+ "help.faq.language_q": "How do I change the app language?",
+ "help.faq.language_a": "Pick one from the “{0}” list at the top of this window. While the visualizer is running, you can also change it in the overlay settings. “{1}” follows the Windows display language.",
+ "help.report.title": "Report a problem",
+ "help.report.desc": "Tell us about a bug, or a sound that was shown as the wrong type. GitHub opens in your browser with the environment info below filled in, so it needs a GitHub account. Without one, send us an email instead. No audio is ever sent.",
+ "help.report.github": "Report on GitHub",
+ "help.report.mail": "Report by email",
+ "help.report.copy": "Copy environment info",
+ "help.report.copied": "Environment info copied.",
+ "help.report.copy_failed": "Couldn't copy to the clipboard. Try again.",
+ "help.report.no_browser": "Couldn't open a browser. Copy the environment info and send it to us instead.",
+ "help.report.no_mail": "No email app to send with. Copy the environment info and send it another way.",
+ "help.report.issue_title": "Report from the app",
+ "help.report.issue_body": "What happened?\n\nWhat were you listening to (app, kind of sound)?\n\nWhat did you expect instead?",
+ "help.about.title": "About",
+ "help.about.version": "Version {0}",
+ "help.about.license": "This app is distributed under the GNU Affero General Public License v3.0 (AGPL-3.0). The sound names and sound groups in the Sounds tab are based on the AudioSet Ontology and are licensed under CC BY-SA 4.0 (see “Open source licenses” below).",
+ "help.licenses.button": "Open source licenses",
+ "help.licenses.title": "Open source licenses",
+ "help.licenses.close": "Close",
+
+ "notice.ai_unavailable": "Sound type detection isn't working, so every sound is shown in the ambient color. Try turning the visualizer off and on again.",
+ "notice.ai_unavailable_hidden": "Sound type detection isn't working, so every sound counts as ambient, and ambient sounds are turned off, so nothing is shown. Turn on “{0}” in the Settings tab, or try turning the visualizer off and on again.",
+ "notice.stopped.title": "SoundVisualizer turned off",
+ "notice.stopped.no_device": "No playback device could be opened, so sound can't be captured. Connect speakers or headphones, or set a default playback device in the Windows sound settings, then start again.",
+ "notice.stopped.capture_error": "Sound capture stopped because of an error, so the graphics are off.",
+ "notice.dismiss": "Dismiss notice",
+
+ "tray.open": "Open launcher",
+ "tray.exit": "Exit",
+
+ "more_info.title": "GitHub repository and contributing",
+ "more_info.body": "SoundVisualizer is an open-source project, and its source code is public on GitHub. You can take part by reporting bugs, suggesting features, or contributing to development. Click the button below to visit the official GitHub repository! You can also check for and download the latest version on the Releases tab on GitHub.",
+ "more_info.github_button": "Visit the GitHub repository",
+
+ "dialog.notice_title": "Notice",
+ "dialog.already_running": "The overlay is already running.",
+ "dialog.reset_title": "Reset settings",
+ "dialog.reset_wave": "Reset the Wave mode settings to their defaults?",
+ "dialog.reset_pad": "Reset the Pad mode settings to their defaults?",
+ "dialog.reset_circle": "Reset the Circle mode settings to their defaults?",
+ "dialog.reset_outline": "Reset the Outline mode settings to their defaults?",
+ "dialog.reset_sound_types": "Reset the sound type settings to their defaults?",
+ "dialog.reset_all_title": "Reset to defaults",
+ "dialog.reset_all": "Are you sure you want to reset all settings to their defaults?",
+ "dialog.link_error": "Couldn't open the link: {0}",
+
+ "editor.title": "Overlay settings",
+ "editor.drag_tip": "Drag the guidelines on the screen with your mouse to adjust the height limit and center size of the overlay wave in real time!",
+ "editor.glow": "Glow effect",
+ "editor.glow_strength": "Glow strength",
+ "editor.color": "Color",
+ "editor.hotkeys": "Hotkey settings",
+ "editor.developer_mode_desc": "Shows what the AI heard (top left) and the FPS (top right)",
+ "editor.save_close_hint": "key to save and close",
+ "editor.close_overlay": "Exit overlay",
+ "editor.guide.limit": "{0} limit",
+ "editor.guide.size": "{0} size: {1}%",
+ "editor.guide.base_radius": "Base radius: {0}",
+ "editor.guide.circle_size": "Circle size: {0}%",
+
+ "hud.visual_mode": "Visual mode: [{0}] {1}",
+ "hud.sound_mode": "Sound mode: [{0}] {1}",
+ "hud.overlay_settings": "Overlay settings: [{0}]",
+ "hud.status.ready": "System ready",
+ "hud.status.stereo": "PC audio: {0}ch (stereo)\nSource: waiting...",
+ "hud.status.surround": "PC audio: 8ch (7.1)\nSource: waiting...",
+
+ "color_picker.title": "Pick a color",
+ "color_picker.hex": "HEX code",
+ "color_picker.cancel": "Cancel",
+ "color_picker.apply": "Apply",
+ "color_picker.area": "Saturation and brightness",
+ "color_picker.area_value": "Saturation {0}%, brightness {1}%",
+ "color_picker.hue": "Hue",
+ "color_picker.keys_help": "Use the arrow keys to change it. Hold Shift to change it in bigger steps.",
+ "color_picker.presets": "Common colors",
+ "color_picker.preset.white": "White",
+ "color_picker.preset.red": "Red",
+ "color_picker.preset.orange": "Orange",
+ "color_picker.preset.yellow": "Yellow",
+ "color_picker.preset.green": "Green",
+ "color_picker.preset.cyan": "Cyan",
+ "color_picker.preset.blue": "Blue",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "No output device was found.",
+ "audio.error.capture_stopped": "Capture stopped unexpectedly: {0}",
+ "audio.error.capture_start_failed": "Couldn't start the capture engine: {0}",
+
+ "tutorial.window_title": "Tutorial",
+ "tutorial.skip": "Skip",
+ "tutorial.previous": "Previous page",
+ "tutorial.next": "Next",
+ "tutorial.done": "Got it",
+ "tutorial.page_status": "Page {0} of {1}",
+ "tutorial.pause": "Pause animation",
+ "tutorial.play": "Play animation",
+ "tutorial.sound_on": "Turn on sound",
+ "tutorial.sound_off": "Turn off sound",
+ "tutorial.sound.title": "See sound on your screen",
+ "tutorial.sound.body": "Sound from your game or video is drawn along the edges of the screen. The louder it is, the bigger it is drawn. Only sound playing on this PC is shown. Sounds around you are not picked up.",
+ "tutorial.direction.title": "Shows where sound comes from",
+ "tutorial.direction.body": "With 2-channel (stereo) output, a sound on the left is drawn bigger on the left, and a sound on the right on the right. With 5.1 or 7.1 channel output, it also tells front from back: sound from the front is drawn at the top of the screen and sound from behind at the bottom.",
+ "tutorial.types.title": "Each type of sound gets its own color",
+ "tutorial.types.body": "AI sorts sounds into three types and draws each in its own color. You can change the colors in the Settings tab.",
+ "tutorial.type.ambient_examples": "Music, rain and everything else",
+ "tutorial.type.speech_examples": "Voices",
+ "tutorial.type.danger_examples": "Gunshots, explosions, sirens",
+ "tutorial.start.title": "How to turn it on and off",
+ "tutorial.start.hotkeys": "While it's running, press {0} to switch the sound mode, {1} to switch the visual mode and {2} to open the overlay settings."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/es.json b/SoundVisualizer.Core/Localization/Strings/es.json
new file mode 100644
index 0000000..119fadf
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/es.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Idioma",
+ "common.reset_defaults": "Restablecer valores predeterminados",
+ "common.minimize": "Minimizar",
+ "common.close": "Cerrar",
+ "language.system": "Idioma del sistema",
+
+ "launcher.window_title": "Lanzador de SoundVisualizer",
+ "launcher.tab.home": "Inicio",
+ "launcher.tab.settings": "Ajustes",
+ "launcher.tab.sounds": "Sonidos",
+ "launcher.tab.help": "Ayuda",
+ "launcher.tab.more_info": "Más información",
+ "launcher.home.desc": "Dibuja en tu pantalla los sonidos que no puedes ver.\nDe los juegos al cine, descubre una experiencia nueva.",
+ "launcher.home.start": "Iniciar",
+ "launcher.home.stop": "Detener",
+ "launcher.home.tutorial": "Ver tutorial",
+ "launcher.status.running": "Estado: en ejecución",
+ "launcher.status.idle": "Estado: listo",
+
+ "settings.section.mode": "Ajustes de modo",
+ "settings.visual_mode.title": "Modo visual",
+ "settings.visual_mode.desc": "Elige la forma básica de los gráficos que se dibujan en la pantalla.",
+ "settings.mode.wave": "Onda",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Círculo",
+ "settings.mode.outline": "Contorno",
+ "settings.sound_mode.title": "Modo de sonido",
+ "settings.sound_mode.desc": "Elige los canales de sonido que correspondan a tus altavoces.\nCon altavoces de 2 canales, elige “2 canales”. Con altavoces envolventes, elige “5.1 canales” o “7.1 canales”.",
+ "settings.channels.2": "2 canales",
+ "settings.channels.5_1": "5.1 canales",
+ "settings.channels.7_1": "7.1 canales",
+ "settings.max_fps.title": "Velocidad de fotogramas máx.",
+ "settings.max_fps.desc": "Ajusta la velocidad de fotogramas máxima a la que se dibujan los gráficos.",
+ "settings.section.mode_detail": "Ajustes por modo",
+ "settings.mode_card.wave": "Modo Onda",
+ "settings.mode_card.pad": "Modo Pad",
+ "settings.mode_card.circle": "Modo Círculo",
+ "settings.mode_card.outline": "Modo Contorno",
+ "settings.size_lock.title": "Bloquear tamaño",
+ "settings.size_lock.desc": "Mantiene los gráficos con un tamaño fijo y muestra el volumen del sonido mediante la transparencia.",
+ "settings.fixed_size.wave": "Tamaño de la onda",
+ "settings.fixed_size.pad": "Tamaño del pad",
+ "settings.fixed_size.circle": "Tamaño del círculo",
+ "settings.fixed_size.outline": "Grosor del contorno",
+ "settings.fixed_size.desc": "Ajusta el tamaño fijo que mantienen los gráficos mientras cambia su transparencia.",
+ "settings.max_opacity.title": "Opacidad máx.",
+ "settings.max_opacity.desc": "La opacidad que alcanzan los gráficos con el sonido más fuerte.",
+ "settings.size.title": "Tamaño",
+ "settings.size.desc.wave": "Ajusta la altura de la onda y lo voluminosa que se ve en conjunto.",
+ "settings.size.desc.pad": "Ajusta el grosor y el ancho que alcanzan los pads al reaccionar al sonido.",
+ "settings.size.desc.circle": "Ajusta cuánto sobresale el ecualizador circular cuando hay sonido.",
+ "settings.size.desc.outline": "Ajusta la altura de la onda de contorno y lo voluminosa que se ve en conjunto.",
+ "settings.opacity.title": "Opacidad",
+ "settings.opacity.desc.wave": "Con valores altos, los gráficos se ven más intensos; con valores bajos, más tenues y transparentes.",
+ "settings.opacity.desc.pad": "Con valores altos, los gráficos se ven más intensos; con valores bajos, más tenues y transparentes.",
+ "settings.opacity.desc.circle": "Con valores altos, los gráficos se ven más intensos; con valores bajos, más tenues y transparentes.",
+ "settings.opacity.desc.outline": "Con valores altos, los gráficos se ven más intensos; con valores bajos, más tenues y transparentes.",
+ "settings.speed.title": "Velocidad",
+ "settings.speed.desc.wave": "Ajusta la velocidad con la que la onda fluye y ondula.",
+ "settings.speed.desc.pad": "Ajusta la rapidez con la que los pads giran hacia la dirección de la que viene un sonido.",
+ "settings.speed.desc.circle": "Ajusta la rapidez con la que el ecualizador circular ondula y gira en respuesta al sonido.",
+ "settings.speed.desc.outline": "Ajusta la velocidad con la que la onda de contorno fluye y ondula.",
+ "settings.sensitivity.title": "Sensibilidad",
+ "settings.sensitivity.desc.wave": "Ajusta con qué sensibilidad reacciona y se agita la onda, incluso con sonidos suaves.",
+ "settings.sensitivity.desc.pad": "Ajusta los pads para que capten mejor el sonido y destaquen más.",
+ "settings.sensitivity.desc.circle": "Ajusta con qué detalle se fragmenta la amplitud del círculo al reaccionar al sonido.",
+ "settings.sensitivity.desc.outline": "Ajusta con qué sensibilidad reacciona y se agita la onda de contorno, incluso con sonidos suaves.",
+ "settings.glow.title": "Resplandor",
+ "settings.glow.desc.wave": "Añade un suave resplandor de neón al borde de la onda y ajusta su intensidad.\nNota: consume recursos adicionales de la GPU.",
+ "settings.glow.desc.pad": "Añade un resplandor suave, como un aura, alrededor de los pads.\nNota: consume recursos adicionales de la GPU.",
+ "settings.glow.desc.circle": "Añade un halo reluciente a lo largo del borde del ecualizador circular.\nNota: consume recursos adicionales de la GPU.",
+ "settings.glow.desc.outline": "Añade un suave resplandor de neón al contorno y ajusta su intensidad.\nNota: consume recursos adicionales de la GPU.",
+ "settings.radius.title": "Radio",
+ "settings.radius.desc": "Ajusta el radio del anillo central en el modo Círculo.",
+ "settings.section.sound_types": "Tipos de sonido (IA)",
+ "settings.show_ambient": "Sonido ambiente",
+ "settings.show_speech": "Voz",
+ "settings.show_danger": "Peligro",
+ "settings.set_color": "Elegir color",
+ "settings.set_color_for": "Elegir color: {0}",
+ "settings.section.hotkeys": "Atajos de teclado",
+ "settings.hotkey.visual.title": "Cambiar modo visual",
+ "settings.hotkey.visual.desc": "Atajo que cambia la forma en tiempo real durante la ejecución.",
+ "settings.hotkey.sound.title": "Cambiar modo de sonido",
+ "settings.hotkey.sound.desc": "Atajo que cambia el modo de sonido en tiempo real durante la ejecución.",
+ "settings.hotkey.edit.title": "Ajustes de superposición",
+ "settings.hotkey.edit.desc": "Atajo que abre o cierra el editor de la superposición.",
+ "settings.hotkey.set": "Asignar atajo",
+ "settings.section.advanced": "Ajustes avanzados",
+ "settings.developer_mode.title": "Modo de desarrollador",
+ "settings.developer_mode.desc": "Muestra en la esquina superior izquierda de la superposición lo que oyó la IA (el nombre del sonido que indicó, su grado de seguridad y cuánto tardó) y, en la esquina superior derecha, los FPS. La información aparece en inglés y no está pensada para el uso diario.",
+ "settings.developer_record.title": "Guardar los resultados en un archivo",
+ "settings.developer_record.desc": "Guarda cada resultado de la IA (los nombres de sonido que puntuó más alto, su grado de seguridad y los tiempos) en un archivo CSV nuevo dentro de la carpeta ai-log, junto a SoundVisualizer.exe, para que puedas revisar después lo que oyó. Si no se puede escribir en esa carpeta, el archivo se guarda en %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Los archivos pueden ocupar mucho, así que desactívalo cuando termines.",
+ "settings.auto_minimize.title": "Minimizar el lanzador al iniciar",
+ "settings.auto_minimize.desc": "Minimiza automáticamente esta ventana del lanzador cuando se inicia la superposición.",
+
+ "sounds.title": "Elige los tipos de sonido",
+ "sounds.desc": "Los sonidos que reconoce la IA están agrupados con otros parecidos, según el tipo que tienen por defecto. Si cambias el tipo de un grupo, el color de sus sonidos cambia al instante.",
+ "sounds.sibling_note": "La IA suele reconocer un mismo sonido con varios nombres a la vez. Por ejemplo, una sirena también se reconoce como “Alarma”, que está en otro grupo. Si después de un cambio un sonido se sigue mostrando con su tipo anterior, cambia también los grupos o sonidos relacionados.",
+ "sounds.advanced.title": "Modo avanzado",
+ "sounds.advanced.desc": "Muestra cada sonido dentro de su grupo para que puedas cambiarlos uno a uno.",
+ "sounds.search.hint": "Buscar sonidos",
+ "sounds.search.clear": "Borrar búsqueda",
+ "sounds.filter.title": "Filtro",
+ "sounds.filter.all": "Todos",
+ "sounds.filter.changed": "Cambiados",
+ "sounds.type.ambient": "Sonido ambiente",
+ "sounds.type.speech": "Voz",
+ "sounds.type.danger": "Peligro",
+ "sounds.type.default": "{0} (por defecto)",
+ "sounds.type.mixed": "Mixto",
+ "sounds.changed_count": "Sonidos cambiados: {0}",
+ "sounds.changed_from": "Por defecto: {0}",
+ "sounds.reset.button": "Restablecer todo",
+ "sounds.reset.title": "¿Restablecer todos los sonidos?",
+ "sounds.reset.message": "Todos los sonidos que hayas cambiado volverán a su tipo por defecto.",
+ "sounds.reset.confirm": "Restablecer",
+ "sounds.reset.cancel": "Cancelar",
+ "sounds.empty": "No hay sonidos que coincidan.",
+ "sounds.group.mixed_note": "Algunos sonidos cambiados uno a uno",
+ "sounds.group.changed_line": "Ahora: {0} · Por defecto: {1}",
+ "sounds.overwrite.title": "¿Cambiar todo el grupo?",
+ "sounds.overwrite.message": "Sonidos del grupo “{0}” que pasarán a “{2}”: {1}. También se sobrescribirán los que cambiaste uno a uno.",
+ "sounds.overwrite.confirm": "Cambiar",
+ "sounds.overwrite.cancel": "Cancelar",
+ "sounds.basic_warning.title": "¿Cambiar al modo básico?",
+ "sounds.basic_warning.message": "Algunos grupos tienen sonidos que cambiaste uno a uno. En el modo básico, si eliges un tipo para uno de esos grupos, todos sus sonidos pasan a ese tipo y se sobrescriben los cambios que hiciste uno a uno en ese grupo. No cambia nada hasta que elijas.",
+ "sounds.basic_warning.confirm": "Cambiar a básico",
+ "sounds.basic_warning.cancel": "Seguir en avanzado",
+ "sounds.cd.sound": "{0}, tipo: {1}",
+ "sounds.cd.sound_changed": "{0}, tipo: {1}, por defecto: {2}",
+ "sounds.cd.group": "Grupo {0}, tipo: {1}, sonidos: {2} ({3})",
+ "sounds.cd.group_changed": "Grupo {0}, tipo: {1}, por defecto: {2}, sonidos: {3} ({4})",
+ "sounds.cd.group_header": "Grupo {0}, sonidos: {1}",
+
+ "hotkey.prompt": "Presiona una tecla (Esc para cancelar)",
+ "hotkey.none": "Ninguno",
+ "hotkey.button_name": "{0}, atajo: {1}",
+
+ "help.start.title": "Primeros pasos",
+ "help.start.1": "Elige el idioma en la parte superior de esta ventana y, si quieres, cambia el aspecto de los gráficos en la pestaña Ajustes.",
+ "help.start.2": "En la pestaña Inicio, haz clic en “{0}”.",
+ "help.start.3": "Los gráficos se dibujan en los bordes de la pantalla, por encima de otras ventanas, como juegos y videos. Los clics atraviesan los gráficos, así que puedes seguir usando la ventana que está debajo.",
+ "help.start.4": "Para apagarlo, haz clic en “{0}” en la pestaña Inicio, o haz clic con el botón derecho en el icono de SoundVisualizer del área de notificación de la barra de tareas y elige “{0}”.",
+ "help.modes.title": "Modos visuales",
+ "help.mode.wave": "Todo el borde de la pantalla avanza hacia dentro según el volumen. El lado del que viene el sonido avanza más.",
+ "help.mode.pad": "Se iluminan barras en 8 puntos del borde de la pantalla. Es el modo que muestra la dirección con más claridad.",
+ "help.mode.circle": "Un anillo en el centro de la pantalla se expande hacia el lado del que viene el sonido.",
+ "help.mode.outline": "Dibuja solo un contorno fino de la onda, así que tapa menos la pantalla del juego.",
+ "help.modes.note": "Opciones como Tamaño y Opacidad se guardan por separado para cada modo y puedes cambiarlas en la pestaña Ajustes.",
+ "help.sound.title": "Tipos de sonido",
+ "help.sound.kinds": "La IA clasifica el audio en sonido ambiente, voz y peligro. De forma predeterminada, el sonido ambiente se muestra en blanco, la voz en amarillo y el peligro en rojo. En la pestaña Ajustes puedes cambiar el color de cada tipo y elegir si se muestra o no.",
+ "help.sound.groups": "En la pestaña Sonidos, los sonidos parecidos están agrupados y eliges de qué tipo es cada grupo. Por ejemplo, si cambias el grupo “Timbre de la puerta” a Peligro, cuando la IA reconozca un timbre de la puerta o golpes en la puerta, el sonido tendrá el color de peligro. Eso sí, un sonido que tú cambias a Peligro solo participa en la votación de la IA sobre el tipo, así que no significa que cada timbre de la puerta se vaya a mostrar como peligro.",
+ "help.sound.advanced": "Activa el modo avanzado para cambiar los sonidos uno a uno.",
+ "help.sound.danger": "Cuando se detecta un sonido de peligro (como un disparo), el color cambia al instante en lugar de hacerlo poco a poco.",
+ "help.pc.title": "Ajustes de PC y atajos de teclado",
+ "help.sound_mode.title": "Modo de sonido",
+ "help.sound_mode.body": "Para distinguir también los sonidos de delante y de detrás, además de los de la izquierda y la derecha, configura el dispositivo de salida como “Envolvente 5.1” (6 canales) o “Envolvente 7.1” (8 canales) en la configuración de sonido de Windows. Con un estéreo normal (2 canales), la app distingue la izquierda de la derecha, pero no lo que está delante de lo que está detrás. El “Modo de sonido” de la pestaña Ajustes se adapta al dispositivo de salida al iniciar y cuando cambia el dispositivo, y también puedes cambiarlo tú.",
+ "help.sound_mode.tip": "💡 Si no tienes un dispositivo de salida 7.1, puedes instalar el {0} y configurar el dispositivo de salida en 7.1 canales para mostrar la superposición con sonido envolvente 7.1.",
+ "help.sound_mode.tip_link": "controlador virtual VB-CABLE",
+ "help.hotkeys.title": "Atajos personalizados en tiempo real",
+ "help.hotkeys.body": "En la pestaña Ajustes, haz clic en un botón de atajo y presiona una tecla o una combinación de teclas (p. ej., Ctrl + Shift + A) para cambiar el atajo a tu gusto. Al presionar el atajo, el modo cambia al instante, incluso con la app en segundo plano.",
+ "help.hotkeys.defaults": "De forma predeterminada, F2 cambia el modo de sonido, F3 cambia el modo visual y F4 abre los ajustes de superposición.",
+ "help.size_lock.title": "Modo de tamaño bloqueado",
+ "help.size_lock.body": "El modo de tamaño bloqueado mantiene los gráficos con un tamaño fijo y usa una transparencia proporcional al volumen del sonido. Es útil si quieres que los gráficos conserven siempre el mismo tamaño y aun así ver cómo reaccionan al sonido.",
+ "help.editor.title": "Modo de edición de la superposición en tiempo real",
+ "help.editor.body": "Presiona el atajo asignado (F4 de forma predeterminada) para activar el modo de edición de la superposición. Puedes arrastrar sobre la pantalla para ajustar el límite de tamaño de los gráficos y afinar los detalles en el panel de ajustes que aparece.",
+ "help.developer.title": "Modo de desarrollador",
+ "help.developer.body": "Al activar el modo de desarrollador, la superposición muestra arriba a la izquierda lo que oyó la IA (el nombre del sonido, su grado de seguridad y cuánto tardó) y arriba a la derecha los FPS. Si además activas “{0}”, cada resultado se guarda en un archivo CSV. Es útil para comprobar que el sistema funciona correctamente.",
+ "help.notes.title": "Ten en cuenta",
+ "help.note.direction_surround": "Con salida de 5.1 o 7.1 canales, la parte superior de la pantalla muestra el sonido que viene de delante, y la inferior, el que viene de detrás. Los lados izquierdo y derecho muestran el sonido que viene de la izquierda y de la derecha, y los gráficos que aparecen a la vez desde todos los lados de la pantalla corresponden a sonido ambiente o a sonidos sin dirección.",
+ "help.note.direction_stereo": "Con salida de 2 canales (estéreo), la app distingue la izquierda de la derecha, pero no lo que está delante de lo que está detrás. En ese caso, los gráficos de las partes superior e inferior de la pantalla representan los sonidos que se oyen por igual en ambos lados y la sensación de espacio.",
+ "help.note.sources": "Se muestra el sonido de todas las apps que se reproducen por el dispositivo de reproducción predeterminado de Windows. No se muestra el sonido que una app envía a otro dispositivo, como unos auriculares que solo usas para el chat de voz.",
+ "help.note.volume": "Si el volumen de una app está muy bajo o silenciado, en la propia app o en el mezclador de volumen de Windows, su sonido puede apenas mostrarse o no mostrarse en absoluto. Si los gráficos no responden bien, sube un poco ese sonido o aumenta la Sensibilidad en la pestaña Ajustes.",
+ "help.note.ai": "La IA puede equivocarse al clasificar los sonidos. No confíes solo en esta app cuando tu seguridad esté en juego.",
+ "help.faq.title": "Preguntas frecuentes",
+ "help.faq.stopped_q": "Se apagó sin que yo lo detuviera.",
+ "help.faq.stopped_a": "Se apaga cuando la captura de sonido se detiene y no se recupera en unos segundos, por ejemplo, cuando se desconecta el dispositivo de reproducción y no hay otro disponible. En ese caso, la pestaña Inicio te indica el motivo. Conecta altavoces o auriculares, o establece un dispositivo de reproducción predeterminado en la configuración de sonido de Windows, y vuelve a iniciarlo.",
+ "help.faq.no_graphic_q": "No veo ningún gráfico.",
+ "help.faq.no_graphic_a": "Comprueba que el sonido se reproduce por el dispositivo de reproducción predeterminado de Windows y no está silenciado, y si la pestaña Inicio muestra algún aviso. Si un juego se ejecuta en pantalla completa exclusiva, prueba su modo sin bordes o en ventana. En la pestaña Ajustes, asegúrate también de que la Opacidad no esté demasiado baja y de que no esté desactivada la visualización del tipo de sonido que se está reproduciendo.",
+ "help.faq.performance_q": "¿Usa mucha CPU o GPU?",
+ "help.faq.performance_a": "Para que consuma menos, baja “{0}” en la pestaña Ajustes o desactiva “{1}”.",
+ "help.faq.language_q": "¿Cómo cambio el idioma de la app?",
+ "help.faq.language_a": "Elige uno en la lista “{0}” de la parte superior de esta ventana. Mientras se ejecuta, también puedes cambiarlo en los ajustes de superposición. Con “{1}”, la app usa el idioma de visualización de Windows.",
+ "help.report.title": "Informar de un problema",
+ "help.report.desc": "Avísanos de un error o de un sonido que se mostró con el tipo de sonido equivocado. GitHub se abre en tu navegador con la información del sistema de abajo ya completada, así que necesitas una cuenta de GitHub. Si no tienes una, envíanos un correo electrónico en su lugar. Nunca se envía ningún audio.",
+ "help.report.github": "Informar en GitHub",
+ "help.report.mail": "Informar por correo",
+ "help.report.copy": "Copiar información del sistema",
+ "help.report.copied": "Información del sistema copiada.",
+ "help.report.copy_failed": "No se pudo copiar al portapapeles. Inténtalo de nuevo.",
+ "help.report.no_browser": "No se pudo abrir un navegador. Copia la información del sistema y envíanosla de otra forma.",
+ "help.report.no_mail": "No hay ninguna app de correo para enviar el informe. Copia la información del sistema y envíala de otra forma.",
+ "help.report.issue_title": "Informe desde la app",
+ "help.report.issue_body": "¿Qué pasó?\n\n¿Qué estabas escuchando (app, tipo de sonido)?\n\n¿Qué esperabas que pasara en su lugar?",
+ "help.about.title": "Acerca de",
+ "help.about.version": "Versión {0}",
+ "help.about.license": "Esta app se distribuye bajo la GNU Affero General Public License v3.0 (AGPL-3.0). Los nombres y los grupos de sonidos de la pestaña Sonidos se basan en la ontología de AudioSet y se distribuyen bajo la licencia CC BY-SA 4.0 (consulta “Licencias de código abierto” más abajo).",
+ "help.licenses.button": "Licencias de código abierto",
+ "help.licenses.title": "Licencias de código abierto",
+ "help.licenses.close": "Cerrar",
+
+ "notice.ai_unavailable": "La detección del tipo de sonido no funciona, así que todos los sonidos se muestran en el color del sonido ambiente. Prueba a apagar y volver a encender la visualización.",
+ "notice.ai_unavailable_hidden": "La detección del tipo de sonido no funciona, así que todos los sonidos cuentan como sonido ambiente, y el sonido ambiente está oculto, por lo que no se muestra nada. Activa “{0}” en la pestaña Ajustes o prueba a apagar y volver a encender la visualización.",
+ "notice.stopped.title": "El visor de sonido se apagó",
+ "notice.stopped.no_device": "No se pudo abrir ningún dispositivo de reproducción, así que no se puede capturar el sonido. Conecta altavoces o auriculares, o establece un dispositivo de reproducción predeterminado en la configuración de sonido de Windows, y vuelve a iniciarlo.",
+ "notice.stopped.capture_error": "La captura de sonido se detuvo por un error, así que los gráficos están desactivados.",
+ "notice.dismiss": "Cerrar aviso",
+
+ "tray.open": "Abrir el lanzador",
+ "tray.exit": "Salir",
+
+ "more_info.title": "Repositorio de GitHub y cómo contribuir",
+ "more_info.body": "SoundVisualizer es un proyecto de código abierto y su código fuente es público en GitHub. Puedes participar informando de errores, sugiriendo funciones o contribuyendo al desarrollo. ¡Haz clic en el botón de abajo para visitar el repositorio oficial de GitHub! También puedes buscar y descargar la versión más reciente en la pestaña Releases de GitHub.",
+ "more_info.github_button": "Visitar el repositorio de GitHub",
+
+ "dialog.notice_title": "Aviso",
+ "dialog.already_running": "La superposición ya se está ejecutando.",
+ "dialog.reset_title": "Restablecer ajustes",
+ "dialog.reset_wave": "¿Restablecer los ajustes del modo Onda a sus valores predeterminados?",
+ "dialog.reset_pad": "¿Restablecer los ajustes del modo Pad a sus valores predeterminados?",
+ "dialog.reset_circle": "¿Restablecer los ajustes del modo Círculo a sus valores predeterminados?",
+ "dialog.reset_outline": "¿Restablecer los ajustes del modo Contorno a sus valores predeterminados?",
+ "dialog.reset_sound_types": "¿Restablecer los ajustes de los tipos de sonido a sus valores predeterminados?",
+ "dialog.reset_all_title": "Restablecer valores predeterminados",
+ "dialog.reset_all": "¿Seguro que quieres restablecer todos los ajustes a sus valores predeterminados?",
+ "dialog.link_error": "No se pudo abrir el enlace: {0}",
+
+ "editor.title": "Ajustes de superposición",
+ "editor.drag_tip": "¡Arrastra las guías de la pantalla para ajustar en tiempo real el límite de altura y el tamaño del centro de la onda de la superposición!",
+ "editor.glow": "Efecto de resplandor",
+ "editor.glow_strength": "Intensidad",
+ "editor.color": "Color",
+ "editor.hotkeys": "Ajustes de atajos",
+ "editor.developer_mode_desc": "Lo que oyó la IA (arriba a la izquierda) y los FPS (arriba a la derecha)",
+ "editor.save_close_hint": "para guardar y cerrar",
+ "editor.close_overlay": "Cerrar la superposición",
+ "editor.guide.limit": "Límite en modo {0}",
+ "editor.guide.size": "Tamaño en modo {0}: {1} %",
+ "editor.guide.base_radius": "Radio base: {0}",
+ "editor.guide.circle_size": "Tamaño del círculo: {0} %",
+
+ "hud.visual_mode": "Modo visual: [{0}] {1}",
+ "hud.sound_mode": "Modo de sonido: [{0}] {1}",
+ "hud.overlay_settings": "Ajustes de superposición: [{0}]",
+ "hud.status.ready": "Sistema listo",
+ "hud.status.stereo": "Audio del PC: {0}ch (estéreo)\nFuente: esperando...",
+ "hud.status.surround": "Audio del PC: 8ch (7.1)\nFuente: esperando...",
+
+ "color_picker.title": "Elige un color",
+ "color_picker.hex": "Código HEX",
+ "color_picker.cancel": "Cancelar",
+ "color_picker.apply": "Aplicar",
+ "color_picker.area": "Saturación y brillo",
+ "color_picker.area_value": "Saturación {0} %, brillo {1} %",
+ "color_picker.hue": "Tono",
+ "color_picker.keys_help": "Usa las teclas de flecha para cambiarlo. Mantén presionada la tecla Shift para avanzar en pasos más grandes.",
+ "color_picker.presets": "Colores habituales",
+ "color_picker.preset.white": "Blanco",
+ "color_picker.preset.red": "Rojo",
+ "color_picker.preset.orange": "Naranja",
+ "color_picker.preset.yellow": "Amarillo",
+ "color_picker.preset.green": "Verde",
+ "color_picker.preset.cyan": "Turquesa",
+ "color_picker.preset.blue": "Azul",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "No se encontró ningún dispositivo de salida.",
+ "audio.error.capture_stopped": "La captura se detuvo inesperadamente: {0}",
+ "audio.error.capture_start_failed": "No se pudo iniciar el motor de captura: {0}",
+
+ "tutorial.window_title": "Tutorial",
+ "tutorial.skip": "Omitir",
+ "tutorial.previous": "Página anterior",
+ "tutorial.next": "Siguiente",
+ "tutorial.done": "Entendido",
+ "tutorial.page_status": "Página {0} de {1}",
+ "tutorial.pause": "Pausar animación",
+ "tutorial.play": "Reproducir animación",
+ "tutorial.sound_on": "Activar sonido",
+ "tutorial.sound_off": "Desactivar sonido",
+ "tutorial.sound.title": "Mira el sonido en tu pantalla",
+ "tutorial.sound.body": "El sonido de tu juego o video se dibuja en los bordes de la pantalla. Cuanto más fuerte es, más grande se dibuja. Solo se muestra el sonido que se reproduce en este PC. Los sonidos de tu entorno no se captan.",
+ "tutorial.direction.title": "Muestra de dónde viene el sonido",
+ "tutorial.direction.body": "Con salida de 2 canales (estéreo), un sonido que viene de la izquierda se dibuja más grande a la izquierda, y uno que viene de la derecha, a la derecha. Con salida de 5.1 o 7.1 canales, también distingue entre delante y detrás: el sonido que viene de delante se dibuja en la parte superior de la pantalla, y el que viene de detrás, en la inferior.",
+ "tutorial.types.title": "Cada tipo de sonido tiene su color",
+ "tutorial.types.body": "La IA clasifica los sonidos en tres tipos y dibuja cada uno con su propio color. Puedes cambiar los colores en la pestaña Ajustes.",
+ "tutorial.type.ambient_examples": "Música, lluvia y todo lo demás",
+ "tutorial.type.speech_examples": "Personas que hablan",
+ "tutorial.type.danger_examples": "Disparos, explosiones, sirenas",
+ "tutorial.start.title": "Cómo encenderlo y apagarlo",
+ "tutorial.start.hotkeys": "Mientras se ejecuta, pulsa {0} para cambiar el modo de sonido, {1} para cambiar el modo visual y {2} para abrir los ajustes de superposición."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/fr.json b/SoundVisualizer.Core/Localization/Strings/fr.json
new file mode 100644
index 0000000..0f40142
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/fr.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Langue",
+ "common.reset_defaults": "Restaurer les valeurs par défaut",
+ "common.minimize": "Réduire",
+ "common.close": "Fermer",
+ "language.system": "Langue du système",
+
+ "launcher.window_title": "Lanceur SoundVisualizer",
+ "launcher.tab.home": "Accueil",
+ "launcher.tab.settings": "Paramètres",
+ "launcher.tab.sounds": "Sons",
+ "launcher.tab.help": "Aide",
+ "launcher.tab.more_info": "En savoir plus",
+ "launcher.home.desc": "Dessine sur votre écran les sons que vous ne pouvez pas voir.\nDes jeux vidéo aux films, découvrez une toute nouvelle expérience.",
+ "launcher.home.start": "Démarrer",
+ "launcher.home.stop": "Arrêter",
+ "launcher.home.tutorial": "Voir le tutoriel",
+ "launcher.status.running": "État : actif",
+ "launcher.status.idle": "État : prêt",
+
+ "settings.section.mode": "Paramètres du mode",
+ "settings.visual_mode.title": "Mode visuel",
+ "settings.visual_mode.desc": "Choisissez la forme de base des graphismes dessinés à l'écran.",
+ "settings.mode.wave": "Vague",
+ "settings.mode.pad": "Pavé",
+ "settings.mode.circle": "Cercle",
+ "settings.mode.outline": "Contour",
+ "settings.sound_mode.title": "Mode audio",
+ "settings.sound_mode.desc": "Choisissez les canaux audio adaptés à vos haut-parleurs.\nPour des haut-parleurs à 2 canaux, choisissez « 2 canaux ». Pour des haut-parleurs surround, choisissez « 5.1 canaux » ou « 7.1 canaux ».",
+ "settings.channels.2": "2 canaux",
+ "settings.channels.5_1": "5.1 canaux",
+ "settings.channels.7_1": "7.1 canaux",
+ "settings.max_fps.title": "Fréquence d'images max",
+ "settings.max_fps.desc": "Définit la fréquence d'images maximale à laquelle les graphismes sont dessinés.",
+ "settings.section.mode_detail": "Réglages par mode",
+ "settings.mode_card.wave": "Mode Vague",
+ "settings.mode_card.pad": "Mode Pavé",
+ "settings.mode_card.circle": "Mode Cercle",
+ "settings.mode_card.outline": "Mode Contour",
+ "settings.size_lock.title": "Verrouiller la taille",
+ "settings.size_lock.desc": "Garde les graphismes à une taille fixe et traduit plutôt le volume sonore en transparence.",
+ "settings.fixed_size.wave": "Taille de la vague",
+ "settings.fixed_size.pad": "Taille du pavé",
+ "settings.fixed_size.circle": "Taille du cercle",
+ "settings.fixed_size.outline": "Épaisseur du trait",
+ "settings.fixed_size.desc": "Règle la taille fixe que gardent les graphismes pendant que leur transparence varie.",
+ "settings.max_opacity.title": "Opacité max",
+ "settings.max_opacity.desc": "Opacité atteinte par les graphismes quand le son est le plus fort.",
+ "settings.size.title": "Taille",
+ "settings.size.desc.wave": "Règle la hauteur de la vague et son ampleur générale.",
+ "settings.size.desc.pad": "Règle l'épaisseur et la largeur des pavés quand ils réagissent au son.",
+ "settings.size.desc.circle": "Règle à quel point l'égaliseur circulaire gonfle quand il y a du son.",
+ "settings.size.desc.outline": "Règle la hauteur de la vague de contour et son ampleur générale.",
+ "settings.opacity.title": "Opacité",
+ "settings.opacity.desc.wave": "Plus la valeur est élevée, plus les graphismes sont marqués ; plus elle est basse, plus ils sont pâles et transparents.",
+ "settings.opacity.desc.pad": "Plus la valeur est élevée, plus les graphismes sont marqués ; plus elle est basse, plus ils sont pâles et transparents.",
+ "settings.opacity.desc.circle": "Plus la valeur est élevée, plus les graphismes sont marqués ; plus elle est basse, plus ils sont pâles et transparents.",
+ "settings.opacity.desc.outline": "Plus la valeur est élevée, plus les graphismes sont marqués ; plus elle est basse, plus ils sont pâles et transparents.",
+ "settings.speed.title": "Vitesse",
+ "settings.speed.desc.wave": "Règle la vitesse à laquelle la vague s'écoule et ondule.",
+ "settings.speed.desc.pad": "Règle la vitesse à laquelle les pavés pivotent vers la direction d'où vient un son.",
+ "settings.speed.desc.circle": "Règle la vitesse à laquelle l'égaliseur circulaire ondule et tourne en réaction au son.",
+ "settings.speed.desc.outline": "Règle la vitesse à laquelle la vague de contour s'écoule et ondule.",
+ "settings.sensitivity.title": "Sensibilité",
+ "settings.sensitivity.desc.wave": "Règle la sensibilité de la vague, qui réagit et s'agite même aux sons faibles.",
+ "settings.sensitivity.desc.pad": "Règle les pavés pour qu'ils captent mieux le son et ressortent davantage.",
+ "settings.sensitivity.desc.circle": "Règle la finesse avec laquelle l'amplitude du cercle se fragmente en réagissant au son.",
+ "settings.sensitivity.desc.outline": "Règle la sensibilité de la vague de contour, qui réagit et s'agite même aux sons faibles.",
+ "settings.glow.title": "Halo",
+ "settings.glow.desc.wave": "Ajoute un doux halo néon le long du bord de la vague et en règle l'intensité.\nRemarque : consomme davantage de ressources GPU.",
+ "settings.glow.desc.pad": "Ajoute un halo doux, comme une aura, autour des pavés.\nRemarque : consomme davantage de ressources GPU.",
+ "settings.glow.desc.circle": "Ajoute un halo scintillant le long du bord de l'égaliseur circulaire.\nRemarque : consomme davantage de ressources GPU.",
+ "settings.glow.desc.outline": "Ajoute un doux halo néon au contour et en règle l'intensité.\nRemarque : consomme davantage de ressources GPU.",
+ "settings.radius.title": "Rayon",
+ "settings.radius.desc": "Règle le rayon de l'anneau central en mode Cercle.",
+ "settings.section.sound_types": "Types de sons (IA)",
+ "settings.show_ambient": "Afficher l'ambiance",
+ "settings.show_speech": "Afficher les voix",
+ "settings.show_danger": "Afficher les dangers",
+ "settings.set_color": "Choisir la couleur",
+ "settings.set_color_for": "Choisir la couleur : {0}",
+ "settings.section.hotkeys": "Raccourcis clavier",
+ "settings.hotkey.visual.title": "Changer de mode visuel",
+ "settings.hotkey.visual.desc": "Raccourci qui change la forme en temps réel pendant l'exécution.",
+ "settings.hotkey.sound.title": "Changer de mode audio",
+ "settings.hotkey.sound.desc": "Raccourci qui change le mode audio en temps réel pendant l'exécution.",
+ "settings.hotkey.edit.title": "Paramètres de l'overlay",
+ "settings.hotkey.edit.desc": "Raccourci qui ouvre ou ferme l'éditeur de l'overlay.",
+ "settings.hotkey.set": "Définir le raccourci",
+ "settings.section.advanced": "Paramètres avancés",
+ "settings.developer_mode.title": "Mode développeur",
+ "settings.developer_mode.desc": "Affiche en haut à gauche de l'overlay ce que l'IA a entendu (le nom du son indiqué, son degré de certitude et le temps de traitement), et les FPS en haut à droite. Ces informations sont en anglais et ne sont pas destinées à un usage quotidien.",
+ "settings.developer_record.title": "Enregistrer les résultats dans un fichier",
+ "settings.developer_record.desc": "Enregistre chaque résultat de l'IA (les noms de sons les mieux classés, son degré de certitude et les temps de traitement) dans un nouveau fichier CSV du dossier ai-log, à côté de SoundVisualizer.exe, pour revoir ensuite ce qu'elle a entendu. Si ce dossier n'est pas accessible en écriture, le fichier va dans %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Les fichiers peuvent devenir volumineux. Désactivez l'option une fois terminé.",
+ "settings.auto_minimize.title": "Réduire le lanceur au démarrage",
+ "settings.auto_minimize.desc": "Réduit automatiquement la fenêtre du lanceur au démarrage de l'overlay.",
+
+ "sounds.title": "Choisir les types de sons",
+ "sounds.desc": "Les sons que l'IA reconnaît sont regroupés avec des sons proches, selon le type auquel chacun appartient par défaut. Si vous changez le type d'un groupe, la couleur de ses sons suit aussitôt le nouveau type.",
+ "sounds.sibling_note": "L'IA reconnaît souvent un même son sous plusieurs noms à la fois. Une sirène, par exemple, est aussi reconnue comme « Alarme », qui se trouve dans un autre groupe. Si un son s'affiche encore avec son ancien type après une modification, modifiez aussi les groupes ou les sons liés.",
+ "sounds.advanced.title": "Mode avancé",
+ "sounds.advanced.desc": "Affiche chaque son dans son groupe pour que vous puissiez modifier les sons un par un.",
+ "sounds.search.hint": "Rechercher un son",
+ "sounds.search.clear": "Effacer la recherche",
+ "sounds.filter.title": "Filtre",
+ "sounds.filter.all": "Tous",
+ "sounds.filter.changed": "Modifiés",
+ "sounds.type.ambient": "Ambiance",
+ "sounds.type.speech": "Voix",
+ "sounds.type.danger": "Danger",
+ "sounds.type.default": "{0} (par défaut)",
+ "sounds.type.mixed": "Mixte",
+ "sounds.changed_count": "Sons modifiés : {0}",
+ "sounds.changed_from": "Par défaut : {0}",
+ "sounds.reset.button": "Tout réinitialiser",
+ "sounds.reset.title": "Réinitialiser tous les sons ?",
+ "sounds.reset.message": "Tous les sons que vous avez modifiés reprendront leur type par défaut.",
+ "sounds.reset.confirm": "Réinitialiser",
+ "sounds.reset.cancel": "Annuler",
+ "sounds.empty": "Aucun son ne correspond.",
+ "sounds.group.mixed_note": "Certains sons modifiés un par un",
+ "sounds.group.changed_line": "Actuel : {0} · Par défaut : {1}",
+ "sounds.overwrite.title": "Modifier tout le groupe ?",
+ "sounds.overwrite.message": "Sons du groupe « {0} » qui passeront en {2} : {1}. Les sons modifiés un par un seront eux aussi remplacés.",
+ "sounds.overwrite.confirm": "Modifier",
+ "sounds.overwrite.cancel": "Annuler",
+ "sounds.basic_warning.title": "Passer au mode simple ?",
+ "sounds.basic_warning.message": "Certains groupes contiennent des sons que vous avez modifiés un par un. En mode simple, si vous choisissez un type pour l'un de ces groupes, tous ses sons prennent ce type et vos modifications individuelles dans ce groupe sont remplacées. Rien ne change tant que vous ne choisissez pas.",
+ "sounds.basic_warning.confirm": "Passer au mode simple",
+ "sounds.basic_warning.cancel": "Rester en mode avancé",
+ "sounds.cd.sound": "{0}, type : {1}",
+ "sounds.cd.sound_changed": "{0}, type : {1}, par défaut : {2}",
+ "sounds.cd.group": "Groupe {0}, type : {1}, sons : {2} ({3})",
+ "sounds.cd.group_changed": "Groupe {0}, type : {1}, par défaut : {2}, sons : {3} ({4})",
+ "sounds.cd.group_header": "Groupe {0}, sons : {1}",
+
+ "hotkey.prompt": "Appuyez sur une touche (Esc pour annuler)",
+ "hotkey.none": "Aucun",
+ "hotkey.button_name": "{0}, raccourci : {1}",
+
+ "help.start.title": "Pour commencer",
+ "help.start.1": "Choisissez la langue en haut de cette fenêtre et, si vous le souhaitez, modifiez l'apparence des graphismes dans l'onglet Paramètres.",
+ "help.start.2": "Dans l'onglet Accueil, cliquez sur « {0} ».",
+ "help.start.3": "Les graphismes s'affichent le long des bords de l'écran, par-dessus les autres fenêtres comme les jeux et les vidéos. Les clics les traversent : vous pouvez donc continuer à utiliser la fenêtre en dessous.",
+ "help.start.4": "Pour désactiver la visualisation, cliquez sur « {0} » dans l'onglet Accueil, ou faites un clic droit sur l'icône SoundVisualizer dans la zone de notification de la barre des tâches et choisissez « {0} ».",
+ "help.modes.title": "Modes visuels",
+ "help.mode.wave": "Tous les bords de l'écran se resserrent vers l'intérieur en fonction du volume sonore, et davantage du côté d'où vient le son.",
+ "help.mode.pad": "Des barres s'allument à 8 endroits sur le bord de l'écran. C'est le mode qui montre la direction le plus clairement.",
+ "help.mode.circle": "Un anneau au centre de l'écran gonfle du côté d'où vient le son.",
+ "help.mode.outline": "Dessine uniquement un fin contour de la vague, pour moins masquer votre jeu.",
+ "help.modes.note": "Les réglages comme « Taille » et « Opacité » sont enregistrés séparément pour chaque mode et peuvent être modifiés dans l'onglet Paramètres.",
+ "help.sound.title": "Types de sons",
+ "help.sound.kinds": "L'IA classe les sons entendus en trois types : ambiance, voix et danger. Par défaut, l'ambiance s'affiche en blanc, les voix en jaune et les dangers en rouge. Dans l'onglet Paramètres, vous pouvez changer la couleur de chaque type et choisir de l'afficher ou non.",
+ "help.sound.groups": "Dans l'onglet Sons, les sons proches sont regroupés et vous choisissez à quel type correspond chaque groupe. Par exemple, si vous passez le groupe « Sonnette » en Danger, quand l'IA reconnaît une sonnette ou des coups à la porte, le son prend la couleur des sons de danger. Toutefois, un son que vous passez vous-même en Danger ne participe qu'au vote de l'IA sur le type, donc chaque sonnette ne sera pas forcément affichée comme un danger.",
+ "help.sound.advanced": "Activez le mode avancé pour modifier les sons un par un.",
+ "help.sound.danger": "Lorsqu'un son de danger (comme un coup de feu) est détecté, la couleur change instantanément, sans transition progressive.",
+ "help.pc.title": "Paramètres PC et raccourcis clavier",
+ "help.sound_mode.title": "Mode audio",
+ "help.sound_mode.body": "Pour distinguer aussi l'avant de l'arrière, en plus de la gauche et de la droite, configurez votre périphérique de sortie en « Surround 5.1 » (6 canaux) ou « Surround 7.1 » (8 canaux) dans les paramètres de son de Windows. En stéréo classique (2 canaux), l'application distingue la gauche de la droite, mais pas l'avant de l'arrière. Le « Mode audio » de l'onglet Paramètres s'adapte au périphérique de sortie au démarrage et lorsque le périphérique change, et vous pouvez aussi le modifier vous-même.",
+ "help.sound_mode.tip": "💡 Si vous n'avez pas de périphérique de sortie 7.1, vous pouvez installer le {0} et configurer le périphérique de sortie en 7.1 canaux pour afficher l'overlay en son surround 7.1.",
+ "help.sound_mode.tip_link": "pilote virtuel VB-CABLE",
+ "help.hotkeys.title": "Raccourcis personnalisés en temps réel",
+ "help.hotkeys.body": "Dans l'onglet Paramètres, cliquez sur un bouton de raccourci, puis appuyez sur une touche ou une combinaison de touches (par ex. Ctrl + Shift + A) pour changer le raccourci à votre guise. Appuyer sur le raccourci change aussitôt de mode, même quand l'application est en arrière-plan.",
+ "help.hotkeys.defaults": "Par défaut, F2 change le mode audio, F3 change le mode visuel et F4 ouvre les paramètres de l'overlay.",
+ "help.size_lock.title": "Mode taille verrouillée",
+ "help.size_lock.body": "Le mode taille verrouillée garde les graphismes à une taille fixe en utilisant une transparence proportionnelle au volume sonore. C'est utile si vous voulez que les graphismes gardent toujours la même taille tout en continuant à les voir réagir au son.",
+ "help.editor.title": "Mode d'édition de l'overlay en temps réel",
+ "help.editor.body": "Appuyez sur le raccourci attribué (F4 par défaut) pour activer le mode d'édition de l'overlay. Vous pouvez faire glisser la souris à l'écran pour ajuster la limite de taille des graphismes, puis affiner les détails dans le panneau de paramètres qui s'affiche.",
+ "help.developer.title": "Mode développeur",
+ "help.developer.body": "Une fois activé, le mode développeur affiche en haut à gauche de l'overlay ce que l'IA a entendu (le nom du son, son degré de certitude et le temps de traitement) et les FPS en haut à droite. Si vous activez aussi « {0} », chaque résultat est enregistré dans un fichier CSV. C'est utile pour vérifier que le système fonctionne correctement.",
+ "help.notes.title": "Bon à savoir",
+ "help.note.direction_surround": "Avec une sortie 5.1 ou 7.1 canaux, le haut de l'écran montre les sons venant de l'avant, et le bas, ceux venant de l'arrière. Les côtés gauche et droit montrent les sons venant de la gauche et de la droite, et les graphismes qui apparaissent en même temps de tous les côtés de l'écran correspondent à des sons d'ambiance ou sans direction.",
+ "help.note.direction_stereo": "Avec une sortie 2 canaux (stéréo), l'application distingue la gauche de la droite, mais pas l'avant de l'arrière. Les graphismes en haut et en bas de l'écran représentent alors les sons entendus de façon similaire des deux côtés, ainsi que la sensation d'espace.",
+ "help.note.sources": "L'application affiche le son de toutes les applications diffusé par le périphérique de lecture par défaut de Windows. Le son qu'une application envoie vers un autre périphérique, par exemple un casque réservé au chat vocal, n'est pas affiché.",
+ "help.note.volume": "Si le volume d'une application est très bas ou coupé, dans l'application elle-même ou dans le mélangeur de volume de Windows, son son peut à peine s'afficher, voire pas du tout. Si les graphismes réagissent mal, montez un peu ce son ou augmentez la Sensibilité dans l'onglet Paramètres.",
+ "help.note.ai": "L'IA peut se tromper dans la classification des sons. Lorsque votre sécurité est en jeu, ne comptez pas uniquement sur cette application.",
+ "help.faq.title": "Questions fréquentes",
+ "help.faq.stopped_q": "La visualisation s'est désactivée sans que je l'aie arrêtée.",
+ "help.faq.stopped_a": "La visualisation se désactive quand la capture du son s'arrête et ne reprend pas en quelques secondes, par exemple quand le périphérique de lecture est débranché et qu'aucun autre n'est disponible. L'onglet Accueil en indique alors la raison. Branchez des haut-parleurs ou un casque, ou définissez un périphérique de lecture par défaut dans les paramètres de son de Windows, puis redémarrez-la.",
+ "help.faq.no_graphic_q": "Aucun graphisme ne s'affiche.",
+ "help.faq.no_graphic_a": "Vérifiez que le son passe par le périphérique de lecture par défaut de Windows et qu'il n'est pas coupé, et regardez si l'onglet Accueil affiche un message. Si un jeu tourne en plein écran exclusif, essayez son mode fenêtré sans bordure ou fenêtré. Dans l'onglet Paramètres, vérifiez aussi que le réglage « Opacité » n'est pas trop faible et que l'affichage du type de son entendu n'est pas désactivé.",
+ "help.faq.performance_q": "L'application utilise-t-elle beaucoup le processeur ou la carte graphique ?",
+ "help.faq.performance_a": "Pour l'alléger, baissez « {0} » dans l'onglet Paramètres ou désactivez « {1} ».",
+ "help.faq.language_q": "Comment changer la langue de l'application ?",
+ "help.faq.language_a": "Choisissez-la dans la liste « {0} » en haut de cette fenêtre. Pendant la visualisation, vous pouvez aussi la changer dans les paramètres de l'overlay. Avec « {1} », l'application suit la langue d'affichage de Windows.",
+ "help.report.title": "Signaler un problème",
+ "help.report.desc": "Signalez-nous un bug, ou un son dont le type affiché était incorrect. GitHub s'ouvre dans votre navigateur avec les informations système ci-dessous déjà renseignées : il vous faut donc un compte GitHub. Si vous n'en avez pas, envoyez-nous plutôt un e-mail. Aucune donnée audio n'est jamais envoyée.",
+ "help.report.github": "Signaler sur GitHub",
+ "help.report.mail": "Signaler par e-mail",
+ "help.report.copy": "Copier les informations système",
+ "help.report.copied": "Informations système copiées.",
+ "help.report.copy_failed": "Impossible de copier dans le presse-papiers. Réessayez.",
+ "help.report.no_browser": "Impossible d'ouvrir un navigateur. Copiez plutôt les informations système et envoyez-les-nous.",
+ "help.report.no_mail": "Aucune application d'e-mail pour l'envoi. Copiez les informations système et transmettez-les autrement.",
+ "help.report.issue_title": "Signalement depuis l'application",
+ "help.report.issue_body": "Que s'est-il passé ?\n\nQu'écoutiez-vous (application, type de son) ?\n\nÀ quoi vous attendiez-vous plutôt ?",
+ "help.about.title": "À propos",
+ "help.about.version": "Version {0}",
+ "help.about.license": "Cette application est distribuée sous la GNU Affero General Public License v3.0 (AGPL-3.0). Les noms et les groupes de sons de l'onglet Sons sont basés sur l'ontologie AudioSet et distribués sous la licence CC BY-SA 4.0 (voir « Licences open source » ci-dessous).",
+ "help.licenses.button": "Licences open source",
+ "help.licenses.title": "Licences open source",
+ "help.licenses.close": "Fermer",
+
+ "notice.ai_unavailable": "La détection du type de son ne fonctionne pas : tous les sons s'affichent dans la couleur de l'ambiance. Essayez de désactiver puis de réactiver la visualisation.",
+ "notice.ai_unavailable_hidden": "La détection du type de son ne fonctionne pas : tous les sons comptent comme de l'ambiance, mais l'affichage de l'ambiance est désactivé, donc rien ne s'affiche. Activez « {0} » dans l'onglet Paramètres, ou essayez de désactiver puis de réactiver la visualisation.",
+ "notice.stopped.title": "Visualiseur audio désactivé",
+ "notice.stopped.no_device": "Aucun périphérique de lecture n'a pu être ouvert, le son ne peut donc pas être capturé. Branchez des haut-parleurs ou un casque, ou définissez un périphérique de lecture par défaut dans les paramètres de son de Windows, puis redémarrez.",
+ "notice.stopped.capture_error": "La capture du son s'est arrêtée à cause d'une erreur : les graphismes sont désactivés.",
+ "notice.dismiss": "Masquer le message",
+
+ "tray.open": "Ouvrir le lanceur",
+ "tray.exit": "Quitter",
+
+ "more_info.title": "Dépôt GitHub et contributions",
+ "more_info.body": "SoundVisualizer est un projet open source dont le code source est public sur GitHub. Vous pouvez y participer en signalant des bugs, en suggérant des fonctionnalités ou en contribuant au développement. Cliquez sur le bouton ci-dessous pour visiter le dépôt GitHub officiel ! Vous pouvez aussi vérifier et télécharger la dernière version dans l'onglet Releases de GitHub.",
+ "more_info.github_button": "Visiter le dépôt GitHub",
+
+ "dialog.notice_title": "Information",
+ "dialog.already_running": "L'overlay est déjà en cours d'exécution.",
+ "dialog.reset_title": "Réinitialiser les paramètres",
+ "dialog.reset_wave": "Restaurer les valeurs par défaut du mode Vague ?",
+ "dialog.reset_pad": "Restaurer les valeurs par défaut du mode Pavé ?",
+ "dialog.reset_circle": "Restaurer les valeurs par défaut du mode Cercle ?",
+ "dialog.reset_outline": "Restaurer les valeurs par défaut du mode Contour ?",
+ "dialog.reset_sound_types": "Restaurer les valeurs par défaut des types de sons ?",
+ "dialog.reset_all_title": "Restaurer les valeurs par défaut",
+ "dialog.reset_all": "Voulez-vous vraiment restaurer les valeurs par défaut de tous les paramètres ?",
+ "dialog.link_error": "Impossible d'ouvrir le lien : {0}",
+
+ "editor.title": "Paramètres de l'overlay",
+ "editor.drag_tip": "Faites glisser les repères à l'écran avec la souris pour ajuster en temps réel la limite de hauteur et la taille du centre de la vague de l'overlay !",
+ "editor.glow": "Effet de halo",
+ "editor.glow_strength": "Intensité",
+ "editor.color": "Couleur",
+ "editor.hotkeys": "Paramètres des raccourcis",
+ "editor.developer_mode_desc": "Ce que l'IA a entendu (en haut à gauche) et les FPS (en haut à droite)",
+ "editor.save_close_hint": "pour enregistrer et fermer",
+ "editor.close_overlay": "Quitter l'overlay",
+ "editor.guide.limit": "Limite en mode {0}",
+ "editor.guide.size": "Taille en mode {0} : {1} %",
+ "editor.guide.base_radius": "Rayon de base : {0}",
+ "editor.guide.circle_size": "Taille du cercle : {0} %",
+
+ "hud.visual_mode": "Mode visuel : [{0}] {1}",
+ "hud.sound_mode": "Mode audio : [{0}] {1}",
+ "hud.overlay_settings": "Paramètres de l'overlay : [{0}]",
+ "hud.status.ready": "Système prêt",
+ "hud.status.stereo": "Audio du PC : {0}ch (stéréo)\nSource : en attente...",
+ "hud.status.surround": "Audio du PC : 8ch (7.1)\nSource : en attente...",
+
+ "color_picker.title": "Choisir une couleur",
+ "color_picker.hex": "Code HEX",
+ "color_picker.cancel": "Annuler",
+ "color_picker.apply": "Appliquer",
+ "color_picker.area": "Saturation et luminosité",
+ "color_picker.area_value": "Saturation {0} %, luminosité {1} %",
+ "color_picker.hue": "Teinte",
+ "color_picker.keys_help": "Utilisez les touches fléchées pour le modifier. Maintenez la touche Maj enfoncée pour avancer par grands pas.",
+ "color_picker.presets": "Couleurs courantes",
+ "color_picker.preset.white": "Blanc",
+ "color_picker.preset.red": "Rouge",
+ "color_picker.preset.orange": "Orange",
+ "color_picker.preset.yellow": "Jaune",
+ "color_picker.preset.green": "Vert",
+ "color_picker.preset.cyan": "Turquoise",
+ "color_picker.preset.blue": "Bleu",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "Aucun périphérique de sortie n'a été trouvé.",
+ "audio.error.capture_stopped": "La capture s'est arrêtée de manière inattendue : {0}",
+ "audio.error.capture_start_failed": "Impossible de démarrer le moteur de capture : {0}",
+
+ "tutorial.window_title": "Tutoriel",
+ "tutorial.skip": "Passer",
+ "tutorial.previous": "Page précédente",
+ "tutorial.next": "Suivant",
+ "tutorial.done": "J'ai compris",
+ "tutorial.page_status": "Page {0} sur {1}",
+ "tutorial.pause": "Mettre l'animation en pause",
+ "tutorial.play": "Lire l'animation",
+ "tutorial.sound_on": "Activer le son",
+ "tutorial.sound_off": "Couper le son",
+ "tutorial.sound.title": "Voir le son sur votre écran",
+ "tutorial.sound.body": "Le son de votre jeu ou de votre vidéo est dessiné le long des bords de l'écran. Plus le son est fort, plus le dessin est grand. Seul le son diffusé sur ce PC est affiché. Les sons autour de vous ne sont pas captés.",
+ "tutorial.direction.title": "Indique d'où vient le son",
+ "tutorial.direction.body": "Avec une sortie 2 canaux (stéréo), un son venant de la gauche est dessiné plus grand à gauche, et un son venant de la droite, à droite. Avec une sortie 5.1 ou 7.1 canaux, l'application distingue aussi l'avant de l'arrière : le son venant de l'avant est dessiné en haut de l'écran, et celui venant de l'arrière, en bas.",
+ "tutorial.types.title": "Chaque type de son a sa couleur",
+ "tutorial.types.body": "L'IA classe les sons en trois types et dessine chaque type d'une couleur différente. Vous pouvez changer les couleurs dans l'onglet Paramètres.",
+ "tutorial.type.ambient_examples": "Musique, pluie et tout le reste",
+ "tutorial.type.speech_examples": "Personnes qui parlent",
+ "tutorial.type.danger_examples": "Coups de feu, explosions, sirènes",
+ "tutorial.start.title": "Activer et désactiver la visualisation",
+ "tutorial.start.hotkeys": "Pendant la visualisation, appuyez sur {0} pour changer de mode audio, sur {1} pour changer de mode visuel et sur {2} pour ouvrir les paramètres de l'overlay."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/hi.json b/SoundVisualizer.Core/Localization/Strings/hi.json
new file mode 100644
index 0000000..79765fc
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/hi.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "भाषा",
+ "common.reset_defaults": "डिफ़ॉल्ट पर रीसेट करें",
+ "common.minimize": "छोटा करें",
+ "common.close": "बंद करें",
+ "language.system": "सिस्टम डिफ़ॉल्ट",
+
+ "launcher.window_title": "SoundVisualizer लॉन्चर",
+ "launcher.tab.home": "होम",
+ "launcher.tab.settings": "सेटिंग",
+ "launcher.tab.sounds": "आवाज़ें",
+ "launcher.tab.help": "सहायता",
+ "launcher.tab.more_info": "और जानें",
+ "launcher.home.desc": "जो आवाज़ें दिखती नहीं, उन्हें स्क्रीन पर दिखाता है।\nगेमिंग से लेकर फ़िल्मों तक, एक बिल्कुल नया अनुभव पाएं।",
+ "launcher.home.start": "शुरू करें",
+ "launcher.home.stop": "बंद करें",
+ "launcher.home.tutorial": "ट्यूटोरियल देखें",
+ "launcher.status.running": "स्थिति: चालू है",
+ "launcher.status.idle": "स्थिति: तैयार",
+
+ "settings.section.mode": "मोड सेटिंग",
+ "settings.visual_mode.title": "विज़ुअल मोड",
+ "settings.visual_mode.desc": "स्क्रीन पर बनने वाले ग्राफ़िक्स का बुनियादी आकार चुनें।",
+ "settings.mode.wave": "लहर",
+ "settings.mode.pad": "पैड",
+ "settings.mode.circle": "वृत्त",
+ "settings.mode.outline": "आउटलाइन",
+ "settings.sound_mode.title": "साउंड मोड",
+ "settings.sound_mode.desc": "अपने स्पीकर के हिसाब से साउंड चैनल चुनें।\n2-चैनल स्पीकर के लिए “2 चैनल” चुनें। सराउंड स्पीकर के लिए “5.1 चैनल” या “7.1 चैनल” चुनें।",
+ "settings.channels.2": "2 चैनल",
+ "settings.channels.5_1": "5.1 चैनल",
+ "settings.channels.7_1": "7.1 चैनल",
+ "settings.max_fps.title": "अधिकतम फ़्रेम रेट",
+ "settings.max_fps.desc": "ग्राफ़िक्स ज़्यादा से ज़्यादा किस फ़्रेम रेट पर बनें, यह तय करता है।",
+ "settings.section.mode_detail": "हर मोड की सेटिंग",
+ "settings.mode_card.wave": "लहर मोड",
+ "settings.mode_card.pad": "पैड मोड",
+ "settings.mode_card.circle": "वृत्त मोड",
+ "settings.mode_card.outline": "आउटलाइन मोड",
+ "settings.size_lock.title": "साइज़ तय रखें",
+ "settings.size_lock.desc": "ग्राफ़िक्स का साइज़ एक जैसा रखता है और आवाज़ कितनी तेज़ है, यह पारदर्शिता से दिखाता है।",
+ "settings.fixed_size.wave": "लहर का साइज़",
+ "settings.fixed_size.pad": "पैड का साइज़",
+ "settings.fixed_size.circle": "वृत्त का साइज़",
+ "settings.fixed_size.outline": "आउटलाइन की मोटाई",
+ "settings.fixed_size.desc": "वह तय साइज़ सेट करता है, जिस पर ग्राफ़िक्स बने रहते हैं और सिर्फ़ उनकी पारदर्शिता बदलती है।",
+ "settings.max_opacity.title": "अधिकतम गहरापन",
+ "settings.max_opacity.desc": "सबसे तेज़ आवाज़ पर ग्राफ़िक्स कितने गहरे दिखेंगे।",
+ "settings.size.title": "साइज़",
+ "settings.size.desc.wave": "लहर की ऊंचाई और वह कुल मिलाकर कितनी भरी हुई दिखती है, यह तय करता है।",
+ "settings.size.desc.pad": "आवाज़ पर प्रतिक्रिया देते समय पैड कितने मोटे और चौड़े हों, यह तय करता है।",
+ "settings.size.desc.circle": "आवाज़ चलने पर गोल इक्वलाइज़र बाहर की ओर कितना उभरता है, यह तय करता है।",
+ "settings.size.desc.outline": "आउटलाइन लहर की ऊंचाई और वह कुल मिलाकर कितनी भरी हुई दिखती है, यह तय करता है।",
+ "settings.opacity.title": "गहरापन",
+ "settings.opacity.desc.wave": "वैल्यू ज़्यादा होने पर ग्राफ़िक्स गहरे दिखते हैं। कम होने पर हल्के और पारदर्शी दिखते हैं।",
+ "settings.opacity.desc.pad": "वैल्यू ज़्यादा होने पर ग्राफ़िक्स गहरे दिखते हैं। कम होने पर हल्के और पारदर्शी दिखते हैं।",
+ "settings.opacity.desc.circle": "वैल्यू ज़्यादा होने पर ग्राफ़िक्स गहरे दिखते हैं। कम होने पर हल्के और पारदर्शी दिखते हैं।",
+ "settings.opacity.desc.outline": "वैल्यू ज़्यादा होने पर ग्राफ़िक्स गहरे दिखते हैं। कम होने पर हल्के और पारदर्शी दिखते हैं।",
+ "settings.speed.title": "स्पीड",
+ "settings.speed.desc.wave": "लहर कितनी तेज़ी से बहती और लहराती है, यह तय करता है।",
+ "settings.speed.desc.pad": "पैड कितनी तेज़ी से उस दिशा की ओर मुड़ते हैं जहां से आवाज़ आती है, यह तय करता है।",
+ "settings.speed.desc.circle": "गोल इक्वलाइज़र आवाज़ पर कितनी तेज़ी से लहराता और घूमता है, यह तय करता है।",
+ "settings.speed.desc.outline": "आउटलाइन लहर कितनी तेज़ी से बहती और लहराती है, यह तय करता है।",
+ "settings.sensitivity.title": "संवेदनशीलता",
+ "settings.sensitivity.desc.wave": "धीमी आवाज़ों पर भी लहर कितनी संवेदनशीलता से प्रतिक्रिया देती और उछलती है, यह तय करता है।",
+ "settings.sensitivity.desc.pad": "पैड को इस तरह बदलता है कि वे आवाज़ को बेहतर पकड़ें और ज़्यादा उभरकर दिखें।",
+ "settings.sensitivity.desc.circle": "आवाज़ पर प्रतिक्रिया देते समय वृत्त का आयाम कितनी बारीकी से टूटता है, यह तय करता है।",
+ "settings.sensitivity.desc.outline": "धीमी आवाज़ों पर भी आउटलाइन लहर कितनी संवेदनशीलता से प्रतिक्रिया देती और उछलती है, यह तय करता है।",
+ "settings.glow.title": "चमक",
+ "settings.glow.desc.wave": "लहर के किनारे पर हल्की नियॉन चमक जोड़ता है और उसका स्तर तय करता है।\nध्यान दें: इससे GPU के ज़्यादा संसाधन इस्तेमाल होते हैं।",
+ "settings.glow.desc.pad": "पैड के चारों ओर आभा जैसी हल्की चमक जोड़ता है।\nध्यान दें: इससे GPU के ज़्यादा संसाधन इस्तेमाल होते हैं।",
+ "settings.glow.desc.circle": "गोल इक्वलाइज़र के किनारे पर झिलमिलाता प्रभामंडल बनाता है।\nध्यान दें: इससे GPU के ज़्यादा संसाधन इस्तेमाल होते हैं।",
+ "settings.glow.desc.outline": "आउटलाइन पर हल्की नियॉन चमक जोड़ता है और उसका स्तर तय करता है।\nध्यान दें: इससे GPU के ज़्यादा संसाधन इस्तेमाल होते हैं।",
+ "settings.radius.title": "त्रिज्या",
+ "settings.radius.desc": "वृत्त मोड में बीच वाले घेरे की त्रिज्या बदलता है।",
+ "settings.section.sound_types": "आवाज़ के प्रकार (AI)",
+ "settings.show_ambient": "माहौल की आवाज़ें दिखाएं",
+ "settings.show_speech": "बातचीत दिखाएं",
+ "settings.show_danger": "खतरे की आवाज़ें दिखाएं",
+ "settings.set_color": "रंग सेट करें",
+ "settings.set_color_for": "रंग सेट करें: {0}",
+ "settings.section.hotkeys": "शॉर्टकट कुंजियां",
+ "settings.hotkey.visual.title": "विज़ुअल मोड बदलें",
+ "settings.hotkey.visual.desc": "चलते समय आकार को तुरंत बदलने वाली शॉर्टकट कुंजी।",
+ "settings.hotkey.sound.title": "साउंड मोड बदलें",
+ "settings.hotkey.sound.desc": "चलते समय साउंड मोड को तुरंत बदलने वाली शॉर्टकट कुंजी।",
+ "settings.hotkey.edit.title": "ओवरले सेटिंग",
+ "settings.hotkey.edit.desc": "ओवरले एडिटर को खोलने या बंद करने वाली शॉर्टकट कुंजी।",
+ "settings.hotkey.set": "कुंजी सेट करें",
+ "settings.section.advanced": "एडवांस सेटिंग",
+ "settings.developer_mode.title": "डेवलपर मोड",
+ "settings.developer_mode.desc": "AI ने क्या सुना (उसका बताया ध्वनि नाम, वह कितना पक्का था और कितना समय लगा), यह ओवरले के ऊपरी बाएं कोने में और FPS ऊपरी दाएं कोने में दिखाता है। यह जानकारी अंग्रेज़ी में दिखती है और रोज़मर्रा के इस्तेमाल के लिए नहीं है।",
+ "settings.developer_record.title": "नतीजे फ़ाइल में सेव करें",
+ "settings.developer_record.desc": "AI के हर नतीजे (सबसे ऊंची रैंक वाले ध्वनि नाम, वह कितना पक्का था और कितना समय लगा) को SoundVisualizer.exe के पास वाले ai-log फ़ोल्डर में एक नई CSV फ़ाइल में लिखता है, ताकि आप बाद में देख सकें कि उसने क्या सुना। अगर उस फ़ोल्डर में लिखा नहीं जा सकता, तो फ़ाइल %LOCALAPPDATA%\\SoundVisualizer\\ai-log में सेव होती है। फ़ाइलें बड़ी हो सकती हैं, इसलिए काम पूरा होने पर इसे बंद कर दें।",
+ "settings.auto_minimize.title": "शुरू करने पर लॉन्चर छोटा करें",
+ "settings.auto_minimize.desc": "ओवरले शुरू होने पर इस लॉन्चर विंडो को अपने आप छोटा कर देता है।",
+
+ "sounds.title": "आवाज़ों के प्रकार चुनें",
+ "sounds.desc": "AI जिन आवाज़ों को पहचानता है, वे मिलती-जुलती आवाज़ों के साथ समूहों में रखी गई हैं और अपने डिफ़ॉल्ट प्रकार के हिसाब से दिखती हैं। किसी समूह का प्रकार बदलने पर, उसकी आवाज़ों का रंग तुरंत नए प्रकार के हिसाब से बदल जाता है।",
+ "sounds.sibling_note": "AI अक्सर एक ही आवाज़ को एक साथ कई नामों से पहचानता है। जैसे, सायरन “अलार्म” के रूप में भी पहचाना जाता है, जो किसी दूसरे समूह में है। बदलाव के बाद भी अगर कोई आवाज़ पुराने प्रकार में दिखे, तो उससे जुड़े समूह या आवाज़ें भी बदलें।",
+ "sounds.advanced.title": "एडवांस मोड",
+ "sounds.advanced.desc": "हर आवाज़ को उसके समूह में दिखाता है, ताकि आप आवाज़ें एक-एक करके बदल सकें।",
+ "sounds.search.hint": "आवाज़ें खोजें",
+ "sounds.search.clear": "खोज मिटाएं",
+ "sounds.filter.title": "फ़िल्टर",
+ "sounds.filter.all": "सभी",
+ "sounds.filter.changed": "बदली गई",
+ "sounds.type.ambient": "माहौल की आवाज़ें",
+ "sounds.type.speech": "बातचीत",
+ "sounds.type.danger": "खतरे की आवाज़ें",
+ "sounds.type.default": "{0} (डिफ़ॉल्ट)",
+ "sounds.type.mixed": "मिला-जुला",
+ "sounds.changed_count": "बदली गई आवाज़ें: {0}",
+ "sounds.changed_from": "डिफ़ॉल्ट: {0}",
+ "sounds.reset.button": "सभी रीसेट करें",
+ "sounds.reset.title": "सभी आवाज़ें रीसेट करें?",
+ "sounds.reset.message": "आपने जिन आवाज़ों का प्रकार बदला है, वे सभी वापस अपने डिफ़ॉल्ट प्रकार पर आ जाएंगी।",
+ "sounds.reset.confirm": "रीसेट करें",
+ "sounds.reset.cancel": "रद्द करें",
+ "sounds.empty": "मेल खाने वाली कोई आवाज़ नहीं मिली।",
+ "sounds.group.mixed_note": "कुछ आवाज़ें एक-एक करके बदली गईं",
+ "sounds.group.changed_line": "अभी: {0} · डिफ़ॉल्ट: {1}",
+ "sounds.overwrite.title": "पूरा समूह बदलें?",
+ "sounds.overwrite.message": "“{0}” समूह में “{2}” प्रकार में बदलने वाली आवाज़ें: {1}। एक-एक करके बदली गई आवाज़ों का प्रकार भी बदल जाएगा।",
+ "sounds.overwrite.confirm": "बदलें",
+ "sounds.overwrite.cancel": "रद्द करें",
+ "sounds.basic_warning.title": "बेसिक मोड पर जाएं?",
+ "sounds.basic_warning.message": "कुछ समूहों में ऐसी आवाज़ें हैं जिन्हें आपने एक-एक करके बदला है। बेसिक मोड में ऐसे किसी समूह का प्रकार चुनने पर, उसकी सभी आवाज़ों को वही प्रकार मिल जाता है और उस समूह में एक-एक करके किए गए आपके बदलाव मिट जाते हैं। जब तक आप कुछ नहीं चुनते, कुछ नहीं बदलता।",
+ "sounds.basic_warning.confirm": "बेसिक मोड पर जाएं",
+ "sounds.basic_warning.cancel": "एडवांस मोड में रहें",
+ "sounds.cd.sound": "{0}, प्रकार: {1}",
+ "sounds.cd.sound_changed": "{0}, प्रकार: {1}, डिफ़ॉल्ट: {2}",
+ "sounds.cd.group": "{0} समूह, प्रकार: {1}, आवाज़ें: {2} ({3})",
+ "sounds.cd.group_changed": "{0} समूह, प्रकार: {1}, डिफ़ॉल्ट: {2}, आवाज़ें: {3} ({4})",
+ "sounds.cd.group_header": "{0} समूह, आवाज़ें: {1}",
+
+ "hotkey.prompt": "कोई कुंजी दबाएं (रद्द करने के लिए Esc)",
+ "hotkey.none": "कोई नहीं",
+ "hotkey.button_name": "{0}, शॉर्टकट कुंजी: {1}",
+
+ "help.start.title": "शुरुआत कैसे करें",
+ "help.start.1": "इस विंडो में ऊपर भाषा चुनें, और चाहें तो “सेटिंग” टैब में ग्राफ़िक्स का रूप बदलें।",
+ "help.start.2": "“होम” टैब में “{0}” पर क्लिक करें।",
+ "help.start.3": "ग्राफ़िक्स स्क्रीन के किनारों पर, गेम और वीडियो जैसी दूसरी विंडो के ऊपर बनते हैं। माउस क्लिक ग्राफ़िक्स के आर-पार चले जाते हैं, इसलिए आप नीचे वाली विंडो का इस्तेमाल करते रह सकते हैं।",
+ "help.start.4": "बंद करने के लिए, “होम” टैब में “{0}” पर क्लिक करें, या टास्कबार के सूचना क्षेत्र में SoundVisualizer आइकन पर राइट-क्लिक करके “{0}” चुनें।",
+ "help.modes.title": "विज़ुअल मोड",
+ "help.mode.wave": "आवाज़ जितनी तेज़ होती है, स्क्रीन का पूरा किनारा उतना ही अंदर की ओर बढ़ता है। जिस तरफ़ से आवाज़ आती है, वह हिस्सा और ज़्यादा अंदर तक आता है।",
+ "help.mode.pad": "स्क्रीन के किनारों पर 8 जगह पट्टियां चमकती हैं। इसमें दिशा सबसे साफ़ दिखती है।",
+ "help.mode.circle": "स्क्रीन के बीच में बना घेरा उस ओर फूलता है, जिस ओर से आवाज़ आती है।",
+ "help.mode.outline": "लहर की सिर्फ़ पतली बाहरी रेखा बनाता है, इसलिए गेम की स्क्रीन कम ढकती है।",
+ "help.modes.note": "“साइज़” और “गहरापन” जैसी सेटिंग हर मोड के लिए अलग से सेव होती हैं। इन्हें “सेटिंग” टैब में बदला जा सकता है।",
+ "help.sound.title": "आवाज़ के प्रकार",
+ "help.sound.kinds": "AI सुनाई देने वाली आवाज़ों को तीन प्रकारों में बांटता है: माहौल की आवाज़ें, बातचीत और खतरे की आवाज़ें। डिफ़ॉल्ट रूप से माहौल की आवाज़ें सफ़ेद, बातचीत पीले और खतरे की आवाज़ें लाल रंग में दिखती हैं। “सेटिंग” टैब में हर प्रकार का रंग बदला जा सकता है और यह भी चुना जा सकता है कि उसे दिखाना है या नहीं।",
+ "help.sound.groups": "“आवाज़ें” टैब में मिलती-जुलती आवाज़ें समूहों में रखी जाती हैं, और आप चुनते हैं कि हर समूह किस प्रकार में गिना जाए। जैसे, “दरवाज़े की घंटी” समूह को खतरे की आवाज़ों में बदलने पर, जब AI को दरवाज़े की घंटी या दरवाज़ा खटखटाना सुनाई देता है, तो उस आवाज़ को खतरे की आवाज़ों वाला रंग मिलता है। लेकिन जिस आवाज़ को आप खुद खतरे की आवाज़ों में रखते हैं, वह सिर्फ़ उस वोट में शामिल होती है जिससे AI प्रकार तय करता है, इसलिए इसका मतलब यह नहीं है कि हर बार दरवाज़े की घंटी बजने पर वह खतरे की आवाज़ के रूप में दिखेगी।",
+ "help.sound.advanced": "आवाज़ें एक-एक करके बदलने के लिए एडवांस मोड चालू करें।",
+ "help.sound.danger": "खतरे की आवाज़ (जैसे गोली चलने की आवाज़) पहचानी जाने पर रंग धीरे-धीरे बदलने के बजाय तुरंत बदल जाता है।",
+ "help.pc.title": "PC सेटिंग और शॉर्टकट कुंजियां",
+ "help.sound_mode.title": "साउंड मोड",
+ "help.sound_mode.body": "बाएं-दाएं के साथ आगे-पीछे की आवाज़ें भी अलग से देखने के लिए, Windows की साउंड सेटिंग में अपने आउटपुट डिवाइस को “5.1 सराउंड” (6 चैनल) या “7.1 सराउंड” (8 चैनल) पर सेट करें। सामान्य स्टीरियो (2 चैनल) में ऐप बाएं और दाएं का फ़र्क पहचान सकता है, लेकिन आगे और पीछे का नहीं। “सेटिंग” टैब का “साउंड मोड” शुरू होने पर और आउटपुट डिवाइस बदलने पर उसी डिवाइस के हिसाब से अपने-आप बदल जाता है, और आप इसे खुद भी बदल सकते हैं।",
+ "help.sound_mode.tip": "💡 अगर आपके पास 7.1 आउटपुट डिवाइस नहीं है, तो {0} इंस्टॉल करें और आउटपुट डिवाइस को 7.1 चैनल पर सेट करें। इससे ओवरले को 7.1 सराउंड साउंड के साथ दिखाया जा सकता है।",
+ "help.sound_mode.tip_link": "VB-CABLE वर्चुअल ड्राइवर",
+ "help.hotkeys.title": "अपनी पसंद की शॉर्टकट कुंजियां",
+ "help.hotkeys.body": "“सेटिंग” टैब में किसी शॉर्टकट बटन पर क्लिक करें, फिर कोई एक कुंजी या कुंजियों का कॉम्बिनेशन (जैसे Ctrl + Shift + A) दबाकर शॉर्टकट को अपनी पसंद से बदलें। ऐप बैकग्राउंड में चल रहा हो, तब भी शॉर्टकट कुंजी दबाने पर मोड तुरंत बदल जाता है।",
+ "help.hotkeys.defaults": "डिफ़ॉल्ट रूप से F2 साउंड मोड बदलता है, F3 विज़ुअल मोड बदलता है और F4 ओवरले सेटिंग खोलता है।",
+ "help.size_lock.title": "साइज़ तय रखने वाला मोड",
+ "help.size_lock.body": "साइज़ तय रखने वाला मोड, आवाज़ की तेज़ी के अनुपात में पारदर्शिता बदलकर ग्राफ़िक्स का साइज़ एक जैसा रखता है। यह तब काम आता है, जब आप ग्राफ़िक्स का साइज़ एक जैसा रखते हुए भी देखना चाहते हैं कि वे आवाज़ पर कैसे प्रतिक्रिया देते हैं।",
+ "help.editor.title": "रियल-टाइम ओवरले एडिट मोड",
+ "help.editor.body": "ओवरले एडिट मोड चालू करने के लिए तय की गई शॉर्टकट कुंजी (डिफ़ॉल्ट रूप से F4) दबाएं। स्क्रीन पर माउस से खींचकर ग्राफ़िक्स के साइज़ की सीमा बदली जा सकती है, और खुलने वाले सेटिंग पैनल में बारीकियां ठीक की जा सकती हैं।",
+ "help.developer.title": "डेवलपर मोड",
+ "help.developer.body": "डेवलपर मोड चालू करने पर ओवरले के ऊपरी बाएं कोने में दिखता है कि AI ने क्या सुना (ध्वनि का नाम, वह कितना पक्का था और कितना समय लगा), और ऊपरी दाएं कोने में FPS दिखता है। अगर आप “{0}” भी चालू करते हैं, तो हर नतीजा एक CSV फ़ाइल में सेव होता है। यह तब काम आता है, जब आप जांचना चाहते हैं कि सिस्टम ठीक से काम कर रहा है या नहीं।",
+ "help.notes.title": "काम की बातें",
+ "help.note.direction_surround": "5.1 या 7.1 चैनल आउटपुट में, स्क्रीन का ऊपरी हिस्सा सामने से आने वाली आवाज़ और निचला हिस्सा पीछे से आने वाली आवाज़ दिखाता है। बाईं और दाईं तरफ़ बाएं और दाएं से आने वाली आवाज़ दिखती है, और स्क्रीन के हर तरफ़ से एक साथ आने वाले ग्राफ़िक्स माहौल की आवाज़ें या बिना दिशा वाली आवाज़ें होते हैं।",
+ "help.note.direction_stereo": "2 चैनल (स्टीरियो) आउटपुट में यह ऐप बाएं और दाएं का फ़र्क पहचान सकता है, लेकिन आगे और पीछे का नहीं। तब स्क्रीन के ऊपर और नीचे दिखने वाले ग्राफ़िक्स, दोनों तरफ़ बराबर आने वाली आवाज़ों और स्थानिक प्रभाव को दिखाते हैं।",
+ "help.note.sources": "Windows के डिफ़ॉल्ट प्लेबैक डिवाइस से चलने वाले सभी ऐप की आवाज़ दिखाई जाती है। कोई ऐप जो आवाज़ किसी दूसरे डिवाइस पर भेजता है, जैसे सिर्फ़ वॉइस चैट के लिए इस्तेमाल होने वाला हेडसेट, वह नहीं दिखती।",
+ "help.note.volume": "अगर किसी ऐप का वॉल्यूम, ऐप में या Windows वॉल्यूम मिक्सर में, बहुत कम या म्यूट है, तो उसकी आवाज़ मुश्किल से या बिल्कुल नहीं दिख सकती है। अगर ग्राफ़िक्स ठीक से प्रतिक्रिया न दें, तो उस आवाज़ को थोड़ा बढ़ाएं या “सेटिंग” टैब में “संवेदनशीलता” बढ़ाएं।",
+ "help.note.ai": "AI का वर्गीकरण गलत हो सकता है। जहां सुरक्षा का सवाल हो, वहां सिर्फ़ इसी ऐप पर भरोसा न करें।",
+ "help.faq.title": "अक्सर पूछे जाने वाले सवाल",
+ "help.faq.stopped_q": "मेरे बंद किए बिना ही यह बंद हो गया।",
+ "help.faq.stopped_a": "जब आवाज़ कैप्चर होना रुक जाता है और कुछ सेकंड में फिर शुरू नहीं होता, तो यह बंद हो जाता है, जैसे जब प्लेबैक डिवाइस निकाल दिया जाए और कोई दूसरा डिवाइस उपलब्ध न हो। तब “होम” टैब में वजह दिखती है। स्पीकर या हेडफ़ोन जोड़ें, या Windows की साउंड सेटिंग में डिफ़ॉल्ट प्लेबैक डिवाइस सेट करें, फिर इसे दोबारा शुरू करें।",
+ "help.faq.no_graphic_q": "ग्राफ़िक्स नहीं दिख रहे हैं।",
+ "help.faq.no_graphic_a": "देखें कि आवाज़ Windows के डिफ़ॉल्ट प्लेबैक डिवाइस से चल रही है और म्यूट नहीं है, और “होम” टैब में कोई सूचना तो नहीं दिख रही। अगर कोई गेम एक्सक्लूसिव फ़ुल-स्क्रीन मोड में चल रहा है, तो उसका बॉर्डरलेस या विंडो मोड आज़माएं। यह भी देखें कि “सेटिंग” टैब में “गहरापन” बहुत कम न हो और अभी चल रही आवाज़ के प्रकार को दिखाने की सेटिंग बंद न हो।",
+ "help.faq.performance_q": "क्या यह बहुत ज़्यादा CPU या GPU इस्तेमाल करता है?",
+ "help.faq.performance_a": "इसे हल्का करने के लिए, “सेटिंग” टैब में “{0}” कम करें या “{1}” बंद करें।",
+ "help.faq.language_q": "ऐप की भाषा कैसे बदलें?",
+ "help.faq.language_a": "इस विंडो में ऊपर दी गई “{0}” सूची से भाषा चुनें। चालू रहने के दौरान आप इसे ओवरले सेटिंग में भी बदल सकते हैं। “{1}” चुनने पर ऐप Windows की डिस्प्ले भाषा में दिखेगा।",
+ "help.report.title": "समस्या की रिपोर्ट करें",
+ "help.report.desc": "किसी गड़बड़ी (बग) के बारे में बताएं, या ऐसी आवाज़ के बारे में जिसका प्रकार गलत दिखाया गया हो। आपके ब्राउज़र में GitHub खुलेगा, जिसमें नीचे दी गई सिस्टम की जानकारी पहले से भरी होगी, इसलिए GitHub खाता ज़रूरी है। खाता न हो, तो इसके बजाय हमें ईमेल भेजें। कोई भी ऑडियो कभी नहीं भेजा जाता।",
+ "help.report.github": "GitHub पर रिपोर्ट करें",
+ "help.report.mail": "ईमेल से रिपोर्ट करें",
+ "help.report.copy": "सिस्टम की जानकारी कॉपी करें",
+ "help.report.copied": "सिस्टम की जानकारी कॉपी हो गई।",
+ "help.report.copy_failed": "क्लिपबोर्ड पर कॉपी नहीं हो सका। फिर से कोशिश करें।",
+ "help.report.no_browser": "ब्राउज़र नहीं खुल सका। इसके बजाय सिस्टम की जानकारी कॉपी करके हमें भेजें।",
+ "help.report.no_mail": "भेजने के लिए कोई ईमेल ऐप नहीं है। सिस्टम की जानकारी कॉपी करें और किसी दूसरे तरीके से भेजें।",
+ "help.report.issue_title": "ऐप से भेजी गई रिपोर्ट",
+ "help.report.issue_body": "क्या हुआ?\n\nआप क्या सुन रहे थे (कौन-सा ऐप, किस तरह की आवाज़)?\n\nइसके बजाय आपको क्या होने की उम्मीद थी?",
+ "help.about.title": "ऐप के बारे में",
+ "help.about.version": "वर्शन {0}",
+ "help.about.license": "यह ऐप GNU Affero General Public License v3.0 (AGPL-3.0) के तहत उपलब्ध कराया जाता है। “आवाज़ें” टैब में आवाज़ों के नाम और समूह AudioSet ऑन्टोलॉजी पर आधारित हैं और CC BY-SA 4.0 के तहत उपलब्ध हैं (नीचे “ओपन सोर्स लाइसेंस” देखें)।",
+ "help.licenses.button": "ओपन सोर्स लाइसेंस",
+ "help.licenses.title": "ओपन सोर्स लाइसेंस",
+ "help.licenses.close": "बंद करें",
+
+ "notice.ai_unavailable": "आवाज़ का प्रकार पहचाना नहीं जा पा रहा है, इसलिए सभी आवाज़ें माहौल की आवाज़ों के रंग में दिख रही हैं। विज़ुअलाइज़र को बंद करके दोबारा चालू करके देखें।",
+ "notice.ai_unavailable_hidden": "आवाज़ का प्रकार पहचाना नहीं जा पा रहा है, इसलिए सभी आवाज़ें माहौल की आवाज़ें मानी जा रही हैं, लेकिन माहौल की आवाज़ें दिखाना बंद है, इसलिए कुछ भी नहीं दिख रहा। “सेटिंग” टैब में “{0}” चालू करें, या विज़ुअलाइज़र को बंद करके दोबारा चालू करके देखें।",
+ "notice.stopped.title": "आवाज़ विज़ुअलाइज़र बंद हो गया",
+ "notice.stopped.no_device": "कोई प्लेबैक डिवाइस नहीं खुल सका, इसलिए आवाज़ कैप्चर नहीं हो सकती। स्पीकर या हेडफ़ोन जोड़ें, या Windows की साउंड सेटिंग में डिफ़ॉल्ट प्लेबैक डिवाइस सेट करें, फिर दोबारा शुरू करें।",
+ "notice.stopped.capture_error": "गड़बड़ी की वजह से आवाज़ कैप्चर होना रुक गया, इसलिए ग्राफ़िक्स बंद हो गए।",
+ "notice.dismiss": "जानकारी हटाएं",
+
+ "tray.open": "लॉन्चर खोलें",
+ "tray.exit": "बाहर निकलें",
+
+ "more_info.title": "GitHub रिपॉज़िटरी और योगदान",
+ "more_info.body": "SoundVisualizer एक ओपन-सोर्स प्रोजेक्ट है, और इसका सोर्स कोड GitHub पर सबके लिए उपलब्ध है। आप बग की रिपोर्ट करके, नई सुविधाओं का सुझाव देकर या डेवलपमेंट में योगदान देकर इसमें हिस्सा ले सकते हैं। आधिकारिक GitHub रिपॉज़िटरी पर जाने के लिए नीचे दिए गए बटन पर क्लिक करें! GitHub के Releases टैब में आप नया वर्शन देख और डाउनलोड भी कर सकते हैं।",
+ "more_info.github_button": "GitHub रिपॉज़िटरी पर जाएं",
+
+ "dialog.notice_title": "सूचना",
+ "dialog.already_running": "ओवरले पहले से चल रहा है।",
+ "dialog.reset_title": "सेटिंग रीसेट करें",
+ "dialog.reset_wave": "लहर मोड की सेटिंग को डिफ़ॉल्ट पर रीसेट करें?",
+ "dialog.reset_pad": "पैड मोड की सेटिंग को डिफ़ॉल्ट पर रीसेट करें?",
+ "dialog.reset_circle": "वृत्त मोड की सेटिंग को डिफ़ॉल्ट पर रीसेट करें?",
+ "dialog.reset_outline": "आउटलाइन मोड की सेटिंग को डिफ़ॉल्ट पर रीसेट करें?",
+ "dialog.reset_sound_types": "आवाज़ के प्रकारों की सेटिंग को डिफ़ॉल्ट पर रीसेट करें?",
+ "dialog.reset_all_title": "डिफ़ॉल्ट पर रीसेट करें",
+ "dialog.reset_all": "क्या आप सच में सभी सेटिंग को डिफ़ॉल्ट पर रीसेट करना चाहते हैं?",
+ "dialog.link_error": "लिंक नहीं खुल सका: {0}",
+
+ "editor.title": "ओवरले सेटिंग",
+ "editor.drag_tip": "ओवरले की लहर की ऊंचाई की सीमा और बीच का साइज़ रियल-टाइम में बदलने के लिए, स्क्रीन पर दिख रही गाइड लाइनों को माउस से खींचें!",
+ "editor.glow": "चमक प्रभाव",
+ "editor.glow_strength": "चमक का स्तर",
+ "editor.color": "रंग",
+ "editor.hotkeys": "शॉर्टकट कुंजियों की सेटिंग",
+ "editor.developer_mode_desc": "AI ने क्या सुना (ऊपर बाएं) और FPS (ऊपर दाएं) दिखाता है",
+ "editor.save_close_hint": "दबाकर सेव करें और बंद करें",
+ "editor.close_overlay": "ओवरले बंद करें",
+ "editor.guide.limit": "{0} की सीमा",
+ "editor.guide.size": "{0} का साइज़: {1}%",
+ "editor.guide.base_radius": "मूल त्रिज्या: {0}",
+ "editor.guide.circle_size": "वृत्त का साइज़: {0}%",
+
+ "hud.visual_mode": "विज़ुअल मोड: [{0}] {1}",
+ "hud.sound_mode": "साउंड मोड: [{0}] {1}",
+ "hud.overlay_settings": "ओवरले सेटिंग: [{0}]",
+ "hud.status.ready": "सिस्टम तैयार है",
+ "hud.status.stereo": "PC ऑडियो: {0} चैनल (स्टीरियो)\nस्रोत: इंतज़ार हो रहा है...",
+ "hud.status.surround": "PC ऑडियो: 8 चैनल (7.1)\nस्रोत: इंतज़ार हो रहा है...",
+
+ "color_picker.title": "रंग चुनें",
+ "color_picker.hex": "HEX कोड",
+ "color_picker.cancel": "रद्द करें",
+ "color_picker.apply": "लागू करें",
+ "color_picker.area": "संतृप्ति और चमक",
+ "color_picker.area_value": "संतृप्ति {0}%, चमक {1}%",
+ "color_picker.hue": "रंगत",
+ "color_picker.keys_help": "बदलने के लिए तीर कुंजियों का इस्तेमाल करें। बड़े कदमों में बदलने के लिए Shift दबाकर रखें।",
+ "color_picker.presets": "अक्सर इस्तेमाल होने वाले रंग",
+ "color_picker.preset.white": "सफ़ेद",
+ "color_picker.preset.red": "लाल",
+ "color_picker.preset.orange": "नारंगी",
+ "color_picker.preset.yellow": "पीला",
+ "color_picker.preset.green": "हरा",
+ "color_picker.preset.cyan": "आसमानी",
+ "color_picker.preset.blue": "नीला",
+ "color_picker.preset.magenta": "मैजेंटा",
+
+ "audio.error.no_device": "कोई आउटपुट डिवाइस नहीं मिला।",
+ "audio.error.capture_stopped": "ऑडियो कैप्चर अचानक रुक गया: {0}",
+ "audio.error.capture_start_failed": "कैप्चर इंजन शुरू नहीं हो सका: {0}",
+
+ "tutorial.window_title": "ट्यूटोरियल",
+ "tutorial.skip": "छोड़ें",
+ "tutorial.previous": "पिछला पेज",
+ "tutorial.next": "आगे बढ़ें",
+ "tutorial.done": "ठीक है",
+ "tutorial.page_status": "{1} में से पेज {0}",
+ "tutorial.pause": "ऐनिमेशन रोकें",
+ "tutorial.play": "ऐनिमेशन चलाएं",
+ "tutorial.sound_on": "आवाज़ चालू करें",
+ "tutorial.sound_off": "आवाज़ बंद करें",
+ "tutorial.sound.title": "आवाज़ को स्क्रीन पर देखें",
+ "tutorial.sound.body": "आपके गेम या वीडियो की आवाज़ स्क्रीन के किनारों पर बनाई जाती है। आवाज़ जितनी तेज़ होती है, उतनी ही बड़ी दिखती है। सिर्फ़ इस PC पर चल रही आवाज़ ही दिखाई जाती है। यह ऐप आपके आस-पास की आवाज़ें नहीं सुनता।",
+ "tutorial.direction.title": "आवाज़ किस तरफ़ से आ रही है, यह दिखाता है",
+ "tutorial.direction.body": "2 चैनल (स्टीरियो) आउटपुट में बाईं तरफ़ से आने वाली आवाज़ स्क्रीन की बाईं तरफ़ ज़्यादा बड़ी दिखती है, और दाईं तरफ़ से आने वाली आवाज़ दाईं तरफ़। 5.1 या 7.1 चैनल आउटपुट में यह ऐप आगे और पीछे का फ़र्क भी पहचानता है: सामने से आने वाली आवाज़ स्क्रीन के ऊपरी हिस्से में और पीछे से आने वाली आवाज़ निचले हिस्से में दिखती है।",
+ "tutorial.types.title": "आवाज़ के हर प्रकार का अपना रंग है",
+ "tutorial.types.body": "AI आवाज़ों को तीन प्रकारों में बांटता है और हर प्रकार को अलग रंग में बनाता है। रंग “सेटिंग” टैब में बदले जा सकते हैं।",
+ "tutorial.type.ambient_examples": "संगीत, बारिश और बाकी सब",
+ "tutorial.type.speech_examples": "लोगों की आवाज़ें",
+ "tutorial.type.danger_examples": "गोलियों की आवाज़, धमाके, सायरन",
+ "tutorial.start.title": "चालू और बंद कैसे करें",
+ "tutorial.start.hotkeys": "चालू रहने के दौरान, साउंड मोड बदलने के लिए {0}, विज़ुअल मोड बदलने के लिए {1}, और ओवरले सेटिंग खोलने के लिए {2} दबाएं।"
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/id.json b/SoundVisualizer.Core/Localization/Strings/id.json
new file mode 100644
index 0000000..9359d17
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/id.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Bahasa",
+ "common.reset_defaults": "Reset ke default",
+ "common.minimize": "Minimalkan",
+ "common.close": "Tutup",
+ "language.system": "Default sistem",
+
+ "launcher.window_title": "Peluncur SoundVisualizer",
+ "launcher.tab.home": "Beranda",
+ "launcher.tab.settings": "Setelan",
+ "launcher.tab.sounds": "Suara",
+ "launcher.tab.help": "Bantuan",
+ "launcher.tab.more_info": "Pelajari lebih lanjut",
+ "launcher.home.desc": "Suara yang tak terlihat kini tergambar langsung di layar Anda.\nDari bermain game hingga menonton film, rasakan pengalaman yang benar-benar baru.",
+ "launcher.home.start": "Mulai",
+ "launcher.home.stop": "Hentikan",
+ "launcher.home.tutorial": "Lihat tutorial",
+ "launcher.status.running": "Status: Berjalan",
+ "launcher.status.idle": "Status: Siap",
+
+ "settings.section.mode": "Setelan mode",
+ "settings.visual_mode.title": "Mode visual",
+ "settings.visual_mode.desc": "Pilih bentuk dasar grafik yang digambar di layar.",
+ "settings.mode.wave": "Ombak",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Lingkar",
+ "settings.mode.outline": "Kontur",
+ "settings.sound_mode.title": "Mode suara",
+ "settings.sound_mode.desc": "Pilih saluran suara yang sesuai dengan speaker Anda.\nUntuk speaker 2 saluran, pilih “2 saluran”. Untuk speaker surround, pilih “5.1 saluran” atau “7.1 saluran”.",
+ "settings.channels.2": "2 saluran",
+ "settings.channels.5_1": "5.1 saluran",
+ "settings.channels.7_1": "7.1 saluran",
+ "settings.max_fps.title": "Frame rate maksimum",
+ "settings.max_fps.desc": "Mengatur frame rate tertinggi untuk menggambar grafik.",
+ "settings.section.mode_detail": "Setelan per mode",
+ "settings.mode_card.wave": "Mode Ombak",
+ "settings.mode_card.pad": "Mode Pad",
+ "settings.mode_card.circle": "Mode Lingkar",
+ "settings.mode_card.outline": "Mode Kontur",
+ "settings.size_lock.title": "Kunci ukuran",
+ "settings.size_lock.desc": "Menjaga ukuran grafik tetap dan menunjukkan kerasnya suara melalui opasitas.",
+ "settings.fixed_size.wave": "Ukuran ombak",
+ "settings.fixed_size.pad": "Ukuran pad",
+ "settings.fixed_size.circle": "Ukuran lingkar",
+ "settings.fixed_size.outline": "Ketebalan kontur",
+ "settings.fixed_size.desc": "Mengatur ukuran tetap yang dipertahankan grafik saat opasitasnya berubah.",
+ "settings.max_opacity.title": "Opasitas maks",
+ "settings.max_opacity.desc": "Seberapa pekat grafik saat suara paling keras.",
+ "settings.size.title": "Ukuran",
+ "settings.size.desc.wave": "Mengatur tinggi ombak dan seberapa penuh tampilannya secara keseluruhan.",
+ "settings.size.desc.pad": "Mengatur ketebalan dan lebar pad saat bereaksi terhadap suara.",
+ "settings.size.desc.circle": "Mengatur seberapa jauh equalizer lingkar menggembung keluar saat suara terdengar.",
+ "settings.size.desc.outline": "Mengatur tinggi ombak kontur dan seberapa penuh tampilannya secara keseluruhan.",
+ "settings.opacity.title": "Opasitas",
+ "settings.opacity.desc.wave": "Makin tinggi nilainya, makin pekat grafiknya; makin rendah, makin samar dan transparan.",
+ "settings.opacity.desc.pad": "Makin tinggi nilainya, makin pekat grafiknya; makin rendah, makin samar dan transparan.",
+ "settings.opacity.desc.circle": "Makin tinggi nilainya, makin pekat grafiknya; makin rendah, makin samar dan transparan.",
+ "settings.opacity.desc.outline": "Makin tinggi nilainya, makin pekat grafiknya; makin rendah, makin samar dan transparan.",
+ "settings.speed.title": "Kecepatan",
+ "settings.speed.desc.wave": "Mengatur seberapa cepat ombak mengalir dan beriak.",
+ "settings.speed.desc.pad": "Mengatur seberapa cepat pad berputar ke arah datangnya suara.",
+ "settings.speed.desc.circle": "Mengatur seberapa cepat equalizer lingkar beriak dan berputar mengikuti suara.",
+ "settings.speed.desc.outline": "Mengatur seberapa cepat ombak kontur mengalir dan beriak.",
+ "settings.sensitivity.title": "Kepekaan",
+ "settings.sensitivity.desc.wave": "Mengatur seberapa peka ombak bereaksi dan bergelora, bahkan terhadap suara pelan.",
+ "settings.sensitivity.desc.pad": "Mengatur pad agar lebih mudah menangkap suara dan tampak lebih menonjol.",
+ "settings.sensitivity.desc.circle": "Mengatur seberapa halus amplitudo lingkar terpecah saat bereaksi terhadap suara.",
+ "settings.sensitivity.desc.outline": "Mengatur seberapa peka ombak kontur bereaksi dan bergelora, bahkan terhadap suara pelan.",
+ "settings.glow.title": "Efek cahaya",
+ "settings.glow.desc.wave": "Menambahkan pendar neon yang lembut di sepanjang tepi ombak dan mengatur kekuatannya.\nCatatan: menggunakan sumber daya GPU tambahan.",
+ "settings.glow.desc.pad": "Menambahkan pendar lembut seperti aura di sekitar pad.\nCatatan: menggunakan sumber daya GPU tambahan.",
+ "settings.glow.desc.circle": "Menambahkan lingkaran cahaya berkilau di sepanjang tepi equalizer lingkar.\nCatatan: menggunakan sumber daya GPU tambahan.",
+ "settings.glow.desc.outline": "Menambahkan pendar neon yang lembut pada kontur dan mengatur kekuatannya.\nCatatan: menggunakan sumber daya GPU tambahan.",
+ "settings.radius.title": "Radius",
+ "settings.radius.desc": "Mengatur radius cincin di tengah layar pada mode Lingkar.",
+ "settings.section.sound_types": "Jenis suara (AI)",
+ "settings.show_ambient": "Tampilkan suara latar",
+ "settings.show_speech": "Tampilkan percakapan",
+ "settings.show_danger": "Tampilkan suara bahaya",
+ "settings.set_color": "Atur warna",
+ "settings.set_color_for": "Atur warna: {0}",
+ "settings.section.hotkeys": "Tombol pintasan",
+ "settings.hotkey.visual.title": "Ganti mode visual",
+ "settings.hotkey.visual.desc": "Tombol pintasan untuk mengganti bentuk secara langsung saat berjalan.",
+ "settings.hotkey.sound.title": "Ganti mode suara",
+ "settings.hotkey.sound.desc": "Tombol pintasan untuk mengganti mode suara secara langsung saat berjalan.",
+ "settings.hotkey.edit.title": "Setelan overlay",
+ "settings.hotkey.edit.desc": "Tombol pintasan untuk membuka atau menutup editor overlay.",
+ "settings.hotkey.set": "Atur pintasan",
+ "settings.section.advanced": "Setelan lanjutan",
+ "settings.developer_mode.title": "Mode pengembang",
+ "settings.developer_mode.desc": "Menampilkan apa yang didengar AI (nama suara yang dilaporkan, seberapa yakin, dan berapa lama prosesnya) di sudut kiri atas overlay, serta FPS di sudut kanan atas. Tampilan ini berbahasa Inggris dan tidak ditujukan untuk penggunaan sehari-hari.",
+ "settings.developer_record.title": "Catat hasil ke file",
+ "settings.developer_record.desc": "Menulis setiap hasil AI (nama suara dengan peringkat tertinggi, seberapa yakin, dan waktu prosesnya) ke file CSV baru di folder ai-log di samping SoundVisualizer.exe, sehingga Anda bisa melihat kembali apa yang didengarnya. Jika folder itu tidak bisa ditulisi, file disimpan di %LOCALAPPDATA%\\SoundVisualizer\\ai-log. File bisa menjadi besar, jadi matikan setelah selesai.",
+ "settings.auto_minimize.title": "Minimalkan peluncur saat mulai",
+ "settings.auto_minimize.desc": "Otomatis meminimalkan jendela peluncur ini saat overlay dimulai.",
+
+ "sounds.title": "Pilih jenis suara",
+ "sounds.desc": "Suara yang dikenali AI dikelompokkan dengan suara yang mirip, sesuai jenis default masing-masing. Jika Anda mengubah jenis suatu grup, warna suara di dalamnya langsung mengikuti jenis yang baru.",
+ "sounds.sibling_note": "AI sering mengenali satu suara dengan beberapa nama sekaligus. Misalnya, sirene juga dikenali sebagai “Alarm”, yang ada di grup lain. Jika suatu suara masih tampil dengan jenis lamanya setelah diubah, ubah juga grup atau suara yang terkait.",
+ "sounds.advanced.title": "Mode lanjutan",
+ "sounds.advanced.desc": "Menampilkan setiap suara dalam grupnya agar Anda dapat mengubah suara satu per satu.",
+ "sounds.search.hint": "Cari suara",
+ "sounds.search.clear": "Hapus pencarian",
+ "sounds.filter.title": "Filter",
+ "sounds.filter.all": "Semua",
+ "sounds.filter.changed": "Diubah",
+ "sounds.type.ambient": "Suara latar",
+ "sounds.type.speech": "Percakapan",
+ "sounds.type.danger": "Suara bahaya",
+ "sounds.type.default": "{0} (default)",
+ "sounds.type.mixed": "Campuran",
+ "sounds.changed_count": "Suara diubah: {0}",
+ "sounds.changed_from": "Default: {0}",
+ "sounds.reset.button": "Reset semua",
+ "sounds.reset.title": "Reset semua suara?",
+ "sounds.reset.message": "Semua suara yang telah Anda ubah akan kembali ke jenis default.",
+ "sounds.reset.confirm": "Reset",
+ "sounds.reset.cancel": "Batal",
+ "sounds.empty": "Tidak ada suara yang cocok.",
+ "sounds.group.mixed_note": "Beberapa suara diubah satu per satu",
+ "sounds.group.changed_line": "Sekarang: {0} · Default: {1}",
+ "sounds.overwrite.title": "Ubah seluruh grup?",
+ "sounds.overwrite.message": "{1} suara di grup “{0}” akan diubah menjadi “{2}”. Suara yang Anda ubah satu per satu juga akan ditimpa.",
+ "sounds.overwrite.confirm": "Ubah",
+ "sounds.overwrite.cancel": "Batal",
+ "sounds.basic_warning.title": "Beralih ke mode dasar?",
+ "sounds.basic_warning.message": "Beberapa grup berisi suara yang Anda ubah satu per satu. Di mode dasar, jika Anda memilih jenis untuk salah satu grup tersebut, semua suara di dalamnya akan mendapat jenis itu dan perubahan satu per satu di grup itu akan ditimpa. Tidak ada yang berubah sampai Anda memilih.",
+ "sounds.basic_warning.confirm": "Beralih ke dasar",
+ "sounds.basic_warning.cancel": "Tetap di lanjutan",
+ "sounds.cd.sound": "{0}, jenis: {1}",
+ "sounds.cd.sound_changed": "{0}, jenis: {1}, default: {2}",
+ "sounds.cd.group": "Grup {0}, jenis: {1}, jumlah suara: {2} ({3})",
+ "sounds.cd.group_changed": "Grup {0}, jenis: {1}, default: {2}, jumlah suara: {3} ({4})",
+ "sounds.cd.group_header": "Grup {0}, jumlah suara: {1}",
+
+ "hotkey.prompt": "Tekan tombol (Esc untuk batal)",
+ "hotkey.none": "Tidak ada",
+ "hotkey.button_name": "{0}, tombol pintasan: {1}",
+
+ "help.start.title": "Memulai",
+ "help.start.1": "Pilih bahasa di bagian atas jendela ini, lalu ubah tampilan grafik di tab Setelan jika Anda mau.",
+ "help.start.2": "Di Beranda, klik “{0}”.",
+ "help.start.3": "Grafik digambar di sepanjang tepi layar, di atas jendela lain seperti game dan video. Klik mouse menembus grafik, jadi Anda tetap bisa menggunakan jendela di bawahnya.",
+ "help.start.4": "Untuk mematikannya, klik “{0}” di Beranda, atau klik kanan ikon SoundVisualizer di area notifikasi taskbar lalu pilih “{0}”.",
+ "help.modes.title": "Mode visual",
+ "help.mode.wave": "Seluruh tepi layar terdorong ke dalam sesuai kerasnya suara. Sisi asal suara terdorong lebih dalam.",
+ "help.mode.pad": "Bilah menyala di 8 titik di sekeliling tepi layar. Mode ini paling jelas menunjukkan arah.",
+ "help.mode.circle": "Cincin di tengah layar mengembang ke arah datangnya suara.",
+ "help.mode.outline": "Hanya menggambar garis tipis di sepanjang kontur ombak, sehingga tidak banyak menutupi layar game Anda.",
+ "help.modes.note": "Setelan seperti Ukuran dan Opasitas disimpan terpisah untuk setiap mode dan dapat diubah di tab Setelan.",
+ "help.sound.title": "Jenis suara",
+ "help.sound.kinds": "AI mengelompokkan suara yang terdengar menjadi suara latar, percakapan, dan suara bahaya. Secara default, suara latar berwarna putih, percakapan kuning, dan suara bahaya merah. Di tab Setelan, Anda dapat mengubah warna setiap jenis dan memilih apakah jenis tersebut ditampilkan.",
+ "help.sound.groups": "Di tab Suara, suara yang mirip dikelompokkan, dan Anda memilih jenis untuk setiap grup. Misalnya, jika grup “Bel pintu” diubah menjadi suara bahaya, saat AI mengenali bel pintu atau ketukan pintu, suara itu akan mendapat warna suara bahaya. Namun, suara yang Anda ubah sendiri menjadi suara bahaya hanya ikut dihitung dalam voting AI untuk menentukan jenisnya, jadi bukan berarti setiap bel pintu akan ditampilkan sebagai suara bahaya.",
+ "help.sound.advanced": "Aktifkan mode lanjutan untuk mengubah suara satu per satu.",
+ "help.sound.danger": "Saat suara bahaya (seperti suara tembakan) terdeteksi, warna langsung berubah tanpa memudar perlahan.",
+ "help.pc.title": "Setelan PC dan tombol pintas",
+ "help.sound_mode.title": "Mode suara",
+ "help.sound_mode.body": "Agar suara dari depan dan belakang juga dapat dibedakan, bukan hanya kiri dan kanan, setel perangkat output ke surround 5.1 (6 saluran) atau surround 7.1 (8 saluran) di setelan suara Windows. Dengan stereo biasa (2 saluran), aplikasi dapat membedakan kiri dan kanan, tetapi tidak dapat membedakan depan dan belakang. “Mode suara” di tab Setelan akan menyesuaikan dengan perangkat output saat mulai dijalankan atau saat perangkat berubah, dan Anda tetap dapat mengubahnya sendiri.",
+ "help.sound_mode.tip": "💡 Jika tidak punya perangkat output 7.1, Anda dapat menginstal {0} lalu menyetel perangkat output ke 7.1 saluran untuk menampilkan overlay dengan suara surround 7.1.",
+ "help.sound_mode.tip_link": "driver virtual VB-CABLE",
+ "help.hotkeys.title": "Tombol pintasan kustom real-time",
+ "help.hotkeys.body": "Di tab Setelan, klik tombol pintasan, lalu tekan satu tombol keyboard atau kombinasi tombol (misalnya Ctrl + Shift + A) untuk mengubah pintasan sesuai keinginan Anda. Menekan tombol pintasan akan langsung mengganti mode, bahkan saat aplikasi berjalan di latar belakang.",
+ "help.hotkeys.defaults": "Secara default, F2 mengganti mode suara, F3 mengganti mode visual, dan F4 membuka setelan overlay.",
+ "help.size_lock.title": "Mode kunci ukuran",
+ "help.size_lock.body": "Mode kunci ukuran menjaga ukuran grafik tetap dengan menggunakan opasitas yang sebanding dengan kerasnya suara. Mode ini berguna jika Anda ingin ukuran grafik tetap sama sambil tetap melihatnya bereaksi terhadap suara.",
+ "help.editor.title": "Mode edit overlay real-time",
+ "help.editor.body": "Tekan tombol pintasan yang ditetapkan (default F4) untuk menyalakan mode edit overlay. Anda dapat menyeret mouse di layar untuk mengatur batas ukuran grafik, dan menyempurnakan detailnya di panel setelan yang muncul.",
+ "help.developer.title": "Mode pengembang",
+ "help.developer.body": "Menyalakan Mode pengembang akan menampilkan apa yang didengar AI (nama suara, seberapa yakin, dan berapa lama prosesnya) di sudut kiri atas overlay, serta FPS di sudut kanan atas. Jika Anda juga menyalakan “{0}”, setiap hasil disimpan ke file CSV. Ini berguna jika Anda ingin memeriksa apakah sistem berfungsi dengan baik.",
+ "help.notes.title": "Perlu diketahui",
+ "help.note.direction_surround": "Dengan output 5.1 atau 7.1 saluran, bagian atas layar menunjukkan suara dari depan, dan bagian bawah menunjukkan suara dari belakang. Sisi kiri dan kanan menunjukkan suara dari kiri dan kanan, sedangkan grafik yang muncul dari semua sisi layar sekaligus adalah suara latar atau suara yang tidak berarah.",
+ "help.note.direction_stereo": "Dengan output 2 saluran (stereo), aplikasi dapat membedakan kiri dan kanan, tetapi tidak dapat membedakan depan dan belakang. Dalam hal ini, grafik di bagian atas dan bawah layar menunjukkan suara yang terdengar sama kuat di kedua sisi, serta memberi kesan ruang.",
+ "help.note.sources": "Aplikasi menampilkan suara semua aplikasi yang diputar melalui perangkat pemutaran default Windows. Suara yang dikirim aplikasi ke perangkat lain, misalnya headset yang hanya dipakai untuk obrolan suara, tidak ditampilkan.",
+ "help.note.volume": "Jika volume sebuah aplikasi diturunkan sangat rendah atau dibisukan, baik di aplikasi itu sendiri maupun di mixer volume Windows, suaranya mungkin hampir tidak terlihat atau tidak muncul sama sekali. Jika grafik kurang merespons, naikkan sedikit suara tersebut atau naikkan Kepekaan di tab Setelan.",
+ "help.note.ai": "Klasifikasi AI bisa salah. Jangan mengandalkan aplikasi ini sebagai satu-satunya pengaman dalam situasi yang menyangkut keselamatan.",
+ "help.faq.title": "Pertanyaan umum",
+ "help.faq.stopped_q": "Visualisasi mati padahal saya tidak menghentikannya.",
+ "help.faq.stopped_a": "Visualisasi mati saat penangkapan suara berhenti dan tidak pulih dalam beberapa detik, misalnya saat perangkat pemutaran dilepas dan tidak ada perangkat lain yang tersedia. Beranda akan menampilkan alasannya. Sambungkan speaker atau headphone, atau setel perangkat pemutaran default di setelan suara Windows, lalu mulai lagi.",
+ "help.faq.no_graphic_q": "Grafik tidak muncul.",
+ "help.faq.no_graphic_a": "Pastikan suara diputar melalui perangkat pemutaran default Windows dan tidak dibisukan, serta periksa apakah Beranda menampilkan info. Jika game berjalan dalam mode layar penuh eksklusif, coba mode jendela tanpa bingkai atau mode jendela. Pastikan juga Opasitas di tab Setelan tidak terlalu rendah, dan opsi tampilan untuk jenis suara yang sedang terdengar tidak dimatikan.",
+ "help.faq.performance_q": "Apakah aplikasi ini banyak memakai CPU atau GPU?",
+ "help.faq.performance_a": "Agar lebih ringan, turunkan “{0}” di tab Setelan atau matikan “{1}”.",
+ "help.faq.language_q": "Bagaimana cara mengubah bahasa aplikasi?",
+ "help.faq.language_a": "Pilih dari daftar “{0}” di bagian atas jendela ini. Saat berjalan, Anda juga dapat mengubahnya di setelan overlay. “{1}” mengikuti bahasa tampilan Windows.",
+ "help.report.title": "Laporkan masalah",
+ "help.report.desc": "Beri tahu kami tentang bug, atau suara yang ditampilkan sebagai jenis suara yang salah. GitHub akan terbuka di browser Anda dengan info sistem di bawah yang sudah terisi, jadi Anda memerlukan akun GitHub. Jika tidak punya akun, kirim email kepada kami saja. Audio tidak pernah dikirim ke mana pun.",
+ "help.report.github": "Laporkan di GitHub",
+ "help.report.mail": "Laporkan melalui email",
+ "help.report.copy": "Salin info sistem",
+ "help.report.copied": "Info sistem disalin.",
+ "help.report.copy_failed": "Tidak dapat menyalin ke papan klip. Coba lagi.",
+ "help.report.no_browser": "Browser tidak dapat dibuka. Sebagai gantinya, salin info sistem dan kirimkan kepada kami.",
+ "help.report.no_mail": "Tidak ada aplikasi email untuk mengirim. Salin info sistem, lalu kirimkan dengan cara lain.",
+ "help.report.issue_title": "Laporan dari aplikasi",
+ "help.report.issue_body": "Apa yang terjadi?\n\nApa yang sedang Anda dengarkan (aplikasi apa, jenis suara apa)?\n\nMenurut Anda, apa yang seharusnya terjadi?",
+ "help.about.title": "Tentang",
+ "help.about.version": "Versi {0}",
+ "help.about.license": "Aplikasi ini didistribusikan berdasarkan GNU Affero General Public License v3.0 (AGPL-3.0). Nama dan grup suara di tab Suara didasarkan pada ontologi AudioSet dan didistribusikan berdasarkan CC BY-SA 4.0 (lihat “Lisensi open source” di bawah).",
+ "help.licenses.button": "Lisensi open source",
+ "help.licenses.title": "Lisensi open source",
+ "help.licenses.close": "Tutup",
+
+ "notice.ai_unavailable": "Deteksi jenis suara tidak berfungsi, sehingga semua suara ditampilkan dengan warna suara latar. Coba matikan lalu nyalakan lagi visualisasinya.",
+ "notice.ai_unavailable_hidden": "Deteksi jenis suara tidak berfungsi, sehingga semua suara dianggap suara latar, tetapi tampilan suara latar dimatikan, jadi tidak ada yang ditampilkan. Aktifkan “{0}” di tab Setelan, atau coba matikan lalu nyalakan lagi visualisasinya.",
+ "notice.stopped.title": "Visualisasi suara dimatikan",
+ "notice.stopped.no_device": "Tidak ada perangkat pemutaran yang dapat dibuka, sehingga suara tidak dapat ditangkap. Sambungkan speaker atau headphone, atau setel perangkat pemutaran default di setelan suara Windows, lalu mulai lagi.",
+ "notice.stopped.capture_error": "Penangkapan suara berhenti karena terjadi kesalahan, sehingga grafik dimatikan.",
+ "notice.dismiss": "Tutup info",
+
+ "tray.open": "Buka peluncur",
+ "tray.exit": "Keluar",
+
+ "more_info.title": "Repositori GitHub dan kontribusi",
+ "more_info.body": "SoundVisualizer adalah proyek open source, dan kode sumbernya terbuka untuk umum di GitHub. Anda dapat ikut serta dengan melaporkan bug, menyarankan fitur, atau berkontribusi dalam pengembangan. Klik tombol di bawah untuk mengunjungi repositori GitHub resmi! Anda juga dapat memeriksa dan mengunduh versi terbaru di tab Releases di GitHub.",
+ "more_info.github_button": "Kunjungi repositori GitHub",
+
+ "dialog.notice_title": "Pemberitahuan",
+ "dialog.already_running": "Overlay sudah berjalan.",
+ "dialog.reset_title": "Reset setelan",
+ "dialog.reset_wave": "Reset setelan mode Ombak ke default?",
+ "dialog.reset_pad": "Reset setelan mode Pad ke default?",
+ "dialog.reset_circle": "Reset setelan mode Lingkar ke default?",
+ "dialog.reset_outline": "Reset setelan mode Kontur ke default?",
+ "dialog.reset_sound_types": "Reset setelan jenis suara ke default?",
+ "dialog.reset_all_title": "Reset ke default",
+ "dialog.reset_all": "Yakin ingin mengembalikan semua setelan ke default?",
+ "dialog.link_error": "Tidak dapat membuka tautan: {0}",
+
+ "editor.title": "Setelan overlay",
+ "editor.drag_tip": "Seret garis panduan di layar dengan mouse untuk mengatur batas tinggi dan ukuran tengah ombak overlay secara real-time!",
+ "editor.glow": "Efek cahaya",
+ "editor.glow_strength": "Kuat cahaya",
+ "editor.color": "Warna",
+ "editor.hotkeys": "Setelan tombol pintasan",
+ "editor.developer_mode_desc": "Menampilkan apa yang didengar AI (kiri atas) dan FPS (kanan atas)",
+ "editor.save_close_hint": "untuk menyimpan setelan dan menutup",
+ "editor.close_overlay": "Keluar dari overlay",
+ "editor.guide.limit": "Batas {0}",
+ "editor.guide.size": "Ukuran {0}: {1}%",
+ "editor.guide.base_radius": "Radius dasar: {0}",
+ "editor.guide.circle_size": "Ukuran lingkar: {0}%",
+
+ "hud.visual_mode": "Mode visual: [{0}] {1}",
+ "hud.sound_mode": "Mode suara: [{0}] {1}",
+ "hud.overlay_settings": "Setelan overlay: [{0}]",
+ "hud.status.ready": "Sistem siap",
+ "hud.status.stereo": "Audio PC: {0}ch (stereo)\nSumber: menunggu...",
+ "hud.status.surround": "Audio PC: 8ch (7.1)\nSumber: menunggu...",
+
+ "color_picker.title": "Pilih warna",
+ "color_picker.hex": "Kode HEX",
+ "color_picker.cancel": "Batal",
+ "color_picker.apply": "Terapkan",
+ "color_picker.area": "Saturasi dan kecerahan",
+ "color_picker.area_value": "Saturasi {0}%, kecerahan {1}%",
+ "color_picker.hue": "Rona",
+ "color_picker.keys_help": "Gunakan tombol panah untuk mengubahnya. Tahan Shift untuk mengubah lebih banyak sekaligus.",
+ "color_picker.presets": "Warna umum",
+ "color_picker.preset.white": "Putih",
+ "color_picker.preset.red": "Merah",
+ "color_picker.preset.orange": "Oranye",
+ "color_picker.preset.yellow": "Kuning",
+ "color_picker.preset.green": "Hijau",
+ "color_picker.preset.cyan": "Sian",
+ "color_picker.preset.blue": "Biru",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "Perangkat output tidak ditemukan.",
+ "audio.error.capture_stopped": "Penangkapan audio berhenti tiba-tiba: {0}",
+ "audio.error.capture_start_failed": "Tidak dapat memulai mesin penangkap audio: {0}",
+
+ "tutorial.window_title": "Tutorial",
+ "tutorial.skip": "Lewati",
+ "tutorial.previous": "Halaman sebelumnya",
+ "tutorial.next": "Berikutnya",
+ "tutorial.done": "Mengerti",
+ "tutorial.page_status": "Halaman {0} dari {1}",
+ "tutorial.pause": "Jeda animasi",
+ "tutorial.play": "Putar animasi",
+ "tutorial.sound_on": "Nyalakan suara",
+ "tutorial.sound_off": "Matikan suara",
+ "tutorial.sound.title": "Lihat suara di layar Anda",
+ "tutorial.sound.body": "Suara dari game atau video Anda digambar di sepanjang tepi layar. Makin keras suaranya, makin besar gambarnya. Hanya suara yang diputar di PC ini yang ditampilkan. Aplikasi ini tidak mendengarkan suara di sekitar Anda.",
+ "tutorial.direction.title": "Menunjukkan arah datangnya suara",
+ "tutorial.direction.body": "Dengan output 2 saluran (stereo), suara dari kiri digambar lebih besar di sisi kiri, dan suara dari kanan di sisi kanan. Dengan output 5.1 atau 7.1 saluran, aplikasi juga dapat membedakan depan dan belakang: suara dari depan digambar di bagian atas layar, dan suara dari belakang di bagian bawah.",
+ "tutorial.types.title": "Setiap jenis suara punya warnanya sendiri",
+ "tutorial.types.body": "AI mengelompokkan suara menjadi tiga jenis dan menggambar setiap jenis dengan warna yang berbeda. Anda dapat mengubah warnanya di tab Setelan.",
+ "tutorial.type.ambient_examples": "Musik, hujan, dan suara lainnya",
+ "tutorial.type.speech_examples": "Suara manusia",
+ "tutorial.type.danger_examples": "Tembakan, ledakan, sirene",
+ "tutorial.start.title": "Cara menyalakan dan mematikannya",
+ "tutorial.start.hotkeys": "Saat berjalan, tekan {0} untuk mengganti mode suara, {1} untuk mengganti mode visual, dan {2} untuk membuka setelan overlay."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/it.json b/SoundVisualizer.Core/Localization/Strings/it.json
new file mode 100644
index 0000000..bd45d48
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/it.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Lingua",
+ "common.reset_defaults": "Ripristina valori predefiniti",
+ "common.minimize": "Riduci a icona",
+ "common.close": "Chiudi",
+ "language.system": "Predefinita di sistema",
+
+ "launcher.window_title": "Launcher di SoundVisualizer",
+ "launcher.tab.home": "Home",
+ "launcher.tab.settings": "Impostazioni",
+ "launcher.tab.sounds": "Suoni",
+ "launcher.tab.help": "Guida",
+ "launcher.tab.more_info": "Scopri di più",
+ "launcher.home.desc": "Disegna sullo schermo i suoni che non puoi vedere.\nDai giochi ai film, prova un'esperienza tutta nuova.",
+ "launcher.home.start": "Avvia",
+ "launcher.home.stop": "Interrompi",
+ "launcher.home.tutorial": "Guarda il tutorial",
+ "launcher.status.running": "Stato: in esecuzione",
+ "launcher.status.idle": "Stato: pronto",
+
+ "settings.section.mode": "Impostazioni modalità",
+ "settings.visual_mode.title": "Modalità visiva",
+ "settings.visual_mode.desc": "Scegli la forma di base della grafica disegnata sullo schermo.",
+ "settings.mode.wave": "Onda",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Cerchio",
+ "settings.mode.outline": "Contorno",
+ "settings.sound_mode.title": "Modalità audio",
+ "settings.sound_mode.desc": "Scegli i canali audio adatti ai tuoi altoparlanti.\nCon altoparlanti a 2 canali, scegli “2 canali”. Con altoparlanti surround, scegli “5.1 canali” o “7.1 canali”.",
+ "settings.channels.2": "2 canali",
+ "settings.channels.5_1": "5.1 canali",
+ "settings.channels.7_1": "7.1 canali",
+ "settings.max_fps.title": "Frequenza fotogrammi max",
+ "settings.max_fps.desc": "Imposta la frequenza fotogrammi massima con cui viene disegnata la grafica.",
+ "settings.section.mode_detail": "Opzioni per modalità",
+ "settings.mode_card.wave": "Modalità Onda",
+ "settings.mode_card.pad": "Modalità Pad",
+ "settings.mode_card.circle": "Modalità Cerchio",
+ "settings.mode_card.outline": "Modalità Contorno",
+ "settings.size_lock.title": "Blocca dimensione",
+ "settings.size_lock.desc": "Mantiene la grafica a una dimensione fissa e mostra il volume tramite la trasparenza.",
+ "settings.fixed_size.wave": "Dimensione onda",
+ "settings.fixed_size.pad": "Dimensione pad",
+ "settings.fixed_size.circle": "Dimensione cerchio",
+ "settings.fixed_size.outline": "Spessore contorno",
+ "settings.fixed_size.desc": "Imposta la dimensione fissa che la grafica mantiene mentre cambia la trasparenza.",
+ "settings.max_opacity.title": "Opacità max",
+ "settings.max_opacity.desc": "Quanto diventa opaca la grafica quando il suono è al massimo.",
+ "settings.size.title": "Dimensione",
+ "settings.size.desc.wave": "Regola l'altezza dell'onda e quanto appare corposa nel complesso.",
+ "settings.size.desc.pad": "Regola lo spessore e la larghezza dei pad quando reagiscono al suono.",
+ "settings.size.desc.circle": "Regola quanto si gonfia l'equalizzatore circolare quando c'è un suono.",
+ "settings.size.desc.outline": "Regola l'altezza dell'onda di contorno e quanto appare corposa nel complesso.",
+ "settings.opacity.title": "Opacità",
+ "settings.opacity.desc.wave": "Con valori alti la grafica è più marcata; con valori bassi è più tenue e trasparente.",
+ "settings.opacity.desc.pad": "Con valori alti la grafica è più marcata; con valori bassi è più tenue e trasparente.",
+ "settings.opacity.desc.circle": "Con valori alti la grafica è più marcata; con valori bassi è più tenue e trasparente.",
+ "settings.opacity.desc.outline": "Con valori alti la grafica è più marcata; con valori bassi è più tenue e trasparente.",
+ "settings.speed.title": "Velocità",
+ "settings.speed.desc.wave": "Regola la velocità con cui l'onda scorre e si increspa.",
+ "settings.speed.desc.pad": "Regola la velocità con cui i pad ruotano verso la direzione da cui arriva un suono.",
+ "settings.speed.desc.circle": "Regola la velocità con cui l'equalizzatore circolare si increspa e ruota in risposta al suono.",
+ "settings.speed.desc.outline": "Regola la velocità con cui l'onda di contorno scorre e si increspa.",
+ "settings.sensitivity.title": "Sensibilità",
+ "settings.sensitivity.desc.wave": "Regola la sensibilità con cui l'onda reagisce e si agita, anche con i suoni deboli.",
+ "settings.sensitivity.desc.pad": "Regola i pad in modo che captino meglio il suono e risaltino di più.",
+ "settings.sensitivity.desc.circle": "Regola con quanta finezza l'ampiezza del cerchio si frammenta reagendo al suono.",
+ "settings.sensitivity.desc.outline": "Regola la sensibilità con cui l'onda di contorno reagisce e si agita, anche con i suoni deboli.",
+ "settings.glow.title": "Bagliore",
+ "settings.glow.desc.wave": "Aggiunge un morbido bagliore al neon lungo il bordo dell'onda e ne regola l'intensità.\nNota: consuma più risorse della GPU.",
+ "settings.glow.desc.pad": "Aggiunge un morbido bagliore, simile a un'aura, attorno ai pad.\nNota: consuma più risorse della GPU.",
+ "settings.glow.desc.circle": "Aggiunge un alone scintillante lungo il bordo dell'equalizzatore circolare.\nNota: consuma più risorse della GPU.",
+ "settings.glow.desc.outline": "Aggiunge al contorno un morbido bagliore al neon e ne regola l'intensità.\nNota: consuma più risorse della GPU.",
+ "settings.radius.title": "Raggio",
+ "settings.radius.desc": "Imposta il raggio dell'anello centrale nella modalità Cerchio.",
+ "settings.section.sound_types": "Tipi di suono (IA)",
+ "settings.show_ambient": "Suoni ambientali",
+ "settings.show_speech": "Voci",
+ "settings.show_danger": "Suoni di pericolo",
+ "settings.set_color": "Scegli colore",
+ "settings.set_color_for": "Scegli colore: {0}",
+ "settings.section.hotkeys": "Scorciatoie da tastiera",
+ "settings.hotkey.visual.title": "Cambia modalità visiva",
+ "settings.hotkey.visual.desc": "Scorciatoia che cambia la forma in tempo reale durante l'esecuzione.",
+ "settings.hotkey.sound.title": "Cambia modalità audio",
+ "settings.hotkey.sound.desc": "Scorciatoia che cambia la modalità audio in tempo reale durante l'esecuzione.",
+ "settings.hotkey.edit.title": "Impostazioni overlay",
+ "settings.hotkey.edit.desc": "Scorciatoia che apre o chiude l'editor dell'overlay.",
+ "settings.hotkey.set": "Imposta scorciatoia",
+ "settings.section.advanced": "Impostazioni avanzate",
+ "settings.developer_mode.title": "Modalità sviluppatore",
+ "settings.developer_mode.desc": "Mostra in alto a sinistra dell'overlay cosa ha sentito l'IA (il nome del suono indicato, quanto era sicura e quanto tempo ha impiegato) e in alto a destra gli FPS. Le informazioni sono in inglese e non sono pensate per l'uso quotidiano.",
+ "settings.developer_record.title": "Salva i risultati in un file",
+ "settings.developer_record.desc": "Salva ogni risultato dell'IA (i nomi dei suoni con il punteggio più alto, quanto era sicura e i tempi) in un nuovo file CSV nella cartella ai-log accanto a SoundVisualizer.exe, così puoi rivedere in seguito cosa ha sentito. Se non è possibile scrivere in quella cartella, il file va in %LOCALAPPDATA%\\SoundVisualizer\\ai-log. I file possono diventare grandi, quindi disattivalo quando hai finito.",
+ "settings.auto_minimize.title": "Riduci a icona il launcher all'avvio",
+ "settings.auto_minimize.desc": "Riduce automaticamente a icona questa finestra del launcher all'avvio dell'overlay.",
+
+ "sounds.title": "Scegli i tipi di suono",
+ "sounds.desc": "I suoni che l'IA riconosce sono raggruppati con suoni simili, in base al tipo a cui appartengono per impostazione predefinita. Se cambi il tipo di un gruppo, il colore dei suoi suoni segue subito il nuovo tipo.",
+ "sounds.sibling_note": "L'IA spesso riconosce uno stesso suono con più nomi insieme. Una sirena, per esempio, viene riconosciuta anche come “Allarme”, che si trova in un altro gruppo. Se dopo una modifica un suono appare ancora con il tipo precedente, cambia anche i gruppi o i suoni collegati.",
+ "sounds.advanced.title": "Modalità avanzata",
+ "sounds.advanced.desc": "Mostra ogni suono nel suo gruppo, così puoi cambiare i suoni uno per uno.",
+ "sounds.search.hint": "Cerca suoni",
+ "sounds.search.clear": "Cancella ricerca",
+ "sounds.filter.title": "Filtro",
+ "sounds.filter.all": "Tutti",
+ "sounds.filter.changed": "Modificati",
+ "sounds.type.ambient": "Suoni ambientali",
+ "sounds.type.speech": "Voci",
+ "sounds.type.danger": "Suoni di pericolo",
+ "sounds.type.default": "{0} (predefinito)",
+ "sounds.type.mixed": "Misto",
+ "sounds.changed_count": "Suoni modificati: {0}",
+ "sounds.changed_from": "Predefinito: {0}",
+ "sounds.reset.button": "Ripristina tutto",
+ "sounds.reset.title": "Ripristinare tutti i suoni?",
+ "sounds.reset.message": "Tutti i suoni che hai modificato torneranno al loro tipo predefinito.",
+ "sounds.reset.confirm": "Ripristina",
+ "sounds.reset.cancel": "Annulla",
+ "sounds.empty": "Nessun suono corrispondente.",
+ "sounds.group.mixed_note": "Alcuni suoni modificati uno per uno",
+ "sounds.group.changed_line": "Attuale: {0} · Predefinito: {1}",
+ "sounds.overwrite.title": "Cambiare tutto il gruppo?",
+ "sounds.overwrite.message": "Suoni del gruppo “{0}” che passeranno al tipo “{2}”: {1}. Verranno sovrascritti anche i suoni modificati uno per uno.",
+ "sounds.overwrite.confirm": "Cambia",
+ "sounds.overwrite.cancel": "Annulla",
+ "sounds.basic_warning.title": "Passare alla modalità base?",
+ "sounds.basic_warning.message": "Alcuni gruppi contengono suoni che hai modificato uno per uno. In modalità base, se scegli un tipo per uno di questi gruppi, tutti i suoi suoni prendono quel tipo e le modifiche fatte uno per uno in quel gruppo vengono sovrascritte. Non cambia nulla finché non scegli.",
+ "sounds.basic_warning.confirm": "Usa la modalità base",
+ "sounds.basic_warning.cancel": "Resta in modalità avanzata",
+ "sounds.cd.sound": "{0}, tipo: {1}",
+ "sounds.cd.sound_changed": "{0}, tipo: {1}, predefinito: {2}",
+ "sounds.cd.group": "Gruppo {0}, tipo: {1}, suoni: {2} ({3})",
+ "sounds.cd.group_changed": "Gruppo {0}, tipo: {1}, predefinito: {2}, suoni: {3} ({4})",
+ "sounds.cd.group_header": "Gruppo {0}, suoni: {1}",
+
+ "hotkey.prompt": "Premi un tasto (Esc per annullare)",
+ "hotkey.none": "Nessuna",
+ "hotkey.button_name": "{0}, scorciatoia: {1}",
+
+ "help.start.title": "Per iniziare",
+ "help.start.1": "Scegli la lingua in alto in questa finestra e, se vuoi, cambia l'aspetto della grafica nella scheda Impostazioni.",
+ "help.start.2": "Nella scheda Home, fai clic su “{0}”.",
+ "help.start.3": "La grafica viene disegnata lungo i bordi dello schermo, sopra le altre finestre come giochi e video. I clic la attraversano, così puoi continuare a usare la finestra sottostante.",
+ "help.start.4": "Per disattivare la visualizzazione, fai clic su “{0}” nella scheda Home, oppure fai clic con il pulsante destro sull'icona di SoundVisualizer nell'area di notifica della barra delle applicazioni e scegli “{0}”.",
+ "help.modes.title": "Modalità visive",
+ "help.mode.wave": "L'intero bordo dello schermo avanza verso l'interno in base al volume. Il lato da cui proviene il suono avanza di più.",
+ "help.mode.pad": "Lungo il bordo dello schermo si illuminano barre in 8 punti. È la modalità che mostra la direzione in modo più chiaro.",
+ "help.mode.circle": "Un anello al centro dello schermo si gonfia in direzione del suono.",
+ "help.mode.outline": "Disegna solo un sottile contorno dell'onda, così copre meno il gioco.",
+ "help.modes.note": "Opzioni come Dimensione e Opacità vengono salvate separatamente per ogni modalità e si possono modificare nella scheda Impostazioni.",
+ "help.sound.title": "Tipi di suono",
+ "help.sound.kinds": "L'IA classifica l'audio in suoni ambientali, voci e suoni di pericolo. Per impostazione predefinita, i suoni ambientali sono bianchi, le voci gialle e i suoni di pericolo rossi. Nella scheda Impostazioni puoi cambiare il colore di ogni tipo e scegliere se mostrarlo.",
+ "help.sound.groups": "Nella scheda Suoni i suoni simili sono raggruppati e scegli il tipo di ogni gruppo. Per esempio, se sposti il gruppo “Campanello della porta” tra i suoni di pericolo, quando l'IA riconosce un campanello della porta o il bussare, il suono prende il colore dei suoni di pericolo. Un suono che sposti tu tra i suoni di pericolo, però, partecipa solo al voto dell'IA sul tipo, quindi non significa che ogni campanello della porta verrà mostrato come suono di pericolo.",
+ "help.sound.advanced": "Per cambiare i suoni uno per uno, attiva la modalità avanzata.",
+ "help.sound.danger": "Quando viene rilevato un suono di pericolo (come uno sparo), il colore cambia all'istante invece di sfumare gradualmente.",
+ "help.pc.title": "Impostazioni PC e scorciatoie da tastiera",
+ "help.sound_mode.title": "Modalità audio",
+ "help.sound_mode.body": "Per distinguere anche il davanti dal dietro, oltre alla sinistra e alla destra, configura il dispositivo di uscita come “Surround 5.1” (6 canali) o “Surround 7.1” (8 canali) nelle impostazioni audio di Windows. Con un normale stereo (2 canali), l'app distingue la sinistra dalla destra, ma non il davanti dal dietro. La “Modalità audio” nella scheda Impostazioni si adatta al dispositivo di uscita all'avvio e quando il dispositivo cambia, e puoi comunque modificarla tu.",
+ "help.sound_mode.tip": "💡 Se non hai un dispositivo di uscita 7.1, puoi installare il {0} e impostare il dispositivo di uscita su 7.1 canali per mostrare l'overlay con l'audio surround 7.1.",
+ "help.sound_mode.tip_link": "driver virtuale VB-CABLE",
+ "help.hotkeys.title": "Scorciatoie personalizzate in tempo reale",
+ "help.hotkeys.body": "Nella scheda Impostazioni, fai clic su un pulsante di scorciatoia e premi un tasto o una combinazione di tasti (ad es. Ctrl + Shift + A) per cambiare la scorciatoia come preferisci. Premendo la scorciatoia, la modalità cambia subito, anche quando l'app è in background.",
+ "help.hotkeys.defaults": "Per impostazione predefinita, F2 cambia la modalità audio, F3 cambia la modalità visiva e F4 apre le impostazioni dell'overlay.",
+ "help.size_lock.title": "Modalità dimensione bloccata",
+ "help.size_lock.body": "La modalità dimensione bloccata mantiene la grafica a una dimensione fissa usando una trasparenza proporzionale al volume del suono. È utile se vuoi mantenere la grafica sempre della stessa dimensione continuando a vederla reagire al suono.",
+ "help.editor.title": "Modalità di modifica dell'overlay in tempo reale",
+ "help.editor.body": "Premi la scorciatoia assegnata (F4 per impostazione predefinita) per attivare la modalità di modifica dell'overlay. Puoi trascinare con il mouse sullo schermo per regolare il limite di dimensione della grafica e rifinire i dettagli nel pannello delle impostazioni che compare.",
+ "help.developer.title": "Modalità sviluppatore",
+ "help.developer.body": "Attivando la modalità sviluppatore, l'overlay mostra in alto a sinistra cosa ha sentito l'IA (il nome del suono, quanto era sicura e quanto tempo ha impiegato) e in alto a destra gli FPS. Se attivi anche “{0}”, ogni risultato viene salvato in un file CSV. È utile per verificare che il sistema funzioni correttamente.",
+ "help.notes.title": "Buono a sapersi",
+ "help.note.direction_surround": "Con un'uscita a 5.1 o 7.1 canali, la parte alta dello schermo mostra i suoni che arrivano da davanti e quella bassa i suoni che arrivano da dietro. I lati sinistro e destro mostrano i suoni da sinistra e da destra, mentre la grafica che compare contemporaneamente da tutti i lati dello schermo indica suoni ambientali o senza una direzione.",
+ "help.note.direction_stereo": "Con un'uscita a 2 canali (stereo), l'app distingue la sinistra dalla destra, ma non il davanti dal dietro. In questo caso la grafica in alto e in basso sullo schermo rappresenta i suoni che arrivano in modo simile da entrambi i lati e il senso di spazio.",
+ "help.note.sources": "Viene mostrato il suono di tutte le app riprodotte dal dispositivo di riproduzione predefinito di Windows. Il suono che un'app invia a un altro dispositivo, come delle cuffie usate solo per la chat vocale, non viene mostrato.",
+ "help.note.volume": "Se il volume di un'app è molto basso o disattivato, nell'app stessa o nel mixer volume di Windows, il suo suono potrebbe vedersi appena o non vedersi affatto. Se la grafica reagisce poco, alza un po' quel suono o aumenta la Sensibilità nella scheda Impostazioni.",
+ "help.note.ai": "La classificazione dell'IA può essere errata. Non affidarti a questa app come unica protezione quando è in gioco la sicurezza.",
+ "help.faq.title": "Domande frequenti",
+ "help.faq.stopped_q": "La visualizzazione si è disattivata senza che l'abbia interrotta.",
+ "help.faq.stopped_a": "Si disattiva quando l'acquisizione dell'audio si interrompe e non riprende entro pochi secondi, per esempio quando il dispositivo di riproduzione viene scollegato e non ce n'è un altro disponibile. In quel caso la scheda Home ne indica il motivo. Collega altoparlanti o cuffie, oppure imposta un dispositivo di riproduzione predefinito nelle impostazioni audio di Windows, poi avviala di nuovo.",
+ "help.faq.no_graphic_q": "Non vedo nessuna grafica.",
+ "help.faq.no_graphic_a": "Verifica che il suono venga riprodotto dal dispositivo di riproduzione predefinito di Windows e non sia disattivato, e se la scheda Home mostra un avviso. Se un gioco è in esecuzione a schermo intero esclusivo, prova la modalità finestra senza bordi o in finestra. Nella scheda Impostazioni controlla anche che l'Opacità non sia troppo bassa e che la visualizzazione del tipo di suono in riproduzione non sia disattivata.",
+ "help.faq.performance_q": "Usa molta CPU o GPU?",
+ "help.faq.performance_a": "Per alleggerirla, abbassa “{0}” nella scheda Impostazioni o disattiva “{1}”.",
+ "help.faq.language_q": "Come cambio la lingua dell'app?",
+ "help.faq.language_a": "Sceglila dall'elenco “{0}” in alto in questa finestra. Durante la visualizzazione puoi cambiarla anche nelle impostazioni dell'overlay. “{1}” segue la lingua di visualizzazione di Windows.",
+ "help.report.title": "Segnala un problema",
+ "help.report.desc": "Segnalaci un bug o un suono mostrato con il tipo di suono sbagliato. GitHub si apre nel browser con le informazioni di sistema qui sotto già inserite, quindi serve un account GitHub. Se non ce l'hai, inviaci invece un'email. Non viene mai inviato alcun audio.",
+ "help.report.github": "Segnala su GitHub",
+ "help.report.mail": "Segnala via email",
+ "help.report.copy": "Copia le informazioni di sistema",
+ "help.report.copied": "Informazioni di sistema copiate.",
+ "help.report.copy_failed": "Impossibile copiare negli appunti. Riprova.",
+ "help.report.no_browser": "Impossibile aprire un browser. Copia le informazioni di sistema e inviacele in un altro modo.",
+ "help.report.no_mail": "Nessuna app di posta per l'invio. Copia le informazioni di sistema e inviale in un altro modo.",
+ "help.report.issue_title": "Segnalazione dall'app",
+ "help.report.issue_body": "Che cosa è successo?\n\nChe cosa stavi ascoltando (app, tipo di suono)?\n\nChe cosa ti aspettavi invece?",
+ "help.about.title": "Informazioni",
+ "help.about.version": "Versione {0}",
+ "help.about.license": "Questa app è distribuita ai sensi della GNU Affero General Public License v3.0 (AGPL-3.0). I nomi e i gruppi dei suoni nella scheda Suoni si basano sull'ontologia AudioSet e sono distribuiti ai sensi della licenza CC BY-SA 4.0 (vedi “Licenze open source” qui sotto).",
+ "help.licenses.button": "Licenze open source",
+ "help.licenses.title": "Licenze open source",
+ "help.licenses.close": "Chiudi",
+
+ "notice.ai_unavailable": "Il riconoscimento del tipo di suono non funziona: tutti i suoni vengono mostrati con il colore dei suoni ambientali. Prova a disattivare e riattivare la visualizzazione.",
+ "notice.ai_unavailable_hidden": "Il riconoscimento del tipo di suono non funziona: tutti i suoni contano come suoni ambientali, ma la visualizzazione dei suoni ambientali è disattivata, quindi non si vede niente. Attiva “{0}” nella scheda Impostazioni oppure prova a disattivare e riattivare la visualizzazione.",
+ "notice.stopped.title": "Visualizzazione suoni disattivata",
+ "notice.stopped.no_device": "Non è stato possibile aprire nessun dispositivo di riproduzione, quindi l'audio non può essere acquisito. Collega altoparlanti o cuffie, oppure imposta un dispositivo di riproduzione predefinito nelle impostazioni audio di Windows, poi avvia di nuovo.",
+ "notice.stopped.capture_error": "L'acquisizione dell'audio si è interrotta per un errore, quindi la grafica è disattivata.",
+ "notice.dismiss": "Chiudi avviso",
+
+ "tray.open": "Apri il launcher",
+ "tray.exit": "Esci",
+
+ "more_info.title": "Repository GitHub e contributi",
+ "more_info.body": "SoundVisualizer è un progetto open source e il suo codice sorgente è pubblico su GitHub. Puoi partecipare segnalando bug, suggerendo funzionalità o contribuendo allo sviluppo. Fai clic sul pulsante qui sotto per visitare il repository ufficiale su GitHub! Nella scheda Releases di GitHub puoi anche controllare e scaricare l'ultima versione.",
+ "more_info.github_button": "Visita il repository GitHub",
+
+ "dialog.notice_title": "Avviso",
+ "dialog.already_running": "L'overlay è già in esecuzione.",
+ "dialog.reset_title": "Ripristina impostazioni",
+ "dialog.reset_wave": "Ripristinare le impostazioni predefinite della modalità Onda?",
+ "dialog.reset_pad": "Ripristinare le impostazioni predefinite della modalità Pad?",
+ "dialog.reset_circle": "Ripristinare le impostazioni predefinite della modalità Cerchio?",
+ "dialog.reset_outline": "Ripristinare le impostazioni predefinite della modalità Contorno?",
+ "dialog.reset_sound_types": "Ripristinare le impostazioni predefinite dei tipi di suono?",
+ "dialog.reset_all_title": "Ripristina valori predefiniti",
+ "dialog.reset_all": "Vuoi davvero ripristinare tutte le impostazioni ai valori predefiniti?",
+ "dialog.link_error": "Impossibile aprire il link: {0}",
+
+ "editor.title": "Impostazioni overlay",
+ "editor.drag_tip": "Trascina con il mouse le linee guida sullo schermo per regolare in tempo reale il limite di altezza e la dimensione del centro dell'onda dell'overlay!",
+ "editor.glow": "Effetto bagliore",
+ "editor.glow_strength": "Intensità",
+ "editor.color": "Colore",
+ "editor.hotkeys": "Impostazioni scorciatoie",
+ "editor.developer_mode_desc": "Cosa ha sentito l'IA (in alto a sinistra) e gli FPS (in alto a destra)",
+ "editor.save_close_hint": "per salvare e chiudere",
+ "editor.close_overlay": "Chiudi l'overlay",
+ "editor.guide.limit": "Limite in modalità {0}",
+ "editor.guide.size": "Dimensione in modalità {0}: {1}%",
+ "editor.guide.base_radius": "Raggio base: {0}",
+ "editor.guide.circle_size": "Dimensione cerchio: {0}%",
+
+ "hud.visual_mode": "Modalità visiva: [{0}] {1}",
+ "hud.sound_mode": "Modalità audio: [{0}] {1}",
+ "hud.overlay_settings": "Impostazioni overlay: [{0}]",
+ "hud.status.ready": "Sistema pronto",
+ "hud.status.stereo": "Audio del PC: {0}ch (stereo)\nSorgente: in attesa...",
+ "hud.status.surround": "Audio del PC: 8ch (7.1)\nSorgente: in attesa...",
+
+ "color_picker.title": "Scegli un colore",
+ "color_picker.hex": "Codice HEX",
+ "color_picker.cancel": "Annulla",
+ "color_picker.apply": "Applica",
+ "color_picker.area": "Saturazione e luminosità",
+ "color_picker.area_value": "Saturazione {0}%, luminosità {1}%",
+ "color_picker.hue": "Tonalità",
+ "color_picker.keys_help": "Usa i tasti freccia per cambiarlo. Tieni premuto Maiusc per cambiarlo a passi più grandi.",
+ "color_picker.presets": "Colori comuni",
+ "color_picker.preset.white": "Bianco",
+ "color_picker.preset.red": "Rosso",
+ "color_picker.preset.orange": "Arancione",
+ "color_picker.preset.yellow": "Giallo",
+ "color_picker.preset.green": "Verde",
+ "color_picker.preset.cyan": "Turchese",
+ "color_picker.preset.blue": "Blu",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "Nessun dispositivo di uscita trovato.",
+ "audio.error.capture_stopped": "L'acquisizione si è interrotta in modo imprevisto: {0}",
+ "audio.error.capture_start_failed": "Impossibile avviare il motore di acquisizione: {0}",
+
+ "tutorial.window_title": "Tutorial",
+ "tutorial.skip": "Salta",
+ "tutorial.previous": "Pagina precedente",
+ "tutorial.next": "Avanti",
+ "tutorial.done": "Ho capito",
+ "tutorial.page_status": "Pagina {0} di {1}",
+ "tutorial.pause": "Metti in pausa l'animazione",
+ "tutorial.play": "Riproduci l'animazione",
+ "tutorial.sound_on": "Attiva l'audio",
+ "tutorial.sound_off": "Disattiva l'audio",
+ "tutorial.sound.title": "Vedi i suoni sullo schermo",
+ "tutorial.sound.body": "Il suono del gioco o del video viene disegnato lungo i bordi dello schermo. Più il suono è forte, più il disegno è grande. Viene mostrato solo l'audio riprodotto su questo PC. I suoni intorno a te non vengono captati.",
+ "tutorial.direction.title": "Mostra da dove arriva il suono",
+ "tutorial.direction.body": "Con un'uscita a 2 canali (stereo), un suono a sinistra viene disegnato più grande sul lato sinistro, e un suono a destra sul lato destro. Con un'uscita a 5.1 o 7.1 canali, l'app distingue anche il davanti dal dietro: i suoni che arrivano da davanti vengono disegnati in alto sullo schermo e quelli che arrivano da dietro in basso.",
+ "tutorial.types.title": "Ogni tipo di suono ha il suo colore",
+ "tutorial.types.body": "L'IA classifica i suoni in tre tipi e disegna ogni tipo con un colore diverso. Puoi cambiare i colori nella scheda Impostazioni.",
+ "tutorial.type.ambient_examples": "Musica, pioggia e tutto il resto",
+ "tutorial.type.speech_examples": "Persone che parlano",
+ "tutorial.type.danger_examples": "Spari, esplosioni, sirene",
+ "tutorial.start.title": "Come si attiva e si disattiva",
+ "tutorial.start.hotkeys": "Durante la visualizzazione, premi {0} per cambiare la modalità audio, {1} per cambiare la modalità visiva e {2} per aprire le impostazioni dell'overlay."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/ja.json b/SoundVisualizer.Core/Localization/Strings/ja.json
new file mode 100644
index 0000000..ac0e2f1
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/ja.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "言語",
+ "common.reset_defaults": "初期設定に戻す",
+ "common.minimize": "最小化",
+ "common.close": "閉じる",
+ "language.system": "システムのデフォルト",
+
+ "launcher.window_title": "SoundVisualizer ランチャー",
+ "launcher.tab.home": "ホーム",
+ "launcher.tab.settings": "設定",
+ "launcher.tab.sounds": "分類",
+ "launcher.tab.help": "ヘルプ",
+ "launcher.tab.more_info": "詳細情報",
+ "launcher.home.desc": "目に見えない音を画面に描き出します。\nゲームも映画も、新しい体験をどうぞ。",
+ "launcher.home.start": "開始",
+ "launcher.home.stop": "停止",
+ "launcher.home.tutorial": "チュートリアルを見る",
+ "launcher.status.running": "状態:実行中",
+ "launcher.status.idle": "状態:待機中",
+
+ "settings.section.mode": "モード設定",
+ "settings.visual_mode.title": "表示モード",
+ "settings.visual_mode.desc": "画面に描くグラフィックの基本的な形を選びます。",
+ "settings.mode.wave": "波",
+ "settings.mode.pad": "パッド",
+ "settings.mode.circle": "円形",
+ "settings.mode.outline": "輪郭線",
+ "settings.sound_mode.title": "サウンドモード",
+ "settings.sound_mode.desc": "スピーカーに合ったサウンドチャンネルを選びます。\n2チャンネルのスピーカーなら「2チャンネル」、サラウンドスピーカーなら「5.1チャンネル」か「7.1チャンネル」を選んでください。",
+ "settings.channels.2": "2チャンネル",
+ "settings.channels.5_1": "5.1チャンネル",
+ "settings.channels.7_1": "7.1チャンネル",
+ "settings.max_fps.title": "最大フレームレート",
+ "settings.max_fps.desc": "グラフィックを描画するフレームレートの上限を設定します。",
+ "settings.section.mode_detail": "モード別の詳細設定",
+ "settings.mode_card.wave": "波モード",
+ "settings.mode_card.pad": "パッドモード",
+ "settings.mode_card.circle": "円形モード",
+ "settings.mode_card.outline": "輪郭線モード",
+ "settings.size_lock.title": "サイズを固定",
+ "settings.size_lock.desc": "グラフィックのサイズを固定し、音の大きさを濃さで表します。",
+ "settings.fixed_size.wave": "波のサイズ",
+ "settings.fixed_size.pad": "パッドのサイズ",
+ "settings.fixed_size.circle": "円形のサイズ",
+ "settings.fixed_size.outline": "輪郭線の太さ",
+ "settings.fixed_size.desc": "濃さが変わってもグラフィックが保つ大きさを設定します。",
+ "settings.max_opacity.title": "最大の濃さ",
+ "settings.max_opacity.desc": "音が最も大きいときのグラフィックの濃さです。",
+ "settings.size.title": "サイズ",
+ "settings.size.desc.wave": "波の上下の高さと全体のボリューム感を調整します。",
+ "settings.size.desc.pad": "音に反応したときのパッドの太さと横幅を調整します。",
+ "settings.size.desc.circle": "音が鳴ったときに、円形イコライザーがどれだけ外へふくらむかを調整します。",
+ "settings.size.desc.outline": "輪郭線の波の上下の高さと全体のボリューム感を調整します。",
+ "settings.opacity.title": "濃さ",
+ "settings.opacity.desc.wave": "値が大きいほど濃く、小さいほど薄く透けて見えます。",
+ "settings.opacity.desc.pad": "値が大きいほど濃く、小さいほど薄く透けて見えます。",
+ "settings.opacity.desc.circle": "値が大きいほど濃く、小さいほど薄く透けて見えます。",
+ "settings.opacity.desc.outline": "値が大きいほど濃く、小さいほど薄く透けて見えます。",
+ "settings.speed.title": "速度",
+ "settings.speed.desc.wave": "波が流れて揺らめく速さを調整します。",
+ "settings.speed.desc.pad": "音のする方向へパッドが向きを変える速さを調整します。",
+ "settings.speed.desc.circle": "音に反応して円形イコライザーが波打ったり回転したりする速さを調整します。",
+ "settings.speed.desc.outline": "輪郭線の波が流れて揺らめく速さを調整します。",
+ "settings.sensitivity.title": "感度",
+ "settings.sensitivity.desc.wave": "小さな音にも波がどれだけ敏感に反応して揺れるかを調整します。",
+ "settings.sensitivity.desc.pad": "パッドが音をよく捉え、よりはっきり目立つように調整します。",
+ "settings.sensitivity.desc.circle": "音に反応するとき、円形の振幅をどれだけ細かく分けて動かすかを調整します。",
+ "settings.sensitivity.desc.outline": "小さな音にも輪郭線の波がどれだけ敏感に反応して揺れるかを調整します。",
+ "settings.glow.title": "発光",
+ "settings.glow.desc.wave": "波の縁に柔らかなネオン風の光を加え、その強さを調整します。\n注意:GPU の負荷が増えます。",
+ "settings.glow.desc.pad": "パッドの周りに、オーラのような柔らかな光を加えます。\n注意:GPU の負荷が増えます。",
+ "settings.glow.desc.circle": "円形イコライザーの縁に沿って、きらめく光の輪を加えます。\n注意:GPU の負荷が増えます。",
+ "settings.glow.desc.outline": "輪郭線に柔らかなネオン風の光を加え、その強さを調整します。\n注意:GPU の負荷が増えます。",
+ "settings.radius.title": "半径",
+ "settings.radius.desc": "円形モードで、中央のリングの半径を調整します。",
+ "settings.section.sound_types": "音の種類(AI)",
+ "settings.show_ambient": "環境音を表示",
+ "settings.show_speech": "話し声を表示",
+ "settings.show_danger": "危険音を表示",
+ "settings.set_color": "色を設定",
+ "settings.set_color_for": "色を設定:{0}",
+ "settings.section.hotkeys": "ショートカットキー",
+ "settings.hotkey.visual.title": "表示モードの切り替え",
+ "settings.hotkey.visual.desc": "実行中に形をリアルタイムで切り替えるショートカットキーです。",
+ "settings.hotkey.sound.title": "サウンドモードの切り替え",
+ "settings.hotkey.sound.desc": "実行中にサウンドモードをリアルタイムで切り替えるショートカットキーです。",
+ "settings.hotkey.edit.title": "オーバーレイ設定",
+ "settings.hotkey.edit.desc": "オーバーレイの編集画面を開いたり閉じたりするショートカットキーです。",
+ "settings.hotkey.set": "キーを設定",
+ "settings.section.advanced": "詳細設定",
+ "settings.developer_mode.title": "開発者モード",
+ "settings.developer_mode.desc": "AI が聞き取った内容(報告した音の名前、確信度、かかった時間)をオーバーレイの左上に、FPS を右上に表示します。表示は英語で、普段使うためのものではありません。",
+ "settings.developer_record.title": "結果をファイルに記録",
+ "settings.developer_record.desc": "AI の結果を毎回、SoundVisualizer.exe と同じ場所にある ai-log フォルダーの新しい CSV ファイルに書き出します。いちばん高く評価した音の名前、確信度、かかった時間も記録されるので、何の音と判断したかを後から振り返れます。そのフォルダーに書き込めない場合は %LOCALAPPDATA%\\SoundVisualizer\\ai-log に保存します。ファイルが大きくなることがあるので、確認が終わったらオフにしてください。",
+ "settings.auto_minimize.title": "開始時にランチャーを最小化",
+ "settings.auto_minimize.desc": "オーバーレイの開始時に、このランチャーウィンドウを自動で最小化します。",
+
+ "sounds.title": "音の種類を選ぶ",
+ "sounds.desc": "AI が認識する音を、似た音どうしでグループにまとめ、初期設定の種類ごとに表示します。グループの種類を変えると、そのグループの音の色はすぐに新しい種類に合わせて変わります。",
+ "sounds.sibling_note": "AI は一つの音を複数の名前で同時に聞き取ることがよくあります。たとえばサイレンは「警報音」としても聞き取られますが、警報音は別のグループにあります。変更後も前の種類で表示される場合は、関連するグループや音も変更してください。",
+ "sounds.advanced.title": "詳細モード",
+ "sounds.advanced.desc": "すべての音をグループごとに表示し、個別に変更できます。",
+ "sounds.search.hint": "音の名前で検索",
+ "sounds.search.clear": "検索語を消去",
+ "sounds.filter.title": "絞り込み",
+ "sounds.filter.all": "すべて",
+ "sounds.filter.changed": "変更した音",
+ "sounds.type.ambient": "環境音",
+ "sounds.type.speech": "話し声",
+ "sounds.type.danger": "危険音",
+ "sounds.type.default": "{0}(初期設定)",
+ "sounds.type.mixed": "混在",
+ "sounds.changed_count": "変更した音:{0}件",
+ "sounds.changed_from": "初期設定:{0}",
+ "sounds.reset.button": "すべてリセット",
+ "sounds.reset.title": "すべての音を初期設定に戻しますか?",
+ "sounds.reset.message": "変更した音はすべて、初期設定の種類に戻ります。",
+ "sounds.reset.confirm": "リセット",
+ "sounds.reset.cancel": "キャンセル",
+ "sounds.empty": "該当する音がありません。",
+ "sounds.group.mixed_note": "一部の音を個別に変更済み",
+ "sounds.group.changed_line": "現在:{0} · 初期設定:{1}",
+ "sounds.overwrite.title": "グループ全体を変更しますか?",
+ "sounds.overwrite.message": "「{0}」グループの音{1}件が「{2}」に変わります。個別に変更した音も上書きされます。",
+ "sounds.overwrite.confirm": "変更",
+ "sounds.overwrite.cancel": "キャンセル",
+ "sounds.basic_warning.title": "基本モードに切り替えますか?",
+ "sounds.basic_warning.message": "個別に変更した音を含むグループがあります。基本モードでこうしたグループの種類を選ぶと、グループ内のすべての音がその種類になり、個別に変更した内容は上書きされます。種類を選ぶまでは何も変わりません。",
+ "sounds.basic_warning.confirm": "基本モードにする",
+ "sounds.basic_warning.cancel": "詳細モードのまま",
+ "sounds.cd.sound": "{0}、種類:{1}",
+ "sounds.cd.sound_changed": "{0}、種類:{1}、初期設定:{2}",
+ "sounds.cd.group": "グループ:{0}、種類:{1}、音:{2}件({3})",
+ "sounds.cd.group_changed": "グループ:{0}、種類:{1}、初期設定:{2}、音:{3}件({4})",
+ "sounds.cd.group_header": "グループ:{0}、音:{1}件",
+
+ "hotkey.prompt": "キーを押してください(Esc でキャンセル)",
+ "hotkey.none": "なし",
+ "hotkey.button_name": "{0}、ショートカットキー:{1}",
+
+ "help.start.title": "はじめに",
+ "help.start.1": "このウィンドウの上で言語を選び、必要に応じて「設定」タブでグラフィックの見た目を変更します。",
+ "help.start.2": "「ホーム」タブで「{0}」をクリックします。",
+ "help.start.3": "グラフィックは、ゲームや動画などほかのウィンドウの上に、画面の端に沿って描かれます。クリックはグラフィックを通り抜けるので、下にあるウィンドウをそのまま操作できます。",
+ "help.start.4": "オフにするには、「ホーム」タブで「{0}」をクリックするか、タスクバーの通知領域にある SoundVisualizer のアイコンを右クリックして「{0}」を選びます。",
+ "help.modes.title": "表示モード",
+ "help.mode.wave": "画面の端全体が、音の大きさに合わせて内側へ押し寄せます。音のする側は、さらに深く押し寄せます。",
+ "help.mode.pad": "画面の端の8か所にバーが浮かび上がります。方向が最もはっきり分かるモードです。",
+ "help.mode.circle": "画面中央のリングが、音のする方向へふくらみます。",
+ "help.mode.outline": "波の輪郭線だけを細く描くので、ゲーム画面が隠れにくくなります。",
+ "help.modes.note": "「サイズ」や「濃さ」などの設定はモードごとに保存され、「設定」タブで変更できます。",
+ "help.sound.title": "音の種類",
+ "help.sound.kinds": "聞こえてくる音を、AI が環境音・話し声・危険音に分類します。初期設定の色は、環境音が白、話し声が黄色、危険音が赤です。種類ごとの色と表示の有無は「設定」タブで変更できます。",
+ "help.sound.groups": "「分類」タブでは、似た音をグループにまとめ、グループごとにどの種類として扱うかを選びます。たとえば「玄関チャイム」グループを危険音に変えると、AI が玄関チャイムやノックとして聞き取ったときに危険音の色が使われます。ただし、新しく危険音にした音は AI が種類を決める投票に加わるだけなので、玄関チャイムが鳴るたびに危険音として表示されるわけではありません。",
+ "help.sound.advanced": "音を個別に変更するには、詳細モードをオンにします。",
+ "help.sound.danger": "危険音(銃声など)を検出すると、色が徐々にではなく、すぐに切り替わります。",
+ "help.pc.title": "PC の設定とショートカットキー",
+ "help.sound_mode.title": "サウンドモード",
+ "help.sound_mode.body": "前後の音まで区別して表示するには、Windows のサウンド設定で出力デバイスを「5.1 サラウンド」(6チャンネル)または「7.1 サラウンド」(8チャンネル)に設定してください。通常のステレオ(2チャンネル)では左右は区別できますが、前後は区別できません。「設定」タブの「サウンドモード」は、起動時と出力デバイスが変わったときにそのデバイスに合わせて切り替わり、自分で変更することもできます。",
+ "help.sound_mode.tip": "💡 7.1の出力デバイスがない場合は、{0}をインストールして出力デバイスを7.1チャンネルに設定すると、7.1チャンネルのサラウンドでオーバーレイを表示できます。",
+ "help.sound_mode.tip_link": "VB-CABLE 仮想ドライバー",
+ "help.hotkeys.title": "リアルタイムのカスタムショートカットキー",
+ "help.hotkeys.body": "「設定」タブでショートカットキーのボタンをクリックし、単独のキーまたはキーの組み合わせ(例:Ctrl + Shift + A)を押すと、ショートカットキーを好きなように変更できます。アプリがバックグラウンドにあるときでも、ショートカットキーを押すとすぐにモードが切り替わります。",
+ "help.hotkeys.defaults": "初期設定では、F2 でサウンドモード、F3 で表示モードを切り替え、F4 でオーバーレイ設定を開きます。",
+ "help.size_lock.title": "サイズ固定モード",
+ "help.size_lock.body": "サイズ固定モードは、音の大きさをサイズではなく濃さで表し、グラフィックのサイズを一定に保つ設定です。グラフィックの大きさを変えずに、音への反応を見たいときに便利です。",
+ "help.editor.title": "オーバーレイのリアルタイム編集モード",
+ "help.editor.body": "割り当てたショートカットキー(初期設定は F4)を押すと、オーバーレイの編集モードがオンになります。画面上でマウスをドラッグしてグラフィックのサイズの上限を調整でき、表示される設定パネルで細かな調整ができます。",
+ "help.developer.title": "開発者モード",
+ "help.developer.body": "開発者モードをオンにすると、AI が聞き取った内容(音の名前、確信度、かかった時間)がオーバーレイの左上に、FPS が右上に表示されます。「{0}」もオンにすると、すべての結果が CSV ファイルに保存されます。システムが正しく動作しているか確認したいときに便利です。",
+ "help.notes.title": "知っておきたいこと",
+ "help.note.direction_surround": "5.1 または 7.1 チャンネルで出力すると、画面の上側は前方からの音、下側は後方からの音を表します。左右はそれぞれ左と右からの音を表し、画面の四方すべてから同時に現れるグラフィックは、環境音や方向のない音です。",
+ "help.note.direction_stereo": "2 チャンネル(ステレオ)で出力すると、左右は区別できますが、前後は区別できません。このとき画面の上下に表示されるグラフィックは、左右から同じくらい聞こえる音と、空間の広がりを表します。",
+ "help.note.sources": "Windows の既定の再生デバイスから出ている、すべてのアプリの音を表示します。アプリがほかのデバイスに送っている音(ボイスチャット専用のヘッドセットなど)は表示されません。",
+ "help.note.volume": "アプリ内や Windows の音量ミキサーで、そのアプリの音量をかなり下げたりミュートしたりすると、音がほとんど、またはまったく表示されないことがあります。反応が弱いときは、その音を少し大きくするか、「設定」タブで「感度」を上げてください。",
+ "help.note.ai": "AI による分類は間違うことがあります。安全に関わる場面では、このアプリだけに頼らないでください。",
+ "help.faq.title": "よくある質問",
+ "help.faq.stopped_q": "自分でオフにしていないのにオフになりました。",
+ "help.faq.stopped_a": "音の取得が止まり、数秒以内に戻らないとオフになります。たとえば、再生デバイスが外れて、ほかに使えるデバイスがない場合です。そのときは「ホーム」タブに理由が表示されます。スピーカーやヘッドホンを接続するか、Windows のサウンド設定で既定の再生デバイスを設定してから、もう一度開始してください。",
+ "help.faq.no_graphic_q": "グラフィックが表示されません。",
+ "help.faq.no_graphic_a": "音が Windows の既定の再生デバイスから出ていて、ミュートになっていないか、「ホーム」タブにお知らせが出ていないかを確認してください。ゲームを排他的フルスクリーンで実行している場合は、ボーダーレスウィンドウやウィンドウモードを試してください。また、「設定」タブで「濃さ」が低すぎないか、今鳴っている音の種類の表示がオフになっていないかも確認してください。",
+ "help.faq.performance_q": "CPU や GPU を多く使いますか?",
+ "help.faq.performance_a": "負荷を軽くするには、「設定」タブで「{0}」を下げるか、「{1}」をオフにしてください。",
+ "help.faq.language_q": "アプリの言語はどうやって変更しますか?",
+ "help.faq.language_a": "このウィンドウの上にある「{0}」の一覧から選びます。実行中はオーバーレイ設定でも変更できます。「{1}」を選ぶと、Windows の表示言語に合わせて表示されます。",
+ "help.report.title": "問題を報告",
+ "help.report.desc": "バグを見つけたときや、音が間違った種類として表示されたときは、お知らせください。ブラウザで GitHub が開き、下の環境情報があらかじめ入力されます。GitHub のアカウントが必要なため、アカウントがない場合はメールで送ってください。音声は一切送信されません。",
+ "help.report.github": "GitHub で報告",
+ "help.report.mail": "メールで報告",
+ "help.report.copy": "環境情報をコピー",
+ "help.report.copied": "環境情報をコピーしました。",
+ "help.report.copy_failed": "クリップボードにコピーできませんでした。もう一度お試しください。",
+ "help.report.no_browser": "ブラウザを開けませんでした。代わりに、環境情報をコピーして送ってください。",
+ "help.report.no_mail": "メールを送信できるアプリがありません。環境情報をコピーして、別の方法で送ってください。",
+ "help.report.issue_title": "アプリからの報告",
+ "help.report.issue_body": "何が起きましたか?\n\n何を聞いていましたか?(アプリ、音の種類)\n\n本来はどうなることを期待していましたか?",
+ "help.about.title": "このアプリについて",
+ "help.about.version": "バージョン {0}",
+ "help.about.license": "このアプリは GNU Affero General Public License v3.0(AGPL-3.0)のもとで配布されています。「分類」タブの音の名前とグループは AudioSet オントロジーをもとにしており、CC BY-SA 4.0 のもとで配布されています(詳しくは下の「オープンソース ライセンス」をご覧ください)。",
+ "help.licenses.button": "オープンソース ライセンス",
+ "help.licenses.title": "オープンソース ライセンス",
+ "help.licenses.close": "閉じる",
+
+ "notice.ai_unavailable": "音の種類を判別できないため、すべての音が環境音の色で表示されています。可視化を一度オフにして、もう一度オンにしてみてください。",
+ "notice.ai_unavailable_hidden": "音の種類を判別できないため、すべての音が環境音として扱われますが、環境音の表示がオフなので何も表示されません。「設定」タブで「{0}」をオンにするか、可視化を一度オフにして、もう一度オンにしてみてください。",
+ "notice.stopped.title": "音の可視化がオフになりました",
+ "notice.stopped.no_device": "開ける再生デバイスがないため、音を取得できません。スピーカーやヘッドホンを接続するか、Windows のサウンド設定で既定の再生デバイスを設定してから、もう一度開始してください。",
+ "notice.stopped.capture_error": "エラーで音の取得が止まったため、グラフィックがオフになりました。",
+ "notice.dismiss": "お知らせを閉じる",
+
+ "tray.open": "ランチャーを開く",
+ "tray.exit": "終了",
+
+ "more_info.title": "GitHub リポジトリとプロジェクトへの貢献",
+ "more_info.body": "SoundVisualizer はオープンソースのプロジェクトで、ソースコードは GitHub で公開されています。バグの報告や機能の提案、開発への貢献を通じてプロジェクトに参加できます。下のボタンをクリックして、公式の GitHub リポジトリにアクセスしてみてください!GitHub の Releases タブでは、最新バージョンを確認してダウンロードすることもできます。",
+ "more_info.github_button": "GitHub リポジトリにアクセス",
+
+ "dialog.notice_title": "お知らせ",
+ "dialog.already_running": "オーバーレイはすでに実行中です。",
+ "dialog.reset_title": "設定のリセット",
+ "dialog.reset_wave": "波モードの設定を初期設定に戻しますか?",
+ "dialog.reset_pad": "パッドモードの設定を初期設定に戻しますか?",
+ "dialog.reset_circle": "円形モードの設定を初期設定に戻しますか?",
+ "dialog.reset_outline": "輪郭線モードの設定を初期設定に戻しますか?",
+ "dialog.reset_sound_types": "音の種類の設定を初期設定に戻しますか?",
+ "dialog.reset_all_title": "初期設定に戻す",
+ "dialog.reset_all": "すべての設定を初期設定に戻してもよろしいですか?",
+ "dialog.link_error": "リンクを開けませんでした:{0}",
+
+ "editor.title": "オーバーレイ設定",
+ "editor.drag_tip": "画面のガイドラインをマウスでドラッグすると、オーバーレイの波の高さの上限と中央のサイズをリアルタイムで調整できます!",
+ "editor.glow": "発光効果",
+ "editor.glow_strength": "発光の強さ",
+ "editor.color": "色",
+ "editor.hotkeys": "ショートカットキー設定",
+ "editor.developer_mode_desc": "AI が聞き取った内容(左上)と FPS(右上)を表示",
+ "editor.save_close_hint": "キーで設定を保存して閉じる",
+ "editor.close_overlay": "オーバーレイを終了",
+ "editor.guide.limit": "{0}の上限ライン",
+ "editor.guide.size": "{0}のサイズ:{1}%",
+ "editor.guide.base_radius": "基本半径:{0}",
+ "editor.guide.circle_size": "円形のサイズ:{0}%",
+
+ "hud.visual_mode": "表示モード:[{0}] {1}",
+ "hud.sound_mode": "サウンドモード:[{0}] {1}",
+ "hud.overlay_settings": "オーバーレイ設定:[{0}]",
+ "hud.status.ready": "システム準備完了",
+ "hud.status.stereo": "PC のオーディオ:{0}ch(ステレオ)\nソース:待機中…",
+ "hud.status.surround": "PC のオーディオ:8ch(7.1)\nソース:待機中…",
+
+ "color_picker.title": "色を選択",
+ "color_picker.hex": "HEX コード",
+ "color_picker.cancel": "キャンセル",
+ "color_picker.apply": "適用",
+ "color_picker.area": "彩度と明るさ",
+ "color_picker.area_value": "彩度 {0}%、明るさ {1}%",
+ "color_picker.hue": "色相",
+ "color_picker.keys_help": "矢印キーで変更します。Shift キーを押しながら押すと大きく変わります。",
+ "color_picker.presets": "よく使う色",
+ "color_picker.preset.white": "白",
+ "color_picker.preset.red": "赤",
+ "color_picker.preset.orange": "オレンジ",
+ "color_picker.preset.yellow": "黄色",
+ "color_picker.preset.green": "緑",
+ "color_picker.preset.cyan": "水色",
+ "color_picker.preset.blue": "青",
+ "color_picker.preset.magenta": "マゼンタ",
+
+ "audio.error.no_device": "出力デバイスが見つかりません。",
+ "audio.error.capture_stopped": "キャプチャが予期せず停止しました:{0}",
+ "audio.error.capture_start_failed": "キャプチャエンジンを開始できませんでした:{0}",
+
+ "tutorial.window_title": "チュートリアル",
+ "tutorial.skip": "スキップ",
+ "tutorial.previous": "前のページ",
+ "tutorial.next": "次へ",
+ "tutorial.done": "OK",
+ "tutorial.page_status": "{1}ページ中{0}ページ目",
+ "tutorial.pause": "アニメーションを一時停止",
+ "tutorial.play": "アニメーションを再生",
+ "tutorial.sound_on": "音を出す",
+ "tutorial.sound_off": "音を消す",
+ "tutorial.sound.title": "音を画面で見られます",
+ "tutorial.sound.body": "ゲームや動画の音が、画面の端に沿って描かれます。音が大きいほど、大きく描かれます。表示するのは、この PC で再生中の音だけです。周囲の音は拾いません。",
+ "tutorial.direction.title": "音の方向がわかります",
+ "tutorial.direction.body": "2 チャンネル(ステレオ)で出力すると、左からの音は画面の左側が、右からの音は右側が大きく描かれます。5.1 または 7.1 チャンネルで出力すると前後も区別し、前方からの音は画面の上側に、後方からの音は下側に描かれます。",
+ "tutorial.types.title": "音の種類を色で区別します",
+ "tutorial.types.body": "AI が音を3種類に分類し、それぞれ違う色で描きます。色は「設定」タブで変更できます。",
+ "tutorial.type.ambient_examples": "音楽・雨音など、その他の音",
+ "tutorial.type.speech_examples": "人の声",
+ "tutorial.type.danger_examples": "銃声・爆発音・サイレン",
+ "tutorial.start.title": "オン/オフの方法",
+ "tutorial.start.hotkeys": "実行中は、{0} でサウンドモード、{1} で表示モードを切り替え、{2} でオーバーレイ設定を開きます。"
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/ko.json b/SoundVisualizer.Core/Localization/Strings/ko.json
new file mode 100644
index 0000000..c4687a1
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/ko.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "언어",
+ "common.reset_defaults": "기본값으로 되돌리기",
+ "common.minimize": "최소화",
+ "common.close": "닫기",
+ "language.system": "시스템 기본값",
+
+ "launcher.window_title": "SoundVisualizer 런처",
+ "launcher.tab.home": "홈",
+ "launcher.tab.settings": "설정",
+ "launcher.tab.sounds": "분류",
+ "launcher.tab.help": "도움말",
+ "launcher.tab.more_info": "더 알아보기",
+ "launcher.home.desc": "보이지 않던 소리를 화면에 그려냅니다.\n게이밍부터 영화 감상까지 새로운 경험을 시작하세요.",
+ "launcher.home.start": "실행",
+ "launcher.home.stop": "실행 종료",
+ "launcher.home.tutorial": "튜토리얼 보기",
+ "launcher.status.running": "상태: 실행 중",
+ "launcher.status.idle": "상태: 실행 대기 중",
+
+ "settings.section.mode": "모드 설정",
+ "settings.visual_mode.title": "표현 모드",
+ "settings.visual_mode.desc": "화면에 그려질 그래픽의 기본 형태를 선택합니다.",
+ "settings.mode.wave": "파도",
+ "settings.mode.pad": "패드",
+ "settings.mode.circle": "원형",
+ "settings.mode.outline": "외곽선",
+ "settings.sound_mode.title": "사운드 모드",
+ "settings.sound_mode.desc": "스피커 환경에 맞는 사운드 채널을 선택합니다.\n2채널 환경은 '2 채널', 서라운드 환경은 '5.1 채널' 또는 '7.1 채널'을 선택하세요.",
+ "settings.channels.2": "2 채널",
+ "settings.channels.5_1": "5.1 채널",
+ "settings.channels.7_1": "7.1 채널",
+ "settings.max_fps.title": "최대 프레임",
+ "settings.max_fps.desc": "그래픽이 그려지는 최대 프레임 속도를 설정합니다.",
+ "settings.section.mode_detail": "모드별 상세 설정",
+ "settings.mode_card.wave": "파도 모드",
+ "settings.mode_card.pad": "패드 모드",
+ "settings.mode_card.circle": "원형 모드",
+ "settings.mode_card.outline": "외곽선 모드",
+ "settings.size_lock.title": "크기 고정",
+ "settings.size_lock.desc": "세기에 비례하는 투명도를 활용하여 고정된 크기의 그래픽을 보여줍니다.",
+ "settings.fixed_size.wave": "파도 크기",
+ "settings.fixed_size.pad": "패드 크기",
+ "settings.fixed_size.circle": "원형 크기",
+ "settings.fixed_size.outline": "외곽선 두께",
+ "settings.fixed_size.desc": "투명도가 변할 기준이 되는 고정 크기를 설정합니다.",
+ "settings.max_opacity.title": "최대 진하기",
+ "settings.max_opacity.desc": "소리가 가장 클 때 도달할 진하기입니다.",
+ "settings.size.title": "크기",
+ "settings.size.desc.wave": "파도의 위아래 높이와 전체적인 볼륨감을 조절합니다.",
+ "settings.size.desc.pad": "사운드 반응 시 패드의 두께와 가로 영역 넓이를 조절합니다.",
+ "settings.size.desc.circle": "사운드가 울릴 때 원형 이퀄라이저의 돌출 진폭 스케일을 조절합니다.",
+ "settings.size.desc.outline": "외곽선 파도의 위아래 높이와 전체적인 볼륨감을 조절합니다.",
+ "settings.opacity.title": "진하기",
+ "settings.opacity.desc.wave": "값이 클수록 파도가 진하고, 작을수록 옅어져 뒤의 게임이나 화면이 더 비칩니다.",
+ "settings.opacity.desc.pad": "값이 클수록 패드가 진하고, 작을수록 옅고 투명해집니다.",
+ "settings.opacity.desc.circle": "값이 클수록 원형 그래픽이 진하고, 작을수록 옅고 투명해집니다.",
+ "settings.opacity.desc.outline": "값이 클수록 외곽선 파도가 진하고, 작을수록 옅고 투명해집니다.",
+ "settings.speed.title": "속도",
+ "settings.speed.desc.wave": "파도가 흘러가며 일렁이는 속도를 조절합니다.",
+ "settings.speed.desc.pad": "패드가 특정 방향의 사운드로 회전하며 반응하는 반응 속도를 조절합니다.",
+ "settings.speed.desc.circle": "원형 이퀄라이저의 물결이나 회전 반응 속도를 조절합니다.",
+ "settings.speed.desc.outline": "외곽선 파도가 흘러가며 일렁이는 속도를 조절합니다.",
+ "settings.sensitivity.title": "민감도",
+ "settings.sensitivity.desc.wave": "작은 데시벨 소리에도 파도가 얼마나 민감하게 반응하여 출렁일지 조절합니다.",
+ "settings.sensitivity.desc.pad": "패드가 소리를 더 잘 포착하여 더 큰 폭으로 도드라지게 조절합니다.",
+ "settings.sensitivity.desc.circle": "원형 진폭이 소리에 얼마나 섬세하게 쪼개지며 반응할지 조절합니다.",
+ "settings.sensitivity.desc.outline": "작은 데시벨 소리에도 외곽선 파도가 얼마나 민감하게 반응하여 출렁일지 조절합니다.",
+ "settings.glow.title": "광원",
+ "settings.glow.desc.wave": "파도 외곽선에 은은하게 빛나는 네온 광원을 입히고 그 강도를 조절합니다.\n주의: GPU 리소스를 추가로 사용합니다.",
+ "settings.glow.desc.pad": "패드 주변에 부드러운 아우라 형식의 광원 효과를 부여합니다.\n주의: GPU 리소스를 추가로 사용합니다.",
+ "settings.glow.desc.circle": "원형 이퀄라이저의 테두리를 따라 반짝이는 글로우 후광을 만듭니다.\n주의: GPU 리소스를 추가로 사용합니다.",
+ "settings.glow.desc.outline": "외곽선에 은은하게 빛나는 네온 광원을 입히고 그 강도를 조절합니다.\n주의: GPU 리소스를 추가로 사용합니다.",
+ "settings.radius.title": "반지름",
+ "settings.radius.desc": "원형 모드에서 가운데 고리의 반지름을 정합니다.",
+ "settings.section.sound_types": "소리 분류 표시 (AI)",
+ "settings.show_ambient": "환경음 표시",
+ "settings.show_speech": "대화음 표시",
+ "settings.show_danger": "위협음 표시",
+ "settings.set_color": "색상 설정",
+ "settings.set_color_for": "색상 설정: {0}",
+ "settings.section.hotkeys": "단축키",
+ "settings.hotkey.visual.title": "표현 모드 전환",
+ "settings.hotkey.visual.desc": "실행 중 형태를 실시간으로 변경할 단축키입니다.",
+ "settings.hotkey.sound.title": "사운드 모드 전환",
+ "settings.hotkey.sound.desc": "실행 중 사운드 모드를 실시간으로 변경할 단축키입니다.",
+ "settings.hotkey.edit.title": "오버레이 설정",
+ "settings.hotkey.edit.desc": "오버레이 편집 화면을 표시하거나 닫는 단축키입니다.",
+ "settings.hotkey.set": "핫키 설정",
+ "settings.section.advanced": "고급 설정",
+ "settings.developer_mode.title": "개발자 모드",
+ "settings.developer_mode.desc": "AI가 무슨 소리로 들었는지, 얼마나 확신했는지, 얼마나 걸렸는지를 오버레이 왼쪽 위에, FPS를 오른쪽 위에 띄웁니다. 내용은 영어로 나오고, 평소에 쓰라고 만든 것이 아닙니다.",
+ "settings.developer_record.title": "결과를 파일로 기록",
+ "settings.developer_record.desc": "AI가 내놓은 결과를 매번 SoundVisualizer.exe 옆의 ai-log 폴더에 새 CSV 파일로 남깁니다. 가장 높게 본 소리 이름들과 확신도, 걸린 시간이 함께 적혀서 무슨 소리로 들었는지 나중에 되짚어 볼 수 있습니다. 그 폴더에 쓸 수 없으면 %LOCALAPPDATA%\\SoundVisualizer\\ai-log 폴더에 남깁니다. 파일이 커질 수 있으니 다 보고 나면 꺼 주세요.",
+ "settings.auto_minimize.title": "실행 시 런처 최소화",
+ "settings.auto_minimize.desc": "오버레이 실행 시 이 런처 창을 자동으로 최소화합니다.",
+
+ "sounds.title": "소리 종류 선택",
+ "sounds.desc": "AI가 알아듣는 소리를 비슷한 것끼리 묶어, 기본 종류별로 보여 줍니다. 묶음의 종류를 바꾸면 그 소리들의 색이 바로 새 종류를 따릅니다.",
+ "sounds.sibling_note": "AI는 한 소리를 여러 이름으로 함께 들을 때가 많습니다. 예를 들어 사이렌은 ‘경보음’으로도 들리는데, 경보음은 다른 묶음에 있습니다. 바꾼 뒤에도 예전 종류로 보이면 관련된 묶음이나 소리도 함께 바꾸세요.",
+ "sounds.advanced.title": "고급 모드",
+ "sounds.advanced.desc": "모든 소리를 묶음별로 보여 주고, 하나씩 바꿀 수 있습니다.",
+ "sounds.search.hint": "소리 이름으로 찾기",
+ "sounds.search.clear": "검색어 지우기",
+ "sounds.filter.title": "필터",
+ "sounds.filter.all": "전체",
+ "sounds.filter.changed": "바꾼 소리",
+ "sounds.type.ambient": "환경음",
+ "sounds.type.speech": "대화음",
+ "sounds.type.danger": "위협음",
+ "sounds.type.default": "{0} (기본)",
+ "sounds.type.mixed": "일부 다름",
+ "sounds.changed_count": "바꾼 소리: {0}개",
+ "sounds.changed_from": "기본: {0}",
+ "sounds.reset.button": "모두 기본값으로",
+ "sounds.reset.title": "모든 소리를 기본값으로 되돌릴까요?",
+ "sounds.reset.message": "바꾼 소리가 모두 기본 종류로 돌아갑니다.",
+ "sounds.reset.confirm": "되돌리기",
+ "sounds.reset.cancel": "취소",
+ "sounds.empty": "맞는 소리가 없습니다.",
+ "sounds.group.mixed_note": "일부 소리를 하나씩 바꿈",
+ "sounds.group.changed_line": "지금: {0} · 기본: {1}",
+ "sounds.overwrite.title": "묶음 전체를 바꿀까요?",
+ "sounds.overwrite.message": "‘{0}’ 묶음의 소리 {1}개가 ‘{2}’ 종류로 바뀝니다. 하나씩 바꿔 둔 소리도 덮어씁니다.",
+ "sounds.overwrite.confirm": "바꾸기",
+ "sounds.overwrite.cancel": "취소",
+ "sounds.basic_warning.title": "기본 모드로 바꿀까요?",
+ "sounds.basic_warning.message": "하나씩 바꾼 소리가 들어 있는 묶음이 있습니다. 기본 모드에서 이런 묶음의 종류를 선택하면 묶음의 모든 소리가 그 종류가 되어, 하나씩 바꾼 것이 덮어써집니다. 종류를 선택하기 전에는 아무것도 바뀌지 않습니다.",
+ "sounds.basic_warning.confirm": "기본 모드로",
+ "sounds.basic_warning.cancel": "고급 모드 유지",
+ "sounds.cd.sound": "{0}, 종류: {1}",
+ "sounds.cd.sound_changed": "{0}, 종류: {1}, 기본: {2}",
+ "sounds.cd.group": "{0} 묶음, 종류: {1}, 소리 {2}개 ({3})",
+ "sounds.cd.group_changed": "{0} 묶음, 종류: {1}, 기본: {2}, 소리 {3}개 ({4})",
+ "sounds.cd.group_header": "{0} 묶음, 소리 {1}개",
+
+ "hotkey.prompt": "키를 누르세요 (ESC 취소)",
+ "hotkey.none": "없음",
+ "hotkey.button_name": "{0}, 단축키: {1}",
+
+ "help.start.title": "시작하기",
+ "help.start.1": "이 창 위쪽에서 언어를 선택하고, 원하면 설정 탭에서 그래픽 모양을 바꿉니다.",
+ "help.start.2": "홈에서 ‘{0}’ 버튼을 누릅니다.",
+ "help.start.3": "그래픽은 게임이나 영상 같은 다른 창 위에 화면 테두리를 따라 그려집니다. 마우스 클릭은 그래픽을 지나가므로 아래 창을 그대로 쓸 수 있습니다.",
+ "help.start.4": "끌 때는 홈에서 ‘{0}’ 버튼을 누르거나, 작업 표시줄 알림 영역의 SoundVisualizer 아이콘을 마우스 오른쪽 버튼으로 눌러 ‘{0}’ 메뉴를 선택합니다.",
+ "help.modes.title": "표현 모드",
+ "help.mode.wave": "화면 테두리 전체가 소리 세기만큼 안쪽으로 밀려듭니다. 소리가 나는 쪽이 더 깊게 밀려옵니다.",
+ "help.mode.pad": "화면 가장자리 8곳에 막대가 떠오릅니다. 방향을 가장 또렷하게 볼 수 있습니다.",
+ "help.mode.circle": "화면 가운데의 고리가 소리 나는 쪽으로 부풀어 오릅니다.",
+ "help.mode.outline": "파도의 윤곽선만 가늘게 그려 게임 화면을 덜 가립니다.",
+ "help.modes.note": "크기·진하기 같은 설정은 모드마다 따로 저장되고, 설정 탭에서 바꿀 수 있습니다.",
+ "help.sound.title": "소리 종류",
+ "help.sound.kinds": "AI가 들리는 소리를 환경음·대화음·위협음으로 나눕니다. 기본색은 환경음 흰색, 대화음 노란색, 위협음 빨간색이고, 설정 탭에서 종류마다 색과 표시 여부를 바꿀 수 있습니다.",
+ "help.sound.groups": "분류 탭에서는 비슷한 소리를 묶음으로 모아, 묶음마다 어느 종류로 볼지 선택합니다. 예를 들어 ‘초인종’ 묶음을 위협음으로 바꾸면 AI가 초인종이나 노크 소리로 알아들을 때 위협음 색이 나옵니다. 다만 새로 위협음으로 정한 소리는 AI가 종류를 가리는 투표에만 들어가므로, 초인종이 울릴 때마다 위협음으로 알린다는 뜻은 아닙니다.",
+ "help.sound.advanced": "소리를 하나씩 바꾸려면 고급 모드를 켭니다.",
+ "help.sound.danger": "위협음(총소리 등)이 감지되면 색이 서서히 바뀌지 않고 즉시 바뀝니다.",
+ "help.pc.title": "PC 설정과 단축키",
+ "help.sound_mode.title": "사운드 모드",
+ "help.sound_mode.body": "앞뒤 소리까지 구분해서 보려면 Windows 소리 설정에서 출력 장치를 '5.1 서라운드'(6채널)나 '7.1 서라운드'(8채널)로 설정하세요. 일반 스테레오(2채널)에서는 좌우는 구분하지만 앞뒤는 구분하지 못합니다. 설정 탭의 '사운드 모드'는 실행할 때와 출력 장치가 바뀔 때 그 장치에 맞춰지며, 직접 바꿀 수도 있습니다.",
+ "help.sound_mode.tip": "💡 만약 7.1 출력 장치가 없다면 {0}를 설치하고 출력장치를 7.1채널로 설정한다면 7.1 서라운드 사운드를 사용하여 오버레이를 표현할 수 있습니다.",
+ "help.sound_mode.tip_link": "VB-CABLE 가상 드라이버",
+ "help.hotkeys.title": "실시간 커스텀 단축키 제어",
+ "help.hotkeys.body": "설정 탭에서 핫키 버튼을 클릭한 뒤, 단일 키 또는 복합 키 조합(예: Ctrl + Shift + A)을 눌러 자유롭게 단축키를 변경할 수 있습니다. 백그라운드에서 지정된 단축키를 누르면 실시간으로 모드가 즉시 전환됩니다.",
+ "help.hotkeys.defaults": "기본 단축키는 F2가 사운드 모드 전환, F3가 표현 모드 전환, F4가 오버레이 설정입니다.",
+ "help.size_lock.title": "크기 고정 모드",
+ "help.size_lock.body": "크기 고정 모드는 소리의 크기에 비례하는 투명도를 활용하여 그래픽 크기를 고정시키는 설정입니다. 시각화 그래픽의 크기를 일정하게 유지하면서도 소리에 반응하는 모습을 보고 싶을 때 유용합니다.",
+ "help.editor.title": "오버레이 실시간 편집 모드",
+ "help.editor.body": "지정된 단축키(기본 F4)를 누르면 오버레이 편집 모드가 활성화됩니다. 화면에서 직접 마우스를 드래그하여 그래픽의 크기 한계선을 조절할 수 있으며, 팝업된 설정 패널에서 세부 조작이 가능합니다.",
+ "help.developer.title": "개발자 모드",
+ "help.developer.body": "개발자 모드를 켜면 AI가 무슨 소리로 들었는지, 얼마나 확신했는지, 얼마나 걸렸는지를 오버레이 왼쪽 위에, FPS를 오른쪽 위에 표시합니다. ‘{0}’ 스위치도 켜면 모든 결과가 CSV 파일로 남습니다. 시스템의 정상 작동 여부를 확인하고 싶을 때 유용합니다.",
+ "help.notes.title": "알아두세요",
+ "help.note.direction_surround": "5.1채널이나 7.1채널로 출력하면 화면 위쪽은 앞에서, 아래쪽은 뒤에서 나는 소리를 나타냅니다. 왼쪽과 오른쪽은 각각 왼쪽, 오른쪽 소리이고, 화면 모든 방향에서 한꺼번에 나오는 그래픽은 환경음이나 방향성이 없는 소리입니다.",
+ "help.note.direction_stereo": "2채널(스테레오)로 출력하면 좌우는 구분하지만 앞뒤는 구분하지 못합니다. 이때 화면 위·아래쪽 그래픽은 양쪽에서 비슷하게 들리는 소리와 공간감 효과를 나타냅니다.",
+ "help.note.sources": "Windows 기본 출력 장치로 나오는 모든 앱의 소리를 보여 줍니다. 앱이 다른 장치로 보내는 소리(예: 음성 채팅에만 쓰는 헤드셋)는 보이지 않습니다.",
+ "help.note.volume": "앱 안이나 Windows 볼륨 믹서에서 그 앱의 볼륨을 아주 낮추거나 음소거하면 소리가 거의 보이지 않거나 아예 보이지 않을 수 있습니다. 그래픽이 잘 반응하지 않으면 그 소리를 조금 키우거나 설정 탭에서 민감도를 올려 보세요.",
+ "help.note.ai": "AI 분류는 틀릴 수 있습니다. 안전이 걸린 상황에서 이 앱을 유일한 수단으로 쓰지 마세요.",
+ "help.faq.title": "자주 묻는 질문",
+ "help.faq.stopped_q": "직접 끄지 않았는데 꺼졌어요.",
+ "help.faq.stopped_a": "소리 받기가 멈춘 뒤 몇 초 안에 되살아나지 않으면 꺼집니다. 출력 장치가 뽑혔는데 쓸 수 있는 다른 장치가 없을 때가 그렇습니다. 그러면 홈에 이유가 나옵니다. 스피커나 헤드폰을 연결하거나 Windows 소리 설정에서 기본 출력 장치를 정한 뒤 다시 실행해 주세요.",
+ "help.faq.no_graphic_q": "그래픽이 보이지 않아요.",
+ "help.faq.no_graphic_a": "소리가 Windows 기본 출력 장치로 나오고 있는지, 음소거되어 있지 않은지, 홈에 안내가 떠 있지 않은지 확인해 주세요. 게임을 독점 전체 화면 모드로 하고 있다면 테두리 없는 창 모드나 창 모드로 바꿔 보세요. 설정에서 진하기가 너무 낮지 않은지, 지금 나는 소리 종류의 표시가 꺼져 있지 않은지도 확인해 주세요.",
+ "help.faq.performance_q": "CPU나 GPU를 많이 쓰나요?",
+ "help.faq.performance_a": "더 가볍게 하려면 설정 탭에서 ‘{0}’ 값을 낮추거나 ‘{1}’ 스위치를 끄세요.",
+ "help.faq.language_q": "앱 언어는 어떻게 바꾸나요?",
+ "help.faq.language_a": "이 창 위쪽의 ‘{0}’ 목록에서 선택합니다. 실행 중에는 오버레이 설정에서도 바꿀 수 있습니다. ‘{1}’ 항목은 Windows 표시 언어를 따릅니다.",
+ "help.report.title": "제보하기",
+ "help.report.desc": "버그나, 소리가 다른 종류로 표시된 경우를 알려 주세요. 브라우저에서 GitHub이 열리고 아래 환경 정보가 미리 채워집니다. GitHub 계정이 필요하니, 계정이 없으면 메일로 보내 주세요. 소리는 보내지 않습니다.",
+ "help.report.github": "GitHub에 제보하기",
+ "help.report.mail": "메일로 제보하기",
+ "help.report.copy": "환경 정보 복사",
+ "help.report.copied": "환경 정보를 복사했습니다.",
+ "help.report.copy_failed": "클립보드에 복사하지 못했습니다. 다시 시도해 주세요.",
+ "help.report.no_browser": "브라우저를 열지 못했습니다. 환경 정보를 복사해 보내 주세요.",
+ "help.report.no_mail": "메일을 보낼 앱이 없습니다. 환경 정보를 복사해 다른 방법으로 보내 주세요.",
+ "help.report.issue_title": "앱에서 보낸 제보",
+ "help.report.issue_body": "무슨 일이 있었나요?\n\n무엇을 듣고 있었나요? (앱, 소리 종류)\n\n어떻게 표시되길 기대하셨나요?",
+ "help.about.title": "앱 정보",
+ "help.about.version": "버전 {0}",
+ "help.about.license": "이 앱은 GNU Affero General Public License v3.0(AGPL-3.0)으로 배포됩니다. 분류 탭의 소리 이름과 묶음은 AudioSet 온톨로지를 바탕으로 해 CC BY-SA 4.0을 따르며, 자세한 내용은 아래 ‘오픈소스 라이선스’에서 볼 수 있습니다.",
+ "help.licenses.button": "오픈소스 라이선스",
+ "help.licenses.title": "오픈소스 라이선스",
+ "help.licenses.close": "닫기",
+
+ "notice.ai_unavailable": "소리 종류를 구분하지 못하고 있어 모든 소리가 환경음 색으로 표시됩니다. 실행 종료 후 다시 실행해 보세요.",
+ "notice.ai_unavailable_hidden": "소리 종류를 구분하지 못해 모든 소리를 환경음으로 보는데, 환경음 표시가 꺼져 있어 아무것도 보이지 않습니다. 설정 탭에서 ‘{0}’ 스위치를 켜거나, 실행 종료 후 다시 실행해 보세요.",
+ "notice.stopped.title": "소리 시각화가 꺼졌습니다",
+ "notice.stopped.no_device": "열 수 있는 출력 장치가 없어 소리를 받을 수 없습니다. 스피커나 헤드폰을 연결하거나 Windows 소리 설정에서 기본 출력 장치를 정한 뒤 다시 실행해 주세요.",
+ "notice.stopped.capture_error": "오류로 소리 받기가 멈춰 그래픽이 꺼졌습니다.",
+ "notice.dismiss": "안내 닫기",
+
+ "tray.open": "런처 열기",
+ "tray.exit": "프로그램 종료",
+
+ "more_info.title": "GitHub 저장소 및 프로젝트 기여",
+ "more_info.body": "SoundVisualizer는 오픈소스 프로젝트로, GitHub를 통해 소스 코드가 공개되어 있습니다. 버그 제보, 기능 건의 및 개발 기여를 통해 프로젝트에 동참하실 수 있습니다. 아래 버튼을 클릭하여 공식 GitHub 저장소로 이동해 보세요! 또한, GitHub의 Releases 탭에서 최신 버전 업데이트를 확인하고 다운로드할 수 있습니다.",
+ "more_info.github_button": "GitHub 저장소 방문하기",
+
+ "dialog.notice_title": "알림",
+ "dialog.already_running": "오버레이가 이미 실행 중입니다.",
+ "dialog.reset_title": "설정 초기화",
+ "dialog.reset_wave": "파도 모드의 설정을 기본값으로 되돌리시겠습니까?",
+ "dialog.reset_pad": "패드 모드의 설정을 기본값으로 되돌리시겠습니까?",
+ "dialog.reset_circle": "원형 모드의 설정을 기본값으로 되돌리시겠습니까?",
+ "dialog.reset_outline": "외곽선 모드의 설정을 기본값으로 되돌리시겠습니까?",
+ "dialog.reset_sound_types": "소리 분류 표시 설정을 기본값으로 되돌리시겠습니까?",
+ "dialog.reset_all_title": "기본값 초기화",
+ "dialog.reset_all": "정말로 모든 설정을 기본값으로 되돌리시겠습니까?",
+ "dialog.link_error": "링크를 열 수 없습니다: {0}",
+
+ "editor.title": "오버레이 설정",
+ "editor.drag_tip": "화면에 표시된 가이드 라인을 마우스 드래그하여 오버레이 파도의 높이 한계선 및 중앙 크기를 실시간 조절할 수 있습니다!",
+ "editor.glow": "광원 효과",
+ "editor.glow_strength": "광원 강도",
+ "editor.color": "색상",
+ "editor.hotkeys": "단축키 설정",
+ "editor.developer_mode_desc": "AI가 들은 소리(왼쪽 위)와 FPS(오른쪽 위) 표시",
+ "editor.save_close_hint": "키를 누르면 설정 저장 후 닫힘",
+ "editor.close_overlay": "오버레이 실행 종료",
+ "editor.guide.limit": "{0} 한계선",
+ "editor.guide.size": "{0} 크기: {1}%",
+ "editor.guide.base_radius": "기본 반지름: {0}",
+ "editor.guide.circle_size": "원형 크기: {0}%",
+
+ "hud.visual_mode": "표현 모드: [{0}] {1}",
+ "hud.sound_mode": "사운드 모드: [{0}] {1}",
+ "hud.overlay_settings": "오버레이 설정: [{0}]",
+ "hud.status.ready": "시스템 준비 완료",
+ "hud.status.stereo": "PC 오디오 설정: {0}ch (스테레오)\n소스: 대기 중...",
+ "hud.status.surround": "PC 오디오 설정: 8ch (7.1)\n소스: 대기 중...",
+
+ "color_picker.title": "색상 선택",
+ "color_picker.hex": "HEX 코드",
+ "color_picker.cancel": "취소",
+ "color_picker.apply": "적용",
+ "color_picker.area": "채도와 밝기",
+ "color_picker.area_value": "채도 {0}%, 밝기 {1}%",
+ "color_picker.hue": "색조",
+ "color_picker.keys_help": "화살표 키로 바꿉니다. Shift 키를 누른 채 누르면 크게 바뀝니다.",
+ "color_picker.presets": "자주 쓰는 색",
+ "color_picker.preset.white": "흰색",
+ "color_picker.preset.red": "빨간색",
+ "color_picker.preset.orange": "주황색",
+ "color_picker.preset.yellow": "노란색",
+ "color_picker.preset.green": "초록색",
+ "color_picker.preset.cyan": "청록색",
+ "color_picker.preset.blue": "파란색",
+ "color_picker.preset.magenta": "자홍색",
+
+ "audio.error.no_device": "출력 장치를 찾을 수 없습니다.",
+ "audio.error.capture_stopped": "캡처 비정상 종료: {0}",
+ "audio.error.capture_start_failed": "캡처 엔진 시작 실패: {0}",
+
+ "tutorial.window_title": "튜토리얼",
+ "tutorial.skip": "건너뛰기",
+ "tutorial.previous": "이전 쪽",
+ "tutorial.next": "다음",
+ "tutorial.done": "확인",
+ "tutorial.page_status": "{1}쪽 중 {0}쪽",
+ "tutorial.pause": "그림 멈추기",
+ "tutorial.play": "그림 움직이기",
+ "tutorial.sound_on": "소리 켜기",
+ "tutorial.sound_off": "소리 끄기",
+ "tutorial.sound.title": "소리를 눈으로 보여 드립니다",
+ "tutorial.sound.body": "게임이나 영상의 소리를 화면 테두리에 그립니다. 소리가 클수록 크게 그려집니다. 이 PC에서 재생되는 소리만 보여 드리고, 주변에서 나는 소리는 듣지 않습니다.",
+ "tutorial.direction.title": "소리가 나는 방향을 보여 드립니다",
+ "tutorial.direction.body": "2채널(스테레오)로 출력하면 왼쪽에서 나는 소리는 왼쪽이, 오른쪽에서 나는 소리는 오른쪽이 더 크게 그려집니다. 5.1채널이나 7.1채널로 출력하면 앞뒤도 구분해, 앞에서 나는 소리는 화면 위쪽에, 뒤에서 나는 소리는 아래쪽에 그려집니다.",
+ "tutorial.types.title": "소리 종류를 색으로 구분합니다",
+ "tutorial.types.body": "AI가 소리를 세 종류로 나눠 서로 다른 색으로 그립니다. 색은 설정 탭에서 바꿀 수 있습니다.",
+ "tutorial.type.ambient_examples": "음악·빗소리 등 그 밖의 소리",
+ "tutorial.type.speech_examples": "사람 목소리",
+ "tutorial.type.danger_examples": "총소리·폭발음·사이렌",
+ "tutorial.start.title": "켜고 끄는 방법",
+ "tutorial.start.hotkeys": "실행 중에는 {0} 키로 사운드 모드를, {1} 키로 표현 모드를 바꾸고, {2} 키로 오버레이 설정을 엽니다."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/pl.json b/SoundVisualizer.Core/Localization/Strings/pl.json
new file mode 100644
index 0000000..a195734
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/pl.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Język",
+ "common.reset_defaults": "Przywróć domyślne",
+ "common.minimize": "Minimalizuj",
+ "common.close": "Zamknij",
+ "language.system": "Domyślny język systemu",
+
+ "launcher.window_title": "Launcher SoundVisualizer",
+ "launcher.tab.home": "Główna",
+ "launcher.tab.settings": "Ustawienia",
+ "launcher.tab.sounds": "Dźwięki",
+ "launcher.tab.help": "Pomoc",
+ "launcher.tab.more_info": "Więcej informacji",
+ "launcher.home.desc": "Pokazuje na ekranie dźwięki, których nie widać.\nOd gier po filmy – odkryj zupełnie nowe wrażenia.",
+ "launcher.home.start": "Uruchom",
+ "launcher.home.stop": "Zatrzymaj",
+ "launcher.home.tutorial": "Zobacz samouczek",
+ "launcher.status.running": "Stan: działa",
+ "launcher.status.idle": "Stan: w gotowości",
+
+ "settings.section.mode": "Ustawienia trybu",
+ "settings.visual_mode.title": "Tryb wizualizacji",
+ "settings.visual_mode.desc": "Wybierz podstawowy kształt grafiki rysowanej na ekranie.",
+ "settings.mode.wave": "Fala",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Okrąg",
+ "settings.mode.outline": "Kontur",
+ "settings.sound_mode.title": "Tryb dźwięku",
+ "settings.sound_mode.desc": "Wybierz kanały dźwięku pasujące do Twoich głośników.\nDla głośników dwukanałowych wybierz „2 kanały”, a dla zestawów dźwięku przestrzennego – „5.1 kanałów” lub „7.1 kanałów”.",
+ "settings.channels.2": "2 kanały",
+ "settings.channels.5_1": "5.1 kanałów",
+ "settings.channels.7_1": "7.1 kanałów",
+ "settings.max_fps.title": "Maks. liczba klatek na sekundę",
+ "settings.max_fps.desc": "Ustawia maksymalną liczbę klatek na sekundę, z jaką rysowana jest grafika.",
+ "settings.section.mode_detail": "Ustawienia każdego trybu",
+ "settings.mode_card.wave": "Tryb Fala",
+ "settings.mode_card.pad": "Tryb Pad",
+ "settings.mode_card.circle": "Tryb Okrąg",
+ "settings.mode_card.outline": "Tryb Kontur",
+ "settings.size_lock.title": "Zablokuj rozmiar",
+ "settings.size_lock.desc": "Utrzymuje stały rozmiar grafiki, a głośność pokazuje za pomocą przezroczystości.",
+ "settings.fixed_size.wave": "Rozmiar fali",
+ "settings.fixed_size.pad": "Rozmiar padów",
+ "settings.fixed_size.circle": "Rozmiar okręgu",
+ "settings.fixed_size.outline": "Grubość konturu",
+ "settings.fixed_size.desc": "Ustawia stały rozmiar grafiki, przy którym zmienia się tylko jej przezroczystość.",
+ "settings.max_opacity.title": "Maks. krycie",
+ "settings.max_opacity.desc": "Określa krycie grafiki przy najgłośniejszym dźwięku.",
+ "settings.size.title": "Rozmiar",
+ "settings.size.desc.wave": "Reguluje wysokość fali i jej ogólną objętość.",
+ "settings.size.desc.pad": "Reguluje, jak grube i szerokie stają się pady, gdy reagują na dźwięk.",
+ "settings.size.desc.circle": "Reguluje, jak mocno okrągły korektor wybrzusza się na zewnątrz, gdy słychać dźwięk.",
+ "settings.size.desc.outline": "Reguluje wysokość fali konturu i jej ogólną objętość.",
+ "settings.opacity.title": "Krycie",
+ "settings.opacity.desc.wave": "Im wyższa wartość, tym grafika jest wyraźniejsza; im niższa, tym bledsza i bardziej przezroczysta.",
+ "settings.opacity.desc.pad": "Im wyższa wartość, tym grafika jest wyraźniejsza; im niższa, tym bledsza i bardziej przezroczysta.",
+ "settings.opacity.desc.circle": "Im wyższa wartość, tym grafika jest wyraźniejsza; im niższa, tym bledsza i bardziej przezroczysta.",
+ "settings.opacity.desc.outline": "Im wyższa wartość, tym grafika jest wyraźniejsza; im niższa, tym bledsza i bardziej przezroczysta.",
+ "settings.speed.title": "Szybkość",
+ "settings.speed.desc.wave": "Reguluje, jak szybko fala płynie i się kołysze.",
+ "settings.speed.desc.pad": "Reguluje, jak szybko pady obracają się w stronę, z której dochodzi dźwięk.",
+ "settings.speed.desc.circle": "Reguluje, jak szybko okrągły korektor faluje i obraca się w odpowiedzi na dźwięk.",
+ "settings.speed.desc.outline": "Reguluje, jak szybko fala konturu płynie i się kołysze.",
+ "settings.sensitivity.title": "Czułość",
+ "settings.sensitivity.desc.wave": "Reguluje, jak czule fala reaguje nawet na ciche dźwięki i jak mocno wtedy wzbiera.",
+ "settings.sensitivity.desc.pad": "Reguluje pady tak, aby lepiej wychwytywały dźwięk i wyraźniej się wyróżniały.",
+ "settings.sensitivity.desc.circle": "Reguluje, jak drobno rozdziela się amplituda okręgu, gdy reaguje on na dźwięk.",
+ "settings.sensitivity.desc.outline": "Reguluje, jak czule fala konturu reaguje nawet na ciche dźwięki i jak mocno wtedy wzbiera.",
+ "settings.glow.title": "Poświata",
+ "settings.glow.desc.wave": "Dodaje delikatną neonową poświatę wzdłuż krawędzi fali i reguluje jej moc.\nUwaga: zużywa więcej zasobów GPU.",
+ "settings.glow.desc.pad": "Dodaje wokół padów delikatną poświatę przypominającą aurę.\nUwaga: zużywa więcej zasobów GPU.",
+ "settings.glow.desc.circle": "Dodaje migoczącą aureolę wzdłuż krawędzi okrągłego korektora.\nUwaga: zużywa więcej zasobów GPU.",
+ "settings.glow.desc.outline": "Dodaje do konturu delikatną neonową poświatę i reguluje jej moc.\nUwaga: zużywa więcej zasobów GPU.",
+ "settings.radius.title": "Promień",
+ "settings.radius.desc": "Ustawia promień pierścienia na środku ekranu w trybie Okrąg.",
+ "settings.section.sound_types": "Rodzaje dźwięków (AI)",
+ "settings.show_ambient": "Pokaż dźwięki otoczenia",
+ "settings.show_speech": "Pokaż mowę",
+ "settings.show_danger": "Pokaż dźwięki zagrożenia",
+ "settings.set_color": "Ustaw kolor",
+ "settings.set_color_for": "Ustaw kolor: {0}",
+ "settings.section.hotkeys": "Skróty klawiszowe",
+ "settings.hotkey.visual.title": "Zmiana trybu wizualizacji",
+ "settings.hotkey.visual.desc": "Skrót, który zmienia kształt grafiki na bieżąco podczas działania.",
+ "settings.hotkey.sound.title": "Zmiana trybu dźwięku",
+ "settings.hotkey.sound.desc": "Skrót, który zmienia tryb dźwięku na bieżąco podczas działania.",
+ "settings.hotkey.edit.title": "Ustawienia nakładki",
+ "settings.hotkey.edit.desc": "Skrót, który otwiera lub zamyka edytor nakładki.",
+ "settings.hotkey.set": "Ustaw skrót",
+ "settings.section.advanced": "Ustawienia zaawansowane",
+ "settings.developer_mode.title": "Tryb programisty",
+ "settings.developer_mode.desc": "Pokazuje w lewym górnym rogu nakładki, co usłyszała AI (zgłoszoną nazwę dźwięku, pewność i czas przetwarzania), a w prawym górnym rogu FPS. Informacje są po angielsku i nie są przeznaczone do codziennego użytku.",
+ "settings.developer_record.title": "Zapisuj wyniki do pliku",
+ "settings.developer_record.desc": "Zapisuje każdy wynik AI (najwyżej ocenione nazwy dźwięków, pewność i czasy przetwarzania) do nowego pliku CSV w folderze ai-log obok SoundVisualizer.exe, aby można było później sprawdzić, co usłyszała. Jeśli do tego folderu nie da się zapisywać, plik trafia do %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Pliki mogą być duże, więc wyłącz zapis, gdy skończysz.",
+ "settings.auto_minimize.title": "Minimalizuj launcher po uruchomieniu",
+ "settings.auto_minimize.desc": "Automatycznie minimalizuje okno launchera, gdy uruchamia się nakładka.",
+
+ "sounds.title": "Wybierz rodzaje dźwięków",
+ "sounds.desc": "Dźwięki rozpoznawane przez AI są zebrane w grupy podobnych dźwięków według rodzaju, do którego domyślnie należą. Gdy zmienisz rodzaj grupy, kolor jej dźwięków od razu dostosuje się do nowego rodzaju.",
+ "sounds.sibling_note": "AI często rozpoznaje ten sam dźwięk jednocześnie pod kilkoma nazwami. Syrena na przykład jest rozpoznawana także jako „Alarm”, który jest w innej grupie. Jeśli po zmianie dźwięk nadal jest pokazywany jako poprzedni rodzaj, zmień też powiązane grupy lub dźwięki.",
+ "sounds.advanced.title": "Tryb zaawansowany",
+ "sounds.advanced.desc": "Pokazuje każdy dźwięk w jego grupie, aby można było zmieniać dźwięki pojedynczo.",
+ "sounds.search.hint": "Szukaj dźwięków",
+ "sounds.search.clear": "Wyczyść wyszukiwanie",
+ "sounds.filter.title": "Filtr",
+ "sounds.filter.all": "Wszystkie",
+ "sounds.filter.changed": "Zmienione",
+ "sounds.type.ambient": "Dźwięki otoczenia",
+ "sounds.type.speech": "Mowa",
+ "sounds.type.danger": "Dźwięki zagrożenia",
+ "sounds.type.default": "{0} (domyślnie)",
+ "sounds.type.mixed": "Mieszany",
+ "sounds.changed_count": "Zmienione dźwięki: {0}",
+ "sounds.changed_from": "Domyślnie: {0}",
+ "sounds.reset.button": "Przywróć domyślne",
+ "sounds.reset.title": "Przywrócić domyślne rodzaje dźwięków?",
+ "sounds.reset.message": "Każdy zmieniony przez Ciebie dźwięk wróci do swojego domyślnego rodzaju.",
+ "sounds.reset.confirm": "Przywróć",
+ "sounds.reset.cancel": "Anuluj",
+ "sounds.empty": "Brak pasujących dźwięków.",
+ "sounds.group.mixed_note": "Niektóre dźwięki zmienione pojedynczo",
+ "sounds.group.changed_line": "Teraz: {0} · Domyślnie: {1}",
+ "sounds.overwrite.title": "Zmienić całą grupę?",
+ "sounds.overwrite.message": "Dźwięki z grupy „{0}”, które zmienią się na „{2}”: {1}. Dźwięki zmienione pojedynczo też zostaną nadpisane.",
+ "sounds.overwrite.confirm": "Zmień",
+ "sounds.overwrite.cancel": "Anuluj",
+ "sounds.basic_warning.title": "Przełączyć na tryb podstawowy?",
+ "sounds.basic_warning.message": "W niektórych grupach są dźwięki zmienione przez Ciebie pojedynczo. Jeśli w trybie podstawowym wybierzesz rodzaj dla takiej grupy, wszystkie jej dźwięki otrzymają ten rodzaj, a Twoje pojedyncze zmiany w tej grupie zostaną nadpisane. Nic się nie zmieni, dopóki nie wybierzesz.",
+ "sounds.basic_warning.confirm": "Przełącz na podstawowy",
+ "sounds.basic_warning.cancel": "Zostań w zaawansowanym",
+ "sounds.cd.sound": "{0}, rodzaj: {1}",
+ "sounds.cd.sound_changed": "{0}, rodzaj: {1}, domyślnie: {2}",
+ "sounds.cd.group": "Grupa {0}, rodzaj: {1}, liczba dźwięków: {2} ({3})",
+ "sounds.cd.group_changed": "Grupa {0}, rodzaj: {1}, domyślnie: {2}, liczba dźwięków: {3} ({4})",
+ "sounds.cd.group_header": "Grupa {0}, liczba dźwięków: {1}",
+
+ "hotkey.prompt": "Naciśnij klawisz (Esc – anuluj)",
+ "hotkey.none": "Brak",
+ "hotkey.button_name": "{0}, skrót: {1}",
+
+ "help.start.title": "Pierwsze kroki",
+ "help.start.1": "Wybierz język u góry tego okna i w razie potrzeby zmień wygląd grafiki na karcie „Ustawienia”.",
+ "help.start.2": "Na karcie „Główna” kliknij „{0}”.",
+ "help.start.3": "Grafika jest rysowana wzdłuż krawędzi ekranu, nad innymi oknami, takimi jak gry i filmy. Kliknięcia przechodzą przez nią, więc możesz dalej korzystać z okna pod spodem.",
+ "help.start.4": "Aby wyłączyć, kliknij „{0}” na karcie „Główna” albo kliknij prawym przyciskiem myszy ikonę SoundVisualizer w obszarze powiadomień na pasku zadań i wybierz „{0}”.",
+ "help.modes.title": "Tryby wizualizacji",
+ "help.mode.wave": "Cała krawędź ekranu przesuwa się do środka – tym dalej, im głośniejszy dźwięk. Po stronie, z której dochodzi dźwięk, sięga głębiej.",
+ "help.mode.pad": "W 8 miejscach wzdłuż krawędzi ekranu podświetlają się paski. Ten tryb najwyraźniej pokazuje kierunek.",
+ "help.mode.circle": "Pierścień na środku ekranu wybrzusza się w stronę, z której dochodzi dźwięk.",
+ "help.mode.outline": "Rysuje tylko cienki kontur fali, więc mniej zasłania grę.",
+ "help.modes.note": "Ustawienia takie jak „Rozmiar” i „Krycie” są zapisywane osobno dla każdego trybu. Możesz je zmienić na karcie „Ustawienia”.",
+ "help.sound.title": "Rodzaje dźwięków",
+ "help.sound.kinds": "AI dzieli to, co słychać, na dźwięki otoczenia, mowę i dźwięki zagrożenia. Domyślnie dźwięki otoczenia są oznaczane na biało, mowa na żółto, a dźwięki zagrożenia na czerwono. Na karcie „Ustawienia” możesz zmienić kolor każdego rodzaju i to, czy jest pokazywany.",
+ "help.sound.groups": "Na karcie „Dźwięki” podobne dźwięki są zebrane w grupy, a Ty wybierasz, do jakiego rodzaju należy każda grupa. Jeśli na przykład zmienisz grupę „Dzwonek do drzwi” na dźwięki zagrożenia, to gdy AI rozpozna dzwonek do drzwi albo pukanie, dźwięk dostanie kolor dźwięków zagrożenia. Dźwięk, który ustawisz jako dźwięk zagrożenia, bierze jednak udział tylko w głosowaniu AI nad rodzajem, więc nie oznacza to, że każdy dzwonek do drzwi zostanie pokazany jako dźwięk zagrożenia.",
+ "help.sound.advanced": "Aby zmieniać dźwięki pojedynczo, włącz tryb zaawansowany.",
+ "help.sound.danger": "Gdy zostanie wykryty dźwięk zagrożenia (np. strzał), kolor zmienia się natychmiast, a nie stopniowo.",
+ "help.pc.title": "Ustawienia PC i skróty klawiszowe",
+ "help.sound_mode.title": "Tryb dźwięku",
+ "help.sound_mode.body": "Aby rozróżniać także dźwięki z przodu i z tyłu, a nie tylko z lewej i prawej, w ustawieniach dźwięku systemu Windows skonfiguruj urządzenie wyjściowe jako dźwięk przestrzenny 5.1 (6 kanałów) lub 7.1 (8 kanałów). Przy zwykłym stereo (2 kanały) aplikacja odróżnia lewą stronę od prawej, ale nie przód od tyłu. „Tryb dźwięku” na karcie „Ustawienia” dopasowuje się do urządzenia wyjściowego przy uruchomieniu i po zmianie urządzenia, ale możesz go też zmienić samodzielnie.",
+ "help.sound_mode.tip": "💡 Jeśli nie masz urządzenia wyjściowego 7.1, zainstaluj {0} i skonfiguruj urządzenie wyjściowe jako 7.1 – wtedy nakładka będzie pokazywać dźwięk przestrzenny 7.1.",
+ "help.sound_mode.tip_link": "wirtualny sterownik VB-CABLE",
+ "help.hotkeys.title": "Własne skróty klawiszowe",
+ "help.hotkeys.body": "Na karcie „Ustawienia” kliknij przycisk skrótu i naciśnij pojedynczy klawisz lub kombinację klawiszy (np. Ctrl + Shift + A), aby dowolnie zmienić skrót. Naciśnięcie skrótu natychmiast przełącza tryb, nawet gdy aplikacja działa w tle.",
+ "help.hotkeys.defaults": "Domyślnie F2 zmienia tryb dźwięku, F3 zmienia tryb wizualizacji, a F4 otwiera ustawienia nakładki.",
+ "help.size_lock.title": "Tryb blokady rozmiaru",
+ "help.size_lock.body": "Tryb blokady rozmiaru utrzymuje stały rozmiar grafiki, a głośność dźwięku pokazuje za pomocą przezroczystości. Przydaje się, gdy chcesz zachować stały rozmiar grafiki, a jednocześnie widzieć, jak reaguje ona na dźwięk.",
+ "help.editor.title": "Edycja nakładki na żywo",
+ "help.editor.body": "Naciśnij przypisany skrót (domyślnie F4), aby włączyć tryb edycji nakładki. Przeciągając myszą po ekranie, możesz ustawić limit rozmiaru grafiki, a szczegóły dopracować w wyświetlonym panelu ustawień.",
+ "help.developer.title": "Tryb programisty",
+ "help.developer.body": "Po włączeniu trybu programisty w lewym górnym rogu nakładki pojawia się to, co usłyszała AI (nazwa dźwięku, pewność i czas przetwarzania), a w prawym górnym rogu FPS. Jeśli włączysz też „{0}”, każdy wynik zostanie zapisany w pliku CSV. Przydaje się to, gdy chcesz sprawdzić, czy system działa prawidłowo.",
+ "help.notes.title": "Warto wiedzieć",
+ "help.note.direction_surround": "Przy wyjściu 5.1 lub 7.1 góra ekranu pokazuje dźwięk z przodu, a dół – dźwięk z tyłu. Lewa i prawa krawędź pokazują dźwięk z lewej i prawej strony, a grafika wychodząca jednocześnie ze wszystkich stron ekranu to dźwięki otoczenia lub dźwięki bez określonego kierunku.",
+ "help.note.direction_stereo": "Przy wyjściu 2-kanałowym (stereo) aplikacja odróżnia lewą stronę od prawej, ale nie przód od tyłu. Grafika u góry i u dołu ekranu pokazuje wtedy dźwięki słyszalne jednakowo z obu stron oraz wrażenie przestrzeni.",
+ "help.note.sources": "Pokazywany jest dźwięk wszystkich aplikacji odtwarzany przez domyślne urządzenie odtwarzania systemu Windows. Dźwięk, który aplikacja wysyła na inne urządzenie, na przykład na zestaw słuchawkowy używany tylko do czatu głosowego, nie jest pokazywany.",
+ "help.note.volume": "Jeśli głośność aplikacji jest bardzo niska lub wyciszona – w samej aplikacji albo w mikserze głośności systemu Windows – jej dźwięk może być ledwie widoczny albo wcale. Jeśli grafika słabo reaguje, trochę podgłośnij ten dźwięk albo zwiększ „Czułość” na karcie „Ustawienia”.",
+ "help.note.ai": "Klasyfikacja AI może być błędna. Gdy chodzi o bezpieczeństwo, nie polegaj na tej aplikacji jako jedynym zabezpieczeniu.",
+ "help.faq.title": "Najczęstsze pytania",
+ "help.faq.stopped_q": "Wizualizacja wyłączyła się bez mojego udziału.",
+ "help.faq.stopped_a": "Wizualizacja wyłącza się, gdy przechwytywanie dźwięku zatrzyma się i nie wznowi w ciągu kilku sekund, na przykład gdy urządzenie odtwarzania zostanie odłączone, a żadne inne nie jest dostępne. Karta „Główna” pokaże wtedy przyczynę. Podłącz głośniki lub słuchawki albo ustaw domyślne urządzenie odtwarzania w ustawieniach dźwięku systemu Windows, a potem uruchom ją ponownie.",
+ "help.faq.no_graphic_q": "Nie widzę żadnej grafiki.",
+ "help.faq.no_graphic_a": "Sprawdź, czy dźwięk jest odtwarzany przez domyślne urządzenie odtwarzania systemu Windows i nie jest wyciszony oraz czy na karcie „Główna” nie ma komunikatu. Jeśli gra działa w trybie wyłącznego pełnego ekranu, spróbuj trybu okna bez obramowania lub trybu okna. Upewnij się też, że na karcie „Ustawienia” wartość „Krycie” nie jest zbyt niska, a pokazywanie rodzaju dźwięku, który właśnie słychać, nie jest wyłączone.",
+ "help.faq.performance_q": "Czy aplikacja mocno obciąża procesor lub kartę graficzną?",
+ "help.faq.performance_a": "Aby ją odciążyć, zmniejsz „{0}” na karcie „Ustawienia” lub wyłącz „{1}”.",
+ "help.faq.language_q": "Jak zmienić język aplikacji?",
+ "help.faq.language_a": "Wybierz go z listy „{0}” u góry tego okna. Podczas działania możesz go też zmienić w ustawieniach nakładki. Opcja „{1}” sprawia, że aplikacja używa języka wyświetlania systemu Windows.",
+ "help.report.title": "Zgłoś problem",
+ "help.report.desc": "Daj nam znać o błędzie albo o dźwięku, który został pokazany jako dźwięk niewłaściwego rodzaju. GitHub otworzy się w przeglądarce z uzupełnionymi informacjami o systemie widocznymi poniżej, dlatego potrzebne jest konto na GitHubie. Jeśli nie masz konta, wyślij nam e-mail. Żaden dźwięk nie jest nigdy wysyłany.",
+ "help.report.github": "Zgłoś na GitHubie",
+ "help.report.mail": "Zgłoś przez e-mail",
+ "help.report.copy": "Skopiuj informacje o systemie",
+ "help.report.copied": "Skopiowano informacje o systemie.",
+ "help.report.copy_failed": "Nie udało się skopiować do schowka. Spróbuj ponownie.",
+ "help.report.no_browser": "Nie udało się otworzyć przeglądarki. Skopiuj informacje o systemie i wyślij je nam w inny sposób.",
+ "help.report.no_mail": "Brak aplikacji do wysyłania e-maili. Skopiuj informacje o systemie i wyślij je w inny sposób.",
+ "help.report.issue_title": "Zgłoszenie z aplikacji",
+ "help.report.issue_body": "Co się stało?\n\nCo było wtedy odtwarzane (aplikacja, rodzaj dźwięku)?\n\nCo powinno się wydarzyć zamiast tego?",
+ "help.about.title": "O aplikacji",
+ "help.about.version": "Wersja {0}",
+ "help.about.license": "Ta aplikacja jest udostępniana na warunkach GNU Affero General Public License v3.0 (AGPL-3.0). Nazwy i grupy dźwięków na karcie „Dźwięki” są oparte na ontologii AudioSet i udostępniane na warunkach licencji CC BY-SA 4.0 (zobacz „Licencje open source” poniżej).",
+ "help.licenses.button": "Licencje open source",
+ "help.licenses.title": "Licencje open source",
+ "help.licenses.close": "Zamknij",
+
+ "notice.ai_unavailable": "Rozpoznawanie rodzaju dźwięku nie działa, więc wszystkie dźwięki są pokazywane w kolorze dźwięków otoczenia. Spróbuj wyłączyć i ponownie włączyć wizualizację.",
+ "notice.ai_unavailable_hidden": "Rozpoznawanie rodzaju dźwięku nie działa, więc wszystkie dźwięki są traktowane jako dźwięki otoczenia, a ich pokazywanie jest wyłączone, dlatego nic nie jest widoczne. Włącz „{0}” na karcie „Ustawienia” albo spróbuj wyłączyć i ponownie włączyć wizualizację.",
+ "notice.stopped.title": "Wizualizacja dźwięku wyłączona",
+ "notice.stopped.no_device": "Nie udało się otworzyć żadnego urządzenia odtwarzania, więc nie można przechwytywać dźwięku. Podłącz głośniki lub słuchawki albo ustaw domyślne urządzenie odtwarzania w ustawieniach dźwięku systemu Windows, a potem uruchom ponownie.",
+ "notice.stopped.capture_error": "Przechwytywanie dźwięku zostało przerwane przez błąd, więc grafika jest wyłączona.",
+ "notice.dismiss": "Zamknij komunikat",
+
+ "tray.open": "Otwórz launcher",
+ "tray.exit": "Zakończ",
+
+ "more_info.title": "Repozytorium GitHub i współtworzenie",
+ "more_info.body": "SoundVisualizer to projekt open source, którego kod źródłowy jest publicznie dostępny na GitHubie. Możesz wziąć w nim udział, zgłaszając błędy, proponując nowe funkcje lub pomagając w rozwoju. Kliknij przycisk poniżej, aby przejść do oficjalnego repozytorium na GitHubie! Na karcie Releases na GitHubie możesz też sprawdzić i pobrać najnowszą wersję.",
+ "more_info.github_button": "Odwiedź repozytorium GitHub",
+
+ "dialog.notice_title": "Informacja",
+ "dialog.already_running": "Nakładka już działa.",
+ "dialog.reset_title": "Resetowanie ustawień",
+ "dialog.reset_wave": "Przywrócić domyślne ustawienia trybu Fala?",
+ "dialog.reset_pad": "Przywrócić domyślne ustawienia trybu Pad?",
+ "dialog.reset_circle": "Przywrócić domyślne ustawienia trybu Okrąg?",
+ "dialog.reset_outline": "Przywrócić domyślne ustawienia trybu Kontur?",
+ "dialog.reset_sound_types": "Przywrócić domyślne ustawienia rodzajów dźwięków?",
+ "dialog.reset_all_title": "Przywracanie ustawień domyślnych",
+ "dialog.reset_all": "Czy na pewno chcesz przywrócić wszystkie ustawienia domyślne?",
+ "dialog.link_error": "Nie udało się otworzyć linku: {0}",
+
+ "editor.title": "Ustawienia nakładki",
+ "editor.drag_tip": "Przeciągaj myszą linie pomocnicze na ekranie, aby na bieżąco ustawiać limit wysokości i rozmiar środka fali nakładki!",
+ "editor.glow": "Efekt poświaty",
+ "editor.glow_strength": "Moc poświaty",
+ "editor.color": "Kolor",
+ "editor.hotkeys": "Ustawienia skrótów",
+ "editor.developer_mode_desc": "Pokazuje, co usłyszała AI (u góry po lewej), i FPS (u góry po prawej)",
+ "editor.save_close_hint": "– zapisz i zamknij",
+ "editor.close_overlay": "Zamknij nakładkę",
+ "editor.guide.limit": "{0}: granica",
+ "editor.guide.size": "{0}: rozmiar {1}%",
+ "editor.guide.base_radius": "Promień bazowy: {0}",
+ "editor.guide.circle_size": "Rozmiar okręgu: {0}%",
+
+ "hud.visual_mode": "Tryb wizualizacji: [{0}] {1}",
+ "hud.sound_mode": "Tryb dźwięku: [{0}] {1}",
+ "hud.overlay_settings": "Ustawienia nakładki: [{0}]",
+ "hud.status.ready": "System gotowy",
+ "hud.status.stereo": "Dźwięk PC: {0} kan. (stereo)\nŹródło: oczekiwanie...",
+ "hud.status.surround": "Dźwięk PC: 8 kan. (7.1)\nŹródło: oczekiwanie...",
+
+ "color_picker.title": "Wybierz kolor",
+ "color_picker.hex": "Kod HEX",
+ "color_picker.cancel": "Anuluj",
+ "color_picker.apply": "Zastosuj",
+ "color_picker.area": "Nasycenie i jasność",
+ "color_picker.area_value": "Nasycenie {0}%, jasność {1}%",
+ "color_picker.hue": "Odcień",
+ "color_picker.keys_help": "Zmieniaj klawiszami strzałek. Przytrzymaj Shift, aby zmieniać większymi krokami.",
+ "color_picker.presets": "Popularne kolory",
+ "color_picker.preset.white": "Biały",
+ "color_picker.preset.red": "Czerwony",
+ "color_picker.preset.orange": "Pomarańczowy",
+ "color_picker.preset.yellow": "Żółty",
+ "color_picker.preset.green": "Zielony",
+ "color_picker.preset.cyan": "Turkusowy",
+ "color_picker.preset.blue": "Niebieski",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "Nie znaleziono urządzenia wyjściowego.",
+ "audio.error.capture_stopped": "Przechwytywanie dźwięku nieoczekiwanie się zatrzymało: {0}",
+ "audio.error.capture_start_failed": "Nie udało się uruchomić mechanizmu przechwytywania dźwięku: {0}",
+
+ "tutorial.window_title": "Samouczek",
+ "tutorial.skip": "Pomiń",
+ "tutorial.previous": "Poprzednia strona",
+ "tutorial.next": "Dalej",
+ "tutorial.done": "Rozumiem",
+ "tutorial.page_status": "Strona {0} z {1}",
+ "tutorial.pause": "Wstrzymaj animację",
+ "tutorial.play": "Odtwórz animację",
+ "tutorial.sound_on": "Włącz dźwięk",
+ "tutorial.sound_off": "Wyłącz dźwięk",
+ "tutorial.sound.title": "Zobacz dźwięk na ekranie",
+ "tutorial.sound.body": "Dźwięk z gry lub filmu jest rysowany wzdłuż krawędzi ekranu. Im głośniejszy dźwięk, tym większa grafika. Pokazywany jest tylko dźwięk odtwarzany na tym komputerze. Dźwięki wokół Ciebie nie są wychwytywane.",
+ "tutorial.direction.title": "Pokazuje, skąd dochodzi dźwięk",
+ "tutorial.direction.body": "Przy wyjściu 2-kanałowym (stereo) dźwięk z lewej strony powiększa grafikę po lewej, a dźwięk z prawej – po prawej. Przy wyjściu 5.1 lub 7.1 aplikacja odróżnia też przód od tyłu: dźwięk z przodu jest rysowany u góry ekranu, a dźwięk z tyłu – u dołu.",
+ "tutorial.types.title": "Każdy rodzaj dźwięku ma swój kolor",
+ "tutorial.types.body": "AI dzieli dźwięki na trzy rodzaje i rysuje każdy w innym kolorze. Kolory możesz zmienić na karcie „Ustawienia”.",
+ "tutorial.type.ambient_examples": "Muzyka, deszcz i cała reszta",
+ "tutorial.type.speech_examples": "Głosy ludzi",
+ "tutorial.type.danger_examples": "Strzały, wybuchy, syreny",
+ "tutorial.start.title": "Jak włączyć i wyłączyć",
+ "tutorial.start.hotkeys": "Podczas działania naciśnij {0}, aby zmienić tryb dźwięku, {1}, aby zmienić tryb wizualizacji, i {2}, aby otworzyć ustawienia nakładki."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/pt-BR.json b/SoundVisualizer.Core/Localization/Strings/pt-BR.json
new file mode 100644
index 0000000..09d4043
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/pt-BR.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Idioma",
+ "common.reset_defaults": "Redefinir para o padrão",
+ "common.minimize": "Minimizar",
+ "common.close": "Fechar",
+ "language.system": "Padrão do sistema",
+
+ "launcher.window_title": "Inicializador do SoundVisualizer",
+ "launcher.tab.home": "Início",
+ "launcher.tab.settings": "Ajustes",
+ "launcher.tab.sounds": "Sons",
+ "launcher.tab.help": "Ajuda",
+ "launcher.tab.more_info": "Saiba mais",
+ "launcher.home.desc": "Desenha na tela os sons que você não consegue ver.\nDos jogos aos filmes, viva uma experiência totalmente nova.",
+ "launcher.home.start": "Iniciar",
+ "launcher.home.stop": "Parar",
+ "launcher.home.tutorial": "Ver tutorial",
+ "launcher.status.running": "Status: Em execução",
+ "launcher.status.idle": "Status: Pronto",
+
+ "settings.section.mode": "Ajustes de modo",
+ "settings.visual_mode.title": "Modo visual",
+ "settings.visual_mode.desc": "Escolha a forma básica dos gráficos desenhados na tela.",
+ "settings.mode.wave": "Onda",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Círculo",
+ "settings.mode.outline": "Contorno",
+ "settings.sound_mode.title": "Modo de som",
+ "settings.sound_mode.desc": "Escolha os canais de som que correspondem aos seus alto-falantes.\nPara alto-falantes de 2 canais, escolha “2 canais”. Para alto-falantes surround, escolha “5.1 canais” ou “7.1 canais”.",
+ "settings.channels.2": "2 canais",
+ "settings.channels.5_1": "5.1 canais",
+ "settings.channels.7_1": "7.1 canais",
+ "settings.max_fps.title": "Taxa de quadros máxima",
+ "settings.max_fps.desc": "Define a taxa de quadros máxima em que os gráficos são desenhados.",
+ "settings.section.mode_detail": "Ajustes de cada modo",
+ "settings.mode_card.wave": "Modo Onda",
+ "settings.mode_card.pad": "Modo Pad",
+ "settings.mode_card.circle": "Modo Círculo",
+ "settings.mode_card.outline": "Modo Contorno",
+ "settings.size_lock.title": "Fixar tamanho",
+ "settings.size_lock.desc": "Mantém os gráficos em um tamanho fixo e mostra o volume do som pela transparência.",
+ "settings.fixed_size.wave": "Tamanho da onda",
+ "settings.fixed_size.pad": "Tamanho do pad",
+ "settings.fixed_size.circle": "Tamanho do círculo",
+ "settings.fixed_size.outline": "Espessura do traço",
+ "settings.fixed_size.desc": "Define o tamanho fixo que os gráficos mantêm enquanto a transparência muda.",
+ "settings.max_opacity.title": "Opacidade máxima",
+ "settings.max_opacity.desc": "O quanto os gráficos ficam opacos quando o som está no volume mais alto.",
+ "settings.size.title": "Tamanho",
+ "settings.size.desc.wave": "Ajusta a altura da onda e o quanto ela parece volumosa no geral.",
+ "settings.size.desc.pad": "Ajusta a espessura e a largura que os pads atingem ao reagir ao som.",
+ "settings.size.desc.circle": "Ajusta o quanto o equalizador circular se projeta para fora quando há som.",
+ "settings.size.desc.outline": "Ajusta a altura da onda de contorno e o quanto ela parece volumosa no geral.",
+ "settings.opacity.title": "Opacidade",
+ "settings.opacity.desc.wave": "Valores mais altos deixam os gráficos mais fortes; valores mais baixos, mais fracos e transparentes.",
+ "settings.opacity.desc.pad": "Valores mais altos deixam os gráficos mais fortes; valores mais baixos, mais fracos e transparentes.",
+ "settings.opacity.desc.circle": "Valores mais altos deixam os gráficos mais fortes; valores mais baixos, mais fracos e transparentes.",
+ "settings.opacity.desc.outline": "Valores mais altos deixam os gráficos mais fortes; valores mais baixos, mais fracos e transparentes.",
+ "settings.speed.title": "Velocidade",
+ "settings.speed.desc.wave": "Ajusta a velocidade com que a onda flui e ondula.",
+ "settings.speed.desc.pad": "Ajusta a rapidez com que os pads giram na direção de onde vem um som.",
+ "settings.speed.desc.circle": "Ajusta a rapidez com que o equalizador circular ondula e gira em resposta ao som.",
+ "settings.speed.desc.outline": "Ajusta a velocidade com que a onda de contorno flui e ondula.",
+ "settings.sensitivity.title": "Sensibilidade",
+ "settings.sensitivity.desc.wave": "Ajusta com que sensibilidade a onda reage e se agita, mesmo com sons baixos.",
+ "settings.sensitivity.desc.pad": "Ajusta os pads para que captem melhor o som e se destaquem mais.",
+ "settings.sensitivity.desc.circle": "Ajusta com que nível de detalhe a amplitude do círculo se fragmenta ao reagir ao som.",
+ "settings.sensitivity.desc.outline": "Ajusta com que sensibilidade a onda de contorno reage e se agita, mesmo com sons baixos.",
+ "settings.glow.title": "Brilho neon",
+ "settings.glow.desc.wave": "Adiciona um brilho neon suave à borda da onda e ajusta a intensidade.\nAtenção: consome mais recursos da GPU.",
+ "settings.glow.desc.pad": "Adiciona um brilho suave, como uma aura, ao redor dos pads.\nAtenção: consome mais recursos da GPU.",
+ "settings.glow.desc.circle": "Adiciona um halo cintilante ao longo da borda do equalizador circular.\nAtenção: consome mais recursos da GPU.",
+ "settings.glow.desc.outline": "Adiciona um brilho neon suave ao contorno e ajusta a intensidade.\nAtenção: consome mais recursos da GPU.",
+ "settings.radius.title": "Raio",
+ "settings.radius.desc": "Define o raio do anel central no modo Círculo.",
+ "settings.section.sound_types": "Tipos de som (IA)",
+ "settings.show_ambient": "Mostrar sons ambientes",
+ "settings.show_speech": "Mostrar vozes",
+ "settings.show_danger": "Mostrar sons de perigo",
+ "settings.set_color": "Escolher cor",
+ "settings.set_color_for": "Escolher cor: {0}",
+ "settings.section.hotkeys": "Teclas de atalho",
+ "settings.hotkey.visual.title": "Alternar modo visual",
+ "settings.hotkey.visual.desc": "Atalho que muda a forma em tempo real durante a execução.",
+ "settings.hotkey.sound.title": "Alternar modo de som",
+ "settings.hotkey.sound.desc": "Atalho que muda o modo de som em tempo real durante a execução.",
+ "settings.hotkey.edit.title": "Ajustes da sobreposição",
+ "settings.hotkey.edit.desc": "Atalho que abre ou fecha o editor da sobreposição.",
+ "settings.hotkey.set": "Definir atalho",
+ "settings.section.advanced": "Ajustes avançados",
+ "settings.developer_mode.title": "Modo de desenvolvedor",
+ "settings.developer_mode.desc": "Mostra no canto superior esquerdo da sobreposição o que a IA ouviu (o nome do som informado, o grau de certeza e quanto tempo levou) e, no canto superior direito, o FPS. As informações aparecem em inglês e não são para o uso diário.",
+ "settings.developer_record.title": "Salvar os resultados em um arquivo",
+ "settings.developer_record.desc": "Salva cada resultado da IA (os nomes de som com maior pontuação, o grau de certeza e os tempos) em um novo arquivo CSV na pasta ai-log, ao lado do SoundVisualizer.exe, para você rever depois o que ela ouviu. Se não for possível gravar nessa pasta, o arquivo vai para %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Os arquivos podem ficar grandes, então desative quando terminar.",
+ "settings.auto_minimize.title": "Minimizar o inicializador ao iniciar",
+ "settings.auto_minimize.desc": "Minimiza automaticamente esta janela do inicializador quando a sobreposição é iniciada.",
+
+ "sounds.title": "Escolha os tipos de som",
+ "sounds.desc": "Os sons que a IA reconhece ficam agrupados com sons parecidos, de acordo com o tipo padrão de cada um. Se você mudar o tipo de um grupo, a cor dos sons dele passa a seguir o novo tipo na hora.",
+ "sounds.sibling_note": "A IA muitas vezes reconhece um mesmo som com vários nomes ao mesmo tempo. Uma sirene, por exemplo, também é reconhecida como “Alarme”, que fica em outro grupo. Se, depois de uma mudança, um som continuar aparecendo com o tipo antigo, mude também os grupos ou sons relacionados.",
+ "sounds.advanced.title": "Modo avançado",
+ "sounds.advanced.desc": "Mostra cada som dentro do grupo para você mudar os sons um a um.",
+ "sounds.search.hint": "Pesquisar sons",
+ "sounds.search.clear": "Limpar pesquisa",
+ "sounds.filter.title": "Filtro",
+ "sounds.filter.all": "Todos",
+ "sounds.filter.changed": "Alterados",
+ "sounds.type.ambient": "Som ambiente",
+ "sounds.type.speech": "Voz",
+ "sounds.type.danger": "Som de perigo",
+ "sounds.type.default": "{0} (padrão)",
+ "sounds.type.mixed": "Misto",
+ "sounds.changed_count": "Sons alterados: {0}",
+ "sounds.changed_from": "Padrão: {0}",
+ "sounds.reset.button": "Redefinir tudo",
+ "sounds.reset.title": "Redefinir todos os sons?",
+ "sounds.reset.message": "Todos os sons que você alterou voltam ao tipo padrão.",
+ "sounds.reset.confirm": "Redefinir",
+ "sounds.reset.cancel": "Cancelar",
+ "sounds.empty": "Nenhum som encontrado.",
+ "sounds.group.mixed_note": "Alguns sons alterados um a um",
+ "sounds.group.changed_line": "Atual: {0} · Padrão: {1}",
+ "sounds.overwrite.title": "Alterar o grupo inteiro?",
+ "sounds.overwrite.message": "Sons do grupo “{0}” que vão mudar para “{2}”: {1}. Os sons que você alterou um a um também serão substituídos.",
+ "sounds.overwrite.confirm": "Alterar",
+ "sounds.overwrite.cancel": "Cancelar",
+ "sounds.basic_warning.title": "Mudar para o modo básico?",
+ "sounds.basic_warning.message": "Alguns grupos têm sons que você alterou um a um. No modo básico, se você escolher um tipo para um desses grupos, todos os sons dele passam a ter esse tipo e as alterações feitas um a um nesse grupo são substituídas. Nada muda até você escolher.",
+ "sounds.basic_warning.confirm": "Mudar para básico",
+ "sounds.basic_warning.cancel": "Ficar no avançado",
+ "sounds.cd.sound": "{0}, tipo: {1}",
+ "sounds.cd.sound_changed": "{0}, tipo: {1}, padrão: {2}",
+ "sounds.cd.group": "Grupo {0}, tipo: {1}, sons: {2} ({3})",
+ "sounds.cd.group_changed": "Grupo {0}, tipo: {1}, padrão: {2}, sons: {3} ({4})",
+ "sounds.cd.group_header": "Grupo {0}, sons: {1}",
+
+ "hotkey.prompt": "Pressione uma tecla (Esc para cancelar)",
+ "hotkey.none": "Nenhum",
+ "hotkey.button_name": "{0}, atalho: {1}",
+
+ "help.start.title": "Primeiros passos",
+ "help.start.1": "Escolha o idioma no topo desta janela e, se quiser, mude a aparência dos gráficos na aba Ajustes.",
+ "help.start.2": "Na aba Início, clique em “{0}”.",
+ "help.start.3": "Os gráficos são desenhados nas bordas da tela, por cima de outras janelas, como jogos e vídeos. Os cliques atravessam os gráficos, então você pode continuar usando a janela que está embaixo.",
+ "help.start.4": "Para desligar, clique em “{0}” na aba Início ou clique com o botão direito no ícone do SoundVisualizer na área de notificação da barra de tarefas e escolha “{0}”.",
+ "help.modes.title": "Modos visuais",
+ "help.mode.wave": "Toda a borda da tela avança para dentro de acordo com o volume do som. O lado de onde vem o som avança ainda mais.",
+ "help.mode.pad": "Barras se acendem em 8 pontos ao redor da borda da tela. É o modo que mostra a direção com mais clareza.",
+ "help.mode.circle": "Um anel no centro da tela se expande na direção do som.",
+ "help.mode.outline": "Desenha só um contorno fino da onda, então cobre menos a tela do jogo.",
+ "help.modes.note": "Ajustes como Tamanho e Opacidade são salvos separadamente para cada modo e podem ser alterados na aba Ajustes.",
+ "help.sound.title": "Tipos de som",
+ "help.sound.kinds": "A IA separa os sons em três tipos: sons ambientes, vozes e sons de perigo. Por padrão, os sons ambientes aparecem em branco, as vozes em amarelo e os sons de perigo em vermelho. Na aba Ajustes, você pode mudar a cor de cada tipo e escolher se ele é mostrado.",
+ "help.sound.groups": "Na aba Sons, os sons parecidos ficam agrupados e você escolhe o tipo de cada grupo. Por exemplo, se mudar o grupo “Campainha” para som de perigo, quando a IA reconhecer uma campainha ou uma batida na porta, o som passa a ter a cor dos sons de perigo. Mas um som que você mesmo muda para som de perigo só participa da votação da IA sobre o tipo, então isso não quer dizer que toda campainha vai aparecer como som de perigo.",
+ "help.sound.advanced": "Ative o modo avançado para mudar os sons um a um.",
+ "help.sound.danger": "Quando um som de perigo (como um tiro) é detectado, a cor muda na hora, sem transição gradual.",
+ "help.pc.title": "Ajustes do PC e teclas de atalho",
+ "help.sound_mode.title": "Modo de som",
+ "help.sound_mode.body": "Para diferenciar também os sons da frente e de trás, além dos da esquerda e da direita, configure o dispositivo de saída como “Surround 5.1” (6 canais) ou “Surround 7.1” (8 canais) nas configurações de som do Windows. Com um estéreo comum (2 canais), o app diferencia esquerda e direita, mas não frente e trás. O “Modo de som” na aba Ajustes se ajusta ao dispositivo de saída ao iniciar e quando o dispositivo muda, e você também pode alterá-lo.",
+ "help.sound_mode.tip": "💡 Se você não tiver um dispositivo de saída 7.1, pode instalar o {0} e configurar o dispositivo de saída em 7.1 canais para mostrar a sobreposição com som surround 7.1.",
+ "help.sound_mode.tip_link": "driver virtual VB-CABLE",
+ "help.hotkeys.title": "Atalhos personalizados em tempo real",
+ "help.hotkeys.body": "Na aba Ajustes, clique em um botão de atalho e pressione uma tecla ou uma combinação de teclas (por exemplo, Ctrl + Shift + A) para mudar o atalho como quiser. Ao pressionar o atalho, o modo muda na hora, mesmo com o app em segundo plano.",
+ "help.hotkeys.defaults": "Por padrão, F2 alterna o modo de som, F3 alterna o modo visual e F4 abre os ajustes da sobreposição.",
+ "help.size_lock.title": "Modo de tamanho fixo",
+ "help.size_lock.body": "O modo de tamanho fixo mantém os gráficos sempre do mesmo tamanho e usa uma transparência proporcional ao volume do som. É útil quando você quer que o tamanho dos gráficos não mude, mas ainda quer ver como eles reagem ao som.",
+ "help.editor.title": "Modo de edição da sobreposição em tempo real",
+ "help.editor.body": "Pressione o atalho definido (F4 por padrão) para ativar o modo de edição da sobreposição. Você pode arrastar o mouse na tela para ajustar o limite de tamanho dos gráficos e refinar os detalhes no painel de ajustes que aparece.",
+ "help.developer.title": "Modo de desenvolvedor",
+ "help.developer.body": "Ao ativar o modo de desenvolvedor, a sobreposição mostra no canto superior esquerdo o que a IA ouviu (o nome do som, o grau de certeza e quanto tempo levou) e, no canto superior direito, o FPS. Se você também ativar “{0}”, cada resultado é salvo em um arquivo CSV. É útil para verificar se o sistema está funcionando corretamente.",
+ "help.notes.title": "Bom saber",
+ "help.note.direction_surround": "Com saída de 5.1 ou 7.1 canais, a parte de cima da tela mostra o som que vem da frente, e a de baixo, o som que vem de trás. As laterais esquerda e direita mostram o som que vem da esquerda e da direita, e os gráficos que surgem de todos os lados da tela ao mesmo tempo representam sons ambientes ou sem direção.",
+ "help.note.direction_stereo": "Com saída de 2 canais (estéreo), o app diferencia esquerda e direita, mas não frente e trás. Nesse caso, os gráficos nas partes de cima e de baixo da tela representam sons que chegam igualmente dos dois lados e a sensação de espaço.",
+ "help.note.sources": "O app mostra o som de todos os apps que tocam pelo dispositivo de reprodução padrão do Windows. O som que um app envia para outro dispositivo, como um headset usado só para chat de voz, não é mostrado.",
+ "help.note.volume": "Se o volume de um app estiver muito baixo ou no mudo, no próprio app ou no mixer de volume do Windows, o som dele pode quase não aparecer ou nem aparecer. Se os gráficos não reagirem bem, aumente um pouco esse som ou aumente a Sensibilidade na aba Ajustes.",
+ "help.note.ai": "A IA pode errar na classificação. Não use este app como sua única proteção em situações que envolvam segurança.",
+ "help.faq.title": "Perguntas frequentes",
+ "help.faq.stopped_q": "Desligou sozinha, sem que eu tenha parado.",
+ "help.faq.stopped_a": "Ela desliga quando a captura de som para e não volta em alguns segundos, por exemplo, quando o dispositivo de reprodução é desconectado e não há outro disponível. A aba Início mostra o motivo. Conecte alto-falantes ou fones de ouvido, ou defina um dispositivo de reprodução padrão nas configurações de som do Windows, e inicie de novo.",
+ "help.faq.no_graphic_q": "Não vejo nenhum gráfico.",
+ "help.faq.no_graphic_a": "Verifique se o som está tocando pelo dispositivo de reprodução padrão do Windows e não está no mudo, e se a aba Início mostra algum aviso. Se um jogo estiver em tela cheia exclusiva, experimente o modo sem bordas ou em janela. Confira também, na aba Ajustes, se a Opacidade não está baixa demais e se a exibição do tipo de som atual não está desativada.",
+ "help.faq.performance_q": "Usa muita CPU ou GPU?",
+ "help.faq.performance_a": "Para deixar mais leve, diminua “{0}” na aba Ajustes ou desative “{1}”.",
+ "help.faq.language_q": "Como mudo o idioma do app?",
+ "help.faq.language_a": "Escolha um na lista “{0}” no topo desta janela. Enquanto estiver em execução, você também pode mudar nos ajustes da sobreposição. “{1}” segue o idioma de exibição do Windows.",
+ "help.report.title": "Relatar um problema",
+ "help.report.desc": "Conte para nós sobre um bug ou sobre um som que foi mostrado com o tipo errado. O GitHub é aberto no seu navegador com as informações do sistema abaixo já preenchidas, por isso é preciso ter uma conta no GitHub. Se não tiver uma, envie um e-mail para nós. Nenhum áudio é enviado em momento algum.",
+ "help.report.github": "Relatar no GitHub",
+ "help.report.mail": "Relatar por e-mail",
+ "help.report.copy": "Copiar informações do sistema",
+ "help.report.copied": "Informações do sistema copiadas.",
+ "help.report.copy_failed": "Não foi possível copiar para a área de transferência. Tente de novo.",
+ "help.report.no_browser": "Não foi possível abrir um navegador. Copie as informações do sistema e envie para nós de outra forma.",
+ "help.report.no_mail": "Não há um app de e-mail para enviar. Copie as informações do sistema e envie de outra forma.",
+ "help.report.issue_title": "Relato enviado pelo app",
+ "help.report.issue_body": "O que aconteceu?\n\nO que você estava ouvindo (app, tipo de som)?\n\nO que você esperava em vez disso?",
+ "help.about.title": "Sobre",
+ "help.about.version": "Versão {0}",
+ "help.about.license": "Este app é distribuído sob a GNU Affero General Public License v3.0 (AGPL-3.0). Os nomes e os grupos de sons da aba Sons são baseados na ontologia AudioSet e distribuídos sob a licença CC BY-SA 4.0 (veja “Licenças de código aberto” abaixo).",
+ "help.licenses.button": "Licenças de código aberto",
+ "help.licenses.title": "Licenças de código aberto",
+ "help.licenses.close": "Fechar",
+
+ "notice.ai_unavailable": "A detecção do tipo de som não está funcionando, então todos os sons aparecem na cor dos sons ambientes. Tente desligar e ligar a visualização de novo.",
+ "notice.ai_unavailable_hidden": "A detecção do tipo de som não está funcionando, então todos os sons contam como sons ambientes, mas a exibição dos sons ambientes está desligada, por isso nada aparece. Ative “{0}” na aba Ajustes ou tente desligar e ligar a visualização de novo.",
+ "notice.stopped.title": "Visualização de sons desligada",
+ "notice.stopped.no_device": "Não foi possível abrir nenhum dispositivo de reprodução, então o som não pode ser capturado. Conecte alto-falantes ou fones de ouvido, ou defina um dispositivo de reprodução padrão nas configurações de som do Windows, e inicie de novo.",
+ "notice.stopped.capture_error": "A captura de som parou por causa de um erro, então os gráficos estão desligados.",
+ "notice.dismiss": "Fechar aviso",
+
+ "tray.open": "Abrir o inicializador",
+ "tray.exit": "Sair",
+
+ "more_info.title": "Repositório no GitHub e contribuições",
+ "more_info.body": "O SoundVisualizer é um projeto de código aberto, e o código-fonte é público no GitHub. Você pode participar relatando bugs, sugerindo recursos ou contribuindo com o desenvolvimento. Clique no botão abaixo para visitar o repositório oficial no GitHub! Você também pode conferir e baixar a versão mais recente na aba Releases do GitHub.",
+ "more_info.github_button": "Visitar o repositório no GitHub",
+
+ "dialog.notice_title": "Aviso",
+ "dialog.already_running": "A sobreposição já está em execução.",
+ "dialog.reset_title": "Redefinir ajustes",
+ "dialog.reset_wave": "Redefinir os ajustes do modo Onda para o padrão?",
+ "dialog.reset_pad": "Redefinir os ajustes do modo Pad para o padrão?",
+ "dialog.reset_circle": "Redefinir os ajustes do modo Círculo para o padrão?",
+ "dialog.reset_outline": "Redefinir os ajustes do modo Contorno para o padrão?",
+ "dialog.reset_sound_types": "Redefinir os ajustes dos tipos de som para o padrão?",
+ "dialog.reset_all_title": "Redefinir para o padrão",
+ "dialog.reset_all": "Tem certeza de que quer redefinir todos os ajustes para o padrão?",
+ "dialog.link_error": "Não foi possível abrir o link: {0}",
+
+ "editor.title": "Ajustes da sobreposição",
+ "editor.drag_tip": "Arraste com o mouse as linhas-guia na tela para ajustar em tempo real o limite de altura e o tamanho do centro da onda da sobreposição!",
+ "editor.glow": "Efeito de brilho neon",
+ "editor.glow_strength": "Intensidade",
+ "editor.color": "Cor",
+ "editor.hotkeys": "Ajustes de atalhos",
+ "editor.developer_mode_desc": "O que a IA ouviu (no alto à esquerda) e o FPS (no alto à direita)",
+ "editor.save_close_hint": "para salvar e fechar",
+ "editor.close_overlay": "Fechar a sobreposição",
+ "editor.guide.limit": "Limite no modo {0}",
+ "editor.guide.size": "Tamanho no modo {0}: {1}%",
+ "editor.guide.base_radius": "Raio base: {0}",
+ "editor.guide.circle_size": "Tamanho do círculo: {0}%",
+
+ "hud.visual_mode": "Modo visual: [{0}] {1}",
+ "hud.sound_mode": "Modo de som: [{0}] {1}",
+ "hud.overlay_settings": "Ajustes da sobreposição: [{0}]",
+ "hud.status.ready": "Sistema pronto",
+ "hud.status.stereo": "Áudio do PC: {0}ch (estéreo)\nFonte: aguardando...",
+ "hud.status.surround": "Áudio do PC: 8ch (7.1)\nFonte: aguardando...",
+
+ "color_picker.title": "Escolha uma cor",
+ "color_picker.hex": "Código HEX",
+ "color_picker.cancel": "Cancelar",
+ "color_picker.apply": "Aplicar",
+ "color_picker.area": "Saturação e brilho",
+ "color_picker.area_value": "Saturação {0}%, brilho {1}%",
+ "color_picker.hue": "Matiz",
+ "color_picker.keys_help": "Use as teclas de seta para alterar. Mantenha a tecla Shift pressionada para avançar em passos maiores.",
+ "color_picker.presets": "Cores comuns",
+ "color_picker.preset.white": "Branco",
+ "color_picker.preset.red": "Vermelho",
+ "color_picker.preset.orange": "Laranja",
+ "color_picker.preset.yellow": "Amarelo",
+ "color_picker.preset.green": "Verde",
+ "color_picker.preset.cyan": "Turquesa",
+ "color_picker.preset.blue": "Azul",
+ "color_picker.preset.magenta": "Magenta",
+
+ "audio.error.no_device": "Nenhum dispositivo de saída foi encontrado.",
+ "audio.error.capture_stopped": "A captura parou inesperadamente: {0}",
+ "audio.error.capture_start_failed": "Não foi possível iniciar o mecanismo de captura: {0}",
+
+ "tutorial.window_title": "Tutorial",
+ "tutorial.skip": "Pular",
+ "tutorial.previous": "Página anterior",
+ "tutorial.next": "Próximo",
+ "tutorial.done": "Entendi",
+ "tutorial.page_status": "Página {0} de {1}",
+ "tutorial.pause": "Pausar animação",
+ "tutorial.play": "Reproduzir animação",
+ "tutorial.sound_on": "Ativar som",
+ "tutorial.sound_off": "Desativar som",
+ "tutorial.sound.title": "Veja o som na sua tela",
+ "tutorial.sound.body": "O som do seu jogo ou vídeo é desenhado nas bordas da tela. Quanto mais alto o som, maior o desenho. Só aparece o som que toca neste PC. Os sons ao seu redor não são captados.",
+ "tutorial.direction.title": "Mostra de onde vem o som",
+ "tutorial.direction.body": "Com saída de 2 canais (estéreo), um som da esquerda é desenhado maior do lado esquerdo, e um som da direita, do lado direito. Com saída de 5.1 ou 7.1 canais, o app também diferencia frente e trás: o som que vem da frente é desenhado na parte de cima da tela, e o que vem de trás, na parte de baixo.",
+ "tutorial.types.title": "Cada tipo de som tem sua própria cor",
+ "tutorial.types.body": "A IA separa os sons em três tipos e desenha cada um com uma cor diferente. Você pode mudar as cores na aba Ajustes.",
+ "tutorial.type.ambient_examples": "Música, chuva e todo o resto",
+ "tutorial.type.speech_examples": "Pessoas falando",
+ "tutorial.type.danger_examples": "Tiros, explosões, sirenes",
+ "tutorial.start.title": "Como ligar e desligar",
+ "tutorial.start.hotkeys": "Enquanto estiver em execução, pressione {0} para alternar o modo de som, {1} para alternar o modo visual e {2} para abrir os ajustes da sobreposição."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/ru.json b/SoundVisualizer.Core/Localization/Strings/ru.json
new file mode 100644
index 0000000..e01c9e3
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/ru.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Язык",
+ "common.reset_defaults": "Сбросить настройки",
+ "common.minimize": "Свернуть",
+ "common.close": "Закрыть",
+ "language.system": "Язык системы",
+
+ "launcher.window_title": "Лаунчер SoundVisualizer",
+ "launcher.tab.home": "Главная",
+ "launcher.tab.settings": "Настройки",
+ "launcher.tab.sounds": "Звуки",
+ "launcher.tab.help": "Справка",
+ "launcher.tab.more_info": "Подробнее",
+ "launcher.home.desc": "Показывает на экране звуки, которые нельзя увидеть.\nОт игр до фильмов — откройте для себя новые впечатления.",
+ "launcher.home.start": "Запустить",
+ "launcher.home.stop": "Остановить",
+ "launcher.home.tutorial": "Показать обучение",
+ "launcher.status.running": "Статус: работает",
+ "launcher.status.idle": "Статус: ожидает запуска",
+
+ "settings.section.mode": "Настройки режима",
+ "settings.visual_mode.title": "Режим отображения",
+ "settings.visual_mode.desc": "Выберите основную форму графики, которая рисуется на экране.",
+ "settings.mode.wave": "Волна",
+ "settings.mode.pad": "Панель",
+ "settings.mode.circle": "Круг",
+ "settings.mode.outline": "Контур",
+ "settings.sound_mode.title": "Звуковой режим",
+ "settings.sound_mode.desc": "Выберите звуковые каналы, которые подходят вашим динамикам.\nДля двухканальных динамиков выберите «2 канала», а для систем объёмного звука — «5.1 каналов» или «7.1 каналов».",
+ "settings.channels.2": "2 канала",
+ "settings.channels.5_1": "5.1 каналов",
+ "settings.channels.7_1": "7.1 каналов",
+ "settings.max_fps.title": "Макс. частота кадров",
+ "settings.max_fps.desc": "Задаёт максимальную частоту кадров, с которой рисуется графика.",
+ "settings.section.mode_detail": "Параметры по режимам",
+ "settings.mode_card.wave": "Режим «Волна»",
+ "settings.mode_card.pad": "Режим «Панель»",
+ "settings.mode_card.circle": "Режим «Круг»",
+ "settings.mode_card.outline": "Режим «Контур»",
+ "settings.size_lock.title": "Фиксировать размер",
+ "settings.size_lock.desc": "Размер графики не меняется, а громкость передаётся её прозрачностью.",
+ "settings.fixed_size.wave": "Размер волны",
+ "settings.fixed_size.pad": "Размер панелей",
+ "settings.fixed_size.circle": "Размер круга",
+ "settings.fixed_size.outline": "Толщина контура",
+ "settings.fixed_size.desc": "Задаёт постоянный размер графики, при котором меняется только её прозрачность.",
+ "settings.max_opacity.title": "Макс. плотность",
+ "settings.max_opacity.desc": "Насколько плотной становится графика при самом громком звуке.",
+ "settings.size.title": "Размер",
+ "settings.size.desc.wave": "Регулирует высоту волны и то, насколько объёмной она выглядит.",
+ "settings.size.desc.pad": "Регулирует, насколько толстыми и широкими становятся панели, когда реагируют на звук.",
+ "settings.size.desc.circle": "Регулирует, насколько далеко круговой эквалайзер выступает наружу при воспроизведении звука.",
+ "settings.size.desc.outline": "Регулирует высоту контурной волны и то, насколько объёмной она выглядит.",
+ "settings.opacity.title": "Плотность",
+ "settings.opacity.desc.wave": "Чем выше значение, тем насыщеннее графика; чем ниже, тем она бледнее и прозрачнее.",
+ "settings.opacity.desc.pad": "Чем выше значение, тем насыщеннее графика; чем ниже, тем она бледнее и прозрачнее.",
+ "settings.opacity.desc.circle": "Чем выше значение, тем насыщеннее графика; чем ниже, тем она бледнее и прозрачнее.",
+ "settings.opacity.desc.outline": "Чем выше значение, тем насыщеннее графика; чем ниже, тем она бледнее и прозрачнее.",
+ "settings.speed.title": "Скорость",
+ "settings.speed.desc.wave": "Регулирует, как быстро волна течёт и колышется.",
+ "settings.speed.desc.pad": "Регулирует, как быстро панели поворачиваются в сторону, откуда идёт звук.",
+ "settings.speed.desc.circle": "Регулирует, как быстро круговой эквалайзер колышется и вращается в ответ на звук.",
+ "settings.speed.desc.outline": "Регулирует, как быстро контурная волна течёт и колышется.",
+ "settings.sensitivity.title": "Отклик",
+ "settings.sensitivity.desc.wave": "Регулирует, насколько чутко волна откликается даже на тихие звуки и как сильно при этом вздымается.",
+ "settings.sensitivity.desc.pad": "Настраивает панели так, чтобы они лучше улавливали звук и заметнее выделялись.",
+ "settings.sensitivity.desc.circle": "Регулирует, насколько дробно распадается амплитуда круга, когда он реагирует на звук.",
+ "settings.sensitivity.desc.outline": "Регулирует, насколько чутко контурная волна откликается даже на тихие звуки и как сильно при этом вздымается.",
+ "settings.glow.title": "Сияние",
+ "settings.glow.desc.wave": "Добавляет мягкое неоновое сияние по краю волны и регулирует его силу.\nВнимание: требует дополнительных ресурсов GPU.",
+ "settings.glow.desc.pad": "Добавляет вокруг панелей мягкое сияние, похожее на ауру.\nВнимание: требует дополнительных ресурсов GPU.",
+ "settings.glow.desc.circle": "Добавляет мерцающий ореол по краю кругового эквалайзера.\nВнимание: требует дополнительных ресурсов GPU.",
+ "settings.glow.desc.outline": "Добавляет контуру мягкое неоновое сияние и регулирует его силу.\nВнимание: требует дополнительных ресурсов GPU.",
+ "settings.radius.title": "Радиус",
+ "settings.radius.desc": "Задаёт радиус центрального кольца в режиме «Круг».",
+ "settings.section.sound_types": "Типы звуков (ИИ)",
+ "settings.show_ambient": "Показывать фоновые звуки",
+ "settings.show_speech": "Показывать речь",
+ "settings.show_danger": "Показывать опасные звуки",
+ "settings.set_color": "Задать цвет",
+ "settings.set_color_for": "Задать цвет: {0}",
+ "settings.section.hotkeys": "Горячие клавиши",
+ "settings.hotkey.visual.title": "Смена режима отображения",
+ "settings.hotkey.visual.desc": "Горячая клавиша, которая меняет форму графики прямо во время работы.",
+ "settings.hotkey.sound.title": "Смена звукового режима",
+ "settings.hotkey.sound.desc": "Горячая клавиша, которая меняет звуковой режим прямо во время работы.",
+ "settings.hotkey.edit.title": "Настройки оверлея",
+ "settings.hotkey.edit.desc": "Горячая клавиша, которая открывает и закрывает редактор оверлея.",
+ "settings.hotkey.set": "Назначить клавишу",
+ "settings.section.advanced": "Дополнительные настройки",
+ "settings.developer_mode.title": "Режим разработчика",
+ "settings.developer_mode.desc": "Показывает в левом верхнем углу оверлея, что услышал ИИ (название звука, уверенность и время обработки), а в правом верхнем — FPS. Эти сведения выводятся на английском и не предназначены для повседневного использования.",
+ "settings.developer_record.title": "Записывать результаты в файл",
+ "settings.developer_record.desc": "Записывает каждый результат ИИ (названия звуков с самой высокой оценкой, уверенность и время обработки) в новый CSV-файл в папке ai-log рядом с SoundVisualizer.exe, чтобы потом можно было посмотреть, что он услышал. Если в эту папку нельзя записывать, файл сохраняется в %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Файлы могут быть большими, поэтому выключите запись, когда закончите.",
+ "settings.auto_minimize.title": "Сворачивать лаунчер при запуске",
+ "settings.auto_minimize.desc": "Автоматически сворачивает окно лаунчера, когда запускается оверлей.",
+
+ "sounds.title": "Выберите типы звуков",
+ "sounds.desc": "Звуки, которые распознаёт ИИ, собраны в группы похожих звуков по типам, к которым они относятся по умолчанию. Если изменить тип группы, цвет её звуков сразу станет таким же, как у нового типа.",
+ "sounds.sibling_note": "ИИ часто распознаёт один и тот же звук сразу под несколькими названиями. Сирена, например, распознаётся ещё и как «Сигнал тревоги», а этот звук в другой группе. Если после изменения звук всё ещё отображается со старым типом, измените и связанные группы или звуки.",
+ "sounds.advanced.title": "Расширенный режим",
+ "sounds.advanced.desc": "Показывает каждый звук в его группе, чтобы можно было менять звуки по одному.",
+ "sounds.search.hint": "Поиск звуков",
+ "sounds.search.clear": "Очистить поиск",
+ "sounds.filter.title": "Фильтр",
+ "sounds.filter.all": "Все",
+ "sounds.filter.changed": "Изменённые",
+ "sounds.type.ambient": "Фоновые звуки",
+ "sounds.type.speech": "Речь",
+ "sounds.type.danger": "Опасные звуки",
+ "sounds.type.default": "{0} (по умолчанию)",
+ "sounds.type.mixed": "Смешанный",
+ "sounds.changed_count": "Изменено звуков: {0}",
+ "sounds.changed_from": "По умолчанию: {0}",
+ "sounds.reset.button": "Сбросить все",
+ "sounds.reset.title": "Сбросить типы всех звуков?",
+ "sounds.reset.message": "Каждый звук, который вы изменили, вернётся к своему типу по умолчанию.",
+ "sounds.reset.confirm": "Сбросить",
+ "sounds.reset.cancel": "Отмена",
+ "sounds.empty": "Подходящих звуков нет.",
+ "sounds.group.mixed_note": "Некоторые звуки изменены по отдельности",
+ "sounds.group.changed_line": "Сейчас: {0} · По умолчанию: {1}",
+ "sounds.overwrite.title": "Изменить всю группу?",
+ "sounds.overwrite.message": "Звуков в группе «{0}», которые получат тип «{2}»: {1}. Звуки, изменённые по отдельности, тоже будут перезаписаны.",
+ "sounds.overwrite.confirm": "Изменить",
+ "sounds.overwrite.cancel": "Отмена",
+ "sounds.basic_warning.title": "Перейти в базовый режим?",
+ "sounds.basic_warning.message": "В некоторых группах есть звуки, которые вы изменили по отдельности. Если в базовом режиме выбрать тип для такой группы, все её звуки получат этот тип, а изменения, сделанные по отдельности в этой группе, будут перезаписаны. Пока вы ничего не выберете, ничего не изменится.",
+ "sounds.basic_warning.confirm": "В базовый режим",
+ "sounds.basic_warning.cancel": "Остаться в расширенном",
+ "sounds.cd.sound": "{0}, тип: {1}",
+ "sounds.cd.sound_changed": "{0}, тип: {1}, по умолчанию: {2}",
+ "sounds.cd.group": "Группа {0}, тип: {1}, звуков: {2} ({3})",
+ "sounds.cd.group_changed": "Группа {0}, тип: {1}, по умолчанию: {2}, звуков: {3} ({4})",
+ "sounds.cd.group_header": "Группа {0}, звуков: {1}",
+
+ "hotkey.prompt": "Нажмите клавишу (Esc — отмена)",
+ "hotkey.none": "Нет",
+ "hotkey.button_name": "{0}, горячая клавиша: {1}",
+
+ "help.start.title": "Начало работы",
+ "help.start.1": "Выберите язык в верхней части этого окна и при желании измените вид графики на вкладке «Настройки».",
+ "help.start.2": "На вкладке «Главная» нажмите «{0}».",
+ "help.start.3": "Графика рисуется по краям экрана поверх других окон, например игр и видео. Щелчки мыши проходят сквозь неё, поэтому окном под ней можно пользоваться как обычно.",
+ "help.start.4": "Чтобы выключить, нажмите «{0}» на вкладке «Главная» или щёлкните правой кнопкой мыши значок SoundVisualizer в области уведомлений на панели задач и выберите «{0}».",
+ "help.modes.title": "Режимы отображения",
+ "help.mode.wave": "Края экрана по всему периметру надвигаются внутрь — тем сильнее, чем громче звук. Со стороны, откуда идёт звук, волна заходит глубже.",
+ "help.mode.pad": "В 8 точках по краю экрана загораются полоски. В этом режиме направление видно лучше всего.",
+ "help.mode.circle": "Кольцо в центре экрана вытягивается в сторону звука.",
+ "help.mode.outline": "Рисуется только тонкий контур волны, поэтому графика меньше загораживает игру.",
+ "help.modes.note": "Такие настройки, как «Размер» и «Плотность», сохраняются отдельно для каждого режима. Изменить их можно на вкладке «Настройки».",
+ "help.sound.title": "Типы звуков",
+ "help.sound.kinds": "ИИ делит всё, что слышно, на фоновые звуки, речь и опасные звуки. По умолчанию фоновые звуки отображаются белым, речь — жёлтым, а опасные звуки — красным. На вкладке «Настройки» можно изменить цвет каждого типа и выбрать, показывать ли его.",
+ "help.sound.groups": "На вкладке «Звуки» похожие звуки собраны в группы, и вы выбираете, к какому типу относится каждая группа. Например, если перевести группу «Дверной звонок» в опасные звуки, то когда ИИ распознаёт дверной звонок или стук в дверь, звук получит цвет опасных звуков. Однако звук, который вы сами перевели в опасные звуки, лишь участвует в голосовании ИИ о типе, поэтому это не значит, что каждый дверной звонок будет показан как опасный звук.",
+ "help.sound.advanced": "Чтобы менять звуки по одному, включите расширенный режим.",
+ "help.sound.danger": "Когда обнаружен опасный звук (например, выстрел), цвет меняется сразу, а не плавно.",
+ "help.pc.title": "Настройки ПК и горячие клавиши",
+ "help.sound_mode.title": "Звуковой режим",
+ "help.sound_mode.body": "Чтобы различать звуки не только слева и справа, но и спереди и сзади, выберите для устройства вывода в параметрах звука Windows конфигурацию объёмного звука 5.1 (6 каналов) или 7.1 (8 каналов). При обычном стерео (2 канала) приложение различает звуки слева и справа, но не спереди и сзади. «Звуковой режим» на вкладке «Настройки» подстраивается под устройство вывода при запуске и при смене устройства, но его можно изменить и вручную.",
+ "help.sound_mode.tip": "💡 Если у вас нет устройства вывода 7.1, установите {0} и выберите для устройства вывода конфигурацию 7.1 — тогда оверлей сможет показывать объёмный звук 7.1.",
+ "help.sound_mode.tip_link": "виртуальный драйвер VB-CABLE",
+ "help.hotkeys.title": "Настраиваемые горячие клавиши",
+ "help.hotkeys.body": "На вкладке «Настройки» нажмите кнопку горячей клавиши, а затем нажмите одну клавишу или сочетание клавиш (например, Ctrl + Shift + A), чтобы изменить горячую клавишу по своему усмотрению. Горячая клавиша мгновенно переключает режим, даже когда приложение работает в фоне.",
+ "help.hotkeys.defaults": "По умолчанию F2 переключает звуковой режим, F3 — режим отображения, а F4 открывает настройки оверлея.",
+ "help.size_lock.title": "Режим фиксации размера",
+ "help.size_lock.body": "В режиме фиксации размера графика сохраняет постоянный размер, а громкость звука передаётся прозрачностью. Это удобно, если вы хотите, чтобы графика не меняла размер, но при этом было видно, как она реагирует на звук.",
+ "help.editor.title": "Редактирование оверлея в реальном времени",
+ "help.editor.body": "Нажмите назначенную горячую клавишу (по умолчанию F4), чтобы включить режим редактирования оверлея. Перетаскивая мышью прямо на экране, можно задать предельный размер графики, а в появившейся панели настроек — точно настроить детали.",
+ "help.developer.title": "Режим разработчика",
+ "help.developer.body": "Если включить режим разработчика, в левом верхнем углу оверлея появится то, что услышал ИИ (название звука, уверенность и время обработки), а в правом верхнем — FPS. Если также включить «{0}», каждый результат будет сохраняться в CSV-файл. Это полезно, когда нужно проверить, правильно ли работает система.",
+ "help.notes.title": "Полезно знать",
+ "help.note.direction_surround": "При выводе звука 5.1 или 7.1 верх экрана показывает звук спереди, а низ — звук сзади. Левый и правый края показывают звук слева и справа, а графика, появляющаяся одновременно со всех сторон экрана, — это фоновые или ненаправленные звуки.",
+ "help.note.direction_stereo": "При двухканальном (стерео) выводе приложение различает звуки слева и справа, но не спереди и сзади. Графика вверху и внизу экрана в этом случае обозначает звуки, одинаково слышные с обеих сторон, а также передаёт ощущение пространства.",
+ "help.note.sources": "Показывается звук всех приложений, который идёт через устройство воспроизведения Windows по умолчанию. Звук, который приложение отправляет на другое устройство, например на гарнитуру только для голосового чата, не показывается.",
+ "help.note.volume": "Если громкость приложения сильно убавлена или выключена — в самом приложении или в микшере громкости Windows, — его звук может почти или совсем не отображаться. Если графика реагирует слабо, немного прибавьте этот звук или увеличьте «Отклик» на вкладке «Настройки».",
+ "help.note.ai": "ИИ может ошибаться при определении типа звука. В ситуациях, где важна безопасность, не полагайтесь только на это приложение.",
+ "help.faq.title": "Часто задаваемые вопросы",
+ "help.faq.stopped_q": "Визуализатор выключился сам, без моей команды.",
+ "help.faq.stopped_a": "Он выключается, если захват звука прекращается и не восстанавливается за несколько секунд, например когда устройство воспроизведения отключено, а другого нет. Причина отобразится на вкладке «Главная». Подключите колонки или наушники либо выберите устройство воспроизведения по умолчанию в параметрах звука Windows, а затем запустите снова.",
+ "help.faq.no_graphic_q": "Графика не отображается.",
+ "help.faq.no_graphic_a": "Проверьте, что звук идёт через устройство воспроизведения Windows по умолчанию и не выключен, а также нет ли сообщения на вкладке «Главная». Если игра работает в монопольном полноэкранном режиме, попробуйте оконный режим без рамки или оконный режим. Также убедитесь, что на вкладке «Настройки» значение «Плотность» не слишком низкое и что показ текущего типа звука не выключен.",
+ "help.faq.performance_q": "Приложение сильно нагружает процессор или видеокарту?",
+ "help.faq.performance_a": "Чтобы снизить нагрузку, уменьшите «{0}» на вкладке «Настройки» или выключите «{1}».",
+ "help.faq.language_q": "Как изменить язык приложения?",
+ "help.faq.language_a": "Выберите его в списке «{0}» в верхней части этого окна. Во время работы язык можно сменить и в настройках оверлея. При выборе варианта «{1}» используется язык интерфейса Windows.",
+ "help.report.title": "Сообщить о проблеме",
+ "help.report.desc": "Сообщите нам об ошибке или о звуке, для которого был показан неверный тип. В браузере откроется GitHub, где уже будут заполнены сведения о системе, показанные ниже, поэтому нужен аккаунт GitHub. Если аккаунта нет, напишите нам по электронной почте. Звук никогда никуда не отправляется.",
+ "help.report.github": "Сообщить через GitHub",
+ "help.report.mail": "Сообщить по эл. почте",
+ "help.report.copy": "Скопировать сведения о системе",
+ "help.report.copied": "Сведения о системе скопированы.",
+ "help.report.copy_failed": "Не удалось скопировать в буфер обмена. Попробуйте ещё раз.",
+ "help.report.no_browser": "Не удалось открыть браузер. Скопируйте сведения о системе и отправьте их нам другим способом.",
+ "help.report.no_mail": "Нет приложения для отправки почты. Скопируйте сведения о системе и отправьте их другим способом.",
+ "help.report.issue_title": "Сообщение из приложения",
+ "help.report.issue_body": "Что произошло?\n\nЧто вы слушали (приложение, тип звука)?\n\nЧего вы ожидали вместо этого?",
+ "help.about.title": "О приложении",
+ "help.about.version": "Версия {0}",
+ "help.about.license": "Это приложение распространяется на условиях GNU Affero General Public License v3.0 (AGPL-3.0). Названия и группы звуков на вкладке «Звуки» основаны на онтологии AudioSet и распространяются на условиях лицензии CC BY-SA 4.0 (см. «Лицензии открытого ПО» ниже).",
+ "help.licenses.button": "Лицензии открытого ПО",
+ "help.licenses.title": "Лицензии открытого ПО",
+ "help.licenses.close": "Закрыть",
+
+ "notice.ai_unavailable": "Определение типа звука не работает, поэтому все звуки показываются цветом фоновых звуков. Попробуйте выключить и снова включить визуализатор.",
+ "notice.ai_unavailable_hidden": "Определение типа звука не работает, поэтому все звуки считаются фоновыми, а показ фоновых звуков выключен, и ничего не отображается. Включите «{0}» на вкладке «Настройки» или попробуйте выключить и снова включить визуализатор.",
+ "notice.stopped.title": "Визуализатор звука выключен",
+ "notice.stopped.no_device": "Не удалось открыть ни одно устройство воспроизведения, поэтому захватить звук невозможно. Подключите колонки или наушники либо выберите устройство воспроизведения по умолчанию в параметрах звука Windows, а затем запустите снова.",
+ "notice.stopped.capture_error": "Захват звука прервался из-за ошибки, поэтому графика отключена.",
+ "notice.dismiss": "Скрыть",
+
+ "tray.open": "Открыть лаунчер",
+ "tray.exit": "Выход",
+
+ "more_info.title": "Репозиторий GitHub и участие в проекте",
+ "more_info.body": "SoundVisualizer — проект с открытым исходным кодом, и его код опубликован на GitHub. Вы можете принять участие в проекте: сообщать об ошибках, предлагать новые функции или помогать с разработкой. Нажмите кнопку ниже, чтобы перейти в официальный репозиторий на GitHub! Там же на вкладке Releases можно проверить, вышла ли новая версия, и скачать её.",
+ "more_info.github_button": "Перейти в репозиторий GitHub",
+
+ "dialog.notice_title": "Уведомление",
+ "dialog.already_running": "Оверлей уже запущен.",
+ "dialog.reset_title": "Сброс настроек",
+ "dialog.reset_wave": "Вернуть настройки режима «Волна» к значениям по умолчанию?",
+ "dialog.reset_pad": "Вернуть настройки режима «Панель» к значениям по умолчанию?",
+ "dialog.reset_circle": "Вернуть настройки режима «Круг» к значениям по умолчанию?",
+ "dialog.reset_outline": "Вернуть настройки режима «Контур» к значениям по умолчанию?",
+ "dialog.reset_sound_types": "Вернуть настройки типов звуков к значениям по умолчанию?",
+ "dialog.reset_all_title": "Сброс всех настроек",
+ "dialog.reset_all": "Вы действительно хотите вернуть все настройки к значениям по умолчанию?",
+ "dialog.link_error": "Не удалось открыть ссылку: {0}",
+
+ "editor.title": "Настройки оверлея",
+ "editor.drag_tip": "Перетаскивайте мышью направляющие на экране, чтобы в реальном времени менять предельную высоту и размер центра волны оверлея!",
+ "editor.glow": "Эффект сияния",
+ "editor.glow_strength": "Сила сияния",
+ "editor.color": "Цвет",
+ "editor.hotkeys": "Настройка горячих клавиш",
+ "editor.developer_mode_desc": "Показывает, что услышал ИИ (слева вверху), и FPS (справа вверху)",
+ "editor.save_close_hint": "— сохранить и закрыть",
+ "editor.close_overlay": "Закрыть оверлей",
+ "editor.guide.limit": "{0}: граница",
+ "editor.guide.size": "{0}: размер {1} %",
+ "editor.guide.base_radius": "Базовый радиус: {0}",
+ "editor.guide.circle_size": "Размер круга: {0} %",
+
+ "hud.visual_mode": "Режим отображения: [{0}] {1}",
+ "hud.sound_mode": "Звуковой режим: [{0}] {1}",
+ "hud.overlay_settings": "Настройки оверлея: [{0}]",
+ "hud.status.ready": "Система готова",
+ "hud.status.stereo": "Звук ПК: {0} кан. (стерео)\nИсточник: ожидание...",
+ "hud.status.surround": "Звук ПК: 8 кан. (7.1)\nИсточник: ожидание...",
+
+ "color_picker.title": "Выбор цвета",
+ "color_picker.hex": "HEX-код",
+ "color_picker.cancel": "Отмена",
+ "color_picker.apply": "Применить",
+ "color_picker.area": "Насыщенность и яркость",
+ "color_picker.area_value": "Насыщенность {0} %, яркость {1} %",
+ "color_picker.hue": "Оттенок",
+ "color_picker.keys_help": "Меняйте значение клавишами со стрелками. С нажатой клавишей Shift шаг больше.",
+ "color_picker.presets": "Основные цвета",
+ "color_picker.preset.white": "Белый",
+ "color_picker.preset.red": "Красный",
+ "color_picker.preset.orange": "Оранжевый",
+ "color_picker.preset.yellow": "Жёлтый",
+ "color_picker.preset.green": "Зелёный",
+ "color_picker.preset.cyan": "Голубой",
+ "color_picker.preset.blue": "Синий",
+ "color_picker.preset.magenta": "Пурпурный",
+
+ "audio.error.no_device": "Устройство вывода не найдено.",
+ "audio.error.capture_stopped": "Захват звука неожиданно остановился: {0}",
+ "audio.error.capture_start_failed": "Не удалось запустить движок захвата звука: {0}",
+
+ "tutorial.window_title": "Обучение",
+ "tutorial.skip": "Пропустить",
+ "tutorial.previous": "Предыдущая страница",
+ "tutorial.next": "Далее",
+ "tutorial.done": "Понятно",
+ "tutorial.page_status": "Страница {0} из {1}",
+ "tutorial.pause": "Приостановить анимацию",
+ "tutorial.play": "Воспроизвести анимацию",
+ "tutorial.sound_on": "Включить звук",
+ "tutorial.sound_off": "Выключить звук",
+ "tutorial.sound.title": "Показываем звук на экране",
+ "tutorial.sound.body": "Звук из игры или видео рисуется по краям экрана. Чем громче звук, тем крупнее рисунок. Показывается только звук, который воспроизводится на этом ПК. Приложение не слышит звуки вокруг вас.",
+ "tutorial.direction.title": "Показывает, откуда идёт звук",
+ "tutorial.direction.body": "При двухканальном (стерео) выводе звук слева рисуется крупнее у левого края экрана, а звук справа — у правого. При выводе звука 5.1 или 7.1 приложение различает и звуки спереди и сзади: звук спереди рисуется вверху экрана, а звук сзади — внизу.",
+ "tutorial.types.title": "У каждого типа звука свой цвет",
+ "tutorial.types.body": "ИИ делит звуки на три типа и рисует каждый своим цветом. Цвета можно изменить на вкладке «Настройки».",
+ "tutorial.type.ambient_examples": "Музыка, дождь и всё остальное",
+ "tutorial.type.speech_examples": "Голоса людей",
+ "tutorial.type.danger_examples": "Выстрелы, взрывы, сирены",
+ "tutorial.start.title": "Как включить и выключить",
+ "tutorial.start.hotkeys": "Во время работы нажмите {0}, чтобы переключить звуковой режим, {1} — чтобы переключить режим отображения, и {2} — чтобы открыть настройки оверлея."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/th.json b/SoundVisualizer.Core/Localization/Strings/th.json
new file mode 100644
index 0000000..79908cd
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/th.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "ภาษา",
+ "common.reset_defaults": "รีเซ็ตเป็นค่าเริ่มต้น",
+ "common.minimize": "ย่อเล็กสุด",
+ "common.close": "ปิด",
+ "language.system": "ค่าเริ่มต้นของระบบ",
+
+ "launcher.window_title": "ตัวเปิดใช้ SoundVisualizer",
+ "launcher.tab.home": "หน้าแรก",
+ "launcher.tab.settings": "การตั้งค่า",
+ "launcher.tab.sounds": "เสียง",
+ "launcher.tab.help": "ความช่วยเหลือ",
+ "launcher.tab.more_info": "ข้อมูลเพิ่มเติม",
+ "launcher.home.desc": "เปลี่ยนเสียงที่มองไม่เห็นให้เป็นภาพบนหน้าจอ\nตั้งแต่เล่นเกมไปจนถึงดูภาพยนตร์ มาสัมผัสประสบการณ์ใหม่กันเลย",
+ "launcher.home.start": "เริ่ม",
+ "launcher.home.stop": "หยุด",
+ "launcher.home.tutorial": "ดูบทแนะนำ",
+ "launcher.status.running": "สถานะ: กำลังทำงาน",
+ "launcher.status.idle": "สถานะ: พร้อมใช้งาน",
+
+ "settings.section.mode": "การตั้งค่าโหมด",
+ "settings.visual_mode.title": "โหมดการแสดงผล",
+ "settings.visual_mode.desc": "เลือกรูปแบบพื้นฐานของกราฟิกที่จะวาดบนหน้าจอ",
+ "settings.mode.wave": "คลื่น",
+ "settings.mode.pad": "แพด",
+ "settings.mode.circle": "วงกลม",
+ "settings.mode.outline": "เส้นขอบ",
+ "settings.sound_mode.title": "โหมดเสียง",
+ "settings.sound_mode.desc": "เลือกจำนวนแชนแนลเสียงให้ตรงกับลำโพงของคุณ\nหากใช้ลำโพง 2 แชนแนล ให้เลือก “2 แชนแนล” หากใช้ลำโพงเซอร์ราวด์ ให้เลือก “5.1 แชนแนล” หรือ “7.1 แชนแนล”",
+ "settings.channels.2": "2 แชนแนล",
+ "settings.channels.5_1": "5.1 แชนแนล",
+ "settings.channels.7_1": "7.1 แชนแนล",
+ "settings.max_fps.title": "อัตราเฟรมสูงสุด",
+ "settings.max_fps.desc": "ตั้งค่าอัตราเฟรมสูงสุดที่ใช้วาดกราฟิก",
+ "settings.section.mode_detail": "การตั้งค่าแต่ละโหมด",
+ "settings.mode_card.wave": "โหมดคลื่น",
+ "settings.mode_card.pad": "โหมดแพด",
+ "settings.mode_card.circle": "โหมดวงกลม",
+ "settings.mode_card.outline": "โหมดเส้นขอบ",
+ "settings.size_lock.title": "ล็อกขนาด",
+ "settings.size_lock.desc": "ให้กราฟิกมีขนาดคงที่ และแสดงความดังของเสียงเป็นความเข้มแทน",
+ "settings.fixed_size.wave": "ขนาดคลื่น",
+ "settings.fixed_size.pad": "ขนาดแพด",
+ "settings.fixed_size.circle": "ขนาดวงกลม",
+ "settings.fixed_size.outline": "ความหนาเส้นขอบ",
+ "settings.fixed_size.desc": "ตั้งขนาดคงที่ที่กราฟิกจะคงไว้ขณะที่ความเข้มเปลี่ยนไป",
+ "settings.max_opacity.title": "ความเข้มสูงสุด",
+ "settings.max_opacity.desc": "ความเข้มของกราฟิกเมื่อเสียงดังที่สุด",
+ "settings.size.title": "ขนาด",
+ "settings.size.desc.wave": "ปรับความสูงของคลื่นและความเต็มของภาพโดยรวม",
+ "settings.size.desc.pad": "ปรับความหนาและความกว้างของแพดเมื่อตอบสนองต่อเสียง",
+ "settings.size.desc.circle": "ปรับว่าอีควอไลเซอร์วงกลมจะพองออกมากแค่ไหนเมื่อมีเสียง",
+ "settings.size.desc.outline": "ปรับความสูงของคลื่นเส้นขอบและความเต็มของภาพโดยรวม",
+ "settings.opacity.title": "ความเข้ม",
+ "settings.opacity.desc.wave": "ค่ายิ่งสูงจะยิ่งดูเข้มชัด ค่ายิ่งต่ำจะยิ่งดูจางและโปร่งใส",
+ "settings.opacity.desc.pad": "ค่ายิ่งสูงจะยิ่งดูเข้มชัด ค่ายิ่งต่ำจะยิ่งดูจางและโปร่งใส",
+ "settings.opacity.desc.circle": "ค่ายิ่งสูงจะยิ่งดูเข้มชัด ค่ายิ่งต่ำจะยิ่งดูจางและโปร่งใส",
+ "settings.opacity.desc.outline": "ค่ายิ่งสูงจะยิ่งดูเข้มชัด ค่ายิ่งต่ำจะยิ่งดูจางและโปร่งใส",
+ "settings.speed.title": "ความเร็ว",
+ "settings.speed.desc.wave": "ปรับความเร็วที่คลื่นไหลและกระเพื่อม",
+ "settings.speed.desc.pad": "ปรับความเร็วที่แพดหันไปทางทิศที่เสียงมา",
+ "settings.speed.desc.circle": "ปรับความเร็วที่อีควอไลเซอร์วงกลมกระเพื่อมและหมุนตามเสียง",
+ "settings.speed.desc.outline": "ปรับความเร็วที่คลื่นเส้นขอบไหลและกระเพื่อม",
+ "settings.sensitivity.title": "ความไว",
+ "settings.sensitivity.desc.wave": "ปรับว่าคลื่นจะตอบสนองและซัดขึ้นไวแค่ไหน แม้กับเสียงเบา",
+ "settings.sensitivity.desc.pad": "ปรับให้แพดจับเสียงได้ดีขึ้นและเด่นชัดขึ้น",
+ "settings.sensitivity.desc.circle": "ปรับว่าแอมพลิจูดของวงกลมจะแบ่งย่อยละเอียดแค่ไหนเมื่อตอบสนองต่อเสียง",
+ "settings.sensitivity.desc.outline": "ปรับว่าคลื่นเส้นขอบจะตอบสนองและซัดขึ้นไวแค่ไหน แม้กับเสียงเบา",
+ "settings.glow.title": "แสงเรือง",
+ "settings.glow.desc.wave": "เพิ่มแสงเรืองแบบนีออนที่นุ่มนวลตามขอบคลื่น และปรับระดับความแรง\nหมายเหตุ: ใช้ทรัพยากร GPU เพิ่มขึ้น",
+ "settings.glow.desc.pad": "เพิ่มแสงเรืองนุ่มนวลคล้ายออร่ารอบแพด\nหมายเหตุ: ใช้ทรัพยากร GPU เพิ่มขึ้น",
+ "settings.glow.desc.circle": "เพิ่มวงแสงระยิบระยับตามขอบของอีควอไลเซอร์วงกลม\nหมายเหตุ: ใช้ทรัพยากร GPU เพิ่มขึ้น",
+ "settings.glow.desc.outline": "เพิ่มแสงเรืองแบบนีออนที่นุ่มนวลให้เส้นขอบ และปรับระดับความแรง\nหมายเหตุ: ใช้ทรัพยากร GPU เพิ่มขึ้น",
+ "settings.radius.title": "รัศมี",
+ "settings.radius.desc": "ปรับรัศมีของวงแหวนตรงกลางในโหมดวงกลม",
+ "settings.section.sound_types": "ประเภทเสียง (AI)",
+ "settings.show_ambient": "แสดงเสียงแวดล้อม",
+ "settings.show_speech": "แสดงเสียงพูด",
+ "settings.show_danger": "แสดงเสียงอันตราย",
+ "settings.set_color": "ตั้งค่าสี",
+ "settings.set_color_for": "ตั้งค่าสี: {0}",
+ "settings.section.hotkeys": "คีย์ลัด",
+ "settings.hotkey.visual.title": "สลับโหมดการแสดงผล",
+ "settings.hotkey.visual.desc": "คีย์ลัดสำหรับเปลี่ยนรูปแบบกราฟิกแบบเรียลไทม์ขณะทำงาน",
+ "settings.hotkey.sound.title": "สลับโหมดเสียง",
+ "settings.hotkey.sound.desc": "คีย์ลัดสำหรับเปลี่ยนโหมดเสียงแบบเรียลไทม์ขณะทำงาน",
+ "settings.hotkey.edit.title": "การตั้งค่าโอเวอร์เลย์",
+ "settings.hotkey.edit.desc": "คีย์ลัดสำหรับเปิดหรือปิดหน้าแก้ไขโอเวอร์เลย์",
+ "settings.hotkey.set": "ตั้งคีย์ลัด",
+ "settings.section.advanced": "การตั้งค่าขั้นสูง",
+ "settings.developer_mode.title": "โหมดนักพัฒนา",
+ "settings.developer_mode.desc": "แสดงสิ่งที่ AI ได้ยิน (ชื่อเสียงที่ AI รายงาน ความมั่นใจ และเวลาที่ใช้) ที่มุมซ้ายบนของโอเวอร์เลย์ และแสดง FPS ที่มุมขวาบน ข้อมูลที่แสดงเป็นภาษาอังกฤษและไม่ได้มีไว้สำหรับการใช้งานทั่วไป",
+ "settings.developer_record.title": "บันทึกผลลัพธ์ลงไฟล์",
+ "settings.developer_record.desc": "บันทึกผลลัพธ์ทุกครั้งของ AI (ชื่อเสียงที่ AI ให้คะแนนสูงสุด ความมั่นใจ และเวลาที่ใช้) ลงในไฟล์ CSV ใหม่ในโฟลเดอร์ ai-log ข้าง SoundVisualizer.exe เพื่อให้คุณย้อนดูได้ว่า AI ได้ยินอะไร หากเขียนลงโฟลเดอร์นั้นไม่ได้ ไฟล์จะถูกบันทึกไว้ที่ %LOCALAPPDATA%\\SoundVisualizer\\ai-log แทน ไฟล์อาจมีขนาดใหญ่ เมื่อดูเสร็จแล้วโปรดปิด",
+ "settings.auto_minimize.title": "ย่อตัวเปิดใช้เมื่อเริ่ม",
+ "settings.auto_minimize.desc": "ย่อหน้าต่างตัวเปิดใช้นี้โดยอัตโนมัติเมื่อโอเวอร์เลย์เริ่มทำงาน",
+
+ "sounds.title": "เลือกประเภทเสียง",
+ "sounds.desc": "เสียงที่ AI รู้จักจะจัดกลุ่มรวมกับเสียงที่คล้ายกัน และแสดงตามประเภทเริ่มต้นของแต่ละเสียง หากเปลี่ยนประเภทของกลุ่ม สีของเสียงในกลุ่มจะเปลี่ยนตามประเภทใหม่ทันที",
+ "sounds.sibling_note": "AI มักได้ยินเสียงเดียวเป็นหลายชื่อพร้อมกัน เช่น ไซเรนยังได้ยินเป็น “สัญญาณเตือนภัย” ด้วย ซึ่งอยู่ในอีกกลุ่มหนึ่ง หากเปลี่ยนแล้วเสียงยังแสดงเป็นประเภทเดิม ให้เปลี่ยนกลุ่มหรือเสียงที่เกี่ยวข้องด้วย",
+ "sounds.advanced.title": "โหมดขั้นสูง",
+ "sounds.advanced.desc": "แสดงทุกเสียงตามกลุ่ม เพื่อให้คุณเปลี่ยนเสียงได้ทีละเสียง",
+ "sounds.search.hint": "ค้นหาเสียง",
+ "sounds.search.clear": "ล้างคำค้นหา",
+ "sounds.filter.title": "ตัวกรอง",
+ "sounds.filter.all": "ทั้งหมด",
+ "sounds.filter.changed": "เปลี่ยนแล้ว",
+ "sounds.type.ambient": "เสียงแวดล้อม",
+ "sounds.type.speech": "เสียงพูด",
+ "sounds.type.danger": "เสียงอันตราย",
+ "sounds.type.default": "{0} (ค่าเริ่มต้น)",
+ "sounds.type.mixed": "ผสม",
+ "sounds.changed_count": "เปลี่ยนแล้ว {0} เสียง",
+ "sounds.changed_from": "ค่าเริ่มต้น: {0}",
+ "sounds.reset.button": "รีเซ็ตทั้งหมด",
+ "sounds.reset.title": "รีเซ็ตเสียงทั้งหมดหรือไม่",
+ "sounds.reset.message": "เสียงทุกเสียงที่คุณเปลี่ยนไว้จะกลับไปเป็นประเภทเริ่มต้น",
+ "sounds.reset.confirm": "รีเซ็ต",
+ "sounds.reset.cancel": "ยกเลิก",
+ "sounds.empty": "ไม่พบเสียงที่ตรงกัน",
+ "sounds.group.mixed_note": "มีบางเสียงที่เปลี่ยนทีละเสียง",
+ "sounds.group.changed_line": "ตอนนี้: {0} · ค่าเริ่มต้น: {1}",
+ "sounds.overwrite.title": "เปลี่ยนทั้งกลุ่มหรือไม่",
+ "sounds.overwrite.message": "เสียง {1} เสียงในกลุ่ม “{0}” จะเปลี่ยนเป็น “{2}” เสียงที่คุณเปลี่ยนทีละเสียงไว้จะถูกเขียนทับด้วย",
+ "sounds.overwrite.confirm": "เปลี่ยน",
+ "sounds.overwrite.cancel": "ยกเลิก",
+ "sounds.basic_warning.title": "เปลี่ยนเป็นโหมดพื้นฐานหรือไม่",
+ "sounds.basic_warning.message": "บางกลุ่มมีเสียงที่คุณเปลี่ยนทีละเสียงไว้ ในโหมดพื้นฐาน หากเลือกประเภทให้กลุ่มเหล่านี้ เสียงทั้งหมดในกลุ่มจะเป็นประเภทนั้น และสิ่งที่เปลี่ยนทีละเสียงไว้ในกลุ่มนั้นจะถูกเขียนทับ จะไม่มีอะไรเปลี่ยนจนกว่าคุณจะเลือก",
+ "sounds.basic_warning.confirm": "ใช้โหมดพื้นฐาน",
+ "sounds.basic_warning.cancel": "อยู่ในโหมดขั้นสูงต่อ",
+ "sounds.cd.sound": "{0}, ประเภท: {1}",
+ "sounds.cd.sound_changed": "{0}, ประเภท: {1}, ค่าเริ่มต้น: {2}",
+ "sounds.cd.group": "กลุ่ม {0}, ประเภท: {1}, จำนวนเสียง: {2} ({3})",
+ "sounds.cd.group_changed": "กลุ่ม {0}, ประเภท: {1}, ค่าเริ่มต้น: {2}, จำนวนเสียง: {3} ({4})",
+ "sounds.cd.group_header": "กลุ่ม {0}, จำนวนเสียง: {1}",
+
+ "hotkey.prompt": "กดคีย์ (Esc เพื่อยกเลิก)",
+ "hotkey.none": "ไม่มี",
+ "hotkey.button_name": "{0}, คีย์ลัด: {1}",
+
+ "help.start.title": "เริ่มต้นใช้งาน",
+ "help.start.1": "เลือกภาษาที่ด้านบนของหน้าต่างนี้ และหากต้องการ ให้เปลี่ยนรูปแบบกราฟิกในแท็บการตั้งค่า",
+ "help.start.2": "ในแท็บหน้าแรก ให้คลิก “{0}”",
+ "help.start.3": "กราฟิกจะวาดตามขอบหน้าจอ ซ้อนอยู่บนหน้าต่างอื่นๆ เช่น เกมและวิดีโอ การคลิกเมาส์จะผ่านกราฟิกไป คุณจึงใช้หน้าต่างด้านล่างได้ตามปกติ",
+ "help.start.4": "หากต้องการปิด ให้คลิก “{0}” ในแท็บหน้าแรก หรือคลิกขวาที่ไอคอน SoundVisualizer ในพื้นที่แจ้งเตือนของแถบงาน แล้วเลือก “{0}”",
+ "help.modes.title": "โหมดการแสดงผล",
+ "help.mode.wave": "ขอบหน้าจอทั้งหมดจะดันเข้าด้านในตามความดังของเสียง และด้านที่เสียงมาจะดันเข้าไปลึกกว่า",
+ "help.mode.pad": "แถบจะสว่างขึ้นที่ 8 จุดรอบขอบหน้าจอ แสดงทิศทางได้ชัดเจนที่สุด",
+ "help.mode.circle": "วงแหวนตรงกลางหน้าจอจะพองออกไปทางทิศทางที่เสียงมา",
+ "help.mode.outline": "วาดเฉพาะเส้นขอบบางของคลื่น จึงบังหน้าจอเกมน้อยกว่า",
+ "help.modes.note": "การตั้งค่าต่างๆ เช่น ขนาดและความเข้ม จะบันทึกแยกกันในแต่ละโหมด และเปลี่ยนได้ในแท็บการตั้งค่า",
+ "help.sound.title": "ประเภทเสียง",
+ "help.sound.kinds": "AI จะแยกเสียงที่ได้ยินออกเป็นเสียงแวดล้อม เสียงพูด และเสียงอันตราย สีเริ่มต้นคือ เสียงแวดล้อมสีขาว เสียงพูดสีเหลือง และเสียงอันตรายสีแดง ในแท็บการตั้งค่า คุณสามารถเปลี่ยนสีของแต่ละประเภทและเลือกได้ว่าจะแสดงหรือไม่",
+ "help.sound.groups": "ในแท็บเสียง เสียงที่คล้ายกันจะอยู่รวมเป็นกลุ่ม และคุณเลือกได้ว่าแต่ละกลุ่มจะนับเป็นประเภทใด เช่น หากเปลี่ยนกลุ่ม “กริ่งประตู” เป็นเสียงอันตราย เมื่อ AI ได้ยินเป็นกริ่งประตูหรือเคาะประตู เสียงนั้นจะใช้สีของเสียงอันตราย อย่างไรก็ตาม เสียงที่คุณตั้งเป็นเสียงอันตรายเองจะเข้าร่วมแค่การโหวตของ AI เพื่อเลือกประเภทเท่านั้น จึงไม่ได้หมายความว่ากริ่งประตูจะแสดงเป็นเสียงอันตรายทุกครั้ง",
+ "help.sound.advanced": "หากต้องการเปลี่ยนเสียงทีละเสียง ให้เปิดโหมดขั้นสูง",
+ "help.sound.danger": "เมื่อตรวจพบเสียงอันตราย (เช่น เสียงปืน) สีจะเปลี่ยนทันทีแทนที่จะค่อยๆ เปลี่ยน",
+ "help.pc.title": "การตั้งค่า PC และปุ่มลัด",
+ "help.sound_mode.title": "โหมดเสียง",
+ "help.sound_mode.body": "หากต้องการแยกเสียงด้านหน้าและด้านหลังได้ด้วย นอกเหนือจากซ้ายและขวา ให้ตั้งค่าอุปกรณ์เอาต์พุตในการตั้งค่าเสียงของ Windows เป็นเซอร์ราวด์ 5.1 (6 แชนแนล) หรือเซอร์ราวด์ 7.1 (8 แชนแนล) หากใช้สเตอริโอทั่วไป (2 แชนแนล) จะแยกซ้ายกับขวาได้ แต่แยกด้านหน้ากับด้านหลังไม่ได้ “โหมดเสียง” ในแท็บการตั้งค่าจะปรับให้ตรงกับอุปกรณ์เอาต์พุตเมื่อเริ่มทำงานหรือเมื่ออุปกรณ์เปลี่ยน และคุณยังเปลี่ยนเองได้",
+ "help.sound_mode.tip": "💡 หากไม่มีอุปกรณ์เอาต์พุต 7.1 คุณสามารถติดตั้ง{0} แล้วตั้งค่าอุปกรณ์เอาต์พุตเป็น 7.1 แชนแนล เพื่อแสดงโอเวอร์เลย์ด้วยเสียงเซอร์ราวด์ 7.1",
+ "help.sound_mode.tip_link": "ไดรเวอร์เสมือน VB-CABLE",
+ "help.hotkeys.title": "คีย์ลัดที่กำหนดเองได้แบบเรียลไทม์",
+ "help.hotkeys.body": "ในแท็บการตั้งค่า ให้คลิกปุ่มคีย์ลัด แล้วกดคีย์เดียวหรือคีย์ผสม (เช่น Ctrl + Shift + A) เพื่อเปลี่ยนคีย์ลัดได้ตามต้องการ เมื่อกดคีย์ลัด โหมดจะสลับทันทีแม้แอปจะทำงานอยู่เบื้องหลัง",
+ "help.hotkeys.defaults": "ค่าเริ่มต้นคือ F2 สลับโหมดเสียง F3 สลับโหมดการแสดงผล และ F4 เปิดการตั้งค่าโอเวอร์เลย์",
+ "help.size_lock.title": "โหมดล็อกขนาด",
+ "help.size_lock.body": "โหมดล็อกขนาดจะคงขนาดของกราฟิกไว้ และใช้ความเข้มที่แปรผันตามความดังของเสียงแทน เหมาะเมื่อคุณต้องการให้กราฟิกมีขนาดเท่าเดิม แต่ยังเห็นการตอบสนองต่อเสียง",
+ "help.editor.title": "โหมดแก้ไขโอเวอร์เลย์แบบเรียลไทม์",
+ "help.editor.body": "กดคีย์ลัดที่กำหนดไว้ (ค่าเริ่มต้นคือ F4) เพื่อเปิดโหมดแก้ไขโอเวอร์เลย์ คุณสามารถใช้เมาส์ลากบนหน้าจอเพื่อปรับขีดจำกัดขนาดของกราฟิก และปรับรายละเอียดในแผงการตั้งค่าที่แสดงขึ้นมา",
+ "help.developer.title": "โหมดนักพัฒนา",
+ "help.developer.body": "เมื่อเปิดโหมดนักพัฒนา มุมซ้ายบนของโอเวอร์เลย์จะแสดงสิ่งที่ AI ได้ยิน (ชื่อเสียง ความมั่นใจ และเวลาที่ใช้) และมุมขวาบนจะแสดง FPS หากเปิด “{0}” ด้วย ผลลัพธ์ทุกรายการจะถูกบันทึกลงไฟล์ CSV เหมาะเมื่อคุณต้องการตรวจสอบว่าระบบทำงานได้ตามปกติหรือไม่",
+ "help.notes.title": "ข้อควรทราบ",
+ "help.note.direction_surround": "เมื่อส่งเสียงออกแบบ 5.1 หรือ 7.1 แชนแนล ด้านบนของหน้าจอแสดงเสียงจากด้านหน้า และด้านล่างแสดงเสียงจากด้านหลัง ด้านซ้ายและขวาแสดงเสียงจากทางซ้ายและทางขวา ส่วนกราฟิกที่ปรากฏจากทุกด้านของหน้าจอพร้อมกันคือเสียงแวดล้อมหรือเสียงที่ไม่มีทิศทาง",
+ "help.note.direction_stereo": "เมื่อส่งเสียงออกแบบ 2 แชนแนล (สเตอริโอ) แอปแยกเสียงซ้ายกับขวาได้ แต่แยกด้านหน้ากับด้านหลังไม่ได้ ในกรณีนี้ กราฟิกที่ด้านบนและด้านล่างของหน้าจอแสดงถึงเสียงที่ได้ยินเท่าๆ กันทั้งสองข้าง รวมถึงมิติเชิงพื้นที่",
+ "help.note.sources": "แอปจะแสดงเสียงของทุกแอปที่เล่นผ่านอุปกรณ์เล่นเสียงเริ่มต้นของ Windows เสียงที่แอปส่งไปยังอุปกรณ์อื่น เช่น หูฟังที่ใช้สำหรับแชทเสียงเท่านั้น จะไม่แสดง",
+ "help.note.volume": "หากลดระดับเสียงของแอปใดลงมากหรือปิดเสียงไว้ ไม่ว่าจะในแอปนั้นเองหรือในตัวผสมเสียงของ Windows เสียงของแอปนั้นอาจแทบไม่แสดงหรือไม่แสดงเลย หากกราฟิกตอบสนองไม่ดี ให้เพิ่มเสียงนั้นขึ้นเล็กน้อย หรือเพิ่มความไวในแท็บการตั้งค่า",
+ "help.note.ai": "AI อาจจำแนกเสียงผิดพลาดได้ โปรดอย่าพึ่งพาแอปนี้เพียงอย่างเดียวในสถานการณ์ที่เกี่ยวข้องกับความปลอดภัย",
+ "help.faq.title": "คำถามที่พบบ่อย",
+ "help.faq.stopped_q": "การแสดงภาพเสียงถูกปิดเองโดยที่ฉันไม่ได้สั่งหยุด",
+ "help.faq.stopped_a": "แอปจะปิดเมื่อการจับเสียงหยุดลงและไม่กลับมาทำงานภายในไม่กี่วินาที เช่น เมื่ออุปกรณ์เล่นเสียงถูกถอดออกและไม่มีอุปกรณ์อื่นให้ใช้ แท็บหน้าแรกจะแสดงสาเหตุ โปรดเชื่อมต่อลำโพงหรือหูฟัง หรือตั้งค่าอุปกรณ์เล่นเสียงเริ่มต้นในการตั้งค่าเสียงของ Windows แล้วเริ่มอีกครั้ง",
+ "help.faq.no_graphic_q": "ไม่เห็นกราฟิกเลย",
+ "help.faq.no_graphic_a": "โปรดตรวจสอบว่าเสียงเล่นผ่านอุปกรณ์เล่นเสียงเริ่มต้นของ Windows และไม่ได้ปิดเสียงไว้ รวมถึงดูว่าแท็บหน้าแรกแสดงข้อความแจ้งหรือไม่ หากเกมทำงานในโหมดเต็มหน้าจอแบบเอกสิทธิ์ ให้ลองใช้โหมดหน้าต่างแบบไม่มีขอบหรือโหมดหน้าต่าง นอกจากนี้ โปรดตรวจสอบในแท็บการตั้งค่าด้วยว่าไม่ได้ตั้งความเข้มไว้ต่ำเกินไป รวมถึงไม่ได้ปิดการแสดงเสียงประเภทที่กำลังดังอยู่",
+ "help.faq.performance_q": "ใช้ CPU หรือ GPU มากไหม",
+ "help.faq.performance_a": "หากต้องการให้เบาลง ให้ลด “{0}” ในแท็บการตั้งค่า หรือปิด “{1}”",
+ "help.faq.language_q": "เปลี่ยนภาษาของแอปได้อย่างไร",
+ "help.faq.language_a": "เลือกจากรายการ “{0}” ที่ด้านบนของหน้าต่างนี้ ขณะทำงานอยู่ คุณยังเปลี่ยนได้ในการตั้งค่าโอเวอร์เลย์ หากเลือก “{1}” แอปจะใช้ภาษาที่แสดงของ Windows",
+ "help.report.title": "รายงานปัญหา",
+ "help.report.desc": "แจ้งให้เราทราบเกี่ยวกับข้อบกพร่อง หรือเสียงที่ถูกแสดงผิดประเภท GitHub จะเปิดขึ้นในเบราว์เซอร์ของคุณ โดยกรอกข้อมูลระบบด้านล่างไว้ให้แล้ว จึงต้องมีบัญชี GitHub หากไม่มีบัญชี โปรดส่งอีเมลถึงเราแทน จะไม่มีการส่งข้อมูลเสียงใดๆ ออกไปเลย",
+ "help.report.github": "รายงานผ่าน GitHub",
+ "help.report.mail": "รายงานทางอีเมล",
+ "help.report.copy": "คัดลอกข้อมูลระบบ",
+ "help.report.copied": "คัดลอกข้อมูลระบบแล้ว",
+ "help.report.copy_failed": "คัดลอกไปยังคลิปบอร์ดไม่สำเร็จ โปรดลองอีกครั้ง",
+ "help.report.no_browser": "เปิดเบราว์เซอร์ไม่ได้ โปรดคัดลอกข้อมูลระบบแล้วส่งถึงเราแทน",
+ "help.report.no_mail": "ไม่มีแอปอีเมลสำหรับส่ง โปรดคัดลอกข้อมูลระบบแล้วส่งด้วยวิธีอื่น",
+ "help.report.issue_title": "รายงานจากแอป",
+ "help.report.issue_body": "เกิดอะไรขึ้น\n\nคุณกำลังฟังอะไรอยู่ (แอปใด เสียงประเภทใด)\n\nคุณคาดว่าควรจะเป็นอย่างไร",
+ "help.about.title": "เกี่ยวกับแอป",
+ "help.about.version": "เวอร์ชัน {0}",
+ "help.about.license": "แอปนี้เผยแพร่ภายใต้ GNU Affero General Public License v3.0 (AGPL-3.0) ชื่อและกลุ่มของเสียงในแท็บเสียงอิงจาก AudioSet Ontology และเผยแพร่ภายใต้ CC BY-SA 4.0 (ดู “ใบอนุญาตโอเพนซอร์ส” ด้านล่าง)",
+ "help.licenses.button": "ใบอนุญาตโอเพนซอร์ส",
+ "help.licenses.title": "ใบอนุญาตโอเพนซอร์ส",
+ "help.licenses.close": "ปิด",
+
+ "notice.ai_unavailable": "ระบบแยกประเภทเสียงไม่ทำงาน จึงแสดงทุกเสียงด้วยสีของเสียงแวดล้อม ลองปิดแล้วเปิดการแสดงภาพเสียงอีกครั้ง",
+ "notice.ai_unavailable_hidden": "ระบบแยกประเภทเสียงไม่ทำงาน จึงนับทุกเสียงเป็นเสียงแวดล้อม แต่การแสดงเสียงแวดล้อมถูกปิดอยู่ จึงไม่มีอะไรแสดง โปรดเปิด “{0}” ในแท็บการตั้งค่า หรือลองปิดแล้วเปิดการแสดงภาพเสียงอีกครั้ง",
+ "notice.stopped.title": "การแสดงภาพเสียงถูกปิดแล้ว",
+ "notice.stopped.no_device": "เปิดอุปกรณ์เล่นเสียงไม่ได้ จึงจับเสียงไม่ได้ โปรดเชื่อมต่อลำโพงหรือหูฟัง หรือตั้งค่าอุปกรณ์เล่นเสียงเริ่มต้นในการตั้งค่าเสียงของ Windows แล้วเริ่มอีกครั้ง",
+ "notice.stopped.capture_error": "การจับเสียงหยุดลงเพราะเกิดข้อผิดพลาด จึงปิดกราฟิก",
+ "notice.dismiss": "ปิดข้อความแจ้ง",
+
+ "tray.open": "แสดงตัวเปิดใช้",
+ "tray.exit": "ออก",
+
+ "more_info.title": "ที่เก็บโค้ดบน GitHub และการมีส่วนร่วม",
+ "more_info.body": "SoundVisualizer เป็นโปรเจกต์โอเพนซอร์ส และเปิดเผยซอร์สโค้ดไว้บน GitHub คุณสามารถมีส่วนร่วมได้ด้วยการรายงานข้อบกพร่อง เสนอฟีเจอร์ หรือร่วมพัฒนา คลิกปุ่มด้านล่างเพื่อไปยังที่เก็บโค้ดทางการบน GitHub ได้เลย นอกจากนี้ คุณยังตรวจสอบและดาวน์โหลดเวอร์ชันล่าสุดได้ที่แท็บ Releases บน GitHub",
+ "more_info.github_button": "เปิดที่เก็บโค้ดบน GitHub",
+
+ "dialog.notice_title": "ข้อความแจ้ง",
+ "dialog.already_running": "โอเวอร์เลย์กำลังทำงานอยู่แล้ว",
+ "dialog.reset_title": "รีเซ็ตการตั้งค่า",
+ "dialog.reset_wave": "รีเซ็ตการตั้งค่าโหมดคลื่นเป็นค่าเริ่มต้นหรือไม่",
+ "dialog.reset_pad": "รีเซ็ตการตั้งค่าโหมดแพดเป็นค่าเริ่มต้นหรือไม่",
+ "dialog.reset_circle": "รีเซ็ตการตั้งค่าโหมดวงกลมเป็นค่าเริ่มต้นหรือไม่",
+ "dialog.reset_outline": "รีเซ็ตการตั้งค่าโหมดเส้นขอบเป็นค่าเริ่มต้นหรือไม่",
+ "dialog.reset_sound_types": "รีเซ็ตการตั้งค่าประเภทเสียงเป็นค่าเริ่มต้นหรือไม่",
+ "dialog.reset_all_title": "รีเซ็ตเป็นค่าเริ่มต้น",
+ "dialog.reset_all": "ต้องการรีเซ็ตการตั้งค่าทั้งหมดเป็นค่าเริ่มต้นใช่หรือไม่",
+ "dialog.link_error": "เปิดลิงก์ไม่ได้: {0}",
+
+ "editor.title": "การตั้งค่าโอเวอร์เลย์",
+ "editor.drag_tip": "ใช้เมาส์ลากเส้นนำบนหน้าจอเพื่อปรับขีดจำกัดความสูงและขนาดตรงกลางของคลื่นโอเวอร์เลย์ได้แบบเรียลไทม์",
+ "editor.glow": "เอฟเฟกต์แสงเรือง",
+ "editor.glow_strength": "ระดับแสงเรือง",
+ "editor.color": "สี",
+ "editor.hotkeys": "การตั้งค่าคีย์ลัด",
+ "editor.developer_mode_desc": "แสดงสิ่งที่ AI ได้ยิน (ซ้ายบน) และ FPS (ขวาบน)",
+ "editor.save_close_hint": "เพื่อบันทึกการตั้งค่าและปิด",
+ "editor.close_overlay": "ปิดโอเวอร์เลย์",
+ "editor.guide.limit": "ขีดจำกัด{0}",
+ "editor.guide.size": "ขนาด{0}: {1}%",
+ "editor.guide.base_radius": "รัศมีพื้นฐาน: {0}",
+ "editor.guide.circle_size": "ขนาดวงกลม: {0}%",
+
+ "hud.visual_mode": "โหมดการแสดงผล: [{0}] {1}",
+ "hud.sound_mode": "โหมดเสียง: [{0}] {1}",
+ "hud.overlay_settings": "การตั้งค่าโอเวอร์เลย์: [{0}]",
+ "hud.status.ready": "ระบบพร้อมแล้ว",
+ "hud.status.stereo": "เสียง PC: {0}ch (สเตอริโอ)\nแหล่งเสียง: กำลังรอ...",
+ "hud.status.surround": "เสียง PC: 8ch (7.1)\nแหล่งเสียง: กำลังรอ...",
+
+ "color_picker.title": "เลือกสี",
+ "color_picker.hex": "รหัส HEX",
+ "color_picker.cancel": "ยกเลิก",
+ "color_picker.apply": "นำไปใช้",
+ "color_picker.area": "ความอิ่มตัวและความสว่าง",
+ "color_picker.area_value": "ความอิ่มตัว {0}%, ความสว่าง {1}%",
+ "color_picker.hue": "เฉดสี",
+ "color_picker.keys_help": "ใช้ปุ่มลูกศรเพื่อเปลี่ยน กด Shift ค้างไว้เพื่อเปลี่ยนทีละมากขึ้น",
+ "color_picker.presets": "สีที่ใช้บ่อย",
+ "color_picker.preset.white": "สีขาว",
+ "color_picker.preset.red": "สีแดง",
+ "color_picker.preset.orange": "สีส้ม",
+ "color_picker.preset.yellow": "สีเหลือง",
+ "color_picker.preset.green": "สีเขียว",
+ "color_picker.preset.cyan": "สีฟ้า",
+ "color_picker.preset.blue": "สีน้ำเงิน",
+ "color_picker.preset.magenta": "สีบานเย็น",
+
+ "audio.error.no_device": "ไม่พบอุปกรณ์เอาต์พุต",
+ "audio.error.capture_stopped": "การจับเสียงหยุดลงโดยไม่คาดคิด: {0}",
+ "audio.error.capture_start_failed": "เริ่มเอนจินจับเสียงไม่ได้: {0}",
+
+ "tutorial.window_title": "บทแนะนำ",
+ "tutorial.skip": "ข้าม",
+ "tutorial.previous": "หน้าก่อนหน้า",
+ "tutorial.next": "ถัดไป",
+ "tutorial.done": "เข้าใจแล้ว",
+ "tutorial.page_status": "หน้า {0} จาก {1}",
+ "tutorial.pause": "หยุดภาพเคลื่อนไหวชั่วคราว",
+ "tutorial.play": "เล่นภาพเคลื่อนไหว",
+ "tutorial.sound_on": "เปิดเสียง",
+ "tutorial.sound_off": "ปิดเสียง",
+ "tutorial.sound.title": "มองเห็นเสียงบนหน้าจอ",
+ "tutorial.sound.body": "เสียงจากเกมหรือวิดีโอจะแสดงเป็นภาพตามขอบหน้าจอ ยิ่งเสียงดัง ภาพก็ยิ่งใหญ่ แสดงเฉพาะเสียงที่เล่นอยู่ใน PC เครื่องนี้เท่านั้น แอปจะไม่จับเสียงรอบตัวคุณ",
+ "tutorial.direction.title": "บอกได้ว่าเสียงมาจากทางไหน",
+ "tutorial.direction.body": "เมื่อส่งเสียงออกแบบ 2 แชนแนล (สเตอริโอ) หากเสียงมาจากทางซ้าย ภาพฝั่งซ้ายของหน้าจอจะใหญ่กว่า หากมาจากทางขวา ภาพฝั่งขวาจะใหญ่กว่า เมื่อส่งเสียงออกแบบ 5.1 หรือ 7.1 แชนแนล แอปจะแยกด้านหน้ากับด้านหลังได้ด้วย โดยเสียงจากด้านหน้าจะแสดงที่ด้านบนของหน้าจอ และเสียงจากด้านหลังจะแสดงที่ด้านล่าง",
+ "tutorial.types.title": "เสียงแต่ละประเภทมีสีของตัวเอง",
+ "tutorial.types.body": "AI จะแยกเสียงออกเป็น 3 ประเภท และวาดแต่ละประเภทด้วยสีของตัวเอง คุณเปลี่ยนสีได้ในแท็บการตั้งค่า",
+ "tutorial.type.ambient_examples": "เพลง เสียงฝน และเสียงอื่นทั้งหมด",
+ "tutorial.type.speech_examples": "เสียงคน",
+ "tutorial.type.danger_examples": "เสียงปืน เสียงระเบิด เสียงไซเรน",
+ "tutorial.start.title": "วิธีเปิดและปิด",
+ "tutorial.start.hotkeys": "ขณะทำงานอยู่ กด {0} เพื่อสลับโหมดเสียง กด {1} เพื่อสลับโหมดการแสดงผล และกด {2} เพื่อเปิดการตั้งค่าโอเวอร์เลย์"
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/tr.json b/SoundVisualizer.Core/Localization/Strings/tr.json
new file mode 100644
index 0000000..eeac03c
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/tr.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Dil",
+ "common.reset_defaults": "Varsayılanlara sıfırla",
+ "common.minimize": "Simge durumuna küçült",
+ "common.close": "Kapat",
+ "language.system": "Sistem varsayılanı",
+
+ "launcher.window_title": "SoundVisualizer Başlatıcısı",
+ "launcher.tab.home": "Ana sayfa",
+ "launcher.tab.settings": "Ayarlar",
+ "launcher.tab.sounds": "Sesler",
+ "launcher.tab.help": "Yardım",
+ "launcher.tab.more_info": "Daha fazla bilgi",
+ "launcher.home.desc": "Göremediğiniz sesleri doğrudan ekranınıza çizer.\nOyunlardan filmlere, yepyeni bir deneyim yaşayın.",
+ "launcher.home.start": "Başlat",
+ "launcher.home.stop": "Durdur",
+ "launcher.home.tutorial": "Tanıtımı göster",
+ "launcher.status.running": "Durum: Çalışıyor",
+ "launcher.status.idle": "Durum: Hazır",
+
+ "settings.section.mode": "Mod ayarları",
+ "settings.visual_mode.title": "Görsel mod",
+ "settings.visual_mode.desc": "Ekranda çizilecek grafiklerin temel şeklini seçin.",
+ "settings.mode.wave": "Dalga",
+ "settings.mode.pad": "Pad",
+ "settings.mode.circle": "Daire",
+ "settings.mode.outline": "Kontur",
+ "settings.sound_mode.title": "Ses modu",
+ "settings.sound_mode.desc": "Hoparlörlerinize uygun ses kanallarını seçin.\n2 kanallı hoparlörler için “2 kanal”, surround hoparlörler için “5.1 kanal” veya “7.1 kanal” seçeneğini belirleyin.",
+ "settings.channels.2": "2 kanal",
+ "settings.channels.5_1": "5.1 kanal",
+ "settings.channels.7_1": "7.1 kanal",
+ "settings.max_fps.title": "Maks. kare hızı",
+ "settings.max_fps.desc": "Grafiklerin çizileceği en yüksek kare hızını ayarlar.",
+ "settings.section.mode_detail": "Modlara özel ayarlar",
+ "settings.mode_card.wave": "Dalga modu",
+ "settings.mode_card.pad": "Pad modu",
+ "settings.mode_card.circle": "Daire modu",
+ "settings.mode_card.outline": "Kontur modu",
+ "settings.size_lock.title": "Boyutu sabitle",
+ "settings.size_lock.desc": "Grafikleri sabit bir boyutta tutar; ses yüksekliğini boyut yerine saydamlıkla gösterir.",
+ "settings.fixed_size.wave": "Dalga boyutu",
+ "settings.fixed_size.pad": "Pad boyutu",
+ "settings.fixed_size.circle": "Daire boyutu",
+ "settings.fixed_size.outline": "Kontur kalınlığı",
+ "settings.fixed_size.desc": "Saydamlık değişirken grafiklerin sabit kalacağı boyutu ayarlar.",
+ "settings.max_opacity.title": "Maks. opaklık",
+ "settings.max_opacity.desc": "Ses en yüksek olduğunda grafiklerin ulaşacağı opaklık.",
+ "settings.size.title": "Boyut",
+ "settings.size.desc.wave": "Dalganın yüksekliğini ve genel olarak ne kadar dolgun görüneceğini ayarlar.",
+ "settings.size.desc.pad": "Pad çubuklarının sese tepki verirken ne kadar kalın ve geniş olacağını ayarlar.",
+ "settings.size.desc.circle": "Ses çaldığında dairesel ekolayzerin dışa doğru ne kadar şişeceğini ayarlar.",
+ "settings.size.desc.outline": "Kontur dalgasının yüksekliğini ve genel olarak ne kadar dolgun görüneceğini ayarlar.",
+ "settings.opacity.title": "Opaklık",
+ "settings.opacity.desc.wave": "Değer yükseldikçe grafikler daha belirgin, düştükçe daha soluk ve saydam görünür.",
+ "settings.opacity.desc.pad": "Değer yükseldikçe grafikler daha belirgin, düştükçe daha soluk ve saydam görünür.",
+ "settings.opacity.desc.circle": "Değer yükseldikçe grafikler daha belirgin, düştükçe daha soluk ve saydam görünür.",
+ "settings.opacity.desc.outline": "Değer yükseldikçe grafikler daha belirgin, düştükçe daha soluk ve saydam görünür.",
+ "settings.speed.title": "Hız",
+ "settings.speed.desc.wave": "Dalganın ne kadar hızlı akıp dalgalanacağını ayarlar.",
+ "settings.speed.desc.pad": "Pad çubuklarının sesin geldiği yöne ne kadar hızlı döneceğini ayarlar.",
+ "settings.speed.desc.circle": "Dairesel ekolayzerin sese tepki olarak ne kadar hızlı dalgalanıp döneceğini ayarlar.",
+ "settings.speed.desc.outline": "Kontur dalgasının ne kadar hızlı akıp dalgalanacağını ayarlar.",
+ "settings.sensitivity.title": "Hassasiyet",
+ "settings.sensitivity.desc.wave": "Dalganın kısık seslere bile ne kadar hassas tepki verip kabaracağını ayarlar.",
+ "settings.sensitivity.desc.pad": "Pad çubuklarının sesi ne kadar iyi yakalayıp ne kadar belirgin görüneceğini ayarlar.",
+ "settings.sensitivity.desc.circle": "Dairenin genliğinin sese tepki verirken ne kadar ince parçalara bölüneceğini ayarlar.",
+ "settings.sensitivity.desc.outline": "Kontur dalgasının kısık seslere bile ne kadar hassas tepki verip kabaracağını ayarlar.",
+ "settings.glow.title": "Parıltı",
+ "settings.glow.desc.wave": "Dalganın kenarı boyunca yumuşak bir neon parıltısı ekler ve gücünü ayarlar.\nDikkat: Ek GPU kaynağı kullanır.",
+ "settings.glow.desc.pad": "Pad çubuklarının çevresine yumuşak, aura benzeri bir parıltı ekler.\nDikkat: Ek GPU kaynağı kullanır.",
+ "settings.glow.desc.circle": "Dairesel ekolayzerin kenarı boyunca ışıldayan bir hâle ekler.\nDikkat: Ek GPU kaynağı kullanır.",
+ "settings.glow.desc.outline": "Kontura yumuşak bir neon parıltısı ekler ve gücünü ayarlar.\nDikkat: Ek GPU kaynağı kullanır.",
+ "settings.radius.title": "Yarıçap",
+ "settings.radius.desc": "Daire modunda ortadaki halkanın yarıçapını ayarlar.",
+ "settings.section.sound_types": "Ses türleri (yapay zeka)",
+ "settings.show_ambient": "Ortam seslerini göster",
+ "settings.show_speech": "Konuşmayı göster",
+ "settings.show_danger": "Tehlike seslerini göster",
+ "settings.set_color": "Renk ayarla",
+ "settings.set_color_for": "Renk ayarla: {0}",
+ "settings.section.hotkeys": "Kısayol tuşları",
+ "settings.hotkey.visual.title": "Görsel modu değiştir",
+ "settings.hotkey.visual.desc": "Çalışırken şekli anında değiştiren kısayol tuşu.",
+ "settings.hotkey.sound.title": "Ses modunu değiştir",
+ "settings.hotkey.sound.desc": "Çalışırken ses modunu anında değiştiren kısayol tuşu.",
+ "settings.hotkey.edit.title": "Yer paylaşımı ayarları",
+ "settings.hotkey.edit.desc": "Yer paylaşımı düzenleyicisini açan veya kapatan kısayol tuşu.",
+ "settings.hotkey.set": "Kısayol ata",
+ "settings.section.advanced": "Gelişmiş ayarlar",
+ "settings.developer_mode.title": "Geliştirici modu",
+ "settings.developer_mode.desc": "Yapay zekanın ne duyduğunu (bildirdiği ses adı, ne kadar emin olduğu ve ne kadar sürdüğü) yer paylaşımının sol üst köşesinde, FPS'yi de sağ üst köşesinde gösterir. Bilgiler İngilizce gösterilir ve günlük kullanım için tasarlanmamıştır.",
+ "settings.developer_record.title": "Sonuçları dosyaya kaydet",
+ "settings.developer_record.desc": "Yapay zekanın her sonucunu (en yüksek puan verdiği ses adları, ne kadar emin olduğu ve süreler) SoundVisualizer.exe ile aynı yerdeki ai-log klasöründe yeni bir CSV dosyasına yazar, böylece ne duyduğuna sonradan bakabilirsiniz. Bu klasöre yazılamıyorsa dosya %LOCALAPPDATA%\\SoundVisualizer\\ai-log klasörüne kaydedilir. Dosyalar büyüyebilir, işiniz bitince kapatın.",
+ "settings.auto_minimize.title": "Başlatınca başlatıcıyı küçült",
+ "settings.auto_minimize.desc": "Yer paylaşımı başladığında bu başlatıcı penceresini otomatik olarak simge durumuna küçültür.",
+
+ "sounds.title": "Ses türlerini seçin",
+ "sounds.desc": "Yapay zekanın tanıdığı sesler benzer seslerle gruplanır ve varsayılan olarak sayıldıkları türe göre gösterilir. Bir grubun türünü değiştirdiğinizde o gruptaki seslerin rengi hemen yeni türe göre değişir.",
+ "sounds.sibling_note": "Yapay zeka tek bir sesi çoğu zaman aynı anda birkaç adla tanır. Örneğin siren “Alarm” olarak da tanınır ve “Alarm” başka bir gruptadır. Bir ses değişiklikten sonra hâlâ eski türüyle görünüyorsa ilgili grupları veya sesleri de değiştirin.",
+ "sounds.advanced.title": "Gelişmiş mod",
+ "sounds.advanced.desc": "Tüm sesleri gruplarıyla birlikte gösterir; böylece her sesi ayrı olarak değiştirebilirsiniz.",
+ "sounds.search.hint": "Seslerde ara",
+ "sounds.search.clear": "Aramayı temizle",
+ "sounds.filter.title": "Filtre",
+ "sounds.filter.all": "Tümü",
+ "sounds.filter.changed": "Değiştirilenler",
+ "sounds.type.ambient": "Ortam sesi",
+ "sounds.type.speech": "Konuşma",
+ "sounds.type.danger": "Tehlike sesi",
+ "sounds.type.default": "{0} (varsayılan)",
+ "sounds.type.mixed": "Karışık",
+ "sounds.changed_count": "Değiştirilen sesler: {0}",
+ "sounds.changed_from": "Varsayılan: {0}",
+ "sounds.reset.button": "Tümünü sıfırla",
+ "sounds.reset.title": "Tüm sesler sıfırlansın mı?",
+ "sounds.reset.message": "Değiştirdiğiniz tüm sesler varsayılan türlerine döner.",
+ "sounds.reset.confirm": "Sıfırla",
+ "sounds.reset.cancel": "İptal",
+ "sounds.empty": "Eşleşen ses yok.",
+ "sounds.group.mixed_note": "Bazı sesler ayrı olarak değiştirildi",
+ "sounds.group.changed_line": "Şu an: {0} · Varsayılan: {1}",
+ "sounds.overwrite.title": "Grubun tamamı değiştirilsin mi?",
+ "sounds.overwrite.message": "“{0}” grubundaki {1} ses “{2}” türüne geçecek. Ayrı olarak değiştirdiğiniz seslerin de üzerine yazılacak.",
+ "sounds.overwrite.confirm": "Değiştir",
+ "sounds.overwrite.cancel": "İptal",
+ "sounds.basic_warning.title": "Temel moda geçilsin mi?",
+ "sounds.basic_warning.message": "Bazı gruplarda ayrı olarak değiştirdiğiniz sesler var. Temel modda bu gruplardan biri için tür seçerseniz gruptaki tüm sesler o türü alır ve o grupta ayrı olarak yaptığınız değişikliklerin üzerine yazılır. Siz seçene kadar hiçbir şey değişmez.",
+ "sounds.basic_warning.confirm": "Temel moda geç",
+ "sounds.basic_warning.cancel": "Gelişmiş modda kal",
+ "sounds.cd.sound": "{0}, tür: {1}",
+ "sounds.cd.sound_changed": "{0}, tür: {1}, varsayılan: {2}",
+ "sounds.cd.group": "{0} grubu, tür: {1}, ses sayısı: {2} ({3})",
+ "sounds.cd.group_changed": "{0} grubu, tür: {1}, varsayılan: {2}, ses sayısı: {3} ({4})",
+ "sounds.cd.group_header": "{0} grubu, ses sayısı: {1}",
+
+ "hotkey.prompt": "Bir tuşa basın (iptal için Esc)",
+ "hotkey.none": "Yok",
+ "hotkey.button_name": "{0}, kısayol: {1}",
+
+ "help.start.title": "Başlarken",
+ "help.start.1": "Bu pencerenin üst kısmından dili seçin ve isterseniz “Ayarlar” sekmesinde grafiklerin görünümünü değiştirin.",
+ "help.start.2": "“Ana sayfa” sekmesinde “{0}” düğmesine tıklayın.",
+ "help.start.3": "Grafikler, oyun ve video gibi diğer pencerelerin üzerinde, ekranın kenarları boyunca çizilir. Tıklamalar grafiklerin içinden geçer; böylece alttaki pencereyi kullanmaya devam edebilirsiniz.",
+ "help.start.4": "Kapatmak için “Ana sayfa” sekmesindeki “{0}” düğmesine tıklayın ya da görev çubuğunun bildirim alanındaki SoundVisualizer simgesine sağ tıklayıp “{0}” seçeneğini seçin.",
+ "help.modes.title": "Görsel modlar",
+ "help.mode.wave": "Ekranın tüm kenarları ses yüksekliğine göre içeri doğru ilerler. Sesin geldiği taraf daha derine uzanır.",
+ "help.mode.pad": "Ekran kenarındaki 8 noktada çubuklar yanar. Yönü en net gösteren moddur.",
+ "help.mode.circle": "Ekranın ortasındaki halka, sesin geldiği yöne doğru genişler.",
+ "help.mode.outline": "Dalganın yalnızca ince dış çizgisini çizer; böylece oyun ekranınızı daha az örter.",
+ "help.modes.note": "“Boyut” ve “Opaklık” gibi ayarlar her mod için ayrı kaydedilir ve “Ayarlar” sekmesinden değiştirilebilir.",
+ "help.sound.title": "Ses türleri",
+ "help.sound.kinds": "Yapay zeka, duyduğunuz sesleri ortam sesleri, konuşma ve tehlike sesleri olarak ayırır. Varsayılan olarak ortam sesleri beyaz, konuşma sarı, tehlike sesleri ise kırmızı renkte gösterilir. Her türün rengini ve gösterilip gösterilmeyeceğini “Ayarlar” sekmesinden değiştirebilirsiniz.",
+ "help.sound.groups": "“Sesler” sekmesinde benzer sesler gruplanır ve her grubun hangi türden sayılacağını siz seçersiniz. Örneğin “Kapı zili” grubunu tehlike sesi yaparsanız yapay zeka bir kapı zili veya kapıya vurma sesi tanıdığında bu ses tehlike sesi rengini alır. Ancak yeni tehlike sesi yaptığınız bir ses, yalnızca yapay zekanın türü belirlemek için yaptığı oylamaya katılır; bu yüzden her kapı zilinin tehlike sesi olarak gösterileceği anlamına gelmez.",
+ "help.sound.advanced": "Her sesi ayrı olarak değiştirmek için gelişmiş modu açın.",
+ "help.sound.danger": "Bir tehlike sesi (ör. silah sesi) algılandığında renk yavaşça geçiş yapmak yerine anında değişir.",
+ "help.pc.title": "PC ayarları ve kısayol tuşları",
+ "help.sound_mode.title": "Ses modu",
+ "help.sound_mode.body": "Sağ ve solun yanı sıra önü ve arkayı da ayırt etmek için Windows ses ayarlarında çıkış cihazınızı “5.1 Surround” (6 kanal) veya “7.1 Surround” (8 kanal) olarak ayarlayın. Normal stereoda (2 kanal) uygulama sağı ve solu ayırt edebilir, ancak önü ve arkayı ayırt edemez. “Ayarlar” sekmesindeki “Ses modu”, başlatıldığında ve çıkış cihazı değiştiğinde o cihaza göre ayarlanır; dilerseniz kendiniz de değiştirebilirsiniz.",
+ "help.sound_mode.tip": "💡 7.1 çıkış cihazınız yoksa {0} yükleyip çıkış cihazını 7.1 kanala ayarlayarak yer paylaşımını 7.1 surround sesle gösterebilirsiniz.",
+ "help.sound_mode.tip_link": "VB-CABLE sanal sürücüsünü",
+ "help.hotkeys.title": "Anlık özel kısayol tuşları",
+ "help.hotkeys.body": "“Ayarlar” sekmesinde bir kısayol düğmesine tıklayın ve tek bir tuşa ya da bir tuş birleşimine (ör. Ctrl + Shift + A) basarak kısayolu dilediğiniz gibi değiştirin. Uygulama arka planda çalışırken bile kısayol tuşuna bastığınızda mod anında değişir.",
+ "help.hotkeys.defaults": "Varsayılan olarak F2 ses modunu, F3 görsel modu değiştirir; F4 ise yer paylaşımı ayarlarını açar.",
+ "help.size_lock.title": "Boyut sabitleme modu",
+ "help.size_lock.body": "Boyut sabitleme modu, ses yüksekliğiyle orantılı saydamlık kullanarak grafikleri sabit bir boyutta tutar. Grafiklerin boyutunu aynı tutup sese nasıl tepki verdiklerini yine de görmek istediğinizde işe yarar.",
+ "help.editor.title": "Anlık yer paylaşımı düzenleme modu",
+ "help.editor.body": "Yer paylaşımı düzenleme modunu açmak için atanmış kısayol tuşuna (varsayılan: F4) basın. Ekranda fareyle sürükleyerek grafiklerin boyut sınırını ayarlayabilir, açılan ayarlar panelinde ayrıntılara ince ayar yapabilirsiniz.",
+ "help.developer.title": "Geliştirici modu",
+ "help.developer.body": "Geliştirici modunu açtığınızda yer paylaşımının sol üst köşesinde yapay zekanın ne duyduğu (ses adı, ne kadar emin olduğu ve ne kadar sürdüğü), sağ üst köşesinde de FPS gösterilir. “{0}” seçeneğini de açarsanız her sonuç bir CSV dosyasına kaydedilir. Sistemin düzgün çalışıp çalışmadığını kontrol etmek istediğinizde işe yarar.",
+ "help.notes.title": "Bilmeniz gerekenler",
+ "help.note.direction_surround": "5.1 veya 7.1 kanal çıkışta ekranın üst kısmı önden, alt kısmı arkadan gelen sesi gösterir. Sol ve sağ kenarlar soldan ve sağdan gelen sesi gösterir; ekranın her yanından aynı anda gelen grafikler ise ortam sesleri veya yönü olmayan seslerdir.",
+ "help.note.direction_stereo": "2 kanal (stereo) çıkışta uygulama sağı ve solu ayırt edebilir, ancak önü ve arkayı ayırt edemez. Bu durumda ekranın üst ve alt kısmındaki grafikler, her iki tarafta eşit duyulan sesleri ve uzamsal etkiyi gösterir.",
+ "help.note.sources": "Windows'un varsayılan oynatma cihazından çalan tüm uygulamaların sesi gösterilir. Bir uygulamanın başka bir cihaza, örneğin yalnızca sesli sohbet için kullanılan bir kulaklığa gönderdiği ses gösterilmez.",
+ "help.note.volume": "Bir uygulamanın ses düzeyi, uygulamanın kendisinde veya Windows ses karıştırıcısında çok kısıksa ya da kapalıysa sesi neredeyse hiç veya hiç görünmeyebilir. Grafikler iyi tepki vermiyorsa o sesi biraz açın veya “Ayarlar” sekmesinde “Hassasiyet” değerini artırın.",
+ "help.note.ai": "Yapay zeka sınıflandırması hatalı olabilir. Güvenliğin söz konusu olduğu durumlarda yalnızca bu uygulamaya güvenmeyin.",
+ "help.faq.title": "Sık sorulan sorular",
+ "help.faq.stopped_q": "Ben durdurmadığım hâlde kapandı.",
+ "help.faq.stopped_a": "Ses yakalama durur ve birkaç saniye içinde yeniden başlamazsa kapanır; örneğin oynatma cihazının bağlantısı kesildiğinde ve başka bir cihaz olmadığında. Bu durumda nedeni “Ana sayfa” sekmesinde gösterilir. Hoparlör veya kulaklık bağlayın ya da Windows ses ayarlarında varsayılan bir oynatma cihazı belirleyin, sonra yeniden başlatın.",
+ "help.faq.no_graphic_q": "Hiç grafik göremiyorum.",
+ "help.faq.no_graphic_a": "Sesin Windows'un varsayılan oynatma cihazından çaldığını ve kapalı olmadığını, ayrıca “Ana sayfa” sekmesinde bir uyarı olup olmadığını kontrol edin. Bir oyun özel tam ekran modunda çalışıyorsa çerçevesiz pencere veya pencere modunu deneyin. Ayrıca “Ayarlar” sekmesinde “Opaklık” değerinin çok düşük olmadığından ve o an çalan ses türünün gösteriminin kapalı olmadığından emin olun.",
+ "help.faq.performance_q": "Çok fazla CPU veya GPU kullanır mı?",
+ "help.faq.performance_a": "Yükü azaltmak için “Ayarlar” sekmesinde “{0}” değerini düşürün veya “{1}” özelliğini kapatın.",
+ "help.faq.language_q": "Uygulama dilini nasıl değiştiririm?",
+ "help.faq.language_a": "Bu pencerenin üst kısmındaki “{0}” listesinden seçin. Çalışırken yer paylaşımı ayarlarından da değiştirebilirsiniz. “{1}” seçeneği Windows'un görüntüleme dilini kullanır.",
+ "help.report.title": "Sorun bildir",
+ "help.report.desc": "Bir hatayı ya da yanlış türde gösterilen bir sesi bize bildirin. Tarayıcınızda GitHub açılır ve aşağıdaki sistem bilgileri önceden doldurulur; bu yüzden bir GitHub hesabı gerekir. Hesabınız yoksa bize e-posta gönderin. Ses hiçbir zaman gönderilmez.",
+ "help.report.github": "GitHub'da bildir",
+ "help.report.mail": "E-postayla bildir",
+ "help.report.copy": "Sistem bilgilerini kopyala",
+ "help.report.copied": "Sistem bilgileri kopyalandı.",
+ "help.report.copy_failed": "Panoya kopyalanamadı. Yeniden deneyin.",
+ "help.report.no_browser": "Tarayıcı açılamadı. Sistem bilgilerini kopyalayıp bize başka bir yolla gönderin.",
+ "help.report.no_mail": "E-posta gönderebilecek bir uygulama yok. Sistem bilgilerini kopyalayıp başka bir yolla gönderin.",
+ "help.report.issue_title": "Uygulamadan gönderilen rapor",
+ "help.report.issue_body": "Ne oldu?\n\nNe dinliyordunuz (uygulama, ses türü)?\n\nBunun yerine ne olmasını bekliyordunuz?",
+ "help.about.title": "Hakkında",
+ "help.about.version": "Sürüm {0}",
+ "help.about.license": "Bu uygulama GNU Affero General Public License v3.0 (AGPL-3.0) kapsamında dağıtılmaktadır. “Sesler” sekmesindeki ses adları ve ses grupları AudioSet ontolojisine dayanır ve CC BY-SA 4.0 kapsamında dağıtılır (ayrıntılar için aşağıdaki “Açık kaynak lisansları” düğmesine tıklayın).",
+ "help.licenses.button": "Açık kaynak lisansları",
+ "help.licenses.title": "Açık kaynak lisansları",
+ "help.licenses.close": "Kapat",
+
+ "notice.ai_unavailable": "Ses türü algılama çalışmıyor; bu yüzden tüm sesler ortam sesi renginde gösteriliyor. Görselleştirmeyi kapatıp yeniden açmayı deneyin.",
+ "notice.ai_unavailable_hidden": "Ses türü algılama çalışmıyor; bu yüzden tüm sesler ortam sesi sayılıyor, ancak ortam seslerinin gösterimi kapalı olduğundan hiçbir şey görünmüyor. “Ayarlar” sekmesinde “{0}” seçeneğini açın veya görselleştirmeyi kapatıp yeniden açmayı deneyin.",
+ "notice.stopped.title": "Ses görselleştirme kapandı",
+ "notice.stopped.no_device": "Hiçbir oynatma cihazı açılamadığı için ses yakalanamıyor. Hoparlör veya kulaklık bağlayın ya da Windows ses ayarlarında varsayılan bir oynatma cihazı belirleyin, sonra yeniden başlatın.",
+ "notice.stopped.capture_error": "Bir hata nedeniyle ses yakalama durdu; grafikler kapalı.",
+ "notice.dismiss": "Uyarıyı kapat",
+
+ "tray.open": "Başlatıcıyı aç",
+ "tray.exit": "Çıkış",
+
+ "more_info.title": "GitHub deposu ve katkıda bulunma",
+ "more_info.body": "SoundVisualizer açık kaynaklı bir projedir ve kaynak kodu GitHub'da herkese açıktır. Hata bildirerek, özellik önererek veya geliştirmeye katkıda bulunarak projeye dahil olabilirsiniz. Resmî GitHub deposunu ziyaret etmek için aşağıdaki düğmeye tıklayın! Ayrıca GitHub'daki Releases sekmesinden en son sürümü kontrol edip indirebilirsiniz.",
+ "more_info.github_button": "GitHub deposunu ziyaret et",
+
+ "dialog.notice_title": "Bilgi",
+ "dialog.already_running": "Yer paylaşımı zaten çalışıyor.",
+ "dialog.reset_title": "Ayarları sıfırla",
+ "dialog.reset_wave": "Dalga modu ayarları varsayılanlara sıfırlansın mı?",
+ "dialog.reset_pad": "Pad modu ayarları varsayılanlara sıfırlansın mı?",
+ "dialog.reset_circle": "Daire modu ayarları varsayılanlara sıfırlansın mı?",
+ "dialog.reset_outline": "Kontur modu ayarları varsayılanlara sıfırlansın mı?",
+ "dialog.reset_sound_types": "Ses türü ayarları varsayılanlara sıfırlansın mı?",
+ "dialog.reset_all_title": "Varsayılanlara sıfırla",
+ "dialog.reset_all": "Tüm ayarları varsayılanlara sıfırlamak istediğinizden emin misiniz?",
+ "dialog.link_error": "Bağlantı açılamadı: {0}",
+
+ "editor.title": "Yer paylaşımı ayarları",
+ "editor.drag_tip": "Yer paylaşımındaki dalganın yükseklik sınırını ve merkez boyutunu anında ayarlamak için ekrandaki kılavuz çizgilerini fareyle sürükleyin!",
+ "editor.glow": "Parıltı efekti",
+ "editor.glow_strength": "Parıltı gücü",
+ "editor.color": "Renk",
+ "editor.hotkeys": "Kısayol tuşu ayarları",
+ "editor.developer_mode_desc": "Yapay zekanın ne duyduğunu (sol üst) ve FPS'yi (sağ üst) gösterir",
+ "editor.save_close_hint": "tuşuyla kaydedip kapatın",
+ "editor.close_overlay": "Yer paylaşımını kapat",
+ "editor.guide.limit": "{0} sınırı",
+ "editor.guide.size": "{0} boyutu: %{1}",
+ "editor.guide.base_radius": "Temel yarıçap: {0}",
+ "editor.guide.circle_size": "Daire boyutu: %{0}",
+
+ "hud.visual_mode": "Görsel mod: [{0}] {1}",
+ "hud.sound_mode": "Ses modu: [{0}] {1}",
+ "hud.overlay_settings": "Yer paylaşımı ayarları: [{0}]",
+ "hud.status.ready": "Sistem hazır",
+ "hud.status.stereo": "PC sesi: {0} kanal (stereo)\nKaynak: bekleniyor...",
+ "hud.status.surround": "PC sesi: 8 kanal (7.1)\nKaynak: bekleniyor...",
+
+ "color_picker.title": "Renk seçin",
+ "color_picker.hex": "HEX kodu",
+ "color_picker.cancel": "İptal",
+ "color_picker.apply": "Uygula",
+ "color_picker.area": "Doygunluk ve parlaklık",
+ "color_picker.area_value": "Doygunluk %{0}, parlaklık %{1}",
+ "color_picker.hue": "Ton",
+ "color_picker.keys_help": "Ok tuşlarıyla değiştirin. Daha büyük adımlarla değiştirmek için Shift tuşunu basılı tutun.",
+ "color_picker.presets": "Sık kullanılan renkler",
+ "color_picker.preset.white": "Beyaz",
+ "color_picker.preset.red": "Kırmızı",
+ "color_picker.preset.orange": "Turuncu",
+ "color_picker.preset.yellow": "Sarı",
+ "color_picker.preset.green": "Yeşil",
+ "color_picker.preset.cyan": "Turkuaz",
+ "color_picker.preset.blue": "Mavi",
+ "color_picker.preset.magenta": "Macenta",
+
+ "audio.error.no_device": "Çıkış cihazı bulunamadı.",
+ "audio.error.capture_stopped": "Ses yakalama beklenmedik şekilde durdu: {0}",
+ "audio.error.capture_start_failed": "Ses yakalama motoru başlatılamadı: {0}",
+
+ "tutorial.window_title": "Tanıtım",
+ "tutorial.skip": "Atla",
+ "tutorial.previous": "Önceki sayfa",
+ "tutorial.next": "İleri",
+ "tutorial.done": "Anladım",
+ "tutorial.page_status": "Sayfa {0}, toplam {1}",
+ "tutorial.pause": "Animasyonu duraklat",
+ "tutorial.play": "Animasyonu oynat",
+ "tutorial.sound_on": "Sesi aç",
+ "tutorial.sound_off": "Sesi kapat",
+ "tutorial.sound.title": "Sesleri ekranınızda görün",
+ "tutorial.sound.body": "Oyununuzun veya videonuzun sesi ekranın kenarlarında çizilir. Ses ne kadar yüksekse o kadar büyük çizilir. Yalnızca bu bilgisayarda çalan ses gösterilir. Uygulama çevrenizdeki sesleri dinlemez.",
+ "tutorial.direction.title": "Sesin nereden geldiğini gösterir",
+ "tutorial.direction.body": "2 kanal (stereo) çıkışta soldan gelen ses solda, sağdan gelen ses sağda daha büyük çizilir. 5.1 veya 7.1 kanal çıkışta uygulama önü ve arkayı da ayırt eder: önden gelen ses ekranın üst kısmında, arkadan gelen ses alt kısmında çizilir.",
+ "tutorial.types.title": "Her ses türünün kendi rengi var",
+ "tutorial.types.body": "Yapay zeka, sesleri üç türe ayırır ve her türü farklı bir renkte çizer. Renkleri “Ayarlar” sekmesinden değiştirebilirsiniz.",
+ "tutorial.type.ambient_examples": "Müzik, yağmur ve diğer tüm sesler",
+ "tutorial.type.speech_examples": "İnsan sesleri",
+ "tutorial.type.danger_examples": "Silah sesi, patlama, siren",
+ "tutorial.start.title": "Nasıl açılıp kapatılır",
+ "tutorial.start.hotkeys": "Çalışırken ses modunu değiştirmek için {0} tuşuna, görsel modu değiştirmek için {1} tuşuna, yer paylaşımı ayarlarını açmak için {2} tuşuna basın."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/vi.json b/SoundVisualizer.Core/Localization/Strings/vi.json
new file mode 100644
index 0000000..d74dd03
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/vi.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "Ngôn ngữ",
+ "common.reset_defaults": "Đặt lại về mặc định",
+ "common.minimize": "Thu nhỏ",
+ "common.close": "Đóng",
+ "language.system": "Mặc định của hệ thống",
+
+ "launcher.window_title": "Trình khởi chạy SoundVisualizer",
+ "launcher.tab.home": "Trang chủ",
+ "launcher.tab.settings": "Cài đặt",
+ "launcher.tab.sounds": "Phân loại",
+ "launcher.tab.help": "Trợ giúp",
+ "launcher.tab.more_info": "Tìm hiểu thêm",
+ "launcher.home.desc": "Vẽ lên màn hình những âm thanh mà bạn không nhìn thấy.\nTừ trò chơi đến phim ảnh, hãy khám phá một trải nghiệm hoàn toàn mới.",
+ "launcher.home.start": "Bắt đầu",
+ "launcher.home.stop": "Dừng",
+ "launcher.home.tutorial": "Xem hướng dẫn",
+ "launcher.status.running": "Trạng thái: Đang chạy",
+ "launcher.status.idle": "Trạng thái: Sẵn sàng",
+
+ "settings.section.mode": "Cài đặt chế độ",
+ "settings.visual_mode.title": "Chế độ hiển thị",
+ "settings.visual_mode.desc": "Chọn hình dạng cơ bản của đồ họa được vẽ trên màn hình.",
+ "settings.mode.wave": "Sóng",
+ "settings.mode.pad": "Vạch",
+ "settings.mode.circle": "Vòng",
+ "settings.mode.outline": "Viền",
+ "settings.sound_mode.title": "Chế độ âm thanh",
+ "settings.sound_mode.desc": "Chọn số kênh âm thanh phù hợp với loa của bạn.\nVới loa 2 kênh, hãy chọn “2 kênh”. Với loa vòm, hãy chọn “5.1 kênh” hoặc “7.1 kênh”.",
+ "settings.channels.2": "2 kênh",
+ "settings.channels.5_1": "5.1 kênh",
+ "settings.channels.7_1": "7.1 kênh",
+ "settings.max_fps.title": "Tốc độ khung hình tối đa",
+ "settings.max_fps.desc": "Đặt tốc độ khung hình cao nhất để vẽ đồ họa.",
+ "settings.section.mode_detail": "Cài đặt theo chế độ",
+ "settings.mode_card.wave": "Chế độ Sóng",
+ "settings.mode_card.pad": "Chế độ Vạch",
+ "settings.mode_card.circle": "Chế độ Vòng",
+ "settings.mode_card.outline": "Chế độ Viền",
+ "settings.size_lock.title": "Khóa kích thước",
+ "settings.size_lock.desc": "Giữ đồ họa ở kích thước cố định và thể hiện độ lớn âm thanh bằng độ đậm.",
+ "settings.fixed_size.wave": "Kích thước sóng",
+ "settings.fixed_size.pad": "Kích thước vạch",
+ "settings.fixed_size.circle": "Kích thước vòng",
+ "settings.fixed_size.outline": "Độ dày viền",
+ "settings.fixed_size.desc": "Đặt kích thước cố định mà đồ họa giữ nguyên trong khi độ đậm thay đổi.",
+ "settings.max_opacity.title": "Đậm tối đa",
+ "settings.max_opacity.desc": "Độ đậm của đồ họa khi âm thanh lớn nhất.",
+ "settings.size.title": "Kích thước",
+ "settings.size.desc.wave": "Điều chỉnh chiều cao của sóng và độ đầy đặn tổng thể.",
+ "settings.size.desc.pad": "Điều chỉnh độ dày và độ rộng của các vạch khi phản ứng với âm thanh.",
+ "settings.size.desc.circle": "Điều chỉnh mức equalizer hình tròn phồng ra khi có âm thanh.",
+ "settings.size.desc.outline": "Điều chỉnh chiều cao của sóng viền và độ đầy đặn tổng thể.",
+ "settings.opacity.title": "Độ đậm",
+ "settings.opacity.desc.wave": "Giá trị càng cao thì càng đậm; càng thấp thì càng nhạt và trong suốt.",
+ "settings.opacity.desc.pad": "Giá trị càng cao thì càng đậm; càng thấp thì càng nhạt và trong suốt.",
+ "settings.opacity.desc.circle": "Giá trị càng cao thì càng đậm; càng thấp thì càng nhạt và trong suốt.",
+ "settings.opacity.desc.outline": "Giá trị càng cao thì càng đậm; càng thấp thì càng nhạt và trong suốt.",
+ "settings.speed.title": "Tốc độ",
+ "settings.speed.desc.wave": "Điều chỉnh tốc độ sóng trôi và gợn lên.",
+ "settings.speed.desc.pad": "Điều chỉnh tốc độ các vạch xoay về phía âm thanh phát ra.",
+ "settings.speed.desc.circle": "Điều chỉnh tốc độ equalizer hình tròn gợn sóng và xoay theo âm thanh.",
+ "settings.speed.desc.outline": "Điều chỉnh tốc độ sóng viền trôi và gợn lên.",
+ "settings.sensitivity.title": "Độ nhạy",
+ "settings.sensitivity.desc.wave": "Điều chỉnh mức độ sóng phản ứng nhạy và dâng lên, kể cả với âm thanh nhỏ.",
+ "settings.sensitivity.desc.pad": "Điều chỉnh để các vạch bắt âm thanh tốt hơn và nổi bật hơn.",
+ "settings.sensitivity.desc.circle": "Điều chỉnh mức độ chi tiết mà biên độ của vòng được chia nhỏ khi phản ứng với âm thanh.",
+ "settings.sensitivity.desc.outline": "Điều chỉnh mức độ sóng viền phản ứng nhạy và dâng lên, kể cả với âm thanh nhỏ.",
+ "settings.glow.title": "Phát sáng",
+ "settings.glow.desc.wave": "Thêm ánh sáng neon dịu nhẹ dọc theo mép sóng và điều chỉnh cường độ.\nLưu ý: tốn thêm tài nguyên GPU.",
+ "settings.glow.desc.pad": "Thêm quầng sáng dịu nhẹ như hào quang quanh các vạch.\nLưu ý: tốn thêm tài nguyên GPU.",
+ "settings.glow.desc.circle": "Thêm vầng sáng lấp lánh dọc theo mép equalizer hình tròn.\nLưu ý: tốn thêm tài nguyên GPU.",
+ "settings.glow.desc.outline": "Thêm ánh sáng neon dịu nhẹ cho viền và điều chỉnh cường độ.\nLưu ý: tốn thêm tài nguyên GPU.",
+ "settings.radius.title": "Bán kính",
+ "settings.radius.desc": "Chỉnh bán kính của vòng tròn ở giữa màn hình trong chế độ Vòng.",
+ "settings.section.sound_types": "Loại âm thanh (AI)",
+ "settings.show_ambient": "Hiện âm thanh môi trường",
+ "settings.show_speech": "Hiện giọng nói",
+ "settings.show_danger": "Hiện âm thanh nguy hiểm",
+ "settings.set_color": "Đặt màu",
+ "settings.set_color_for": "Đặt màu: {0}",
+ "settings.section.hotkeys": "Phím tắt",
+ "settings.hotkey.visual.title": "Chuyển chế độ hiển thị",
+ "settings.hotkey.visual.desc": "Phím tắt để đổi hình dạng theo thời gian thực khi đang chạy.",
+ "settings.hotkey.sound.title": "Chuyển chế độ âm thanh",
+ "settings.hotkey.sound.desc": "Phím tắt để đổi chế độ âm thanh theo thời gian thực khi đang chạy.",
+ "settings.hotkey.edit.title": "Cài đặt lớp phủ",
+ "settings.hotkey.edit.desc": "Phím tắt để mở hoặc đóng trình chỉnh sửa lớp phủ.",
+ "settings.hotkey.set": "Đặt phím tắt",
+ "settings.section.advanced": "Cài đặt nâng cao",
+ "settings.developer_mode.title": "Chế độ nhà phát triển",
+ "settings.developer_mode.desc": "Hiển thị nội dung AI nghe được (tên âm thanh AI báo, mức độ chắc chắn và thời gian xử lý) ở góc trên bên trái lớp phủ, cùng FPS ở góc trên bên phải. Nội dung hiển thị bằng tiếng Anh và không dành cho việc sử dụng hằng ngày.",
+ "settings.developer_record.title": "Ghi kết quả vào tệp",
+ "settings.developer_record.desc": "Ghi mọi kết quả của AI (các tên âm thanh được đánh giá cao nhất, mức độ chắc chắn và thời gian xử lý) vào một tệp CSV mới trong thư mục ai-log cạnh SoundVisualizer.exe, để bạn có thể xem lại AI đã nghe thấy gì. Nếu không ghi được vào thư mục đó, tệp sẽ được lưu vào %LOCALAPPDATA%\\SoundVisualizer\\ai-log. Tệp có thể rất lớn, hãy tắt khi bạn xem xong.",
+ "settings.auto_minimize.title": "Thu nhỏ trình khởi chạy khi bắt đầu",
+ "settings.auto_minimize.desc": "Tự động thu nhỏ cửa sổ trình khởi chạy này khi lớp phủ bắt đầu chạy.",
+
+ "sounds.title": "Chọn loại âm thanh",
+ "sounds.desc": "Các âm thanh AI nhận diện được nằm chung nhóm với những âm thanh tương tự, xếp theo loại mặc định của từng âm thanh. Khi bạn đổi loại của một nhóm, màu của các âm thanh trong nhóm sẽ đổi theo loại mới ngay lập tức.",
+ "sounds.sibling_note": "AI thường nhận ra một âm thanh bằng nhiều tên cùng lúc. Ví dụ, còi hụ cũng được nhận ra là “Báo động”, vốn nằm ở nhóm khác. Nếu sau khi đổi mà một âm thanh vẫn hiện theo loại cũ, hãy đổi cả các nhóm hoặc âm thanh liên quan.",
+ "sounds.advanced.title": "Chế độ nâng cao",
+ "sounds.advanced.desc": "Hiện mọi âm thanh theo nhóm để bạn đổi riêng từng âm thanh.",
+ "sounds.search.hint": "Tìm âm thanh",
+ "sounds.search.clear": "Xóa từ khóa tìm kiếm",
+ "sounds.filter.title": "Bộ lọc",
+ "sounds.filter.all": "Tất cả",
+ "sounds.filter.changed": "Đã đổi",
+ "sounds.type.ambient": "Âm thanh môi trường",
+ "sounds.type.speech": "Giọng nói",
+ "sounds.type.danger": "Âm thanh nguy hiểm",
+ "sounds.type.default": "{0} (mặc định)",
+ "sounds.type.mixed": "Hỗn hợp",
+ "sounds.changed_count": "Âm thanh đã đổi: {0}",
+ "sounds.changed_from": "Mặc định: {0}",
+ "sounds.reset.button": "Đặt lại tất cả",
+ "sounds.reset.title": "Đặt lại tất cả âm thanh về mặc định?",
+ "sounds.reset.message": "Mọi âm thanh bạn đã đổi sẽ trở về loại mặc định.",
+ "sounds.reset.confirm": "Đặt lại",
+ "sounds.reset.cancel": "Hủy",
+ "sounds.empty": "Không có âm thanh nào phù hợp.",
+ "sounds.group.mixed_note": "Một số âm thanh đã được đổi riêng",
+ "sounds.group.changed_line": "Hiện tại: {0} · Mặc định: {1}",
+ "sounds.overwrite.title": "Đổi cả nhóm?",
+ "sounds.overwrite.message": "{1} âm thanh trong nhóm “{0}” sẽ chuyển thành “{2}”. Những âm thanh bạn đã đổi riêng cũng sẽ bị ghi đè.",
+ "sounds.overwrite.confirm": "Đổi",
+ "sounds.overwrite.cancel": "Hủy",
+ "sounds.basic_warning.title": "Chuyển sang chế độ cơ bản?",
+ "sounds.basic_warning.message": "Một số nhóm có âm thanh bạn đã đổi riêng. Ở chế độ cơ bản, nếu bạn chọn loại cho một nhóm như vậy, mọi âm thanh trong nhóm sẽ nhận loại đó và những thay đổi riêng trong nhóm sẽ bị ghi đè. Không có gì thay đổi cho đến khi bạn chọn.",
+ "sounds.basic_warning.confirm": "Chuyển sang cơ bản",
+ "sounds.basic_warning.cancel": "Giữ chế độ nâng cao",
+ "sounds.cd.sound": "{0}, loại: {1}",
+ "sounds.cd.sound_changed": "{0}, loại: {1}, mặc định: {2}",
+ "sounds.cd.group": "Nhóm {0}, loại: {1}, số âm thanh: {2} ({3})",
+ "sounds.cd.group_changed": "Nhóm {0}, loại: {1}, mặc định: {2}, số âm thanh: {3} ({4})",
+ "sounds.cd.group_header": "Nhóm {0}, số âm thanh: {1}",
+
+ "hotkey.prompt": "Nhấn một phím (Esc để hủy)",
+ "hotkey.none": "Không có",
+ "hotkey.button_name": "{0}, phím tắt: {1}",
+
+ "help.start.title": "Bắt đầu sử dụng",
+ "help.start.1": "Chọn ngôn ngữ ở đầu cửa sổ này, và nếu muốn, hãy đổi giao diện đồ họa trong thẻ Cài đặt.",
+ "help.start.2": "Ở thẻ Trang chủ, nhấp “{0}”.",
+ "help.start.3": "Đồ họa được vẽ dọc theo các cạnh màn hình, phía trên các cửa sổ khác như trò chơi và video. Thao tác nhấp chuột đi xuyên qua đồ họa, nên bạn vẫn dùng được cửa sổ bên dưới.",
+ "help.start.4": "Để tắt, hãy nhấp “{0}” ở thẻ Trang chủ, hoặc nhấp chuột phải vào biểu tượng SoundVisualizer trong vùng thông báo của thanh tác vụ rồi chọn “{0}”.",
+ "help.modes.title": "Chế độ hiển thị",
+ "help.mode.wave": "Toàn bộ cạnh màn hình dâng vào trong theo độ lớn âm thanh. Phía có âm thanh phát ra sẽ dâng vào sâu hơn.",
+ "help.mode.pad": "Các vạch sáng lên ở 8 vị trí quanh cạnh màn hình. Thể hiện hướng âm thanh rõ ràng nhất.",
+ "help.mode.circle": "Một vòng tròn ở giữa màn hình phồng lên về phía có âm thanh.",
+ "help.mode.outline": "Chỉ vẽ đường viền mảnh của sóng, nên ít che màn hình trò chơi hơn.",
+ "help.modes.note": "Các cài đặt như Kích thước và Độ đậm được lưu riêng cho từng chế độ và có thể thay đổi trong thẻ Cài đặt.",
+ "help.sound.title": "Loại âm thanh",
+ "help.sound.kinds": "AI phân loại âm thanh bạn nghe thấy thành âm thanh môi trường, giọng nói và âm thanh nguy hiểm. Theo mặc định, âm thanh môi trường có màu trắng, giọng nói màu vàng và âm thanh nguy hiểm màu đỏ. Bạn có thể đổi màu và chọn có hiện hay không cho từng loại trong thẻ Cài đặt.",
+ "help.sound.groups": "Trong thẻ Phân loại, các âm thanh tương tự được gom thành nhóm và bạn chọn mỗi nhóm được tính là loại nào. Ví dụ, nếu đổi nhóm “Chuông cửa” thành âm thanh nguy hiểm, khi AI nhận ra chuông cửa hoặc gõ cửa, âm thanh đó sẽ có màu của âm thanh nguy hiểm. Tuy nhiên, âm thanh do bạn tự đổi thành âm thanh nguy hiểm chỉ tham gia vào việc bỏ phiếu chọn loại của AI, nên không có nghĩa là chuông cửa lúc nào cũng sẽ hiện là âm thanh nguy hiểm.",
+ "help.sound.advanced": "Để đổi riêng từng âm thanh, hãy bật chế độ nâng cao.",
+ "help.sound.danger": "Khi phát hiện âm thanh nguy hiểm (như tiếng súng), màu sẽ đổi ngay lập tức thay vì chuyển dần.",
+ "help.pc.title": "Cài đặt PC và phím tắt",
+ "help.sound_mode.title": "Chế độ âm thanh",
+ "help.sound_mode.body": "Để phân biệt cả âm thanh phía trước và phía sau, ngoài trái và phải, hãy đặt thiết bị đầu ra thành âm thanh vòm 5.1 (6 kênh) hoặc 7.1 (8 kênh) trong cài đặt âm thanh của Windows. Với âm thanh nổi thông thường (2 kênh), ứng dụng phân biệt được trái và phải nhưng không phân biệt được trước và sau. “Chế độ âm thanh” trong thẻ Cài đặt sẽ tự khớp với thiết bị đầu ra khi bắt đầu chạy hoặc khi thiết bị thay đổi, và bạn vẫn có thể tự thay đổi.",
+ "help.sound_mode.tip": "💡 Nếu không có thiết bị đầu ra 7.1, bạn có thể cài {0} rồi đặt thiết bị đầu ra thành 7.1 kênh để hiển thị lớp phủ bằng âm thanh vòm 7.1.",
+ "help.sound_mode.tip_link": "trình điều khiển ảo VB-CABLE",
+ "help.hotkeys.title": "Phím tắt tùy chỉnh theo thời gian thực",
+ "help.hotkeys.body": "Trong thẻ Cài đặt, hãy nhấp vào nút phím tắt rồi nhấn một phím hoặc tổ hợp phím (ví dụ: Ctrl + Shift + A) để đổi phím tắt theo ý bạn. Khi nhấn phím tắt, chế độ sẽ chuyển ngay lập tức, kể cả khi ứng dụng đang chạy nền.",
+ "help.hotkeys.defaults": "Theo mặc định, F2 chuyển chế độ âm thanh, F3 chuyển chế độ hiển thị và F4 mở cài đặt lớp phủ.",
+ "help.size_lock.title": "Chế độ khóa kích thước",
+ "help.size_lock.body": "Chế độ khóa kích thước giữ đồ họa ở kích thước cố định và dùng độ đậm tương ứng với độ lớn âm thanh để thể hiện âm thanh. Chế độ này hữu ích khi bạn muốn giữ nguyên kích thước đồ họa mà vẫn thấy đồ họa phản ứng với âm thanh.",
+ "help.editor.title": "Chế độ chỉnh sửa lớp phủ theo thời gian thực",
+ "help.editor.body": "Nhấn phím tắt đã gán (mặc định là F4) để bật chế độ chỉnh sửa lớp phủ. Bạn có thể kéo chuột trên màn hình để điều chỉnh giới hạn kích thước của đồ họa, rồi tinh chỉnh chi tiết trong bảng cài đặt hiện ra.",
+ "help.developer.title": "Chế độ nhà phát triển",
+ "help.developer.body": "Khi bật Chế độ nhà phát triển, góc trên bên trái lớp phủ sẽ hiện nội dung AI nghe được (tên âm thanh, mức độ chắc chắn và thời gian xử lý), còn góc trên bên phải hiện FPS. Nếu bật thêm “{0}”, mọi kết quả sẽ được lưu vào tệp CSV. Chế độ này hữu ích khi bạn muốn kiểm tra xem hệ thống có hoạt động bình thường không.",
+ "help.notes.title": "Lưu ý",
+ "help.note.direction_surround": "Khi xuất âm thanh 5.1 hoặc 7.1 kênh, cạnh trên màn hình thể hiện âm thanh từ phía trước và cạnh dưới thể hiện âm thanh từ phía sau. Hai bên trái và phải thể hiện âm thanh từ bên trái và bên phải, còn đồ họa xuất hiện cùng lúc từ mọi phía của màn hình là âm thanh môi trường hoặc âm thanh không có hướng.",
+ "help.note.direction_stereo": "Khi xuất âm thanh 2 kênh (âm thanh nổi), ứng dụng phân biệt được trái và phải, nhưng không phân biệt được phía trước và phía sau. Khi đó, đồ họa ở cạnh trên và cạnh dưới màn hình biểu thị âm thanh nghe như nhau ở cả hai bên, cũng như cảm giác không gian.",
+ "help.note.sources": "Ứng dụng hiển thị âm thanh của mọi ứng dụng phát qua thiết bị phát mặc định của Windows. Âm thanh mà một ứng dụng gửi đến thiết bị khác, chẳng hạn tai nghe chỉ dùng để trò chuyện thoại, sẽ không được hiển thị.",
+ "help.note.volume": "Nếu âm lượng của một ứng dụng bị giảm rất thấp hoặc tắt tiếng, trong chính ứng dụng đó hoặc trong bộ trộn âm lượng của Windows, âm thanh của nó có thể gần như không hiện hoặc hoàn toàn không hiện. Nếu đồ họa phản hồi yếu, hãy tăng âm thanh đó lên một chút hoặc tăng Độ nhạy trong thẻ Cài đặt.",
+ "help.note.ai": "AI có thể phân loại sai. Khi liên quan đến an toàn, đừng coi ứng dụng này là biện pháp bảo vệ duy nhất.",
+ "help.faq.title": "Câu hỏi thường gặp",
+ "help.faq.stopped_q": "Ứng dụng tự tắt dù tôi không tắt.",
+ "help.faq.stopped_a": "Ứng dụng sẽ tắt khi việc thu âm thanh dừng lại và không phục hồi trong vài giây, chẳng hạn khi thiết bị phát bị ngắt kết nối mà không còn thiết bị nào khác. Khi đó, thẻ Trang chủ sẽ cho biết lý do. Hãy kết nối loa hoặc tai nghe, hoặc đặt thiết bị phát mặc định trong cài đặt âm thanh của Windows, rồi bắt đầu lại.",
+ "help.faq.no_graphic_q": "Tôi không thấy đồ họa nào.",
+ "help.faq.no_graphic_a": "Hãy kiểm tra xem âm thanh có đang phát qua thiết bị phát mặc định của Windows và không bị tắt tiếng hay không, và thẻ Trang chủ có hiện thông báo nào không. Nếu trò chơi chạy ở chế độ toàn màn hình độc quyền, hãy thử chế độ cửa sổ không viền hoặc chế độ cửa sổ. Ngoài ra, hãy đảm bảo Độ đậm trong thẻ Cài đặt không quá thấp và tùy chọn hiện loại âm thanh đang phát vẫn đang bật.",
+ "help.faq.performance_q": "Ứng dụng có dùng nhiều CPU hoặc GPU không?",
+ "help.faq.performance_a": "Để ứng dụng nhẹ hơn, hãy giảm “{0}” trong thẻ Cài đặt hoặc tắt “{1}”.",
+ "help.faq.language_q": "Làm cách nào để đổi ngôn ngữ ứng dụng?",
+ "help.faq.language_a": "Chọn trong danh sách “{0}” ở đầu cửa sổ này. Khi đang chạy, bạn cũng có thể đổi trong cài đặt lớp phủ. “{1}” sẽ dùng ngôn ngữ hiển thị của Windows.",
+ "help.report.title": "Báo cáo sự cố",
+ "help.report.desc": "Hãy cho chúng tôi biết khi bạn gặp lỗi, hoặc khi một âm thanh bị hiện sai loại. GitHub sẽ mở trong trình duyệt với thông tin hệ thống bên dưới đã được điền sẵn, nên bạn cần có tài khoản GitHub. Nếu không có tài khoản, hãy gửi email cho chúng tôi. Ứng dụng không bao giờ gửi đi bất kỳ âm thanh nào.",
+ "help.report.github": "Báo cáo trên GitHub",
+ "help.report.mail": "Báo cáo qua email",
+ "help.report.copy": "Sao chép thông tin hệ thống",
+ "help.report.copied": "Đã sao chép thông tin hệ thống.",
+ "help.report.copy_failed": "Không thể sao chép vào bộ nhớ tạm. Hãy thử lại.",
+ "help.report.no_browser": "Không mở được trình duyệt. Thay vào đó, hãy sao chép thông tin hệ thống và gửi cho chúng tôi.",
+ "help.report.no_mail": "Không có ứng dụng email nào để gửi. Hãy sao chép thông tin hệ thống và gửi bằng cách khác.",
+ "help.report.issue_title": "Báo cáo từ ứng dụng",
+ "help.report.issue_body": "Chuyện gì đã xảy ra?\n\nLúc đó bạn đang nghe gì (ứng dụng nào, loại âm thanh nào)?\n\nBạn đã mong đợi kết quả như thế nào?",
+ "help.about.title": "Giới thiệu",
+ "help.about.version": "Phiên bản {0}",
+ "help.about.license": "Ứng dụng này được phân phối theo GNU Affero General Public License v3.0 (AGPL-3.0). Tên và nhóm âm thanh trong thẻ Phân loại dựa trên AudioSet Ontology và được phân phối theo giấy phép CC BY-SA 4.0 (xem “Giấy phép nguồn mở” bên dưới).",
+ "help.licenses.button": "Giấy phép nguồn mở",
+ "help.licenses.title": "Giấy phép nguồn mở",
+ "help.licenses.close": "Đóng",
+
+ "notice.ai_unavailable": "Ứng dụng không nhận diện được loại âm thanh nên mọi âm thanh đều hiện bằng màu của âm thanh môi trường. Hãy thử tắt âm thanh trực quan rồi bật lại.",
+ "notice.ai_unavailable_hidden": "Ứng dụng không nhận diện được loại âm thanh nên mọi âm thanh đều được tính là âm thanh môi trường, nhưng âm thanh môi trường đang bị ẩn nên không có gì hiện ra. Hãy bật “{0}” trong thẻ Cài đặt, hoặc thử tắt âm thanh trực quan rồi bật lại.",
+ "notice.stopped.title": "Âm thanh trực quan đã tắt",
+ "notice.stopped.no_device": "Không mở được thiết bị phát nào nên không thể thu âm thanh. Hãy kết nối loa hoặc tai nghe, hoặc đặt thiết bị phát mặc định trong cài đặt âm thanh của Windows, rồi bắt đầu lại.",
+ "notice.stopped.capture_error": "Việc thu âm thanh đã dừng do lỗi nên đồ họa đã tắt.",
+ "notice.dismiss": "Đóng thông báo",
+
+ "tray.open": "Mở trình khởi chạy",
+ "tray.exit": "Thoát",
+
+ "more_info.title": "Kho lưu trữ GitHub và đóng góp",
+ "more_info.body": "SoundVisualizer là dự án nguồn mở, mã nguồn được công khai trên GitHub. Bạn có thể tham gia bằng cách báo lỗi, đề xuất tính năng hoặc đóng góp vào việc phát triển. Hãy nhấp vào nút bên dưới để truy cập kho lưu trữ GitHub chính thức! Bạn cũng có thể xem và tải phiên bản mới nhất trong thẻ Releases trên GitHub.",
+ "more_info.github_button": "Truy cập kho lưu trữ GitHub",
+
+ "dialog.notice_title": "Thông báo",
+ "dialog.already_running": "Lớp phủ hiện đang chạy.",
+ "dialog.reset_title": "Đặt lại cài đặt",
+ "dialog.reset_wave": "Đặt lại cài đặt của chế độ Sóng về mặc định?",
+ "dialog.reset_pad": "Đặt lại cài đặt của chế độ Vạch về mặc định?",
+ "dialog.reset_circle": "Đặt lại cài đặt của chế độ Vòng về mặc định?",
+ "dialog.reset_outline": "Đặt lại cài đặt của chế độ Viền về mặc định?",
+ "dialog.reset_sound_types": "Đặt lại cài đặt loại âm thanh về mặc định?",
+ "dialog.reset_all_title": "Đặt lại về mặc định",
+ "dialog.reset_all": "Bạn có chắc muốn đặt lại tất cả cài đặt về mặc định không?",
+ "dialog.link_error": "Không thể mở liên kết: {0}",
+
+ "editor.title": "Cài đặt lớp phủ",
+ "editor.drag_tip": "Dùng chuột kéo các đường hướng dẫn trên màn hình để điều chỉnh giới hạn chiều cao và kích thước ở giữa của sóng lớp phủ theo thời gian thực!",
+ "editor.glow": "Hiệu ứng phát sáng",
+ "editor.glow_strength": "Độ phát sáng",
+ "editor.color": "Màu",
+ "editor.hotkeys": "Cài đặt phím tắt",
+ "editor.developer_mode_desc": "Hiện nội dung AI nghe được (trên trái) và FPS (trên phải)",
+ "editor.save_close_hint": "để lưu cài đặt và đóng",
+ "editor.close_overlay": "Thoát lớp phủ",
+ "editor.guide.limit": "Giới hạn {0}",
+ "editor.guide.size": "Kích thước {0}: {1}%",
+ "editor.guide.base_radius": "Bán kính cơ bản: {0}",
+ "editor.guide.circle_size": "Kích thước vòng: {0}%",
+
+ "hud.visual_mode": "Chế độ hiển thị: [{0}] {1}",
+ "hud.sound_mode": "Chế độ âm thanh: [{0}] {1}",
+ "hud.overlay_settings": "Cài đặt lớp phủ: [{0}]",
+ "hud.status.ready": "Hệ thống đã sẵn sàng",
+ "hud.status.stereo": "Âm thanh PC: {0}ch (âm thanh nổi)\nNguồn: đang chờ...",
+ "hud.status.surround": "Âm thanh PC: 8ch (7.1)\nNguồn: đang chờ...",
+
+ "color_picker.title": "Chọn màu",
+ "color_picker.hex": "Mã HEX",
+ "color_picker.cancel": "Hủy",
+ "color_picker.apply": "Áp dụng",
+ "color_picker.area": "Độ bão hòa và độ sáng",
+ "color_picker.area_value": "Độ bão hòa {0}%, độ sáng {1}%",
+ "color_picker.hue": "Sắc độ",
+ "color_picker.keys_help": "Dùng các phím mũi tên để thay đổi. Giữ Shift để thay đổi nhiều hơn mỗi lần.",
+ "color_picker.presets": "Màu thường dùng",
+ "color_picker.preset.white": "Trắng",
+ "color_picker.preset.red": "Đỏ",
+ "color_picker.preset.orange": "Cam",
+ "color_picker.preset.yellow": "Vàng",
+ "color_picker.preset.green": "Xanh lá cây",
+ "color_picker.preset.cyan": "Xanh lơ",
+ "color_picker.preset.blue": "Xanh dương",
+ "color_picker.preset.magenta": "Hồng cánh sen",
+
+ "audio.error.no_device": "Không tìm thấy thiết bị đầu ra.",
+ "audio.error.capture_stopped": "Việc thu âm thanh đã dừng đột ngột: {0}",
+ "audio.error.capture_start_failed": "Không thể khởi động bộ thu âm thanh: {0}",
+
+ "tutorial.window_title": "Hướng dẫn",
+ "tutorial.skip": "Bỏ qua",
+ "tutorial.previous": "Trang trước",
+ "tutorial.next": "Tiếp theo",
+ "tutorial.done": "Đã hiểu",
+ "tutorial.page_status": "Trang {0} trên {1}",
+ "tutorial.pause": "Tạm dừng hoạt ảnh",
+ "tutorial.play": "Phát hoạt ảnh",
+ "tutorial.sound_on": "Bật âm thanh",
+ "tutorial.sound_off": "Tắt âm thanh",
+ "tutorial.sound.title": "Nhìn thấy âm thanh trên màn hình",
+ "tutorial.sound.body": "Âm thanh từ trò chơi hoặc video được vẽ dọc theo các cạnh màn hình. Âm thanh càng lớn thì đồ họa càng lớn. Chỉ âm thanh đang phát trên PC này mới được hiển thị. Ứng dụng không thu âm thanh xung quanh bạn.",
+ "tutorial.direction.title": "Cho biết âm thanh đến từ hướng nào",
+ "tutorial.direction.body": "Khi xuất âm thanh 2 kênh (âm thanh nổi), âm thanh từ bên trái được vẽ lớn hơn ở bên trái, âm thanh từ bên phải được vẽ lớn hơn ở bên phải. Khi xuất âm thanh 5.1 hoặc 7.1 kênh, ứng dụng còn phân biệt được phía trước và phía sau: âm thanh từ phía trước được vẽ ở cạnh trên màn hình, âm thanh từ phía sau được vẽ ở cạnh dưới.",
+ "tutorial.types.title": "Mỗi loại âm thanh có một màu riêng",
+ "tutorial.types.body": "AI phân loại âm thanh thành ba loại và vẽ mỗi loại bằng một màu riêng. Bạn có thể đổi màu trong thẻ Cài đặt.",
+ "tutorial.type.ambient_examples": "Nhạc, tiếng mưa và mọi âm thanh khác",
+ "tutorial.type.speech_examples": "Tiếng người nói",
+ "tutorial.type.danger_examples": "Tiếng súng, tiếng nổ, tiếng còi báo động",
+ "tutorial.start.title": "Cách bật và tắt",
+ "tutorial.start.hotkeys": "Khi đang chạy, nhấn {0} để chuyển chế độ âm thanh, {1} để chuyển chế độ hiển thị và {2} để mở cài đặt lớp phủ."
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/zh-Hans.json b/SoundVisualizer.Core/Localization/Strings/zh-Hans.json
new file mode 100644
index 0000000..75761ee
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/zh-Hans.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "语言",
+ "common.reset_defaults": "恢复默认设置",
+ "common.minimize": "最小化",
+ "common.close": "关闭",
+ "language.system": "系统默认",
+
+ "launcher.window_title": "SoundVisualizer 启动器",
+ "launcher.tab.home": "首页",
+ "launcher.tab.settings": "设置",
+ "launcher.tab.sounds": "分类",
+ "launcher.tab.help": "帮助",
+ "launcher.tab.more_info": "了解更多",
+ "launcher.home.desc": "将看不见的声音呈现在屏幕上。\n从游戏到观影,开启全新体验。",
+ "launcher.home.start": "启动",
+ "launcher.home.stop": "停止",
+ "launcher.home.tutorial": "查看教程",
+ "launcher.status.running": "状态:运行中",
+ "launcher.status.idle": "状态:待启动",
+
+ "settings.section.mode": "模式设置",
+ "settings.visual_mode.title": "显示模式",
+ "settings.visual_mode.desc": "选择屏幕上所绘图形的基本形状。",
+ "settings.mode.wave": "波浪",
+ "settings.mode.pad": "光条",
+ "settings.mode.circle": "圆环",
+ "settings.mode.outline": "轮廓",
+ "settings.sound_mode.title": "声音模式",
+ "settings.sound_mode.desc": "选择与扬声器相符的声道。\n2 声道扬声器请选择“2 声道”,环绕声扬声器请选择“5.1 声道”或“7.1 声道”。",
+ "settings.channels.2": "2 声道",
+ "settings.channels.5_1": "5.1 声道",
+ "settings.channels.7_1": "7.1 声道",
+ "settings.max_fps.title": "最大帧率",
+ "settings.max_fps.desc": "设置绘制图形时的最高帧率。",
+ "settings.section.mode_detail": "各模式详细设置",
+ "settings.mode_card.wave": "波浪模式",
+ "settings.mode_card.pad": "光条模式",
+ "settings.mode_card.circle": "圆环模式",
+ "settings.mode_card.outline": "轮廓模式",
+ "settings.size_lock.title": "锁定大小",
+ "settings.size_lock.desc": "将图形保持在固定大小,改用不透明度来表示声音强弱。",
+ "settings.fixed_size.wave": "波浪大小",
+ "settings.fixed_size.pad": "光条大小",
+ "settings.fixed_size.circle": "圆环大小",
+ "settings.fixed_size.outline": "轮廓粗细",
+ "settings.fixed_size.desc": "设置不透明度变化时图形保持的固定大小。",
+ "settings.max_opacity.title": "最大不透明度",
+ "settings.max_opacity.desc": "声音最大时图形达到的不透明度。",
+ "settings.size.title": "大小",
+ "settings.size.desc.wave": "调整波浪的上下高度和整体饱满感。",
+ "settings.size.desc.pad": "调整光条响应声音时的粗细和宽度。",
+ "settings.size.desc.circle": "调整声音响起时圆环均衡器向外鼓起的幅度。",
+ "settings.size.desc.outline": "调整轮廓波浪的上下高度和整体饱满感。",
+ "settings.opacity.title": "不透明度",
+ "settings.opacity.desc.wave": "数值越大,图形越浓重;数值越小,图形越淡、越透明。",
+ "settings.opacity.desc.pad": "数值越大,图形越浓重;数值越小,图形越淡、越透明。",
+ "settings.opacity.desc.circle": "数值越大,图形越浓重;数值越小,图形越淡、越透明。",
+ "settings.opacity.desc.outline": "数值越大,图形越浓重;数值越小,图形越淡、越透明。",
+ "settings.speed.title": "速度",
+ "settings.speed.desc.wave": "调整波浪流动和起伏的速度。",
+ "settings.speed.desc.pad": "调整光条转向声音来源方向的速度。",
+ "settings.speed.desc.circle": "调整圆环均衡器随声音起伏和转动的速度。",
+ "settings.speed.desc.outline": "调整轮廓波浪流动和起伏的速度。",
+ "settings.sensitivity.title": "灵敏度",
+ "settings.sensitivity.desc.wave": "调整波浪对较小声音的反应灵敏度和起伏程度。",
+ "settings.sensitivity.desc.pad": "调整光条捕捉声音的能力,使其更加醒目。",
+ "settings.sensitivity.desc.circle": "调整圆环振幅响应声音时分割的精细程度。",
+ "settings.sensitivity.desc.outline": "调整轮廓波浪对较小声音的反应灵敏度和起伏程度。",
+ "settings.glow.title": "发光",
+ "settings.glow.desc.wave": "在波浪边缘添加柔和的霓虹光晕,并调整其强度。\n注意:会占用额外的 GPU 资源。",
+ "settings.glow.desc.pad": "在光条周围添加光环般柔和的光晕。\n注意:会占用额外的 GPU 资源。",
+ "settings.glow.desc.circle": "沿圆环均衡器的边缘添加闪烁的光晕。\n注意:会占用额外的 GPU 资源。",
+ "settings.glow.desc.outline": "为轮廓添加柔和的霓虹光晕,并调整其强度。\n注意:会占用额外的 GPU 资源。",
+ "settings.radius.title": "半径",
+ "settings.radius.desc": "调整圆环模式下中央圆环的半径。",
+ "settings.section.sound_types": "声音类型(AI)",
+ "settings.show_ambient": "显示环境音",
+ "settings.show_speech": "显示人声",
+ "settings.show_danger": "显示危险音",
+ "settings.set_color": "设置颜色",
+ "settings.set_color_for": "设置颜色:{0}",
+ "settings.section.hotkeys": "快捷键",
+ "settings.hotkey.visual.title": "切换显示模式",
+ "settings.hotkey.visual.desc": "运行时实时切换图形形状的快捷键。",
+ "settings.hotkey.sound.title": "切换声音模式",
+ "settings.hotkey.sound.desc": "运行时实时切换声音模式的快捷键。",
+ "settings.hotkey.edit.title": "覆盖层设置",
+ "settings.hotkey.edit.desc": "打开或关闭覆盖层编辑界面的快捷键。",
+ "settings.hotkey.set": "设置快捷键",
+ "settings.section.advanced": "高级设置",
+ "settings.developer_mode.title": "开发者模式",
+ "settings.developer_mode.desc": "在覆盖层左上角显示 AI 听到的内容(它报告的声音名称、把握程度和耗时),并在右上角显示 FPS。显示内容为英文,不适合日常使用。",
+ "settings.developer_record.title": "将结果记录到文件",
+ "settings.developer_record.desc": "将 AI 的每个结果(它评分最高的声音名称、把握程度和耗时)写入 SoundVisualizer.exe 旁边 ai-log 文件夹中的新 CSV 文件,方便日后回看它听到了什么。如果无法写入该文件夹,文件会改存到 %LOCALAPPDATA%\\SoundVisualizer\\ai-log。文件可能会变得很大,用完后请关闭。",
+ "settings.auto_minimize.title": "启动时最小化启动器",
+ "settings.auto_minimize.desc": "启动覆盖层时,自动最小化此启动器窗口。",
+
+ "sounds.title": "选择声音类型",
+ "sounds.desc": "AI 能识别的声音与相似的声音归为一组,并按各自的默认类型显示。更改一组的类型后,组内声音的颜色会立即随新类型改变。",
+ "sounds.sibling_note": "AI 常常会把同一个声音同时识别为多个名称。例如警笛也会被识别为“警报”,而“警报”在另一组中。如果更改后某个声音仍按原来的类型显示,请一并更改相关的组或声音。",
+ "sounds.advanced.title": "高级模式",
+ "sounds.advanced.desc": "按组显示所有声音,可以逐个更改。",
+ "sounds.search.hint": "搜索声音",
+ "sounds.search.clear": "清除搜索内容",
+ "sounds.filter.title": "筛选",
+ "sounds.filter.all": "全部",
+ "sounds.filter.changed": "已更改",
+ "sounds.type.ambient": "环境音",
+ "sounds.type.speech": "人声",
+ "sounds.type.danger": "危险音",
+ "sounds.type.default": "{0}(默认)",
+ "sounds.type.mixed": "混合",
+ "sounds.changed_count": "已更改的声音:{0} 个",
+ "sounds.changed_from": "默认:{0}",
+ "sounds.reset.button": "全部恢复默认",
+ "sounds.reset.title": "要将所有声音恢复默认吗?",
+ "sounds.reset.message": "您更改过的所有声音都将恢复为默认类型。",
+ "sounds.reset.confirm": "恢复默认",
+ "sounds.reset.cancel": "取消",
+ "sounds.empty": "没有符合条件的声音。",
+ "sounds.group.mixed_note": "部分声音已单独更改",
+ "sounds.group.changed_line": "当前:{0} · 默认:{1}",
+ "sounds.overwrite.title": "要更改整组吗?",
+ "sounds.overwrite.message": "“{0}”组中的 {1} 个声音将改为“{2}”。您单独更改过的声音也会被覆盖。",
+ "sounds.overwrite.confirm": "更改",
+ "sounds.overwrite.cancel": "取消",
+ "sounds.basic_warning.title": "要切换到基本模式吗?",
+ "sounds.basic_warning.message": "有些组中含有您单独更改过的声音。在基本模式下为这类组选择类型时,组内所有声音都会变为该类型,您在该组中单独做的更改会被覆盖。在您选择之前,不会有任何更改。",
+ "sounds.basic_warning.confirm": "切换到基本模式",
+ "sounds.basic_warning.cancel": "保留高级模式",
+ "sounds.cd.sound": "{0},类型:{1}",
+ "sounds.cd.sound_changed": "{0},类型:{1},默认:{2}",
+ "sounds.cd.group": "{0}组,类型:{1},声音:{2} 个({3})",
+ "sounds.cd.group_changed": "{0}组,类型:{1},默认:{2},声音:{3} 个({4})",
+ "sounds.cd.group_header": "{0}组,声音:{1} 个",
+
+ "hotkey.prompt": "请按下按键(按 Esc 取消)",
+ "hotkey.none": "无",
+ "hotkey.button_name": "{0},快捷键:{1}",
+
+ "help.start.title": "开始使用",
+ "help.start.1": "在此窗口顶部选择语言,如有需要,可在“设置”标签页中更改图形的外观。",
+ "help.start.2": "在“首页”点击“{0}”。",
+ "help.start.3": "图形会沿着屏幕边缘,绘制在游戏、视频等其他窗口之上。鼠标点击会穿过图形,因此可以继续操作下方的窗口。",
+ "help.start.4": "如需关闭,请在“首页”点击“{0}”,或右键点击任务栏通知区域中的 SoundVisualizer 图标,然后选择“{0}”。",
+ "help.modes.title": "显示模式",
+ "help.mode.wave": "整个屏幕边缘会随声音大小向内推进,声音传来的一侧推进得更深。",
+ "help.mode.pad": "屏幕边缘的 8 个位置会亮起光条,方向显示最为清晰。",
+ "help.mode.circle": "屏幕中央的圆环会朝声音传来的方向鼓起。",
+ "help.mode.outline": "只绘制细细的波浪轮廓线,对游戏画面的遮挡更少。",
+ "help.modes.note": "“大小”“不透明度”等设置会按模式分别保存,可在“设置”标签页中更改。",
+ "help.sound.title": "声音类型",
+ "help.sound.kinds": "AI 会将听到的声音分为环境音、人声和危险音。默认情况下,环境音为白色,人声为黄色,危险音为红色。您可以在“设置”标签页中更改每种类型的颜色以及是否显示。",
+ "help.sound.groups": "在“分类”标签页中,相似的声音会归为一组,您可以为每组选择归入哪一类。例如,将“门铃”组改为危险音后,AI 识别到门铃或敲门声时,会使用危险音的颜色。不过,新设为危险音的声音只会参与 AI 判断类型的投票,因此并不代表每次门铃响都会显示为危险音。",
+ "help.sound.advanced": "要逐个更改声音,请开启高级模式。",
+ "help.sound.danger": "检测到危险音(如枪声)时,颜色会立即改变,而不是逐渐过渡。",
+ "help.pc.title": "PC 设置与快捷键",
+ "help.sound_mode.title": "声音模式",
+ "help.sound_mode.body": "若要同时分辨前后和左右的声音,请在 Windows 声音设置中将输出设备配置为 5.1 环绕声(6 声道)或 7.1 环绕声(8 声道)。使用普通立体声(2 声道)时,可以分辨左右,但无法分辨前后。“设置”标签页中的“声音模式”会在启动时以及输出设备更改时自动与该设备匹配,你也可以自行更改。",
+ "help.sound_mode.tip": "💡 如果没有 7.1 输出设备,可以安装 {0},并将输出设备设为 7.1 声道,即可用 7.1 环绕声显示覆盖层。",
+ "help.sound_mode.tip_link": "VB-CABLE 虚拟驱动程序",
+ "help.hotkeys.title": "实时自定义快捷键",
+ "help.hotkeys.body": "在“设置”标签页中点击快捷键按钮,然后按下单个按键或组合键(例如 Ctrl + Shift + A),即可随意更改快捷键。即使应用在后台运行,按下快捷键也会立即切换模式。",
+ "help.hotkeys.defaults": "默认情况下,F2 切换声音模式,F3 切换显示模式,F4 打开覆盖层设置。",
+ "help.size_lock.title": "锁定大小模式",
+ "help.size_lock.body": "锁定大小模式用与声音大小成正比的不透明度来表示声音,从而让图形保持固定大小。如果想在保持图形大小不变的同时看到它对声音的反应,这个模式会很有用。",
+ "help.editor.title": "覆盖层实时编辑模式",
+ "help.editor.body": "按下指定的快捷键(默认为 F4)即可开启覆盖层编辑模式。您可以直接在屏幕上用鼠标拖动来调整图形的大小上限,并在弹出的设置面板中微调细节。",
+ "help.developer.title": "开发者模式",
+ "help.developer.body": "开启开发者模式后,覆盖层左上角会显示 AI 听到的内容(声音名称、把握程度和耗时),右上角会显示 FPS。如果同时开启“{0}”,每个结果都会保存到 CSV 文件中。需要确认系统是否正常运行时,这个模式会很有用。",
+ "help.notes.title": "注意事项",
+ "help.note.direction_surround": "以 5.1 或 7.1 声道输出时,屏幕顶部表示来自前方的声音,底部表示来自后方的声音。左右两侧分别表示来自左边和右边的声音,而从屏幕四周同时出现的图形表示环境音或没有方向的声音。",
+ "help.note.direction_stereo": "以 2 声道(立体声)输出时,可以分辨左右,但无法分辨前后。此时屏幕顶部和底部的图形表示左右两侧听起来相近的声音以及空间感。",
+ "help.note.sources": "会显示通过 Windows 默认播放设备播放的所有应用的声音。应用发送到其他设备的声音(例如只用于语音聊天的耳机)不会显示。",
+ "help.note.volume": "如果在应用内或 Windows 音量合成器中把某个应用的音量调得很低或静音,它的声音可能几乎不显示或完全不显示。如果图形反应不明显,请把该声音调大一些,或在“设置”标签页中调高“灵敏度”。",
+ "help.note.ai": "AI 分类可能出错。在涉及安全的情况下,请勿将本应用作为唯一的保障手段。",
+ "help.faq.title": "常见问题",
+ "help.faq.stopped_q": "我没有手动停止,它却自己关闭了。",
+ "help.faq.stopped_a": "声音捕获停止且几秒内没有恢复时,它就会关闭,例如播放设备被断开、又没有其他可用设备时。此时“首页”会显示原因。请连接扬声器或耳机,或在 Windows 声音设置中设置默认播放设备,然后重新启动。",
+ "help.faq.no_graphic_q": "看不到任何图形。",
+ "help.faq.no_graphic_a": "请确认声音正通过 Windows 默认播放设备播放且没有静音,并查看“首页”是否显示了提示。如果游戏以独占全屏模式运行,请尝试改用无边框窗口或窗口模式。另外,请确认“设置”标签页中的“不透明度”没有调得太低,并且当前声音类型的显示没有被关闭。",
+ "help.faq.performance_q": "会占用很多 CPU 或 GPU 吗?",
+ "help.faq.performance_a": "如需减轻负担,请在“设置”标签页中调低“{0}”,或关闭“{1}”。",
+ "help.faq.language_q": "如何更改应用语言?",
+ "help.faq.language_a": "在此窗口顶部的“{0}”列表中选择。运行时也可以在覆盖层设置中更改。选择“{1}”将跟随 Windows 的显示语言。",
+ "help.report.title": "报告问题",
+ "help.report.desc": "如果遇到程序错误,或者某个声音被显示成了错误的类型,请告诉我们。浏览器中会打开 GitHub,并预先填入下方的环境信息,因此需要 GitHub 账号。如果没有账号,请改用电子邮件发送给我们。本应用绝不会发送任何音频。",
+ "help.report.github": "在 GitHub 上报告",
+ "help.report.mail": "通过电子邮件报告",
+ "help.report.copy": "复制环境信息",
+ "help.report.copied": "已复制环境信息。",
+ "help.report.copy_failed": "无法复制到剪贴板。请重试。",
+ "help.report.no_browser": "无法打开浏览器。请改为复制环境信息并发送给我们。",
+ "help.report.no_mail": "没有可用于发送邮件的应用。请复制环境信息,并通过其他方式发送。",
+ "help.report.issue_title": "来自应用的报告",
+ "help.report.issue_body": "发生了什么?\n\n您当时在听什么(哪个应用、哪种声音)?\n\n您原本期望的结果是什么?",
+ "help.about.title": "关于",
+ "help.about.version": "版本 {0}",
+ "help.about.license": "本应用基于 GNU Affero General Public License v3.0(AGPL-3.0)发布。“分类”标签页中的声音名称和分组基于 AudioSet 本体,并以 CC BY-SA 4.0 许可发布(详见下方的“开源许可”)。",
+ "help.licenses.button": "开源许可",
+ "help.licenses.title": "开源许可",
+ "help.licenses.close": "关闭",
+
+ "notice.ai_unavailable": "当前无法识别声音类型,所有声音都以环境音的颜色显示。请尝试将可视化关闭后重新开启。",
+ "notice.ai_unavailable_hidden": "当前无法识别声音类型,所有声音都会被视为环境音,但环境音的显示已关闭,因此不会显示任何内容。请在“设置”标签页中开启“{0}”,或尝试将可视化关闭后重新开启。",
+ "notice.stopped.title": "声音可视化已关闭",
+ "notice.stopped.no_device": "没有可打开的播放设备,因此无法捕获声音。请连接扬声器或耳机,或在 Windows 声音设置中设置默认播放设备,然后重新启动。",
+ "notice.stopped.capture_error": "声音捕获因出错而停止,图形已关闭。",
+ "notice.dismiss": "关闭提示",
+
+ "tray.open": "打开启动器",
+ "tray.exit": "退出",
+
+ "more_info.title": "GitHub 仓库与项目贡献",
+ "more_info.body": "SoundVisualizer 是一个开源项目,源代码公开在 GitHub 上。您可以通过报告错误、提出功能建议或参与开发来加入这个项目。点击下方按钮即可访问官方 GitHub 仓库!您还可以在 GitHub 的 Releases 页面查看并下载最新版本。",
+ "more_info.github_button": "访问 GitHub 仓库",
+
+ "dialog.notice_title": "提示",
+ "dialog.already_running": "覆盖层已在运行。",
+ "dialog.reset_title": "重置设置",
+ "dialog.reset_wave": "要将波浪模式的设置恢复为默认值吗?",
+ "dialog.reset_pad": "要将光条模式的设置恢复为默认值吗?",
+ "dialog.reset_circle": "要将圆环模式的设置恢复为默认值吗?",
+ "dialog.reset_outline": "要将轮廓模式的设置恢复为默认值吗?",
+ "dialog.reset_sound_types": "要将声音类型的设置恢复为默认值吗?",
+ "dialog.reset_all_title": "恢复默认设置",
+ "dialog.reset_all": "确定要将所有设置恢复为默认值吗?",
+ "dialog.link_error": "无法打开链接:{0}",
+
+ "editor.title": "覆盖层设置",
+ "editor.drag_tip": "用鼠标拖动屏幕上的辅助线,即可实时调整覆盖层波浪的高度上限和中央大小!",
+ "editor.glow": "发光效果",
+ "editor.glow_strength": "发光强度",
+ "editor.color": "颜色",
+ "editor.hotkeys": "快捷键设置",
+ "editor.developer_mode_desc": "显示 AI 听到的内容(左上角)和 FPS(右上角)",
+ "editor.save_close_hint": "键保存设置并关闭",
+ "editor.close_overlay": "退出覆盖层",
+ "editor.guide.limit": "{0}上限线",
+ "editor.guide.size": "{0}大小:{1}%",
+ "editor.guide.base_radius": "基础半径:{0}",
+ "editor.guide.circle_size": "圆环大小:{0}%",
+
+ "hud.visual_mode": "显示模式:[{0}] {1}",
+ "hud.sound_mode": "声音模式:[{0}] {1}",
+ "hud.overlay_settings": "覆盖层设置:[{0}]",
+ "hud.status.ready": "系统就绪",
+ "hud.status.stereo": "PC 音频:{0}ch(立体声)\n音源:等待中……",
+ "hud.status.surround": "PC 音频:8ch(7.1)\n音源:等待中……",
+
+ "color_picker.title": "选择颜色",
+ "color_picker.hex": "HEX 代码",
+ "color_picker.cancel": "取消",
+ "color_picker.apply": "应用",
+ "color_picker.area": "饱和度和亮度",
+ "color_picker.area_value": "饱和度 {0}%,亮度 {1}%",
+ "color_picker.hue": "色相",
+ "color_picker.keys_help": "用方向键调整。按住 Shift 键可大幅调整。",
+ "color_picker.presets": "常用颜色",
+ "color_picker.preset.white": "白色",
+ "color_picker.preset.red": "红色",
+ "color_picker.preset.orange": "橙色",
+ "color_picker.preset.yellow": "黄色",
+ "color_picker.preset.green": "绿色",
+ "color_picker.preset.cyan": "青色",
+ "color_picker.preset.blue": "蓝色",
+ "color_picker.preset.magenta": "洋红色",
+
+ "audio.error.no_device": "未找到输出设备。",
+ "audio.error.capture_stopped": "音频捕获意外停止:{0}",
+ "audio.error.capture_start_failed": "无法启动音频捕获引擎:{0}",
+
+ "tutorial.window_title": "教程",
+ "tutorial.skip": "跳过",
+ "tutorial.previous": "上一页",
+ "tutorial.next": "下一步",
+ "tutorial.done": "知道了",
+ "tutorial.page_status": "第 {0} 页,共 {1} 页",
+ "tutorial.pause": "暂停动画",
+ "tutorial.play": "播放动画",
+ "tutorial.sound_on": "打开声音",
+ "tutorial.sound_off": "关闭声音",
+ "tutorial.sound.title": "在屏幕上看见声音",
+ "tutorial.sound.body": "游戏或视频中的声音会以图形显示在屏幕边缘。声音越大,图形就越大。只显示这台电脑上播放的声音。本应用不会捕获您周围的声音。",
+ "tutorial.direction.title": "显示声音来自哪个方向",
+ "tutorial.direction.body": "以 2 声道(立体声)输出时,来自左边的声音会在左侧显示得更大,来自右边的声音会在右侧显示得更大。以 5.1 或 7.1 声道输出时,还能分辨前后:来自前方的声音显示在屏幕顶部,来自后方的声音显示在底部。",
+ "tutorial.types.title": "用颜色区分声音类型",
+ "tutorial.types.body": "AI 会将声音分为三种类型,并用不同的颜色显示。您可以在“设置”标签页中更改颜色。",
+ "tutorial.type.ambient_examples": "音乐、雨声等其他声音",
+ "tutorial.type.speech_examples": "说话声",
+ "tutorial.type.danger_examples": "枪声、爆炸声、警报声",
+ "tutorial.start.title": "如何开启和关闭",
+ "tutorial.start.hotkeys": "运行时,按 {0} 切换声音模式,按 {1} 切换显示模式,按 {2} 打开覆盖层设置。"
+}
diff --git a/SoundVisualizer.Core/Localization/Strings/zh-Hant.json b/SoundVisualizer.Core/Localization/Strings/zh-Hant.json
new file mode 100644
index 0000000..3b8088b
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/Strings/zh-Hant.json
@@ -0,0 +1,294 @@
+{
+ "common.language": "語言",
+ "common.reset_defaults": "重設為預設值",
+ "common.minimize": "最小化",
+ "common.close": "關閉",
+ "language.system": "系統預設",
+
+ "launcher.window_title": "SoundVisualizer 啟動器",
+ "launcher.tab.home": "首頁",
+ "launcher.tab.settings": "設定",
+ "launcher.tab.sounds": "分類",
+ "launcher.tab.help": "說明",
+ "launcher.tab.more_info": "深入了解",
+ "launcher.home.desc": "將看不見的聲音直接描繪在螢幕上。\n從遊戲到電影,開啟全新的體驗。",
+ "launcher.home.start": "開始",
+ "launcher.home.stop": "停止",
+ "launcher.home.tutorial": "查看教學",
+ "launcher.status.running": "狀態:執行中",
+ "launcher.status.idle": "狀態:準備就緒",
+
+ "settings.section.mode": "模式設定",
+ "settings.visual_mode.title": "視覺模式",
+ "settings.visual_mode.desc": "選擇螢幕上所繪製圖形的基本形狀。",
+ "settings.mode.wave": "波浪",
+ "settings.mode.pad": "光條",
+ "settings.mode.circle": "圓形",
+ "settings.mode.outline": "輪廓",
+ "settings.sound_mode.title": "聲音模式",
+ "settings.sound_mode.desc": "選擇符合喇叭配置的聲道。\n2 聲道喇叭請選擇「2 聲道」,環繞音效喇叭請選擇「5.1 聲道」或「7.1 聲道」。",
+ "settings.channels.2": "2 聲道",
+ "settings.channels.5_1": "5.1 聲道",
+ "settings.channels.7_1": "7.1 聲道",
+ "settings.max_fps.title": "最大畫面播放速率",
+ "settings.max_fps.desc": "設定繪製圖形時的最高畫面播放速率。",
+ "settings.section.mode_detail": "各模式詳細設定",
+ "settings.mode_card.wave": "波浪模式",
+ "settings.mode_card.pad": "光條模式",
+ "settings.mode_card.circle": "圓形模式",
+ "settings.mode_card.outline": "輪廓模式",
+ "settings.size_lock.title": "鎖定大小",
+ "settings.size_lock.desc": "讓圖形維持固定大小,改以不透明度呈現音量大小。",
+ "settings.fixed_size.wave": "波浪大小",
+ "settings.fixed_size.pad": "光條大小",
+ "settings.fixed_size.circle": "圓形大小",
+ "settings.fixed_size.outline": "輪廓粗細",
+ "settings.fixed_size.desc": "設定不透明度變化時,圖形維持的固定大小。",
+ "settings.max_opacity.title": "最大不透明度",
+ "settings.max_opacity.desc": "聲音最大時圖形達到的不透明度。",
+ "settings.size.title": "大小",
+ "settings.size.desc.wave": "調整波浪的上下高度與整體飽滿感。",
+ "settings.size.desc.pad": "調整光條回應聲音時的粗細與寬度。",
+ "settings.size.desc.circle": "調整聲音響起時,圓形等化器向外膨脹的幅度。",
+ "settings.size.desc.outline": "調整輪廓波浪的上下高度與整體飽滿感。",
+ "settings.opacity.title": "不透明度",
+ "settings.opacity.desc.wave": "數值越高越鮮明;數值越低越淡、越透明。",
+ "settings.opacity.desc.pad": "數值越高越鮮明;數值越低越淡、越透明。",
+ "settings.opacity.desc.circle": "數值越高越鮮明;數值越低越淡、越透明。",
+ "settings.opacity.desc.outline": "數值越高越鮮明;數值越低越淡、越透明。",
+ "settings.speed.title": "速度",
+ "settings.speed.desc.wave": "調整波浪流動與起伏的速度。",
+ "settings.speed.desc.pad": "調整光條轉向聲音來源方向的速度。",
+ "settings.speed.desc.circle": "調整圓形等化器隨聲音起伏與旋轉的速度。",
+ "settings.speed.desc.outline": "調整輪廓波浪流動與起伏的速度。",
+ "settings.sensitivity.title": "靈敏度",
+ "settings.sensitivity.desc.wave": "調整波浪對較小聲音的反應靈敏度與起伏程度。",
+ "settings.sensitivity.desc.pad": "調整光條捕捉聲音的能力,讓它更加醒目。",
+ "settings.sensitivity.desc.circle": "調整圓形振幅回應聲音時分割的細緻程度。",
+ "settings.sensitivity.desc.outline": "調整輪廓波浪對較小聲音的反應靈敏度與起伏程度。",
+ "settings.glow.title": "光暈",
+ "settings.glow.desc.wave": "在波浪邊緣加上柔和的霓虹光暈,並調整其強度。\n注意:會使用額外的 GPU 資源。",
+ "settings.glow.desc.pad": "在光條周圍加上如光環般柔和的光暈。\n注意:會使用額外的 GPU 資源。",
+ "settings.glow.desc.circle": "沿著圓形等化器的邊緣加上閃爍的光環。\n注意:會使用額外的 GPU 資源。",
+ "settings.glow.desc.outline": "為輪廓加上柔和的霓虹光暈,並調整其強度。\n注意:會使用額外的 GPU 資源。",
+ "settings.radius.title": "半徑",
+ "settings.radius.desc": "調整圓形模式中央圓環的半徑。",
+ "settings.section.sound_types": "聲音類型 (AI)",
+ "settings.show_ambient": "顯示環境音",
+ "settings.show_speech": "顯示人聲",
+ "settings.show_danger": "顯示危險音",
+ "settings.set_color": "設定顏色",
+ "settings.set_color_for": "設定顏色:{0}",
+ "settings.section.hotkeys": "快速鍵",
+ "settings.hotkey.visual.title": "切換視覺模式",
+ "settings.hotkey.visual.desc": "執行時即時切換圖形形狀的快速鍵。",
+ "settings.hotkey.sound.title": "切換聲音模式",
+ "settings.hotkey.sound.desc": "執行時即時切換聲音模式的快速鍵。",
+ "settings.hotkey.edit.title": "覆疊畫面設定",
+ "settings.hotkey.edit.desc": "開啟或關閉覆疊畫面編輯器的快速鍵。",
+ "settings.hotkey.set": "設定快速鍵",
+ "settings.section.advanced": "進階設定",
+ "settings.developer_mode.title": "開發人員模式",
+ "settings.developer_mode.desc": "在覆疊畫面左上角顯示 AI 聽到的內容(它回報的聲音名稱、把握程度和耗時),並在右上角顯示 FPS。顯示內容為英文,不適合日常使用。",
+ "settings.developer_record.title": "將結果記錄到檔案",
+ "settings.developer_record.desc": "將 AI 的每個結果(它評分最高的聲音名稱、把握程度和耗時)寫入 SoundVisualizer.exe 旁邊 ai-log 資料夾中的新 CSV 檔案,方便日後回顧它聽到了什麼。如果無法寫入該資料夾,檔案會改存到 %LOCALAPPDATA%\\SoundVisualizer\\ai-log。檔案可能會變得很大,用完後請關閉。",
+ "settings.auto_minimize.title": "開始時將啟動器最小化",
+ "settings.auto_minimize.desc": "覆疊畫面開始執行時,自動將這個啟動器視窗最小化。",
+
+ "sounds.title": "選擇聲音類型",
+ "sounds.desc": "AI 能辨識的聲音會和相似的聲音歸為同一群組,並依各自的預設類型顯示。變更群組的類型後,其中聲音的顏色會立即跟著新類型改變。",
+ "sounds.sibling_note": "AI 常常會把同一個聲音同時辨識為多個名稱。例如警笛也會被辨識為「警報」,而「警報」在另一個群組。如果變更後某個聲音仍以原本的類型顯示,請一併變更相關的群組或聲音。",
+ "sounds.advanced.title": "進階模式",
+ "sounds.advanced.desc": "依群組顯示所有聲音,可以逐一變更。",
+ "sounds.search.hint": "搜尋聲音",
+ "sounds.search.clear": "清除搜尋內容",
+ "sounds.filter.title": "篩選",
+ "sounds.filter.all": "全部",
+ "sounds.filter.changed": "已變更",
+ "sounds.type.ambient": "環境音",
+ "sounds.type.speech": "人聲",
+ "sounds.type.danger": "危險音",
+ "sounds.type.default": "{0} (預設)",
+ "sounds.type.mixed": "混合",
+ "sounds.changed_count": "已變更的聲音:{0} 個",
+ "sounds.changed_from": "預設:{0}",
+ "sounds.reset.button": "全部重設",
+ "sounds.reset.title": "要將所有聲音重設為預設值嗎?",
+ "sounds.reset.message": "你變更過的所有聲音,都會恢復為預設類型。",
+ "sounds.reset.confirm": "重設",
+ "sounds.reset.cancel": "取消",
+ "sounds.empty": "沒有符合的聲音。",
+ "sounds.group.mixed_note": "部分聲音已個別變更",
+ "sounds.group.changed_line": "目前:{0} · 預設:{1}",
+ "sounds.overwrite.title": "要變更整個群組嗎?",
+ "sounds.overwrite.message": "「{0}」群組中的 {1} 個聲音將改為「{2}」。你個別變更過的聲音也會被覆寫。",
+ "sounds.overwrite.confirm": "變更",
+ "sounds.overwrite.cancel": "取消",
+ "sounds.basic_warning.title": "要切換到基本模式嗎?",
+ "sounds.basic_warning.message": "有些群組含有你個別變更過的聲音。在基本模式下為這類群組選擇類型時,群組內所有聲音都會變成該類型,你在該群組中個別做的變更會被覆寫。在你選擇之前,不會有任何變更。",
+ "sounds.basic_warning.confirm": "切換到基本模式",
+ "sounds.basic_warning.cancel": "保留進階模式",
+ "sounds.cd.sound": "{0},類型:{1}",
+ "sounds.cd.sound_changed": "{0},類型:{1},預設:{2}",
+ "sounds.cd.group": "{0}群組,類型:{1},聲音:{2} 個 ({3})",
+ "sounds.cd.group_changed": "{0}群組,類型:{1},預設:{2},聲音:{3} 個 ({4})",
+ "sounds.cd.group_header": "{0}群組,聲音:{1} 個",
+
+ "hotkey.prompt": "請按下按鍵 (按 Esc 取消)",
+ "hotkey.none": "無",
+ "hotkey.button_name": "{0},快速鍵:{1}",
+
+ "help.start.title": "開始使用",
+ "help.start.1": "在此視窗頂端選擇語言,如有需要,可以在「設定」分頁中變更圖形的外觀。",
+ "help.start.2": "在「首頁」按一下「{0}」。",
+ "help.start.3": "圖形會沿著螢幕邊緣,繪製在遊戲、影片等其他視窗之上。滑鼠點按會穿過圖形,因此可以繼續操作下方的視窗。",
+ "help.start.4": "如要關閉,請在「首頁」按一下「{0}」,或以滑鼠右鍵按一下工作列通知區域中的 SoundVisualizer 圖示,然後選擇「{0}」。",
+ "help.modes.title": "視覺模式",
+ "help.mode.wave": "整個螢幕邊緣會隨著音量向內推進,聲音來源那一側會推進得更深。",
+ "help.mode.pad": "螢幕邊緣的 8 個位置會亮起光條,最容易看出聲音的方向。",
+ "help.mode.circle": "螢幕中央的圓環會朝聲音來源的方向膨脹。",
+ "help.mode.outline": "只以細線描繪波浪的輪廓,較不會遮住遊戲畫面。",
+ "help.modes.note": "「大小」、「不透明度」等設定會依模式分別儲存,可以在「設定」分頁中變更。",
+ "help.sound.title": "聲音類型",
+ "help.sound.kinds": "AI 會將聽到的聲音分為環境音、人聲和危險音。預設情況下,環境音為白色、人聲為黃色、危險音為紅色。你可以在「設定」分頁中變更各類型的顏色,以及是否顯示。",
+ "help.sound.groups": "在「分類」分頁中,相似的聲音會歸為同一群組,你可以為每個群組選擇歸入哪一類。例如將「門鈴」群組改為危險音後,AI 辨識到門鈴或敲門聲時,就會使用危險音的顏色。不過,新設為危險音的聲音只會參與 AI 判斷類型的投票,因此並不代表每次門鈴響都會顯示為危險音。",
+ "help.sound.advanced": "如要逐一變更聲音,請開啟進階模式。",
+ "help.sound.danger": "偵測到危險音 (例如槍聲) 時,顏色會立即改變,而不是逐漸轉變。",
+ "help.pc.title": "PC 設定與快速鍵",
+ "help.sound_mode.title": "聲音模式",
+ "help.sound_mode.body": "若要同時分辨前後和左右的聲音,請在 Windows 音效設定中將輸出裝置設為 5.1 環繞音效 (6 聲道) 或 7.1 環繞音效 (8 聲道)。使用一般的立體聲 (2 聲道) 時,可以分辨左右,但無法分辨前後。「設定」分頁中的「聲音模式」會在啟動時以及輸出裝置變更時自動配合該裝置,你也可以自行變更。",
+ "help.sound_mode.tip": "💡 如果沒有 7.1 輸出裝置,可以安裝 {0},並將輸出裝置設為 7.1 聲道,就能以 7.1 環繞音效顯示覆疊畫面。",
+ "help.sound_mode.tip_link": "VB-CABLE 虛擬驅動程式",
+ "help.hotkeys.title": "即時自訂快速鍵",
+ "help.hotkeys.body": "在「設定」分頁中按一下快速鍵按鈕,然後按下單一按鍵或組合鍵 (例如 Ctrl + Shift + A),就能隨意變更快速鍵。即使應用程式在背景執行,按下快速鍵也會立即切換模式。",
+ "help.hotkeys.defaults": "預設情況下,F2 切換聲音模式,F3 切換視覺模式,F4 開啟覆疊畫面設定。",
+ "help.size_lock.title": "鎖定大小模式",
+ "help.size_lock.body": "鎖定大小模式會以與音量成正比的不透明度呈現聲音,讓圖形維持固定大小。如果想讓圖形大小保持不變,同時看到它對聲音的反應,這個模式就很實用。",
+ "help.editor.title": "覆疊畫面即時編輯模式",
+ "help.editor.body": "按下指定的快速鍵 (預設為 F4) 即可開啟覆疊畫面編輯模式。你可以直接在螢幕上用滑鼠拖曳來調整圖形的大小上限,並在跳出的設定面板中微調細節。",
+ "help.developer.title": "開發人員模式",
+ "help.developer.body": "開啟開發人員模式後,覆疊畫面左上角會顯示 AI 聽到的內容(聲音名稱、把握程度和耗時),右上角會顯示 FPS。如果同時開啟「{0}」,每個結果都會儲存到 CSV 檔案中。想確認系統是否正常運作時,這個模式就很實用。",
+ "help.notes.title": "注意事項",
+ "help.note.direction_surround": "以 5.1 或 7.1 聲道輸出時,螢幕上方代表來自前方的聲音,下方代表來自後方的聲音。左右兩側分別代表來自左邊和右邊的聲音,而從螢幕四周同時出現的圖形,則是環境音或沒有方向性的聲音。",
+ "help.note.direction_stereo": "以 2 聲道 (立體聲) 輸出時,可以分辨左右,但無法分辨前後。這時螢幕上方和下方的圖形代表兩側聽起來差不多的聲音,以及空間感效果。",
+ "help.note.sources": "會顯示透過 Windows 預設播放裝置播放的所有應用程式的聲音。應用程式傳送到其他裝置的聲音 (例如只用於語音聊天的耳機) 不會顯示。",
+ "help.note.volume": "如果在應用程式內或 Windows 音量混音程式中,把某個應用程式的音量調得很低或靜音,它的聲音可能幾乎不顯示或完全不顯示。如果圖形反應不明顯,請把該聲音調大一些,或在「設定」分頁中調高「靈敏度」。",
+ "help.note.ai": "AI 分類可能會出錯。在攸關安全的情況下,請勿將這個應用程式當作唯一的依靠。",
+ "help.faq.title": "常見問題",
+ "help.faq.stopped_q": "我沒有關閉它,它卻自己關掉了。",
+ "help.faq.stopped_a": "聲音擷取停止且幾秒內沒有恢復時,它就會關閉,例如播放裝置被拔除,又沒有其他可用的裝置時。此時「首頁」會顯示原因。請連接喇叭或耳機,或在 Windows 音效設定中設定預設播放裝置,然後重新開始。",
+ "help.faq.no_graphic_q": "看不到任何圖形。",
+ "help.faq.no_graphic_a": "請確認聲音正透過 Windows 預設播放裝置播放且沒有靜音,並查看「首頁」是否顯示了提示。如果遊戲以獨佔全螢幕模式執行,請試著改用無邊框視窗或視窗模式。另外,也請確認「設定」分頁中的「不透明度」沒有調得太低,以及目前這種聲音類型的顯示沒有關閉。",
+ "help.faq.performance_q": "會占用很多 CPU 或 GPU 嗎?",
+ "help.faq.performance_a": "如要減輕負擔,請在「設定」分頁中調低「{0}」,或關閉「{1}」。",
+ "help.faq.language_q": "如何變更應用程式語言?",
+ "help.faq.language_a": "從此視窗頂端的「{0}」清單中選擇。執行時也可以在覆疊畫面設定中變更。選擇「{1}」會跟隨 Windows 的顯示語言。",
+ "help.report.title": "回報問題",
+ "help.report.desc": "如果遇到程式錯誤,或是聲音被顯示成錯誤的類型,請告訴我們。瀏覽器會開啟 GitHub,並預先填入下方的環境資訊,因此需要 GitHub 帳號。如果沒有帳號,請改用電子郵件寄給我們。這個應用程式絕不會傳送任何音訊。",
+ "help.report.github": "在 GitHub 上回報",
+ "help.report.mail": "透過電子郵件回報",
+ "help.report.copy": "複製環境資訊",
+ "help.report.copied": "已複製環境資訊。",
+ "help.report.copy_failed": "無法複製到剪貼簿。請再試一次。",
+ "help.report.no_browser": "無法開啟瀏覽器。請改為複製環境資訊並傳送給我們。",
+ "help.report.no_mail": "找不到可以寄送電子郵件的應用程式。請複製環境資訊,再透過其他方式傳送。",
+ "help.report.issue_title": "來自應用程式的回報",
+ "help.report.issue_body": "發生了什麼事?\n\n你當時在聽什麼 (哪個應用程式、哪種聲音)?\n\n你原本預期會是什麼結果?",
+ "help.about.title": "關於",
+ "help.about.version": "版本 {0}",
+ "help.about.license": "本應用程式以 GNU Affero General Public License v3.0 (AGPL-3.0) 授權發布。「分類」分頁中的聲音名稱和群組以 AudioSet 本體為基礎,並以 CC BY-SA 4.0 授權發布 (詳見下方的「開放原始碼授權」)。",
+ "help.licenses.button": "開放原始碼授權",
+ "help.licenses.title": "開放原始碼授權",
+ "help.licenses.close": "關閉",
+
+ "notice.ai_unavailable": "目前無法辨識聲音類型,所有聲音都會以環境音的顏色顯示。請試著關閉聲音視覺化後再重新開啟。",
+ "notice.ai_unavailable_hidden": "目前無法辨識聲音類型,所有聲音都會被視為環境音,但環境音的顯示已關閉,因此不會顯示任何內容。請在「設定」分頁中開啟「{0}」,或試著關閉聲音視覺化後再重新開啟。",
+ "notice.stopped.title": "聲音視覺化已關閉",
+ "notice.stopped.no_device": "沒有可開啟的播放裝置,因此無法擷取聲音。請連接喇叭或耳機,或在 Windows 音效設定中設定預設播放裝置,然後重新開始。",
+ "notice.stopped.capture_error": "聲音擷取因發生錯誤而停止,因此圖形已關閉。",
+ "notice.dismiss": "關閉提示",
+
+ "tray.open": "開啟啟動器",
+ "tray.exit": "結束",
+
+ "more_info.title": "GitHub 存放庫與專案貢獻",
+ "more_info.body": "SoundVisualizer 是開放原始碼專案,原始碼公開在 GitHub 上。你可以透過回報錯誤、建議功能或參與開發來加入這個專案。按一下下方按鈕,前往官方 GitHub 存放庫吧!你也可以在 GitHub 的 Releases 頁面查看並下載最新版本。",
+ "more_info.github_button": "前往 GitHub 存放庫",
+
+ "dialog.notice_title": "提示",
+ "dialog.already_running": "覆疊畫面已在執行中。",
+ "dialog.reset_title": "重設設定",
+ "dialog.reset_wave": "要將波浪模式的設定重設為預設值嗎?",
+ "dialog.reset_pad": "要將光條模式的設定重設為預設值嗎?",
+ "dialog.reset_circle": "要將圓形模式的設定重設為預設值嗎?",
+ "dialog.reset_outline": "要將輪廓模式的設定重設為預設值嗎?",
+ "dialog.reset_sound_types": "要將聲音類型的設定重設為預設值嗎?",
+ "dialog.reset_all_title": "重設為預設值",
+ "dialog.reset_all": "確定要將所有設定重設為預設值嗎?",
+ "dialog.link_error": "無法開啟連結:{0}",
+
+ "editor.title": "覆疊畫面設定",
+ "editor.drag_tip": "用滑鼠拖曳螢幕上的輔助線,就能即時調整覆疊畫面波浪的高度上限和中央大小!",
+ "editor.glow": "光暈效果",
+ "editor.glow_strength": "光暈強度",
+ "editor.color": "顏色",
+ "editor.hotkeys": "快速鍵設定",
+ "editor.developer_mode_desc": "顯示 AI 聽到的內容(左上角)和 FPS(右上角)",
+ "editor.save_close_hint": "鍵儲存設定並關閉",
+ "editor.close_overlay": "結束覆疊畫面",
+ "editor.guide.limit": "{0}上限線",
+ "editor.guide.size": "{0}大小:{1}%",
+ "editor.guide.base_radius": "基本半徑:{0}",
+ "editor.guide.circle_size": "圓形大小:{0}%",
+
+ "hud.visual_mode": "視覺模式:[{0}] {1}",
+ "hud.sound_mode": "聲音模式:[{0}] {1}",
+ "hud.overlay_settings": "覆疊畫面設定:[{0}]",
+ "hud.status.ready": "系統已就緒",
+ "hud.status.stereo": "PC 音訊:{0}ch (立體聲)\n音源:等待中……",
+ "hud.status.surround": "PC 音訊:8ch (7.1)\n音源:等待中……",
+
+ "color_picker.title": "選擇顏色",
+ "color_picker.hex": "HEX 色碼",
+ "color_picker.cancel": "取消",
+ "color_picker.apply": "套用",
+ "color_picker.area": "飽和度和亮度",
+ "color_picker.area_value": "飽和度 {0}%,亮度 {1}%",
+ "color_picker.hue": "色相",
+ "color_picker.keys_help": "用方向鍵調整。按住 Shift 鍵可大幅調整。",
+ "color_picker.presets": "常用顏色",
+ "color_picker.preset.white": "白色",
+ "color_picker.preset.red": "紅色",
+ "color_picker.preset.orange": "橘色",
+ "color_picker.preset.yellow": "黃色",
+ "color_picker.preset.green": "綠色",
+ "color_picker.preset.cyan": "青色",
+ "color_picker.preset.blue": "藍色",
+ "color_picker.preset.magenta": "洋紅色",
+
+ "audio.error.no_device": "找不到輸出裝置。",
+ "audio.error.capture_stopped": "音訊擷取意外停止:{0}",
+ "audio.error.capture_start_failed": "無法啟動音訊擷取引擎:{0}",
+
+ "tutorial.window_title": "教學",
+ "tutorial.skip": "略過",
+ "tutorial.previous": "上一頁",
+ "tutorial.next": "下一步",
+ "tutorial.done": "我知道了",
+ "tutorial.page_status": "第 {0} 頁,共 {1} 頁",
+ "tutorial.pause": "暫停動畫",
+ "tutorial.play": "播放動畫",
+ "tutorial.sound_on": "開啟聲音",
+ "tutorial.sound_off": "關閉聲音",
+ "tutorial.sound.title": "在螢幕上看見聲音",
+ "tutorial.sound.body": "遊戲或影片的聲音會沿著螢幕邊緣描繪出來。聲音越大,圖形就越大。只會顯示這台電腦上播放的聲音。這個應用程式不會擷取你周遭的聲音。",
+ "tutorial.direction.title": "顯示聲音來自哪個方向",
+ "tutorial.direction.body": "以 2 聲道 (立體聲) 輸出時,來自左邊的聲音會在左側顯示得更大,來自右邊的聲音會在右側顯示得更大。以 5.1 或 7.1 聲道輸出時,還能分辨前後:來自前方的聲音顯示在螢幕上方,來自後方的聲音顯示在下方。",
+ "tutorial.types.title": "用顏色區分聲音類型",
+ "tutorial.types.body": "AI 會將聲音分為三種類型,並以不同顏色顯示。你可以在「設定」分頁中變更顏色。",
+ "tutorial.type.ambient_examples": "音樂、雨聲等其他聲音",
+ "tutorial.type.speech_examples": "說話聲",
+ "tutorial.type.danger_examples": "槍聲、爆炸聲、警報聲",
+ "tutorial.start.title": "如何開啟和關閉",
+ "tutorial.start.hotkeys": "執行時,按 {0} 切換聲音模式,按 {1} 切換視覺模式,按 {2} 開啟覆疊畫面設定。"
+}
diff --git a/SoundVisualizer.Core/Localization/UiLanguage.cs b/SoundVisualizer.Core/Localization/UiLanguage.cs
new file mode 100644
index 0000000..6c5d18d
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/UiLanguage.cs
@@ -0,0 +1,12 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Localization
+{
+ ///
+ /// 화면 언어 하나. 는 BCP-47 꼴 코드로, 설정 파일의 Language 값과 문자열 표 파일 이름
+ /// (Localization/Strings/{Code}.json)에 그대로 쓴다. 은 그 언어로 쓴 이름이라,
+ /// 지금 화면 언어를 읽지 못하는 사람도 언어 목록에서 자기 언어를 찾을 수 있다.
+ ///
+ public sealed record UiLanguage(string Code, string NativeName, bool IsRightToLeft = false);
+}
diff --git a/SoundVisualizer.Core/Localization/UiLanguages.cs b/SoundVisualizer.Core/Localization/UiLanguages.cs
new file mode 100644
index 0000000..e2a7b3b
--- /dev/null
+++ b/SoundVisualizer.Core/Localization/UiLanguages.cs
@@ -0,0 +1,136 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Globalization;
+using System.Text;
+
+namespace SoundVisualizer.Localization
+{
+ ///
+ /// 고를 수 있는 화면 언어 18개와, 설정값( 또는 언어 코드)을 실제 화면 언어로 푸는 규칙.
+ /// 목록 순서는 언어 콤보에 그대로 나온다. 안드로이드 판과 같은 언어들이다.
+ ///
+ public static class UiLanguages
+ {
+ /// Windows 화면 언어를 따르는 선택. 새로 설치하면 이것으로 시작한다.
+ public const string SystemCode = "system";
+
+ /// 지원하지 않는 언어와, 표에 없는 문자열이 기대는 언어.
+ public const string DefaultCode = "en";
+
+ public static IReadOnlyList All { get; } = new[]
+ {
+ new UiLanguage("en", "English"),
+ new UiLanguage("ko", "한국어"),
+ new UiLanguage("ja", "日本語"),
+ new UiLanguage("zh-Hans", "简体中文"),
+ new UiLanguage("zh-Hant", "繁體中文"),
+ new UiLanguage("es", "Español"),
+ new UiLanguage("pt-BR", "Português (Brasil)"),
+ new UiLanguage("fr", "Français"),
+ new UiLanguage("de", "Deutsch"),
+ new UiLanguage("ru", "Русский"),
+ new UiLanguage("it", "Italiano"),
+ new UiLanguage("vi", "Tiếng Việt"),
+ new UiLanguage("th", "ไทย"),
+ new UiLanguage("id", "Bahasa Indonesia"),
+ new UiLanguage("tr", "Türkçe"),
+ new UiLanguage("pl", "Polski"),
+ new UiLanguage("hi", "हिन्दी"),
+ new UiLanguage("ar", "العربية", IsRightToLeft: true),
+ };
+
+ // 예전 판이 설정 파일에 남긴 언어 값(런처 콤보에 보이던 이름과 약어). 읽을 때 한 번 코드로 바꾸고, 저장은 코드로 한다.
+ // 첫 실행 기본값이던 "ENG" 도 영어다.
+ private static readonly Dictionary LegacySettingValues = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["KOR"] = "ko",
+ ["English"] = "en",
+ ["ENG"] = "en",
+ ["日本語"] = "ja",
+ ["中文"] = "zh-Hans",
+ ["Español"] = "es",
+ ["Français"] = "fr",
+ ["Deutsch"] = "de",
+ ["Русский"] = "ru",
+ };
+
+ /// 코드로 언어를 찾는다. 대소문자는 가리지 않는다("zh-hans" 도 간체 중국어). 없으면 null.
+ public static UiLanguage? Find(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code)) return null;
+ string trimmed = code.Trim();
+ foreach (var language in All)
+ {
+ if (string.Equals(language.Code, trimmed, StringComparison.OrdinalIgnoreCase)) return language;
+ }
+ return null;
+ }
+
+ public static bool IsRightToLeft(string? code) => Find(code)?.IsRightToLeft ?? false;
+
+ ///
+ /// 설정 파일에서 읽은 값을 저장할 값으로 맞춘다. 결과는 이거나 의 코드다.
+ /// 예전 판의 값("KOR", "日本語" 등)은 코드로 옮긴다. 비었거나 모르는 값(다음 판의 새 언어 등)은 시스템 언어를 따르게 한다.
+ ///
+ public static string NormalizeSetting(string? stored)
+ {
+ if (string.IsNullOrWhiteSpace(stored)) return SystemCode;
+
+ // 예전 값은 XAML 에 적힌 글자 그대로라 보통 조합형(NFC)이지만, 다른 편집기를 거쳤을 수도 있어 맞춰서 찾는다.
+ string value = stored.Trim().Normalize(NormalizationForm.FormC);
+ if (string.Equals(value, SystemCode, StringComparison.OrdinalIgnoreCase)) return SystemCode;
+ if (LegacySettingValues.TryGetValue(value, out var migrated)) return migrated;
+ return Find(value)?.Code ?? SystemCode;
+ }
+
+ /// 설정값을 실제 화면 언어 코드로 푼다. 이면 (Windows 화면 언어)를 따른다.
+ public static string Resolve(string? setting, CultureInfo uiCulture)
+ {
+ string normalized = NormalizeSetting(setting);
+ return normalized == SystemCode ? FromCultureName(uiCulture.Name) : normalized;
+ }
+
+ ///
+ /// Windows 문화권 이름("ko-KR", "zh-TW", "pt-PT" 등)을 지원하는 언어로 바꾼다.
+ /// 중국어는 번체를 쓰는 대만·홍콩·마카오(또는 Hant 문자 표기)만 번체, 나머지는 간체다. 포르투갈어는 모두 브라질 포르투갈어,
+ /// 인도네시아어는 옛 코드 "in" 도 받는다. 그 밖에는 언어 부분이 지원 목록에 있으면 그 언어, 없으면 영어다.
+ ///
+ public static string FromCultureName(string? cultureName)
+ {
+ if (string.IsNullOrWhiteSpace(cultureName)) return DefaultCode;
+
+ string[] parts = cultureName.Trim().Replace('_', '-').Split('-', StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length == 0) return DefaultCode;
+ string[] subtags = parts[1..];
+
+ switch (parts[0].ToLowerInvariant())
+ {
+ case "zh":
+ // 문자 표기가 있으면 그것을 따른다. 옛 .NET 의 "zh-CHT"/"zh-CHS" 도 같은 뜻이다.
+ if (HasSubtag(subtags, "Hant", "CHT")) return "zh-Hant";
+ if (HasSubtag(subtags, "Hans", "CHS")) return "zh-Hans";
+ return HasSubtag(subtags, "TW", "HK", "MO") ? "zh-Hant" : "zh-Hans";
+ case "pt":
+ return "pt-BR";
+ case "in":
+ case "id":
+ return "id";
+ default:
+ return Find(parts[0])?.Code ?? DefaultCode;
+ }
+ }
+
+ private static bool HasSubtag(string[] subtags, params string[] wanted)
+ {
+ foreach (var subtag in subtags)
+ {
+ foreach (var w in wanted)
+ {
+ if (string.Equals(subtag, w, StringComparison.OrdinalIgnoreCase)) return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Rendering/FrameStep.cs b/SoundVisualizer.Core/Rendering/FrameStep.cs
new file mode 100644
index 0000000..e229451
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/FrameStep.cs
@@ -0,0 +1,63 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 프레임마다 곱하던 감쇠·따라붙기 계수를 60fps 한 프레임을 기준으로 시간 정규화한다(Android VisualizerEngine 의 k 와 norm).
+ ///
+ /// 계수를 그린 프레임마다 그대로 곱하면 최대 프레임(30~모니터 주사율)에 따라 움직임이 달라진다. 144fps 에서는 60fps 보다
+ /// 2.4배 빨리 줄고 빨리 따라붙었다. 지난 프레임 뒤로 흐른 시간을 60fps 프레임 수(step)로 재서, 감쇠는 decay^step,
+ /// 따라붙기는 1-(1-c)^step 으로 바꾼다. 60fps 에서는 step 이 1 이라 예전 값 그대로다.
+ ///
+ public static class FrameStep
+ {
+ /// 기준 프레임 길이(60fps). 예전 계수는 모두 이 간격으로 그릴 때의 값이다.
+ public const double ReferenceFrameMs = 1000.0 / 60.0;
+
+ ///
+ /// 한 번에 진행하는 가장 긴 시간(60fps 프레임 4개). 창이 잠깐 멈췄다 돌아와도 한 프레임에 크게 건너뛰지 않는다.
+ /// Android 는 아래로도 0.25(240fps)에서 자르지만 PC 는 최대 프레임을 모니터 주사율(240Hz 넘는 모니터도 있다)까지
+ /// 올릴 수 있어 아래로는 자르지 않는다. 자르면 그보다 빠른 화면에서 다시 빨라진다.
+ ///
+ public const double MaxStep = 4.0;
+
+ /// 지난 프레임 뒤로 흐른 시간을 60fps 프레임 수로 바꾼다. 0 과 사이다.
+ public static double FromElapsed(double elapsedMs)
+ {
+ if (!(elapsedMs > 0)) return 0; // 같은 시각에 두 번 불렸거나 시계가 뒤로 갔다. 진행하지 않는다.
+ return Math.Min(elapsedMs / ReferenceFrameMs, MaxStep);
+ }
+
+ /// 60fps 한 프레임에 을 곱하던 감쇠를 프레임 분량으로 바꾼다.
+ public static float Decay(float perFrame, double step) =>
+ step == 1.0 ? perFrame : (float)Math.Pow(perFrame, step);
+
+ ///
+ /// 60fps 한 프레임에 남은 거리의 만큼 따라붙던 계수를 프레임 분량으로 바꾼다.
+ /// 1 이면 바로 붙고, 0 이면 움직이지 않는다.
+ ///
+ public static double Approach(double perFrame, double step) =>
+ step == 1.0 ? perFrame : 1.0 - Math.Pow(1.0 - perFrame, step);
+ }
+
+ ///
+ /// 그린 프레임 사이의 시간을 재서 의 step 으로 준다. 첫 프레임과 직후는 1 이다
+ /// (쉬다가 깨어난 첫 프레임이 쉰 시간만큼 한꺼번에 진행하지 않게). 렌더 스레드에서만 쓴다.
+ ///
+ public sealed class FrameClock
+ {
+ private double _lastMs = double.NaN;
+
+ /// 이번 프레임의 시각(밀리초, 단조 증가)을 받아 지난 프레임 뒤로 흐른 60fps 프레임 수를 돌려준다.
+ public double Next(double nowMs)
+ {
+ double last = _lastMs;
+ _lastMs = nowMs;
+ return double.IsNaN(last) ? 1.0 : FrameStep.FromElapsed(nowMs - last);
+ }
+
+ /// 다음 프레임을 첫 프레임처럼 다룬다.
+ public void Reset() => _lastMs = double.NaN;
+ }
+}
diff --git a/SoundVisualizer.Core/Rendering/LateDangerHold.cs b/SoundVisualizer.Core/Rendering/LateDangerHold.cs
new file mode 100644
index 0000000..f416c28
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/LateDangerHold.cs
@@ -0,0 +1,99 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using SoundVisualizer.Audio;
+
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 표시를 꺼 둔 종류라 그리지 못한 피크를 약 1초 보관하는 링 버퍼(Android VisualizerEngine 의 holdL/holdR).
+ ///
+ /// 위협음만 보이게 해 두면 총성 같은 짧은 소리는 먼저 숨긴 라벨(환경음 등)로 들어오고, 위협음 판정은 다음 분류(250ms 주기)와
+ /// 추론 시간만큼 늦게 온다. 그때 피크를 이미 버렸다면 남은 잔향으로 0 에서 자라 거의 보이지 않는다. 그래서 칸마다 가장 큰
+ /// 프레임 하나를 남겨 두고, 늦은 판정이 오면 엔진이 이것으로 크기와 방향을 다시 잡는다().
+ ///
+ /// 칸 하나는 60fps 두 프레임(약 33ms)이라 30칸이면 약 1초다. 분류기는 최근 약 1초 창을 250ms 마다 판정하므로, 판정이 몇 번
+ /// 늦어져도 그 판정을 부른 소리는 이 안에 있다. 방향이 섞이지 않게 채널들을 한 프레임으로 묶어 다룬다.
+ /// 렌더 스레드에서만 쓴다. 할당은 없다.
+ ///
+ public sealed class LateDangerHold
+ {
+ public const int Slots = 30;
+ public const int Channels = PeakAccumulator.ChannelCount;
+
+ /// 칸 하나의 길이(60fps 프레임 수).
+ public const double SlotFrames = 2;
+
+ private readonly float[] _ring = new float[Slots * Channels];
+ private readonly float[] _ringLevels = new float[Slots];
+ private readonly float[] _current = new float[Channels]; // 채우는 중인 칸
+ private float _currentLevel;
+ private int _index; // 다음에 덮어쓸(가장 오래된) 칸
+ private double _frames; // 채우는 중인 칸에 쌓인 시간(60fps 프레임 수)
+
+ /// 마지막으로 큰 피크를 넣은 뒤 그 피크가 밀려나기까지 남은 칸 수. 0 이면 보관한 것이 없어 링을 훑지 않는다.
+ private int _liveSlots;
+
+ /// 보관한 피크가 있는지.
+ public bool HasData => _liveSlots > 0;
+
+ ///
+ /// 그리지 못한 프레임의 피크를 지금 칸에 모은다. 은 그 프레임의 크기(지금 사운드 모드가 그리는 채널의
+ /// 최댓값)이고, 이하면 깨울 일도 없으니 넣지 않는다. 칸마다 가장 큰 프레임 하나만 남는다.
+ ///
+ public void Keep(ReadOnlySpan peaks, float level, float threshold)
+ {
+ if (!(level > threshold)) return;
+ if (level > _currentLevel)
+ {
+ peaks[..Channels].CopyTo(_current);
+ _currentLevel = level;
+ }
+ _liveSlots = Slots + 1; // 지금 칸 + 링 한 바퀴
+ }
+
+ ///
+ /// 보관 시간을 (60fps 프레임 수)만큼 흘린다. 칸이 차면 링에 넣고 가장 오래된 칸을 덮어쓴다.
+ /// 큰 피크가 모두 밀려나면 링은 전부 0 이 되므로 그다음부터는 아무것도 하지 않는다.
+ ///
+ public void Advance(double frames)
+ {
+ if (_liveSlots == 0) return;
+ _frames += frames;
+ while (_frames >= SlotFrames)
+ {
+ _frames -= SlotFrames;
+ _current.AsSpan().CopyTo(_ring.AsSpan(_index * Channels, Channels));
+ _ringLevels[_index] = _currentLevel;
+ _index = (_index + 1) % Slots;
+ Array.Clear(_current);
+ _currentLevel = 0;
+ if (--_liveSlots == 0)
+ {
+ _frames = 0;
+ return;
+ }
+ }
+ }
+
+ ///
+ /// 오래된 칸부터 번째 칸(0..). 마지막()은 채우는 중인 칸이다.
+ ///
+ public ReadOnlySpan Slot(int n) =>
+ n < Slots ? _ring.AsSpan((_index + n) % Slots * Channels, Channels) : _current;
+
+ /// 과 같은 순서로 그 칸의 크기.
+ public float SlotLevel(int n) => n < Slots ? _ringLevels[(_index + n) % Slots] : _currentLevel;
+
+ /// 보관한 것을 모두 버린다. 한 번 쓴 소리로 두 번 번쩍이지 않게, 쓰고 나면 부른다.
+ public void Clear()
+ {
+ Array.Clear(_ring);
+ Array.Clear(_ringLevels);
+ Array.Clear(_current);
+ _currentLevel = 0;
+ _frames = 0;
+ _liveSlots = 0;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Rendering/OverlayInput.cs b/SoundVisualizer.Core/Rendering/OverlayInput.cs
new file mode 100644
index 0000000..a4b40fd
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/OverlayInput.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 프레임마다 엔진()에 넘기는 바깥 값(Android VisualizerInputs 가 읽어 오던 것).
+ /// 설정과 AI 결과는 부르는 쪽이 프레임마다 한 번만 읽어 이것으로 넘긴다. 그래야 한 프레임의 판단이 모두 같은 결과로 된다.
+ ///
+ /// 지금 표현 모드의 설정.
+ /// 표현 모드( 등).
+ /// 사운드 모드(0: 2.0, 1: 5.1, 2: 7.1).
+ /// AI 가 확정한 소리 종류. 결과가 없으면 환경음이다.
+ /// 그 종류를 화면에 보이게 해 두었는지.
+ public readonly record struct OverlayInput(VisualModeSettings Settings, int VisualMode, int SoundMode, SoundType Label, bool Shown);
+}
diff --git a/SoundVisualizer.Core/Rendering/SoundWakeSignal.cs b/SoundVisualizer.Core/Rendering/SoundWakeSignal.cs
new file mode 100644
index 0000000..c8197c6
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/SoundWakeSignal.cs
@@ -0,0 +1,51 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 쉬는(idle) 오버레이를 소리가 나는 순간 깨운다(Android SoundWakeSignal, #170).
+ ///
+ /// 쉬는 동안 오버레이는 33ms 마다 단축키와 소리를 확인한다. 캡처 스레드는 어차피 버퍼마다 소리 크기를 알고 있으므로,
+ /// 조용히 쉬는 동안에는 큰 소리가 든 버퍼에서 바로 확인을 앞당기게 한다. 그러면 다음 33ms 를 기다리지 않고 그리기 시작한다.
+ ///
+ /// 순서는 이렇다. 오버레이(UI 스레드)가 확인하기 **직전에** 을 부르고 소리를 확인한다. 그 뒤 캡처 스레드가
+ /// 에서 큰 소리를 보면 한 번만 true 를 돌려주고, 캡처 스레드는 확인을 UI 스레드에 넘긴다.
+ /// ** 뒤에 들어온 버퍼만 센다.** 마지막 소리 크기만 보고 깨우면, 캡처가 멈춰 그 값이 크게 남아 있을 때 쉬지 못한다.
+ /// 표시를 꺼 둔 종류의 소리가 계속 나는 동안에는 오버레이가 해 둔다. 그러지 않으면 버퍼마다(약 10ms) 깨어
+ /// 33ms 확인보다 오히려 잦아진다.
+ ///
+ /// 캡처 스레드가 버퍼마다 더 하는 일은 원자 변수 읽기 한 번이다. 할당은 없다.
+ ///
+ public sealed class SoundWakeSignal
+ {
+ private readonly float _threshold;
+ private int _armed;
+
+ /// 소리 크기가 이 값보다 크면 깨운다.
+ public SoundWakeSignal(float threshold)
+ {
+ _threshold = threshold;
+ }
+
+ /// 지금 큰 소리를 기다리는지. 캡처 스레드가 소리 크기를 잴지 먼저 보는 데 쓴다.
+ public bool IsArmed => Volatile.Read(ref _armed) != 0;
+
+ /// 지금부터 들어오는 큰 소리 한 번을 알린다. 오버레이가 소리를 확인하기 직전에 부른다.
+ public void Arm() => Volatile.Write(ref _armed, 1);
+
+ /// 큰 소리를 기다리지 않는다. 깨어났거나 33ms 확인만 할 때 부른다.
+ public void Disarm() => Volatile.Write(ref _armed, 0);
+
+ ///
+ /// 캡처 스레드가 버퍼를 넘긴 직후 그 버퍼의 소리 크기로 부른다. 기다리는 중이고 크기가 문턱을 넘으면 true 를 한 번만
+ /// 돌려준다(여러 스레드가 동시에 불러도 하나만). 그때 부르는 쪽이 오버레이의 확인을 앞당긴다.
+ ///
+ public bool OnBuffer(float level)
+ {
+ if (Volatile.Read(ref _armed) == 0) return false; // 깨어 있거나 이미 알렸다. 그리는 동안은 대부분 여기서 끝난다.
+ if (!(level > _threshold)) return false;
+ return Interlocked.Exchange(ref _armed, 0) == 1;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Rendering/StereoUpmix.cs b/SoundVisualizer.Core/Rendering/StereoUpmix.cs
new file mode 100644
index 0000000..5b99b24
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/StereoUpmix.cs
@@ -0,0 +1,95 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 사운드 모드 2.0: 앞 좌우 두 채널을 화면 둘레 여덟 자리로 펼친다(Android VisualizerEngine 의 upmix, Mid/Side).
+ ///
+ /// 좌우에 함께 있는 몫(Mid = 작은 쪽)은 앞(상단 가운데·양쪽 위 모서리)에, 한쪽이 더 큰 몫(Side = 차이)만 그쪽 옆에 그린다.
+ /// 그래서 가운데 소리는 5.1·7.1 의 앞 가운데(FC)처럼 위에 서고, 한쪽으로 치우친 소리는 그쪽 옆만 깊어진다. 예전처럼 좌우를
+ /// 옆·뒤로 그대로 복사하면 가운데 소리가 양옆에서 똑같이 그려져 "양쪽에서 나는 소리" 와 구별되지 않았다.
+ ///
+ /// 뒤쪽 자리(좌하단·하단 가운데·우하단)는 같은 계산을 조금 늦은 좌우로 한다. 소리가 앞에서 뒤로 퍼지는 것처럼 보이는
+ /// Android 의 공간 리플(기본값 켜짐)이다. Android 는 5칸 히스토리의 가장 오래된 값(60fps 네 프레임 전)을 쓰지만 틱 수로 세면
+ /// 최대 프레임에 따라 지연이 달라지므로, 여기서는 라는 시간으로 센다.
+ /// 렌더 스레드에서만 쓴다. 할당은 없다.
+ ///
+ public sealed class StereoUpmix
+ {
+ /// 뒤쪽 자리가 따라오는 지연. Android 의 60fps 네 프레임과 같다.
+ public const double RearDelayMs = 4 * FrameStep.ReferenceFrameMs;
+
+ // 프레임 시각이 조금 흔들려도 60fps 네 프레임 전 값을 고르게 1ms 를 봐준다.
+ private const double DelayToleranceMs = 1;
+
+ // 지연만큼의 프레임을 담는 링. 960fps 까지 지연 시간만큼을 담는다.
+ private const int Capacity = 64;
+ private readonly double[] _times = new double[Capacity];
+ private readonly float[] _left = new float[Capacity];
+ private readonly float[] _right = new float[Capacity];
+ private int _head; // 다음에 쓸 칸
+ private int _count;
+
+ ///
+ /// 이번 프레임의 앞 좌우를 넣고, 뒤쪽 자리에 쓸 좌우( 전의 값)를 돌려준다.
+ /// 그만큼 오래된 프레임이 아직 없으면 0 이다(Android 도 히스토리를 0 으로 채워 시작한다).
+ ///
+ public void Push(double nowMs, float left, float right, out float rearLeft, out float rearRight)
+ {
+ _times[_head] = nowMs;
+ _left[_head] = left;
+ _right[_head] = right;
+ _head = (_head + 1) % Capacity;
+ if (_count < Capacity) _count++;
+
+ rearLeft = 0;
+ rearRight = 0;
+ // 가장 최근 것부터 거슬러 올라가, 지연만큼 지난 첫 프레임을 쓴다. 링이 다 차도 그만큼 지난 것이 없으면 가장 오래된 것을 쓴다.
+ for (int n = 1; n <= _count; n++)
+ {
+ int i = (_head - n + Capacity) % Capacity;
+ if (nowMs - _times[i] >= RearDelayMs - DelayToleranceMs || n == Capacity)
+ {
+ rearLeft = _left[i];
+ rearRight = _right[i];
+ return;
+ }
+ }
+ }
+
+ /// 지난 프레임을 모두 버린다. 쉬기로 내려갈 때와 사운드 모드를 바꿀 때 부른다.
+ public void Clear()
+ {
+ _head = 0;
+ _count = 0;
+ }
+
+ ///
+ /// 앞 좌우와 뒤쪽 좌우를 둘레 여덟 자리(0:FC 1:FR 2:SR 3:BR 4:BC 5:BL 6:SL 7:FL)로 펼치고 그 합을 돌려준다.
+ /// 계수는 Android 와 같다.
+ ///
+ public static float Spread(float left, float right, float rearLeft, float rearRight, Span positions)
+ {
+ float mid = Math.Min(left, right);
+ float sideL = Math.Max(0f, left - right);
+ float sideR = Math.Max(0f, right - left);
+ positions[0] = mid * 1.2f; // FC
+ positions[1] = right * 0.9f; // FR
+ positions[7] = left * 0.9f; // FL
+ positions[2] = sideR * 1.5f; // SR
+ positions[6] = sideL * 1.5f; // SL
+
+ float rearMid = Math.Min(rearLeft, rearRight);
+ float rearSideL = Math.Max(0f, rearLeft - rearRight);
+ float rearSideR = Math.Max(0f, rearRight - rearLeft);
+ positions[3] = rearSideR * 1.2f; // BR
+ positions[4] = rearMid * 0.8f; // BC
+ positions[5] = rearSideL * 1.2f; // BL
+
+ float total = 0;
+ for (int i = 0; i < 8; i++) total += positions[i];
+ return total;
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Rendering/VisualizerEngine.cs b/SoundVisualizer.Core/Rendering/VisualizerEngine.cs
new file mode 100644
index 0000000..7d6c74f
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/VisualizerEngine.cs
@@ -0,0 +1,576 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using SoundVisualizer.Ai;
+using SoundVisualizer.Audio;
+
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 오버레이가 그린 프레임마다 하는 계산: 채널 피크 → 전체 크기와 방향 분포의 스무딩 → 화면 둘레 여덟 자리의 깊이 → 패드·원형의 움직임.
+ /// Android VisualizerEngine.kt 에서 그리기를 뺀 계산부다. 도형은 앱의 Visualizers 가 이 결과로 만든다.
+ ///
+ /// 감쇠와 따라붙기는 모두 60fps 한 프레임 기준으로 시간 정규화한다(). 그래서 최대 프레임을 30 으로 두든
+ /// 모니터 주사율로 두든 같은 시간 뒤에 같은 모양이 되고, 60fps 에서는 예전과 같다.
+ ///
+ /// 표시를 꺼 둔 종류라 그리지 못한 피크는 약 1초 보관해(), 위협음 판정이 늦게 와도 표시 중이었을 때의
+ /// 크기로 그린다.
+ ///
+ /// 아무것도 보이지 않은 지 1초가 지나고 조용하면(또는 지금 설정과 라벨로는 그릴 수 없으면) 쉰다().
+ /// 쉬는 동안 오버레이는 프레임마다 그리지 않고 마다 로 확인하며,
+ /// 그릴 수 있는 소리가 나면 깨어난다.
+ ///
+ /// 사운드 모드 2.0 은 앞 좌우를 Mid/Side 로 둘레 여덟 자리에 펼쳐 그린다(). 5.1·7.1 은 채널마다 제자리에 그린다.
+ ///
+ /// 렌더 스레드(UI 스레드)에서만 쓴다. 프레임마다 힙 할당이 없다.
+ /// 채널은 캡처 순서 FL, FR, FC, BL, BR, SL, SR, LFE 다( 와 같다).
+ /// 둘레 자리는 0:FC(상단 중앙) 1:FR(우상단) 2:SR(우측 중앙) 3:BR(우하단) 4:BC(하단 중앙) 5:BL(좌하단) 6:SL(좌측 중앙) 7:FL(좌상단)이다.
+ /// 스무딩에 넣는 값()은 5.1·7.1 이면 채널 순서, 2.0 이면 둘레 자리 순서다.
+ ///
+ public sealed class VisualizerEngine
+ {
+ public const int Channels = PeakAccumulator.ChannelCount;
+ public const int Positions = 8;
+
+ // 캡처 채널 번호
+ private const int FL = 0, FR = 1, FC = 2, BL = 3, BR = 4, SL = 5, SR = 6, LFE = 7;
+
+ // 표현 모드(AppSettings.VisualMode)
+ public const int ModeWave = 0, ModePad = 1, ModeCircle = 2, ModeOutline = 3;
+
+ /// 버퍼가 오지 않은 프레임마다 채널 목표값에 곱하는 감쇠(60fps 기준).
+ public const float TargetDecay = 0.87f;
+
+ /// 패드 두께·원형 깊이가 프레임마다 목표를 따라붙는 비율(60fps 기준).
+ public const double PadLerp = 0.3;
+ public const double CircleLerp = 0.25;
+
+ /// 프레임마다 애니메이션 시각에 더하는 값(60fps 기준).
+ public const double AnimationStep = 0.035;
+
+ /// 크기 100% 가 닿는 한계선은 화면 가운데에서 이만큼(DIP) 바깥이다. 한계선이 이보다 가깝지는 않다.
+ public const double EdgeMargin = 10;
+
+ /// 패드는 파도 깊이의 이 비율을 두께로 쓰고, 에서 자른다.
+ public const double PadDepthScale = 0.25;
+ public const double PadMaxThickness = 55;
+
+ /// 원형은 파도 깊이의 이 비율로 부풀고, 화면 높이의 에서 자른다.
+ public const double CircleDepthScale = 0.35;
+ public const double CircleMaxHeightRatio = 0.4;
+
+ ///
+ /// 이보다 옅으면 보이지 않는 것으로 본다(진하기 0..1). 크기 고정 모드는 진하기가 전체 크기를 따라 0 으로 다가가기만 해서,
+ /// 이 문턱이 없으면 소리가 그친 뒤에도 몇 분 동안 보이지 않는 도형을 화면 전체 크기로 계속 그린다.
+ ///
+ public const double MinVisibleAlpha = 0.002;
+
+ /// 도형이 보이기 시작하는 문턱(DIP). 파도·외곽선은 깊이, 패드는 두께, 원형은 깊이로 본다(Visualizers 와 같다).
+ public const double WaveActiveDepth = 3;
+ public const double PadMinThickness = 0.5;
+ public const double CircleMinDepth = 1;
+
+ /// 아무것도 보이지 않은 채 이만큼(밀리초) 지나면 쉰다.
+ public const double IdleAfterMs = 1000;
+
+ /// 쉬는 동안 단축키와 소리를 확인하는 간격(밀리초).
+ public const int IdlePollMs = 33;
+
+ /// 그리는 채널 가운데 가장 큰 값이 이 이하면 조용한 것으로 본다. AI 무음 게이트()와 같다.
+ public const float WakeThreshold = 0.01f;
+
+ ///
+ /// 늦게 온 위협음 판정으로 그릴 때 가장 센 자리가 적어도 닿는 깊이(DIP).
+ /// 파도·외곽선이 보이기 시작하는 3 의 두 배 넘게 잡아 민감도 기본값에서 0.4초쯤 보이고,
+ /// 패드(깊이 ×0.25, 0.5)와 원형(깊이 ×0.35, 1)도 보이는 문턱을 넘는다.
+ ///
+ public const double LateDangerMinDepth = 8;
+
+ private readonly FrameClock _clock = new();
+ private readonly LateDangerHold _hold = new();
+ private readonly StereoUpmix _upmix = new();
+ private bool _stereoLayout; // 지난 프레임이 2.0 배치(둘레 자리 순서)였는지
+
+ // 채널별 목표(최근 피크). 버퍼가 오면 그 피크로 바꾸고, 안 오면 줄인다.
+ private readonly float[] _targets = new float[Channels];
+ // 이번 프레임에 스무딩에 넣은 값. 5.1·7.1 은 채널 순서, 2.0 은 둘레 자리 순서다.
+ private readonly float[] _sources = new float[Channels];
+ private float _totalTarget;
+ // 늦은 위협음 판정 때 보관한 소리를 채널 값으로 펼쳐 보는 자리
+ private readonly float[] _heldSources = new float[Channels];
+
+ // 전체 크기(민감도로 따라붙음)와 방향 분포(속도로 따라붙음)
+ private double _smoothTotal;
+ private readonly double[] _dist = { 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125 };
+
+ private readonly double[] _depths = new double[Positions];
+ private readonly double[] _padThickness = new double[Positions];
+ private readonly double[] _circleDepths = new double[Positions];
+ private double _baseDepth;
+ private double _animationTime;
+ private double _alpha;
+ private bool _visible;
+
+ // 쉬기
+ private double _invisibleSinceMs = -1; // 보이지 않기 시작한 프레임 시각. 보이는 동안은 -1.
+ private double _lastHoldMs = double.NaN; // 보관 시간을 마지막으로 흘린 시각
+ private bool _quietAtLastPoll;
+
+ private double _width, _height;
+
+ /// 그릴 면의 크기(DIP). 프레임마다 알려 준다.
+ public void SetSurfaceSize(double width, double height)
+ {
+ _width = width;
+ _height = height;
+ }
+
+ ///
+ /// 그린 프레임 하나를 계산한다.
+ ///
+ /// 이번 프레임 시각(밀리초, 단조 증가).
+ /// 지난 프레임 뒤로 들어온 버퍼들의 채널별 최고값. 버퍼가 하나도 오지 않았으면 비운다.
+ /// 이번 프레임의 설정과 AI 라벨.
+ /// 보이는 것이 없어도 쉬지 않는다(오버레이 편집 중).
+ public void Tick(double nowMs, ReadOnlySpan peaks, in OverlayInput input, bool keepAwake = false)
+ {
+ double step = _clock.Next(nowMs);
+ var settings = input.Settings;
+ int soundMode = input.SoundMode;
+ _lastHoldMs = nowMs;
+
+ // 1. 목표값. 버퍼가 왔으면 그 피크, 안 왔으면 60fps 기준 프레임마다 0.87 씩 줄인 값.
+ if (!peaks.IsEmpty)
+ {
+ peaks[..Channels].CopyTo(_targets);
+ }
+ else
+ {
+ float decay = FrameStep.Decay(TargetDecay, step);
+ for (int i = 0; i < Channels; i++) _targets[i] *= decay;
+ }
+
+ // 1-1. 라벨. 숨긴 종류면 이번 피크를 보관하고, 위협음 판정이 새로 왔으면 보관한 크기로 시작한다.
+ // 그릴 수 없는 설정이면 판정이 와도 보일 것이 없으니 보관하지 않는다.
+ // 깨어 있는 동안 받은 소리도 판정이 오기 전에 쉬기로 내려가면 사라지므로 여기서도 똑같이 모은다(PollWake 참고).
+ _hold.Advance(step);
+ bool drawable = CanDraw(settings);
+ if (drawable)
+ {
+ if (!input.Shown)
+ {
+ if (!peaks.IsEmpty) _hold.Keep(peaks, Loudness(peaks, soundMode), WakeThreshold);
+ }
+ else if (input.Label == SoundType.Danger)
+ {
+ ReleaseHold(settings, soundMode);
+ }
+ }
+
+ // 2. 스무딩. 전체 크기(민감도·떨림)와 소리가 어디서 나는지의 분포(위치 변화 속도)를 따로 따라간다.
+ // 2.0 과 5.1·7.1 은 분포의 배치가 달라, 사운드 모드를 바꾸면 분포를 고르게 놓고 새로 따라간다.
+ // 그대로 두면 바꾼 직후 잠깐 엉뚱한 방향이 그려진다.
+ bool stereo = soundMode == 0;
+ if (stereo != _stereoLayout)
+ {
+ _stereoLayout = stereo;
+ Array.Fill(_dist, 1.0 / Channels);
+ _upmix.Clear();
+ }
+ float total = MapSources(nowMs, soundMode);
+ _totalTarget = total;
+
+ _smoothTotal += (total - _smoothTotal) * FrameStep.Approach(TremorCoefficient(settings), step);
+
+ double sfPosition = FrameStep.Approach(PositionCoefficient(settings), step);
+ if (total > 0.0001f)
+ {
+ for (int i = 0; i < Channels; i++) _dist[i] += ((_sources[i] / total) - _dist[i]) * sfPosition;
+ }
+
+ // 3. 깊이. 크기 100% 가 화면 가운데 한계선에 닿게 맞춘다.
+ _baseDepth = BaseDepthFor(settings, _width, _height);
+ FillDepths(settings.IntensityAsOpacity, soundMode);
+
+ // 4. 패드·원형은 깊이를 다시 한 번 부드럽게 따라간다. 그리는 모드만 움직인다.
+ bool anyShape = input.VisualMode switch
+ {
+ ModePad => StepPad(step),
+ ModeCircle => StepCircle(step),
+ _ => AnyDepthAbove(WaveActiveDepth),
+ };
+
+ _animationTime += AnimationStep * step;
+
+ // 5. 진하기와 보이는지. 크기 고정 모드는 진하기가 소리 크기를 따른다.
+ _alpha = AlphaFor(settings, _smoothTotal);
+ _visible = input.Shown && _alpha > MinVisibleAlpha && anyShape;
+
+ // 6. 쉬기. 보이지 않은 지 1초가 지났고 조용하거나, 소리는 나도 지금 라벨과 설정으로는 그릴 수 없으면 쉰다.
+ // 소리 조건만 보면 표시를 끈 종류의 음악이 계속 나올 때 그릴 것도 없이 최대 프레임으로 돈다.
+ if (_visible || keepAwake)
+ {
+ _invisibleSinceMs = -1;
+ }
+ else if (_invisibleSinceMs < 0)
+ {
+ _invisibleSinceMs = nowMs;
+ }
+ else if (nowMs - _invisibleSinceMs > IdleAfterMs &&
+ (Loudness(_targets, soundMode) < WakeThreshold || !(input.Shown && drawable)))
+ {
+ EnterIdle();
+ }
+ }
+
+ ///
+ /// 쉬는 동안 마다(또는 캡처 스레드가 큰 소리로 앞당겨) 부른다. 소리가 나고 지금 설정과 라벨로 그릴 수
+ /// 있을 때만 깨어나 true 를 돌려준다. 그러면 부르는 쪽은 다시 프레임마다 을 부른다.
+ ///
+ /// 표시를 끈 종류의 소리가 계속 나는 동안에는 깨어나지 않는다. 다만 그 피크는 버리지 않고 약 1초 보관한다. 위협음만 보이게
+ /// 해 두면 총성 같은 짧은 소리는 먼저 숨긴 라벨로 들어오고, 위협음 판정은 다음 분류와 추론 시간만큼 늦게 온다.
+ /// 위협음 판정이 오면 소리가 이미 끝났어도 보관한 크기로 깨어난다.
+ ///
+ /// 확인 시각(밀리초, 과 같은 시계).
+ /// 지난 확인 뒤로 들어온 버퍼들의 채널별 최고값. 버퍼가 없었으면 비운다.
+ /// 지금 설정과 AI 라벨.
+ public bool PollWake(double nowMs, ReadOnlySpan peaks, in OverlayInput input)
+ {
+ if (!IsIdle) return false;
+
+ // 보관 시간은 실제로 흐른 시간만큼 흘린다. 확인 간격은 타이머 해상도와 앞당긴 확인 때문에 고르지 않다.
+ double frames = double.IsNaN(_lastHoldMs) ? 0 : Math.Max(0, nowMs - _lastHoldMs) / FrameStep.ReferenceFrameMs;
+ _lastHoldMs = nowMs;
+ _hold.Advance(frames);
+
+ int soundMode = input.SoundMode;
+ float level = peaks.IsEmpty ? 0 : Loudness(peaks, soundMode);
+ bool loud = level > WakeThreshold;
+ // 조용하고 보관한 소리도 없으면 설정·라벨을 볼 필요가 없다. 쉬는 동안의 확인은 대부분 여기서 끝나고,
+ // 다음 확인은 소리가 날 때까지 미룰 수 있다(CanSleepUntilLoud).
+ _quietAtLastPoll = !loud && !_hold.HasData;
+ if (_quietAtLastPoll) return false;
+
+ var settings = input.Settings;
+ if (!CanDraw(settings)) return false;
+ if (!input.Shown)
+ {
+ if (!peaks.IsEmpty) _hold.Keep(peaks, level, WakeThreshold);
+ return false;
+ }
+
+ // 위협음 판정이 새로 왔으면 보관한 크기로 시작한다. 소리가 이미 끝났어도 깨어난다.
+ // 그 밖의 깨어남은 0 에서 민감도 속도로 자란다.
+ bool released = input.Label == SoundType.Danger && ReleaseHold(settings, soundMode);
+ if (!loud && !released) return false;
+
+ if (!peaks.IsEmpty) peaks[..Channels].CopyTo(_targets);
+ Wake();
+ return true;
+ }
+
+ ///
+ /// 소리와 상관없이 깨운다(오버레이 편집을 시작할 때). 이미 깨어 있으면 아무것도 하지 않는다.
+ ///
+ public void Resume()
+ {
+ if (IsIdle) Wake();
+ }
+
+ private void Wake()
+ {
+ IsIdle = false;
+ _invisibleSinceMs = -1;
+ // 쉰 시간만큼 한꺼번에 진행하지 않게, 깨어난 첫 프레임은 한 프레임으로 센다.
+ _clock.Reset();
+ }
+
+ private void EnterIdle()
+ {
+ IsIdle = true;
+ _visible = false;
+ // 깨어나는 길은 조용하지 않은 확인뿐이다. 쉬기로 내려오기 직전에 보관한 소리가 있을 수 있으니 한 번은 확인하고 잠든다.
+ _quietAtLastPoll = false;
+ Array.Clear(_targets);
+ Array.Clear(_sources);
+ _smoothTotal = 0;
+ Array.Clear(_padThickness);
+ Array.Clear(_circleDepths);
+ _upmix.Clear();
+ _clock.Reset();
+ // 보관한 피크는 비우지 않는다. 쉬기로 내려간 뒤에 위협음 판정이 와도 쓸 수 있어야 한다.
+ }
+
+ /// 진하기(0..1). 크기 고정이면 최대 진하기 × 소리 크기(2.5 에서 가득), 아니면 진하기 설정 그대로다.
+ public static double AlphaFor(VisualModeSettings s, double smoothTotal)
+ {
+ if (s.IntensityAsOpacity)
+ {
+ double maxOpacity = Math.Max(0.0, s.OpacityFixedMaxOpacity) / 100.0;
+ return maxOpacity * Math.Max(0.0, Math.Min(1.0, smoothTotal / 2.5));
+ }
+ return Math.Max(0.0, s.VisualOpacity) / 100.0;
+ }
+
+ private bool AnyDepthAbove(double threshold)
+ {
+ for (int i = 0; i < Positions; i++)
+ {
+ if (_depths[i] > threshold) return true;
+ }
+ return false;
+ }
+
+ ///
+ /// 소리 크기와 라벨에 상관없이, 지금 설정으로 무언가 보일 수 있는지. 진하기나 크기가 0 이면 소리가 아무리 커도
+ /// 그릴 것이 없다(Android canDraw). 라벨의 표시 여부는 따로 본다.
+ ///
+ public static bool CanDraw(VisualModeSettings s)
+ {
+ double potentialAlpha = (s.IntensityAsOpacity ? s.OpacityFixedMaxOpacity : s.VisualOpacity) / 100.0;
+ double size = s.IntensityAsOpacity ? s.OpacityFixedSize : s.Intensity;
+ return potentialAlpha > MinVisibleAlpha && size > 0;
+ }
+
+ ///
+ /// 채널 피크 한 프레임의 크기: 지금 사운드 모드가 그리는 채널 가운데 가장 큰 값. 2.0 은 앞 좌우만 보고,
+ /// 5.1·7.1 은 자리가 없는 LFE 를 뺀다. 그리지 못할 채널만 울리는 소리로 깨어나거나 보관하지 않게 한다.
+ /// 캡처 스레드에서도 부른다(할당·상태 없음).
+ ///
+ public static float Loudness(ReadOnlySpan peaks, int soundMode)
+ {
+ int count = soundMode == 0 ? 2 : LFE;
+ float max = 0;
+ for (int i = 0; i < count; i++)
+ {
+ if (peaks[i] > max) max = peaks[i];
+ }
+ return max;
+ }
+
+ ///
+ /// 이번 프레임의 채널 목표값을 스무딩에 넣을 값()으로 펼치고 그 합을 돌려준다.
+ /// 2.0 은 앞 좌우를 둘레 여덟 자리로 펼치고 뒤쪽 자리는 조금 늦은 좌우로 그린다. 5.1·7.1 은 채널 그대로다.
+ ///
+ private float MapSources(double nowMs, int soundMode)
+ {
+ if (soundMode == 0)
+ {
+ _upmix.Push(nowMs, _targets[FL], _targets[FR], out float rearL, out float rearR);
+ return StereoUpmix.Spread(_targets[FL], _targets[FR], rearL, rearR, _sources);
+ }
+ return CopyChannels(_targets, _sources);
+ }
+
+ ///
+ /// 보관한 프레임 하나를 스무딩에 넣을 값으로 펼치고 그 합을 돌려준다. 판정이 올 즈음엔 뒤쪽 지연도 따라잡았다고 보고
+ /// 앞뒤에 같은 좌우를 넣는다(Android upmixTotal).
+ ///
+ private static float MapHeld(ReadOnlySpan frame, int soundMode, Span sources)
+ {
+ if (soundMode == 0) return StereoUpmix.Spread(frame[FL], frame[FR], frame[FL], frame[FR], sources);
+ return CopyChannels(frame, sources);
+ }
+
+ private static float CopyChannels(ReadOnlySpan frame, Span sources)
+ {
+ frame[..Channels].CopyTo(sources);
+ float total = 0;
+ for (int i = 0; i < Channels; i++) total += sources[i];
+ return total;
+ }
+
+ ///
+ /// 펼친 값에서 가장 깊이 그려질 자리의 몫. 2.0 은 여덟 자리가 모두 그려지고, 5.1·7.1 은 LFE 를 뺀 채널이 그려진다
+ /// (하단 가운데는 뒤 좌우의 평균이라 그 둘보다 깊지 않다).
+ ///
+ private static double MaxDrawnShare(ReadOnlySpan sources, double total, int soundMode)
+ {
+ int count = soundMode == 0 ? Positions : LFE;
+ double max = 0;
+ for (int i = 0; i < count; i++)
+ {
+ double share = sources[i] / total;
+ if (share > max) max = share;
+ }
+ return max;
+ }
+
+ ///
+ /// 보관한 소리로 전체 크기와 방향을 바로 잡고 보관을 비운다(Android releaseHold). 보관한 것이 있었으면 true.
+ ///
+ /// 판정이 늦게 온 소리를 민감도 스무딩으로 0 에서 키우면 이미 끝난 뒤라 거의 보이지 않으므로 미리 키워 두고,
+ /// 줄어드는 것은 이후 프레임에서 평소처럼 민감도 속도를 따른다. 한 번 쓰면 비우므로 같은 소리로 두 번 번쩍이지 않는다.
+ ///
+ /// 크기는 그 종류를 표시 중이었다면 평소 경로가 그렸을 크기다. 보관한 피크를 그대로 전체 크기로 쓰면 소리가 1초 넘게
+ /// 이어질 때의 크기라서, 짧은 총성마다 크기 한도까지 뛰고 같은 소리가 직전 라벨에 따라 열 배 넘게 다르게 그려진다.
+ ///
+ private bool ReleaseHold(VisualModeSettings s, int soundMode)
+ {
+ if (!_hold.HasData) return false;
+
+ // 1) 오래된 칸부터 칸 하나(2프레임)씩 평소 스무딩에 다시 흘려, 도달했을 가장 큰 전체 크기를 구한다.
+ // 칸마다 가장 큰 피크만 남아 있으니 실제보다 조금 크게 나온다. 방향은 가장 큰 칸으로 잡는다.
+ double sf = FrameStep.Approach(TremorCoefficient(s), LateDangerHold.SlotFrames);
+ double ramp = 0, rampPeak = 0;
+ int loudest = -1;
+ float loudestLevel = 0;
+ for (int n = 0; n <= LateDangerHold.Slots; n++)
+ {
+ ramp += (MapHeld(_hold.Slot(n), soundMode, _heldSources) - ramp) * sf;
+ if (ramp > rampPeak) rampPeak = ramp;
+ float level = _hold.SlotLevel(n);
+ if (level > loudestLevel)
+ {
+ loudestLevel = level;
+ loudest = n;
+ }
+ }
+
+ // 이 소리가 계속 날 때 다다르는 크기. _heldSources 에 방향이 남는다.
+ double full = loudest < 0 ? 0 : MapHeld(_hold.Slot(loudest), soundMode, _heldSources);
+ _hold.Clear();
+ if (full <= 0) return true;
+
+ double seed = rampPeak;
+ // 2) 그래도 가장 센 자리가 LateDangerMinDepth 에 못 미치면 거기까지 올린다. 판정까지 난 위협음이 안 보이면 안 된다.
+ // 크기 고정 모드는 전체 크기가 깊이가 아니라 진하기라서 올리지 않는다.
+ double depth = BaseDepthFor(s, _width, _height);
+ if (!s.IntensityAsOpacity && depth > 0)
+ {
+ double maxShare = MaxDrawnShare(_heldSources, full, soundMode);
+ if (maxShare > 0) seed = Math.Max(seed, LateDangerMinDepth / (depth * maxShare));
+ }
+ // 같은 소리가 계속 날 때보다 크게 그리지는 않는다.
+ seed = Math.Min(seed, full);
+
+ // 이미 더 크게 그리는 중이면(지금도 큰 소리가 이어짐) 건드리지 않는다.
+ if (seed > _smoothTotal)
+ {
+ _smoothTotal = seed;
+ for (int i = 0; i < Channels; i++) _dist[i] = _heldSources[i] / full;
+ }
+ return true;
+ }
+
+ /// 민감도(떨림) → 60fps 한 프레임에 전체 크기가 소리를 따라붙는 비율.
+ internal static float TremorCoefficient(VisualModeSettings s)
+ {
+ float c = (float)(Math.Max(0.1, s.Sensitivity) / 100.0);
+ return c > 1.0f ? 1.0f : c;
+ }
+
+ /// 위치 변화 속도 → 60fps 한 프레임에 방향 분포가 새 위치를 따라붙는 비율.
+ internal static float PositionCoefficient(VisualModeSettings s)
+ {
+ float c = (float)(Math.Max(0.1, s.PositionSpeed) / 100.0);
+ return c > 1.0f ? 1.0f : c;
+ }
+
+ /// 크기 100% 가 화면 가운데 한계선에 닿는 깊이(DIP). 크기 고정 모드는 고정 크기의 절반을 쓴다.
+ public static double BaseDepthFor(VisualModeSettings s, double width, double height)
+ {
+ double maxBase = Math.Min(width, height) / 2.0 - EdgeMargin;
+ if (maxBase < EdgeMargin) maxBase = EdgeMargin;
+ double size = s.IntensityAsOpacity ? s.OpacityFixedSize / 2.0 : s.Intensity;
+ return maxBase * (Math.Max(0.0, size) / 100.0);
+ }
+
+ private void FillDepths(bool sizeLocked, int soundMode)
+ {
+ double bd = _baseDepth;
+ double total = _smoothTotal;
+ if (soundMode == 0)
+ {
+ // 2.0: 분포가 이미 둘레 자리 순서다.
+ for (int p = 0; p < Positions; p++) _depths[p] = Depth(bd, sizeLocked, total, _dist[p]);
+ return;
+ }
+
+ // 5.1·7.1: 채널마다 제자리에 그린다. 하단 가운데는 뒤 좌우의 가운데(팬텀), LFE 는 자리가 없다.
+ _depths[0] = Depth(bd, sizeLocked, total, _dist[FC]);
+ _depths[1] = Depth(bd, sizeLocked, total, _dist[FR]);
+ _depths[2] = Depth(bd, sizeLocked, total, _dist[SR]);
+ _depths[3] = Depth(bd, sizeLocked, total, _dist[BR]);
+ _depths[4] = sizeLocked
+ ? Math.Min(bd, bd * ((_dist[BL] + _dist[BR]) / 2.0 * 4.0))
+ : Math.Min(bd, bd * ((total * _dist[BL] + total * _dist[BR]) / 2.0));
+ _depths[5] = Depth(bd, sizeLocked, total, _dist[BL]);
+ _depths[6] = Depth(bd, sizeLocked, total, _dist[SL]);
+ _depths[7] = Depth(bd, sizeLocked, total, _dist[FL]);
+ }
+
+ /// 한 자리의 깊이. 크기 고정이면 분포만으로(4배), 아니면 전체 크기 × 분포로 정하고 기준 깊이에서 자른다.
+ private static double Depth(double baseDepth, bool sizeLocked, double smoothTotal, double share) =>
+ Math.Min(baseDepth, baseDepth * (sizeLocked ? share * 4.0 : smoothTotal * share));
+
+ /// 패드 두께를 따라붙이고, 보이는 막대가 있는지 돌려준다.
+ private bool StepPad(double step)
+ {
+ double lerp = FrameStep.Approach(PadLerp, step);
+ bool any = false;
+ for (int c = 0; c < Positions; c++)
+ {
+ double target = _depths[c] * PadDepthScale;
+ if (target > PadMaxThickness) target = PadMaxThickness;
+ _padThickness[c] += (target - _padThickness[c]) * lerp;
+ if (_padThickness[c] >= PadMinThickness) any = true;
+ }
+ return any;
+ }
+
+ /// 원형 깊이를 따라붙이고, 보이게 부푼 자리가 있는지 돌려준다.
+ private bool StepCircle(double step)
+ {
+ double lerp = FrameStep.Approach(CircleLerp, step);
+ double cap = _height * CircleMaxHeightRatio;
+ bool any = false;
+ for (int i = 0; i < Positions; i++)
+ {
+ double target = _depths[i] * CircleDepthScale;
+ if (target > cap) target = cap;
+ _circleDepths[i] += (target - _circleDepths[i]) * lerp;
+ if (_circleDepths[i] > CircleMinDepth) any = true;
+ }
+ return any;
+ }
+
+ /// 이번 프레임에 그릴 것이 보이는지: 라벨을 보이게 해 두었고, 진하기가 문턱을 넘고, 보이는 도형이 있다.
+ public bool Visible => _visible;
+
+ /// 이번 프레임의 진하기(0..1).
+ public double Alpha => _alpha;
+
+ /// 쉬는 중인지. 쉬는 동안은 대신 를 부른다.
+ public bool IsIdle { get; private set; }
+
+ ///
+ /// 쉬는 동안 다음 확인을 큰 소리가 날 때까지 미뤄도 되는지(). 직전 확인이 조용했고 보관한 소리도
+ /// 없을 때만 true 다. 표시를 꺼 둔 종류의 소리가 나는 동안이나, 늦게 올 위협음 판정을 위해 소리를 보관하는 동안에는 false 다.
+ ///
+ public bool CanSleepUntilLoud => IsIdle && _quietAtLastPoll;
+
+ /// 스무딩한 전체 크기. 크기 고정 모드의 진하기도 이것으로 정한다.
+ public double SmoothTotal => _smoothTotal;
+
+ /// 이번 프레임 채널 값의 합.
+ public float TotalTarget => _totalTarget;
+
+ /// 크기 100% 의 깊이(DIP).
+ public double BaseDepth => _baseDepth;
+
+ public double AnimationTime => _animationTime;
+
+ /// 둘레 여덟 자리의 깊이(DIP). 파도·외곽선은 이것으로 그린다.
+ public ReadOnlySpan Depths => _depths;
+
+ /// 채널별 방향 분포(캡처 순서, 합이 대체로 1).
+ public ReadOnlySpan Distribution => _dist;
+
+ /// 둘레 여덟 자리의 패드 두께(DIP). 패드 모드에서만 움직인다.
+ public ReadOnlySpan PadThickness => _padThickness;
+
+ /// 둘레 여덟 자리의 원형 깊이(DIP, 반지름에 더한다). 원형 모드에서만 움직인다.
+ public ReadOnlySpan CircleDepths => _circleDepths;
+ }
+}
diff --git a/SoundVisualizer.Core/Rendering/WaveShape.cs b/SoundVisualizer.Core/Rendering/WaveShape.cs
new file mode 100644
index 0000000..bed9385
--- /dev/null
+++ b/SoundVisualizer.Core/Rendering/WaveShape.cs
@@ -0,0 +1,176 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+namespace SoundVisualizer.Rendering
+{
+ ///
+ /// 파도·외곽선 모드의 안쪽 곡선이 지나는 점들(Android VisualizerEngine 의 buildWavePoints·roundedInnerPoint).
+ ///
+ /// 화면 둘레를 상단 가운데부터 시계 방향으로 고르게 번 짚고, 자리 여덟 곳의 깊이를 Catmull-Rom 으로 이어
+ /// 그만큼 안쪽으로 들인 점을 만든다. 모서리는 그 모서리 깊이의 두 배를 반경으로 2차 베지어로 둥글린다.
+ /// 앱의 WaveVisualizer 와 OutlineVisualizer 가 같은 점으로 곡선을 그린다. 렌더 스레드에서만 쓴다. 할당은 없다.
+ ///
+ public sealed class WaveShape
+ {
+ /// 둘레를 짚는 점 수.
+ public const int Samples = 150;
+
+ private const int Positions = 8;
+ private readonly double[] _positions = new double[Positions];
+
+ /// 점의 좌표(DIP). 가 채운다.
+ public double[] X { get; } = new double[Samples];
+ public double[] Y { get; } = new double[Samples];
+
+ ///
+ /// 크기 × 화면에서 둘레 여덟 자리의 깊이
+ /// (0:상단 가운데부터 시계 방향)로 점을 만든다. 깊이는 에서 자른다.
+ ///
+ public void Build(double w, double h, ReadOnlySpan depths, double maxDepth)
+ {
+ double P = 2 * (w + h);
+
+ // 자리마다 둘레에서의 위치(0..1). 상단 가운데가 0 이다.
+ _positions[0] = 0;
+ _positions[1] = (w / 2.0) / P;
+ _positions[2] = (w / 2.0 + h / 2.0) / P;
+ _positions[3] = (w / 2.0 + h) / P;
+ _positions[4] = (w / 2.0 + h + w / 2.0) / P;
+ _positions[5] = (w / 2.0 + h + w) / P;
+ _positions[6] = (w / 2.0 + h + w + h / 2.0) / P;
+ _positions[7] = (w / 2.0 + h + w + h) / P;
+
+ double d_tr = WaveDepth(_positions[1], depths, maxDepth);
+ double d_br = WaveDepth(_positions[3], depths, maxDepth);
+ double d_bl = WaveDepth(_positions[5], depths, maxDepth);
+ double d_tl = WaveDepth(_positions[7], depths, maxDepth);
+
+ for (int i = 0; i < Samples; i++)
+ {
+ double dist = (P * i) / Samples;
+ double d = WaveDepth(dist / P, depths, maxDepth);
+ RoundedInnerPoint(dist, w, h, P, d, d_tr, d_br, d_bl, d_tl, out X[i], out Y[i]);
+ }
+ }
+
+ ///
+ /// 둘레 거리 (상단 가운데부터 시계 방향)에서 깊이 만큼 들어온 점. 모서리 근처는 둥글린다.
+ ///
+ /// 둥글리는 반경이 0 인 모서리(깊이 0)는 건너뛴다(Android 의 r > 0 조건). 짚는 점이 모서리에 정확히 놓이면 예전에는 0/0 으로
+ /// 점이 NaN 이 되어 도형 전체가 그려지지 않았다. 16:9 화면은 150 점 가운데 넷이 네 모서리에 정확히 놓여, 모서리 자리 하나만
+ /// 조용해도(2.0 의 가운데 소리처럼) 파도와 외곽선이 통째로 사라졌다.
+ ///
+ private static void RoundedInnerPoint(double dist, double w, double h, double P, double d,
+ double d_tr, double d_br, double d_bl, double d_tl, out double x, out double y)
+ {
+ double c_tr = w / 2.0;
+ double c_br = w / 2.0 + h;
+ double c_bl = c_br + w;
+ double c_tl = c_bl + h;
+
+ // 라운딩 반경 (파도 깊이에 비례하도록 설정하여 깊이가 0일 때는 깎이지 않도록 함)
+ double R_tr = Math.Min(d_tr * 2.0, Math.Min(w / 2.0, h / 2.0));
+ double R_br = Math.Min(d_br * 2.0, Math.Min(w / 2.0, h / 2.0));
+ double R_bl = Math.Min(d_bl * 2.0, Math.Min(w / 2.0, h / 2.0));
+ double R_tl = Math.Min(d_tl * 2.0, Math.Min(w / 2.0, h / 2.0));
+
+ double distToTL = dist - c_tl;
+ if (dist < w / 2.0) distToTL = dist + P - c_tl;
+
+ // 1) Top-Right 코너: 상단 가로선 끝 → 구석의 꺾임(제어점) → 우측 세로선 시작
+ if (Math.Abs(dist - c_tr) <= R_tr && R_tr > 0)
+ {
+ double t = (dist - c_tr + R_tr) / (2 * R_tr);
+ QuadBezier(t, w - R_tr, d, w - d, d, w - d, R_tr, out x, out y);
+ return;
+ }
+ // 2) Bottom-Right 코너
+ if (Math.Abs(dist - c_br) <= R_br && R_br > 0)
+ {
+ double t = (dist - c_br + R_br) / (2 * R_br);
+ QuadBezier(t, w - d, h - R_br, w - d, h - d, w - R_br, h - d, out x, out y);
+ return;
+ }
+ // 3) Bottom-Left 코너
+ if (Math.Abs(dist - c_bl) <= R_bl && R_bl > 0)
+ {
+ double t = (dist - c_bl + R_bl) / (2 * R_bl);
+ QuadBezier(t, R_bl, h - d, d, h - d, d, h - R_bl, out x, out y);
+ return;
+ }
+ // 4) Top-Left 코너
+ if (Math.Abs(distToTL) <= R_tl && R_tl > 0)
+ {
+ double t = (distToTL + R_tl) / (2 * R_tl);
+ QuadBezier(t, d, R_tl, d, d, R_tl, d, out x, out y);
+ return;
+ }
+
+ // 모서리가 아닐 경우 (제한된 직선 구간)
+ EdgePosition(dist, w, h, P, out double ex, out double ey);
+ if (dist <= c_tr || dist > c_tl) { x = Math.Max(d, Math.Min(w - d, ex)); y = d; return; } // Top
+ if (dist <= c_br) { x = w - d; y = Math.Max(d, Math.Min(h - d, ey)); return; } // Right
+ if (dist <= c_bl) { x = Math.Max(d, Math.Min(w - d, ex)); y = h - d; return; } // Bottom
+ x = d; y = Math.Max(d, Math.Min(h - d, ey)); // Left
+ }
+
+ /// 2차 베지어로 안쪽으로 부드럽게 감싼다.
+ private static void QuadBezier(double t, double p0x, double p0y, double p1x, double p1y, double p2x, double p2y, out double x, out double y)
+ {
+ x = (1 - t) * (1 - t) * p0x + 2 * (1 - t) * t * p1x + t * t * p2x;
+ y = (1 - t) * (1 - t) * p0y + 2 * (1 - t) * t * p1y + t * t * p2y;
+ }
+
+ /// 둘레 거리(상단 가운데부터 시계 방향) → 화면 테두리 위의 점.
+ private static void EdgePosition(double dist, double w, double h, double P, out double x, out double y)
+ {
+ dist = ((dist % P) + P) % P;
+ if (dist <= w / 2) { x = w / 2 + dist; y = 0; return; }
+ if (dist <= w / 2 + h) { x = w; y = dist - w / 2; return; }
+ if (dist <= w / 2 + h + w) { x = w - (dist - (w / 2 + h)); y = h; return; }
+ if (dist <= w / 2 + h + w + h) { x = 0; y = h - (dist - (w / 2 + h + w)); return; }
+ x = dist - (w / 2 + h + w + h);
+ y = 0;
+ }
+
+ private double WaveDepth(double t, ReadOnlySpan depths, double maxDepth) =>
+ Math.Min(maxDepth, Math.Max(0, InterpolateDepthCatmullRom(t, depths)));
+
+ /// 둘레 위치 t(0..1)의 깊이. 자리 여덟 곳의 깊이를 지나는 Catmull-Rom 곡선이다.
+ private double InterpolateDepthCatmullRom(double t, ReadOnlySpan depths)
+ {
+ t = ((t % 1.0) + 1.0) % 1.0;
+ int n = Positions;
+
+ int idx1 = 0;
+ for (int i = 0; i < n; i++)
+ {
+ if (_positions[i] <= t) idx1 = i;
+ }
+ int idx0 = (idx1 - 1 + n) % n;
+ int idx2 = (idx1 + 1) % n;
+ int idx3 = (idx1 + 2) % n;
+
+ double p1 = _positions[idx1];
+ double p2 = _positions[idx2];
+ if (p2 <= p1) p2 += 1.0;
+
+ double at = t;
+ if (at < p1) at += 1.0;
+
+ double segLen = p2 - p1;
+ double lt = (segLen > 0) ? (at - p1) / segLen : 0;
+ lt = Math.Max(0, Math.Min(1, lt));
+
+ double d0 = depths[idx0], d1 = depths[idx1];
+ double d2 = depths[idx2], d3 = depths[idx3];
+
+ double a = -0.5 * d0 + 1.5 * d1 - 1.5 * d2 + 0.5 * d3;
+ double b = d0 - 2.5 * d1 + 2.0 * d2 - 0.5 * d3;
+ double c = -0.5 * d0 + 0.5 * d2;
+ double dv = d1;
+
+ return Math.Max(0, a * lt * lt * lt + b * lt * lt + c * lt + dv);
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/SoundVisualizer.Core.csproj b/SoundVisualizer.Core/SoundVisualizer.Core.csproj
new file mode 100644
index 0000000..aeeb1da
--- /dev/null
+++ b/SoundVisualizer.Core/SoundVisualizer.Core.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/ar.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ar.json
new file mode 100644
index 0000000..77edde9
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ar.json
@@ -0,0 +1,17 @@
+{
+ "horns": "أبواق السيارات وإنذاراتها",
+ "vehicle_warnings": "تنبيهات المركبات",
+ "breaking": "تحطّم الزجاج والانكسار",
+ "talking": "التحدث والهمس",
+ "distress": "الصياح والصراخ والبكاء",
+ "signals": "المنبّهات والصفير والصفارات",
+ "voices": "التصفيق والدندنة والصفير بالفم",
+ "water": "الصنابير والماء الجاري",
+ "kitchen": "المطبخ والطهي",
+ "appliances": "الأجهزة المنزلية والساعات",
+ "tools": "الأدوات والآلات",
+ "nature": "الطبيعة والطقس",
+ "body": "السعال والتنفّس وأصوات الجسم",
+ "objects": "الأشياء والأصوات القصيرة",
+ "background": "التلفزيون والراديو وأصوات الخلفية"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/de.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/de.json
new file mode 100644
index 0000000..9c9e333
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/de.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Hupen und Autoalarme",
+ "vehicle_warnings": "Warnsignale von Fahrzeugen",
+ "breaking": "Glasbruch und Krachen",
+ "talking": "Sprechen und Flüstern",
+ "distress": "Rufen, Schreien und Weinen",
+ "signals": "Wecker, Pieptöne und Trillerpfeifen",
+ "voices": "Klatschen, Summen und Pfeifen",
+ "water": "Wasserhähne und fließendes Wasser",
+ "kitchen": "Küche und Kochen",
+ "appliances": "Haushaltsgeräte und Uhren",
+ "tools": "Werkzeuge und Maschinen",
+ "nature": "Natur und Wetter",
+ "body": "Husten, Atmen und Körpergeräusche",
+ "objects": "Gegenstände und kurze Geräusche",
+ "background": "Fernseher, Radio und Hintergrundgeräusche"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/en.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/en.json
new file mode 100644
index 0000000..174a886
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/en.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Car horns and car alarms",
+ "vehicle_warnings": "Vehicle warnings",
+ "breaking": "Breaking glass and crashes",
+ "talking": "Talking and whispering",
+ "distress": "Shouting, screaming and crying",
+ "signals": "Alarm clocks, beeps and whistles",
+ "voices": "Clapping, humming and whistling",
+ "water": "Taps and running water",
+ "kitchen": "Kitchen and cooking",
+ "appliances": "Home appliances and clocks",
+ "tools": "Tools and machines",
+ "nature": "Nature and weather",
+ "body": "Coughs, breathing and body sounds",
+ "objects": "Objects and short sounds",
+ "background": "TV, radio and background sound"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/es.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/es.json
new file mode 100644
index 0000000..a1f467a
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/es.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Bocinas y alarmas de automóvil",
+ "vehicle_warnings": "Avisos de vehículos",
+ "breaking": "Vidrios rotos y estampidos",
+ "talking": "Habla y susurros",
+ "distress": "Gritos y llanto",
+ "signals": "Despertadores, pitidos y silbatos",
+ "voices": "Aplausos, tarareo y silbidos",
+ "water": "Grifos y agua corriente",
+ "kitchen": "Cocina y comida",
+ "appliances": "Electrodomésticos y relojes",
+ "tools": "Herramientas y máquinas",
+ "nature": "Naturaleza y clima",
+ "body": "Tos, respiración y sonidos del cuerpo",
+ "objects": "Objetos y sonidos breves",
+ "background": "TV, radio y sonido de fondo"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/fr.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/fr.json
new file mode 100644
index 0000000..56410ca
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/fr.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Klaxons et alarmes de voiture",
+ "vehicle_warnings": "Signaux d'alerte des véhicules",
+ "breaking": "Bris de verre et fracas",
+ "talking": "Parole et chuchotements",
+ "distress": "Cris et pleurs",
+ "signals": "Réveils, bips et sifflets",
+ "voices": "Applaudissements, fredonnements et sifflements",
+ "water": "Robinets et eau qui coule",
+ "kitchen": "Cuisine et cuisson",
+ "appliances": "Électroménager et horloges",
+ "tools": "Outils et machines",
+ "nature": "Nature et météo",
+ "body": "Toux, respiration et bruits du corps",
+ "objects": "Objets et sons brefs",
+ "background": "Télé, radio et fond sonore"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/hi.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/hi.json
new file mode 100644
index 0000000..ad6fcef
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/hi.json
@@ -0,0 +1,17 @@
+{
+ "horns": "हॉर्न और कार के अलार्म",
+ "vehicle_warnings": "वाहनों के चेतावनी संकेत",
+ "breaking": "कांच का टूटना और धड़ाम की आवाज़ें",
+ "talking": "बोलना और फुसफुसाना",
+ "distress": "चिल्लाना, चीखना और रोना",
+ "signals": "अलार्म घड़ी, बीप और सीटी",
+ "voices": "ताली बजाना, गुनगुनाना और सीटी बजाना",
+ "water": "नल और बहता पानी",
+ "kitchen": "रसोई और खाना पकाना",
+ "appliances": "घरेलू उपकरण और घड़ियां",
+ "tools": "औज़ार और मशीनें",
+ "nature": "प्रकृति और मौसम",
+ "body": "खांसी, सांस और शरीर की आवाज़ें",
+ "objects": "चीज़ें और छोटी आवाज़ें",
+ "background": "टीवी, रेडियो और पृष्ठभूमि की आवाज़"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/id.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/id.json
new file mode 100644
index 0000000..80f2910
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/id.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Klakson dan alarm mobil",
+ "vehicle_warnings": "Peringatan kendaraan",
+ "breaking": "Kaca pecah dan benturan",
+ "talking": "Obrolan dan bisikan",
+ "distress": "Teriakan, jeritan, dan tangisan",
+ "signals": "Jam weker, bip, dan peluit",
+ "voices": "Tepuk tangan, senandung, dan siulan",
+ "water": "Keran dan air mengalir",
+ "kitchen": "Dapur dan memasak",
+ "appliances": "Peralatan rumah tangga dan jam",
+ "tools": "Perkakas dan mesin",
+ "nature": "Alam dan cuaca",
+ "body": "Batuk, napas, dan suara tubuh",
+ "objects": "Benda dan suara singkat",
+ "background": "TV, radio, dan bising sekitar"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/it.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/it.json
new file mode 100644
index 0000000..fdf6f6a
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/it.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Clacson e allarmi dell'auto",
+ "vehicle_warnings": "Segnali di avviso dei veicoli",
+ "breaking": "Vetri rotti e schianti",
+ "talking": "Parlato e sussurri",
+ "distress": "Grida, strilli e pianto",
+ "signals": "Sveglie, bip e fischietti",
+ "voices": "Applausi, canticchiare e fischiettare",
+ "water": "Rubinetti e acqua corrente",
+ "kitchen": "Cucina e cottura",
+ "appliances": "Elettrodomestici e orologi",
+ "tools": "Attrezzi e macchinari",
+ "nature": "Natura e meteo",
+ "body": "Tosse, respiro e suoni del corpo",
+ "objects": "Oggetti e suoni brevi",
+ "background": "TV, radio e suoni di sottofondo"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/ja.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ja.json
new file mode 100644
index 0000000..63ccc9c
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ja.json
@@ -0,0 +1,17 @@
+{
+ "horns": "クラクション・車の警報音",
+ "vehicle_warnings": "乗り物の警告音",
+ "breaking": "ガラスが割れる音・物が壊れる音",
+ "talking": "会話・ささやき声",
+ "distress": "大声・悲鳴・泣き声",
+ "signals": "目覚まし時計・電子音・ホイッスル",
+ "voices": "拍手・鼻歌・口笛",
+ "water": "蛇口・流れる水",
+ "kitchen": "台所・料理",
+ "appliances": "家電・時計",
+ "tools": "工具・機械",
+ "nature": "自然・天気",
+ "body": "せき・呼吸・体の音",
+ "objects": "物の音・短い音",
+ "background": "テレビ・ラジオ・背景音"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/ko.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ko.json
new file mode 100644
index 0000000..42049b9
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ko.json
@@ -0,0 +1,17 @@
+{
+ "horns": "경적·자동차 경보음",
+ "vehicle_warnings": "차량 경고음",
+ "breaking": "유리 깨짐·충돌",
+ "talking": "말소리·속삭임",
+ "distress": "외침·비명·울음",
+ "signals": "알람·삐 소리·호루라기",
+ "voices": "박수·콧노래·휘파람",
+ "water": "수돗물·물 흐르는 소리",
+ "kitchen": "부엌·요리",
+ "appliances": "가전제품·시계",
+ "tools": "공구·기계",
+ "nature": "자연·날씨",
+ "body": "기침·숨소리·몸 소리",
+ "objects": "물건 소리·짧은 소리",
+ "background": "TV·라디오·배경 소리"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/pl.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/pl.json
new file mode 100644
index 0000000..336bc94
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/pl.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Klaksony i alarmy samochodowe",
+ "vehicle_warnings": "Sygnały ostrzegawcze pojazdów",
+ "breaking": "Tłuczenie szkła i łoskot",
+ "talking": "Mówienie i szept",
+ "distress": "Krzyki i płacz",
+ "signals": "Budziki, piknięcia i gwizdki",
+ "voices": "Oklaski, nucenie i gwizdanie",
+ "water": "Krany i płynąca woda",
+ "kitchen": "Kuchnia i gotowanie",
+ "appliances": "Sprzęty domowe i zegary",
+ "tools": "Narzędzia i maszyny",
+ "nature": "Przyroda i pogoda",
+ "body": "Kaszel, oddech i odgłosy ciała",
+ "objects": "Przedmioty i krótkie dźwięki",
+ "background": "Telewizor, radio i dźwięki w tle"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/pt-BR.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/pt-BR.json
new file mode 100644
index 0000000..cf7a445
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/pt-BR.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Buzinas e alarmes de carro",
+ "vehicle_warnings": "Sinais de alerta de veículos",
+ "breaking": "Vidro quebrado e pancadas",
+ "talking": "Fala e sussurros",
+ "distress": "Gritos e choro",
+ "signals": "Despertadores, bipes e apitos",
+ "voices": "Aplausos, cantarolar e assobios",
+ "water": "Torneiras e água corrente",
+ "kitchen": "Cozinha e comida",
+ "appliances": "Eletrodomésticos e relógios",
+ "tools": "Ferramentas e máquinas",
+ "nature": "Natureza e clima",
+ "body": "Tosse, respiração e sons do corpo",
+ "objects": "Objetos e sons curtos",
+ "background": "TV, rádio e som de fundo"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/ru.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ru.json
new file mode 100644
index 0000000..42ff960
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/ru.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Гудки и автосигнализации",
+ "vehicle_warnings": "Предупреждающие сигналы транспорта",
+ "breaking": "Бьющееся стекло и громкие удары",
+ "talking": "Разговоры и шёпот",
+ "distress": "Крики и плач",
+ "signals": "Будильники, пиканье и свистки",
+ "voices": "Аплодисменты, напевание и свист",
+ "water": "Краны и текущая вода",
+ "kitchen": "Кухня и готовка",
+ "appliances": "Бытовая техника и часы",
+ "tools": "Инструменты и механизмы",
+ "nature": "Природа и погода",
+ "body": "Кашель, дыхание и звуки тела",
+ "objects": "Предметы и короткие звуки",
+ "background": "Телевизор, радио и шумы"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/th.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/th.json
new file mode 100644
index 0000000..77c6aba
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/th.json
@@ -0,0 +1,17 @@
+{
+ "horns": "แตรรถและสัญญาณกันขโมยรถ",
+ "vehicle_warnings": "เสียงเตือนจากยานพาหนะ",
+ "breaking": "กระจกแตกและเสียงพังโครม",
+ "talking": "การพูดคุยและเสียงกระซิบ",
+ "distress": "เสียงตะโกน กรีดร้อง และร้องไห้",
+ "signals": "นาฬิกาปลุก เสียงบี๊บ และนกหวีด",
+ "voices": "เสียงปรบมือ ฮัมเพลง และผิวปาก",
+ "water": "ก๊อกน้ำและน้ำไหล",
+ "kitchen": "ห้องครัวและการทำอาหาร",
+ "appliances": "เครื่องใช้ไฟฟ้าในบ้านและนาฬิกา",
+ "tools": "เครื่องมือช่างและเครื่องจักร",
+ "nature": "ธรรมชาติและสภาพอากาศ",
+ "body": "เสียงไอ หายใจ และเสียงจากร่างกาย",
+ "objects": "สิ่งของและเสียงสั้นๆ",
+ "background": "ทีวี วิทยุ และเสียงพื้นหลัง"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/tr.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/tr.json
new file mode 100644
index 0000000..1e98628
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/tr.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Kornalar ve araba alarmları",
+ "vehicle_warnings": "Araç uyarı sesleri",
+ "breaking": "Cam kırılması ve parçalanma",
+ "talking": "Sohbet ve fısıltı",
+ "distress": "Bağırma, çığlık ve ağlama",
+ "signals": "Çalar saatler, bip sesleri ve düdükler",
+ "voices": "Alkış, mırıldanma ve ıslık",
+ "water": "Musluklar ve akan su",
+ "kitchen": "Mutfak ve yemek pişirme",
+ "appliances": "Ev aletleri ve saatler",
+ "tools": "Aletler ve makineler",
+ "nature": "Doğa ve hava durumu",
+ "body": "Öksürük, nefes ve vücut sesleri",
+ "objects": "Eşyalar ve kısa sesler",
+ "background": "TV, radyo ve arka plan sesi"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/vi.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/vi.json
new file mode 100644
index 0000000..3cc8bf8
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/vi.json
@@ -0,0 +1,17 @@
+{
+ "horns": "Còi xe và báo động ô tô",
+ "vehicle_warnings": "Tín hiệu cảnh báo của xe cộ",
+ "breaking": "Kính vỡ và tiếng đổ vỡ",
+ "talking": "Nói chuyện và thì thầm",
+ "distress": "Tiếng kêu, tiếng thét và tiếng khóc",
+ "signals": "Đồng hồ báo thức, tiếng bíp và tiếng còi",
+ "voices": "Vỗ tay, ngân nga và huýt sáo",
+ "water": "Vòi nước và nước chảy",
+ "kitchen": "Nhà bếp và nấu ăn",
+ "appliances": "Đồ gia dụng và đồng hồ",
+ "tools": "Dụng cụ và máy móc",
+ "nature": "Thiên nhiên và thời tiết",
+ "body": "Ho, hơi thở và âm thanh cơ thể",
+ "objects": "Đồ vật và âm thanh ngắn",
+ "background": "Tivi, radio và âm thanh nền"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/zh-Hans.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/zh-Hans.json
new file mode 100644
index 0000000..5a68a5e
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/zh-Hans.json
@@ -0,0 +1,17 @@
+{
+ "horns": "汽车喇叭与汽车警报",
+ "vehicle_warnings": "车辆警示音",
+ "breaking": "玻璃破碎与撞击声",
+ "talking": "说话声与耳语",
+ "distress": "喊叫、尖叫与哭声",
+ "signals": "闹钟、哔哔声与哨声",
+ "voices": "拍手、哼歌与吹口哨",
+ "water": "水龙头与流水",
+ "kitchen": "厨房与烹饪",
+ "appliances": "家电与钟表",
+ "tools": "工具与机器",
+ "nature": "自然与天气",
+ "body": "咳嗽、呼吸与身体声音",
+ "objects": "物品与短促声音",
+ "background": "电视、收音机与背景声"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/GroupTitles/zh-Hant.json b/SoundVisualizer.Core/Sounds/Data/GroupTitles/zh-Hant.json
new file mode 100644
index 0000000..c25ea74
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/GroupTitles/zh-Hant.json
@@ -0,0 +1,17 @@
+{
+ "horns": "汽車喇叭與防盜警報",
+ "vehicle_warnings": "車輛警示音",
+ "breaking": "玻璃碎裂與撞擊聲",
+ "talking": "說話聲與悄悄話",
+ "distress": "大喊、尖叫與哭泣聲",
+ "signals": "鬧鐘、嗶聲與哨子聲",
+ "voices": "拍手聲、哼歌與吹口哨",
+ "water": "水龍頭與流水",
+ "kitchen": "廚房與烹飪",
+ "appliances": "家電與時鐘",
+ "tools": "工具與機器",
+ "nature": "自然與天氣",
+ "body": "咳嗽、呼吸與身體聲音",
+ "objects": "物品與短促聲音",
+ "background": "電視、收音機與背景聲"
+}
diff --git a/SoundVisualizer.Core/Sounds/Data/NOTICE.txt b/SoundVisualizer.Core/Sounds/Data/NOTICE.txt
new file mode 100644
index 0000000..9d4f64b
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/NOTICE.txt
@@ -0,0 +1,34 @@
+================================================================================
+AudioSet 온톨로지 (분류 탭의 소리 묶음·소리 이름 번역)
+================================================================================
+SoundVisualizer.Core/Sounds/Data/sound_groups.tsv
+SoundVisualizer.Core/Sounds/Data/SoundNames/*.json
+
+분류 탭의 소리 묶음 표(sound_groups.tsv)와 소리 이름(SoundNames/*.json)은
+AudioSet 온톨로지를 바탕으로 만든 각색물입니다. 소리 이름은 온톨로지의
+클래스 이름과 그 번역이고, 묶음은 온톨로지의 갈래를 읽고 사람이 정했습니다.
+소리 이름을 그대로 쓴 묶음 이름(사이렌, 초인종 등)도 여기에 해당합니다.
+이 자료는 원본과 같은 CC BY-SA 4.0 으로 배포하며, 표 파일 맨 위에도 같은
+고지를 적어 두었습니다. JSON 은 주석을 달 수 없어 이 파일로 고지합니다.
+앱의 코드와 새로 지은 묶음 이름(GroupTitles/*.json)은 이 저장소의 라이선스
+(GNU AGPL v3)를 따릅니다.
+
+"AudioSet Ontology" by Google Inc.
+https://github.com/audioset/ontology (commit d417d32)
+Licensed under the Creative Commons Attribution-ShareAlike 4.0
+International License (CC BY-SA 4.0).
+https://creativecommons.org/licenses/by-sa/4.0/
+
+Changes: the 521 YAMNet sound names were regrouped into everyday groups by
+SoundVisualizer contributors (sound_groups.tsv), and the class names were
+translated into the app's languages (SoundNames/*.json).
+
+이 자료는 있는 그대로 제공되며 어떠한 보증도 하지 않습니다. 자세한 내용은
+CC BY-SA 4.0 의 5조(보증의 부인과 책임의 제한)를 따릅니다.
+
+--------------------------------------------------------------------------------
+모바일 판(app/src/main/assets/sound_groups.tsv, app/src/main/res/values*/
+sound_names.xml, strings.xml 의 sound_group_* 문구)에서 옮겼습니다. 표는 글자
+그대로이고, 이름은 안드로이드 이스케이프(\' 와 &)를 푼 뒤 언어마다 YAMNet
+번호 순서의 배열로 적었습니다. 언어 코드는 PC 판의 것입니다(values-in → id,
+values-zh-rCN → zh-Hans, values-zh-rTW → zh-Hant, values-pt-rBR → pt-BR).
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/ar.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/ar.json
new file mode 100644
index 0000000..1a73fcd
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/ar.json
@@ -0,0 +1,523 @@
+[
+ "كلام",
+ "كلام طفل",
+ "محادثة",
+ "سرد أو حديث فردي",
+ "مناغاة",
+ "كلام اصطناعي",
+ "صياح",
+ "صيحة مدوية",
+ "صيحة فرح",
+ "زعيق",
+ "صياح الأطفال",
+ "صراخ",
+ "همس",
+ "ضحك",
+ "ضحك رضيع",
+ "ضحكة خفيفة",
+ "ضحكة ساخرة",
+ "قهقهة",
+ "ضحكة مكتومة",
+ "بكاء",
+ "بكاء رضيع",
+ "بكاء خافت",
+ "أنين",
+ "تنهّد",
+ "غناء",
+ "جوقة",
+ "غناء اليودل",
+ "ترتيل",
+ "ترتيل مانترا",
+ "غناء طفل",
+ "غناء اصطناعي",
+ "غناء الراب",
+ "دندنة",
+ "تأوّه",
+ "نخير",
+ "صفير بالفم",
+ "تنفّس",
+ "أزيز الصدر",
+ "شخير",
+ "شهقة",
+ "لهاث",
+ "نفخة من الأنف",
+ "سعال",
+ "تنحنح",
+ "عطس",
+ "استنشاق بالأنف",
+ "ركض",
+ "جرّ القدمين",
+ "وقع الأقدام",
+ "مضغ",
+ "عضّ",
+ "غرغرة",
+ "قرقرة البطن",
+ "تجشّؤ",
+ "فواق (حازوقة)",
+ "إطلاق الريح",
+ "أصوات اليدين",
+ "فرقعة الأصابع",
+ "تصفيق",
+ "دقات القلب",
+ "نفخة قلبية",
+ "هتاف",
+ "تصفيق الجمهور",
+ "ثرثرة",
+ "حشد من الناس",
+ "ضجيج كلام متداخل",
+ "أطفال يلعبون",
+ "حيوانات",
+ "حيوانات أليفة",
+ "كلب",
+ "نباح",
+ "نباح كلب صغير",
+ "عواء",
+ "نباح ممدود",
+ "زمجرة",
+ "أنين الكلب",
+ "قطة",
+ "خرخرة",
+ "مواء",
+ "هسهسة",
+ "مواء التزاوج",
+ "حيوانات المزرعة",
+ "حصان",
+ "وقع الحوافر",
+ "صهيل",
+ "أبقار",
+ "خوار",
+ "جرس الأبقار",
+ "خنزير",
+ "نخير الخنزير",
+ "ماعز",
+ "ثغاء",
+ "خروف",
+ "دواجن",
+ "دجاج",
+ "قوقأة الدجاج",
+ "صياح الديك",
+ "ديك رومي",
+ "قرقرة الديك الرومي",
+ "بط",
+ "بطبطة",
+ "إوز",
+ "صياح الإوز",
+ "حيوانات برية",
+ "أسود ونمور",
+ "زئير",
+ "طيور",
+ "تغريد الطيور",
+ "زقزقة",
+ "صياح الطيور",
+ "حمام ويمام",
+ "هديل",
+ "غراب",
+ "نعيق الغراب",
+ "بومة",
+ "نعيق البوم",
+ "رفرفة الأجنحة",
+ "ذئاب وكلاب برية",
+ "قوارض",
+ "فأر",
+ "دبيب",
+ "حشرات",
+ "صرصور الليل",
+ "بعوضة",
+ "ذبابة",
+ "طنين",
+ "نحل ودبابير",
+ "ضفدع",
+ "نقيق",
+ "أفعى",
+ "خشخشة",
+ "أصوات الحيتان",
+ "موسيقى",
+ "آلة موسيقية",
+ "آلة وترية بالنقر",
+ "غيتار",
+ "غيتار كهربائي",
+ "غيتار باص",
+ "غيتار أكوستيك",
+ "غيتار سلايد",
+ "تقنية التابينغ (غيتار)",
+ "ضرب أوتار الغيتار",
+ "بانجو",
+ "سيتار",
+ "ماندولين",
+ "آلة القانون",
+ "يوكوليلي",
+ "لوحة مفاتيح موسيقية",
+ "بيانو",
+ "بيانو كهربائي",
+ "أرغن",
+ "أرغن إلكتروني",
+ "أرغن هاموند",
+ "سينثسايزر",
+ "سامبلر",
+ "هاربسيكورد",
+ "آلة إيقاع",
+ "طقم طبول",
+ "آلة طبول إلكترونية",
+ "طبل",
+ "طبل السنير",
+ "ضربة على حافة الطبل",
+ "قرع طبل متواصل",
+ "طبل جهير",
+ "طبول التيمباني",
+ "تابلا (طبلة هندية)",
+ "صنج",
+ "صنج هاي هات",
+ "قالب خشبي إيقاعي",
+ "دفّ",
+ "خشخيشة",
+ "ماراكاس",
+ "غونغ",
+ "أجراس أنبوبية",
+ "آلة إيقاع لحنية",
+ "زيلوفون",
+ "زيلوفون معدني",
+ "فيبرافون",
+ "طبل فولاذي",
+ "أوركسترا",
+ "آلة نفخ نحاسية",
+ "بوق فرنسي",
+ "ترومبيت",
+ "ترومبون",
+ "آلة وترية بالقوس",
+ "مجموعة الوتريات",
+ "كمان",
+ "نقر أوتار الكمان",
+ "تشيلو",
+ "كونترباص",
+ "آلة نفخ",
+ "فلوت",
+ "ساكسفون",
+ "كلارينيت",
+ "قيثارة (هارب)",
+ "جرس",
+ "جرس الكنيسة",
+ "جلاجل (أجراس صغيرة)",
+ "جرس الدراجة",
+ "شوكة رنانة",
+ "رنين أجراس",
+ "أجراس الرياح",
+ "فن قرع الأجراس",
+ "هارمونيكا",
+ "أكورديون",
+ "مزمار القربة",
+ "ديدجيريدو",
+ "بوق الشوفار",
+ "ثيرمين",
+ "وعاء الغناء التبتي",
+ "سكراتش (دي جي)",
+ "موسيقى البوب",
+ "موسيقى الهيب هوب",
+ "بيت بوكس (إيقاع بالفم)",
+ "موسيقى الروك",
+ "موسيقى الهيفي ميتال",
+ "موسيقى البانك روك",
+ "موسيقى الغرانج",
+ "موسيقى الروك التقدمي",
+ "موسيقى الروك أند رول",
+ "موسيقى الروك السايكيديلي",
+ "موسيقى الآر أند بي",
+ "موسيقى السول",
+ "موسيقى الريغي",
+ "موسيقى الكانتري",
+ "موسيقى السوينغ",
+ "موسيقى البلوغراس",
+ "موسيقى الفانك",
+ "موسيقى شعبية (فولك)",
+ "موسيقى الشرق الأوسط",
+ "موسيقى الجاز",
+ "موسيقى الديسكو",
+ "موسيقى كلاسيكية",
+ "أوبرا",
+ "موسيقى إلكترونية",
+ "موسيقى الهاوس",
+ "موسيقى التكنو",
+ "موسيقى الدبستب",
+ "موسيقى الدرام أند بيس",
+ "موسيقى الإلكترونيكا",
+ "موسيقى رقص إلكترونية",
+ "موسيقى الأمبينت",
+ "موسيقى الترانس",
+ "موسيقى أمريكا اللاتينية",
+ "موسيقى السالسا",
+ "موسيقى الفلامنكو",
+ "موسيقى البلوز",
+ "موسيقى للأطفال",
+ "موسيقى العصر الجديد",
+ "موسيقى غنائية",
+ "غناء بلا آلات (أكابيلا)",
+ "موسيقى أفريقية",
+ "موسيقى الأفروبيت",
+ "موسيقى مسيحية",
+ "موسيقى الغوسبل",
+ "موسيقى آسيوية",
+ "موسيقى كارناتيك (هندية)",
+ "موسيقى بوليوود",
+ "موسيقى السكا",
+ "موسيقى تقليدية",
+ "موسيقى مستقلة (إندي)",
+ "أغنية",
+ "موسيقى خلفية",
+ "موسيقى الشارة",
+ "لحن إعلاني",
+ "موسيقى تصويرية",
+ "تهويدة",
+ "موسيقى ألعاب الفيديو",
+ "موسيقى عيد الميلاد المجيد",
+ "موسيقى رقص",
+ "موسيقى الأعراس",
+ "موسيقى مبهجة",
+ "موسيقى حزينة",
+ "موسيقى رقيقة",
+ "موسيقى حماسية",
+ "موسيقى غاضبة",
+ "موسيقى مخيفة",
+ "رياح",
+ "حفيف الأوراق",
+ "ضجيج الرياح (ميكروفون)",
+ "عاصفة رعدية",
+ "رعد",
+ "ماء",
+ "مطر",
+ "قطرة مطر",
+ "مطر على سطح",
+ "جدول ماء",
+ "شلال",
+ "بحر",
+ "أمواج",
+ "بخار",
+ "بقبقة",
+ "نار",
+ "طقطقة",
+ "مركبة",
+ "قارب",
+ "قارب شراعي",
+ "قارب تجديف",
+ "قارب بمحرك",
+ "سفينة",
+ "مركبة آلية",
+ "سيارة",
+ "بوق السيارة",
+ "نفخة بوق قصيرة",
+ "إنذار السيارة",
+ "زجاج السيارة الكهربائي",
+ "انزلاق السيارة",
+ "صرير الإطارات",
+ "مرور سيارة",
+ "سباق سيارات",
+ "شاحنة",
+ "مكابح هوائية",
+ "بوق الشاحنة",
+ "صفير الرجوع للخلف",
+ "عربة الآيس كريم",
+ "حافلة",
+ "مركبة طوارئ",
+ "سيارة شرطة (صفارة إنذار)",
+ "سيارة إسعاف (صفارة إنذار)",
+ "سيارة إطفاء (صفارة إنذار)",
+ "دراجة نارية",
+ "ضجيج حركة المرور",
+ "السكك الحديدية",
+ "قطار",
+ "صفارة القطار",
+ "بوق القطار",
+ "عربة قطار",
+ "صرير عجلات القطار",
+ "مترو الأنفاق",
+ "مركبة جوية",
+ "محرك طائرة",
+ "محرك نفاث",
+ "مروحة طائرة",
+ "مروحية",
+ "طائرة",
+ "دراجة هوائية",
+ "لوح تزلج",
+ "محرك",
+ "محرك خفيف (تردد مرتفع)",
+ "مثقاب الأسنان",
+ "جزازة العشب",
+ "منشار سلسلي",
+ "محرك متوسط (تردد متوسط)",
+ "محرك ثقيل (تردد منخفض)",
+ "طرق المحرك",
+ "بدء تشغيل المحرك",
+ "محرك دائر دون حركة",
+ "تسارع المحرك",
+ "باب",
+ "جرس الباب",
+ "رنين جرس الباب",
+ "باب منزلق",
+ "إغلاق الباب بقوة",
+ "طرق الباب",
+ "نقر خفيف",
+ "صرير قصير",
+ "فتح الخزانة وإغلاقها",
+ "فتح الدرج وإغلاقه",
+ "أطباق وقدور",
+ "أدوات المائدة",
+ "تقطيع الطعام",
+ "قلي الطعام",
+ "فرن ميكروويف",
+ "خلاط كهربائي",
+ "صنبور الماء",
+ "ماء في حوض الغسيل",
+ "ماء في حوض الاستحمام",
+ "مجفف الشعر",
+ "سيفون المرحاض",
+ "فرشاة أسنان",
+ "فرشاة أسنان كهربائية",
+ "مكنسة كهربائية",
+ "سحّاب الملابس",
+ "خشخشة المفاتيح",
+ "سقوط عملة معدنية",
+ "مقص",
+ "آلة حلاقة كهربائية",
+ "خلط أوراق اللعب",
+ "كتابة على المفاتيح",
+ "آلة كاتبة",
+ "لوحة مفاتيح الكمبيوتر",
+ "كتابة بالقلم",
+ "إنذار",
+ "هاتف",
+ "رنين جرس الهاتف",
+ "نغمة رنين",
+ "نغمات طلب الرقم",
+ "نغمة الخط",
+ "نغمة انشغال الخط",
+ "منبّه",
+ "صفارة إنذار",
+ "صفارة الدفاع المدني",
+ "جرس طنّان",
+ "كاشف الدخان",
+ "إنذار الحريق",
+ "بوق الضباب",
+ "صفارة",
+ "صفارة بخارية",
+ "آليات ميكانيكية",
+ "سقّاطة ميكانيكية",
+ "ساعة",
+ "تكّة معدنية",
+ "تكتكة الساعة",
+ "تروس",
+ "بكرات",
+ "آلة خياطة",
+ "مروحة",
+ "مكيّف هواء",
+ "آلة تسجيل النقد",
+ "طابعة",
+ "كاميرا",
+ "كاميرا عاكسة (SLR)",
+ "أدوات",
+ "مطرقة",
+ "مطرقة هوائية",
+ "نشر بالمنشار",
+ "حكّ بالمبرد",
+ "صنفرة",
+ "أداة كهربائية",
+ "مثقاب",
+ "انفجار",
+ "طلق ناري",
+ "مدفع رشاش",
+ "وابل من الرصاص",
+ "قصف مدفعي",
+ "مسدس ألعاب",
+ "ألعاب نارية",
+ "مفرقعات",
+ "فرقعة",
+ "ثوران بركاني",
+ "دويّ",
+ "خشب",
+ "تقطيع بالفأس",
+ "تشظّي الخشب",
+ "تشقّق",
+ "زجاج",
+ "رنين الكؤوس",
+ "تحطّم الزجاج",
+ "سائل",
+ "تناثر سائل",
+ "خضخضة سائل",
+ "عصر شيء مبلل",
+ "قطرات متساقطة",
+ "سكب سائل",
+ "سيلان خفيف",
+ "تدفق قوي",
+ "ملء وعاء بسائل",
+ "رشّ بالرذاذ",
+ "ضخّ سائل",
+ "تقليب سائل",
+ "غليان",
+ "سونار",
+ "سهم",
+ "شقّ الهواء",
+ "ارتطام مكتوم",
+ "ارتطام أجوف",
+ "جهاز دوزنة إلكتروني",
+ "وحدة مؤثرات صوتية",
+ "مؤثر الكورس",
+ "ارتداد كرة السلة",
+ "صوت قوي مفاجئ",
+ "صفعة",
+ "ضربة قوية",
+ "تحطّم",
+ "انكسار",
+ "ارتداد",
+ "فرقعة سوط",
+ "رفرفة",
+ "خدش",
+ "كشط",
+ "فرك",
+ "تدحرج",
+ "سحق",
+ "تجعيد الورق",
+ "تمزيق",
+ "صفير إلكتروني قصير",
+ "رنين حاد",
+ "رنّة معدنية",
+ "قرقعة معدنية",
+ "صرير حاد",
+ "صرير",
+ "حفيف",
+ "أزيز",
+ "قعقعة",
+ "أزيز الزيت الساخن",
+ "نقرات",
+ "طقطقة إيقاعية",
+ "هدير",
+ "سقوط شيء في الماء",
+ "جلجلة",
+ "طنين متواصل",
+ "طنين حاد قصير",
+ "صوت زنبركي (بوينغ)",
+ "قرمشة",
+ "صمت",
+ "نغمة نقية (موجة جيبية)",
+ "نغمة توافقية",
+ "نغمة صاعدة أو هابطة",
+ "مؤثر صوتي",
+ "نبضات صوتية منتظمة",
+ "في الداخل (غرفة صغيرة)",
+ "في الداخل (قاعة كبيرة)",
+ "في الداخل (مكان عام)",
+ "في الخارج (بيئة حضرية)",
+ "في الخارج (بيئة طبيعية)",
+ "صدى المكان",
+ "صدى",
+ "ضوضاء",
+ "ضوضاء بيئية",
+ "تشويش كهربائي",
+ "طنين الكهرباء",
+ "تشوّه الصوت",
+ "ارتجاع الصوت في الهاتف",
+ "صخب فوضوي",
+ "ضوضاء بيضاء",
+ "ضوضاء وردية",
+ "هدير نابض",
+ "اهتزاز ميكانيكي",
+ "تلفزيون",
+ "راديو",
+ "تسجيل ميداني"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/de.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/de.json
new file mode 100644
index 0000000..d69b5fb
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/de.json
@@ -0,0 +1,523 @@
+[
+ "Sprechen",
+ "Sprechendes Kind",
+ "Gespräch",
+ "Erzählung, Monolog",
+ "Brabbeln",
+ "Computerstimme",
+ "Rufen",
+ "Grölen",
+ "Jubelschrei",
+ "Brüllen",
+ "Kindergeschrei",
+ "Schreien",
+ "Flüstern",
+ "Lachen",
+ "Babylachen",
+ "Kichern",
+ "Hämisches Kichern",
+ "Schallendes Lachen",
+ "Leises Lachen",
+ "Weinen",
+ "Babyweinen",
+ "Wimmern",
+ "Stöhnen",
+ "Seufzen",
+ "Gesang",
+ "Chor",
+ "Jodeln",
+ "Sprechgesang",
+ "Mantra",
+ "Singendes Kind",
+ "Synthetischer Gesang",
+ "Rap",
+ "Summen",
+ "Ächzen",
+ "Grunzen",
+ "Pfeifen",
+ "Atmen",
+ "Pfeifendes Atmen",
+ "Schnarchen",
+ "Nach Luft schnappen",
+ "Keuchen",
+ "Schnauben",
+ "Husten",
+ "Räuspern",
+ "Niesen",
+ "Schniefen",
+ "Rennende Person",
+ "Schlurfen",
+ "Schritte",
+ "Kauen",
+ "Beißen",
+ "Gurgeln",
+ "Magenknurren",
+ "Rülpsen",
+ "Schluckauf",
+ "Pupsen",
+ "Hände",
+ "Fingerschnippen",
+ "Klatschen",
+ "Herzschlag",
+ "Herzgeräusch",
+ "Jubel",
+ "Applaus",
+ "Geplauder",
+ "Menschenmenge",
+ "Stimmengewirr",
+ "Spielende Kinder",
+ "Tier",
+ "Haustiere",
+ "Hund",
+ "Bellen",
+ "Kläffen",
+ "Heulen (Hund, Wolf)",
+ "Wau-wau",
+ "Knurren",
+ "Winseln",
+ "Katze",
+ "Schnurren",
+ "Miauen",
+ "Zischen",
+ "Jaulen (Katze)",
+ "Nutztiere",
+ "Pferd",
+ "Hufgeklapper",
+ "Wiehern",
+ "Rinder",
+ "Muhen",
+ "Kuhglocke",
+ "Schwein",
+ "Grunzen (Schwein)",
+ "Ziege",
+ "Meckern, Blöken",
+ "Schaf",
+ "Geflügel",
+ "Huhn",
+ "Gackern",
+ "Hahnenschrei",
+ "Truthahn",
+ "Kollern (Truthahn)",
+ "Ente",
+ "Quaken (Ente)",
+ "Gans",
+ "Schnattern (Gans)",
+ "Wildtiere",
+ "Großkatzen (Löwe, Tiger)",
+ "Brüllen (Großkatze)",
+ "Vogel",
+ "Vogelgesang",
+ "Zwitschern",
+ "Kreischen (Vogel)",
+ "Taube",
+ "Gurren",
+ "Krähe",
+ "Krächzen",
+ "Eule",
+ "Eulenruf",
+ "Flügelschlagen",
+ "Wölfe, Wildhunde",
+ "Nagetiere",
+ "Maus",
+ "Trippeln",
+ "Insekt",
+ "Grille",
+ "Mücke",
+ "Fliege",
+ "Summen (Insekt)",
+ "Biene, Wespe",
+ "Frosch",
+ "Quaken (Frosch)",
+ "Schlange",
+ "Rasseln",
+ "Walgesang",
+ "Musik",
+ "Musikinstrument",
+ "Zupfinstrument",
+ "Gitarre",
+ "E-Gitarre",
+ "E-Bass",
+ "Akustikgitarre",
+ "Slide-Gitarre",
+ "Tapping (Gitarre)",
+ "Schrammeln (Gitarre)",
+ "Banjo",
+ "Sitar",
+ "Mandoline",
+ "Zither",
+ "Ukulele",
+ "Tasteninstrument",
+ "Klavier",
+ "E-Piano",
+ "Orgel",
+ "Elektronische Orgel",
+ "Hammondorgel",
+ "Synthesizer",
+ "Sampler",
+ "Cembalo",
+ "Schlaginstrument",
+ "Schlagzeug",
+ "Drumcomputer",
+ "Trommel",
+ "Kleine Trommel",
+ "Rimshot",
+ "Trommelwirbel",
+ "Große Trommel",
+ "Pauke",
+ "Tabla",
+ "Becken (Instrument)",
+ "Hi-Hat",
+ "Holzblocktrommel",
+ "Tamburin",
+ "Rassel (Instrument)",
+ "Maracas",
+ "Gong",
+ "Röhrenglocken",
+ "Stabspiele",
+ "Xylophon, Marimba",
+ "Glockenspiel",
+ "Vibraphon",
+ "Steeldrum",
+ "Orchester",
+ "Blechblasinstrument",
+ "Waldhorn",
+ "Trompete",
+ "Posaune",
+ "Streichinstrument",
+ "Streicher",
+ "Geige",
+ "Pizzicato",
+ "Cello",
+ "Kontrabass",
+ "Holzblasinstrument",
+ "Flöte",
+ "Saxophon",
+ "Klarinette",
+ "Harfe",
+ "Glocke",
+ "Kirchenglocke",
+ "Schellen",
+ "Fahrradklingel",
+ "Stimmgabel",
+ "Glockenklang",
+ "Windspiel",
+ "Glockengeläut",
+ "Mundharmonika",
+ "Akkordeon",
+ "Dudelsack",
+ "Didgeridoo",
+ "Schofar",
+ "Theremin",
+ "Klangschale",
+ "Scratching (DJ)",
+ "Popmusik",
+ "Hip-Hop",
+ "Beatboxing",
+ "Rockmusik",
+ "Heavy Metal",
+ "Punkrock",
+ "Grunge",
+ "Progressive Rock",
+ "Rock ’n’ Roll",
+ "Psychedelic Rock",
+ "Rhythm and Blues",
+ "Soul",
+ "Reggae",
+ "Country",
+ "Swing",
+ "Bluegrass",
+ "Funk (Musik)",
+ "Folk",
+ "Orientalische Musik",
+ "Jazz",
+ "Disco",
+ "Klassische Musik",
+ "Oper",
+ "Elektronische Musik",
+ "House-Musik",
+ "Techno",
+ "Dubstep",
+ "Drum and Bass",
+ "Electronica",
+ "Elektronische Tanzmusik",
+ "Ambient-Musik",
+ "Trance",
+ "Lateinamerikanische Musik",
+ "Salsa",
+ "Flamenco",
+ "Blues",
+ "Kindermusik",
+ "New Age",
+ "Vokalmusik",
+ "A cappella",
+ "Afrikanische Musik",
+ "Afrobeat",
+ "Christliche Musik",
+ "Gospel",
+ "Asiatische Musik",
+ "Karnatische Musik",
+ "Bollywood-Musik",
+ "Ska",
+ "Traditionelle Musik",
+ "Indie",
+ "Lied",
+ "Hintergrundmusik",
+ "Titelmusik",
+ "Werbejingle",
+ "Soundtrack",
+ "Schlaflied",
+ "Videospielmusik",
+ "Weihnachtsmusik",
+ "Tanzmusik",
+ "Hochzeitsmusik",
+ "Fröhliche Musik",
+ "Traurige Musik",
+ "Zärtliche Musik",
+ "Aufregende Musik",
+ "Wütende Musik",
+ "Gruselige Musik",
+ "Wind",
+ "Blätterrascheln",
+ "Windgeräusch (Mikrofon)",
+ "Gewitter",
+ "Donner",
+ "Wasser",
+ "Regen",
+ "Regentropfen",
+ "Regenprasseln",
+ "Bach",
+ "Wasserfall",
+ "Meer",
+ "Wellenrauschen",
+ "Dampf",
+ "Gluckern",
+ "Feuer",
+ "Knistern",
+ "Fahrzeug",
+ "Boot",
+ "Segelboot",
+ "Ruderboot, Kanu",
+ "Motorboot",
+ "Schiff",
+ "Kraftfahrzeug",
+ "Auto",
+ "Hupen",
+ "Kurzes Hupen",
+ "Autoalarm",
+ "Elektrische Fensterheber",
+ "Schleudern (Auto)",
+ "Reifenquietschen",
+ "Vorbeifahrendes Auto",
+ "Autorennen",
+ "Lastwagen",
+ "Druckluftbremse",
+ "Lastwagenhupe",
+ "Rückfahrwarnton",
+ "Eiswagen",
+ "Bus",
+ "Einsatzfahrzeug",
+ "Polizeiauto (Sirene)",
+ "Krankenwagen (Sirene)",
+ "Feuerwehrauto (Sirene)",
+ "Motorrad",
+ "Verkehrslärm",
+ "Schienenverkehr",
+ "Zug",
+ "Zugpfeife",
+ "Zughupe",
+ "Eisenbahnwaggon",
+ "Quietschende Zugräder",
+ "U-Bahn",
+ "Luftfahrzeug",
+ "Flugzeugtriebwerk",
+ "Düsentriebwerk",
+ "Propeller",
+ "Hubschrauber",
+ "Flugzeug",
+ "Fahrrad",
+ "Skateboard",
+ "Motor",
+ "Kleiner Motor",
+ "Zahnarztbohrer",
+ "Rasenmäher",
+ "Kettensäge",
+ "Mittelgroßer Motor",
+ "Großer Motor",
+ "Motorklopfen",
+ "Motorstart",
+ "Motor im Leerlauf",
+ "Aufheulender Motor",
+ "Tür",
+ "Türklingel",
+ "Türgong",
+ "Schiebetür",
+ "Türenknallen",
+ "Klopfen",
+ "Leichtes Klopfen",
+ "Quietschen",
+ "Schranktür",
+ "Schublade",
+ "Geschirrklappern",
+ "Besteck",
+ "Schneiden (Küche)",
+ "Braten in der Pfanne",
+ "Mikrowelle",
+ "Mixer",
+ "Wasserhahn",
+ "Wasser im Waschbecken",
+ "Wasser in der Badewanne",
+ "Föhn",
+ "Toilettenspülung",
+ "Zahnbürste",
+ "Elektrische Zahnbürste",
+ "Staubsauger",
+ "Reißverschluss",
+ "Klimpernde Schlüssel",
+ "Fallende Münze",
+ "Schere",
+ "Elektrorasierer",
+ "Kartenmischen",
+ "Tippen",
+ "Schreibmaschine",
+ "Computertastatur",
+ "Schreiben",
+ "Alarm",
+ "Telefon",
+ "Telefonklingeln",
+ "Klingelton",
+ "Wähltastentöne",
+ "Wählton",
+ "Besetztzeichen",
+ "Wecker",
+ "Sirene",
+ "Zivilschutzsirene",
+ "Summer",
+ "Rauchmelder",
+ "Feueralarm",
+ "Nebelhorn",
+ "Trillerpfeife",
+ "Dampfpfeife",
+ "Mechanik",
+ "Ratsche",
+ "Uhr",
+ "Ticken",
+ "Ticktack",
+ "Zahnräder",
+ "Seilrollen",
+ "Nähmaschine",
+ "Ventilator",
+ "Klimaanlage",
+ "Registrierkasse",
+ "Drucker",
+ "Kamera",
+ "Spiegelreflexkamera",
+ "Werkzeug",
+ "Hammer",
+ "Presslufthammer",
+ "Sägen",
+ "Feilen",
+ "Schleifen",
+ "Elektrowerkzeug",
+ "Bohrmaschine",
+ "Explosion",
+ "Schuss",
+ "Maschinengewehr",
+ "Gewehrsalve",
+ "Artilleriefeuer",
+ "Spielzeugpistole",
+ "Feuerwerk",
+ "Böller",
+ "Platzen",
+ "Vulkanausbruch",
+ "Dröhnen",
+ "Holz",
+ "Holzhacken",
+ "Splitterndes Holz",
+ "Knacken",
+ "Glas",
+ "Klirren",
+ "Zersplittern",
+ "Flüssigkeit",
+ "Platschen",
+ "Schwappen",
+ "Nasses Quetschen",
+ "Tropfen",
+ "Gießen",
+ "Rinnsal",
+ "Schwall",
+ "Befüllen",
+ "Sprühen",
+ "Pumpen",
+ "Umrühren",
+ "Kochendes Wasser",
+ "Sonar",
+ "Pfeil",
+ "Sausen",
+ "Dumpfer Schlag",
+ "Hohler Schlag",
+ "Stimmgerät",
+ "Effektgerät",
+ "Chorus-Effekt",
+ "Dribbeln (Basketball)",
+ "Knall",
+ "Ohrfeige, Klaps",
+ "Kräftiger Schlag",
+ "Zertrümmern",
+ "Zerbrechen",
+ "Abprallen",
+ "Peitschenknall",
+ "Flattern",
+ "Kratzen",
+ "Schaben",
+ "Reiben",
+ "Rollen",
+ "Zerdrücken",
+ "Zerknüllen",
+ "Zerreißen",
+ "Piepton",
+ "Ping",
+ "Ding (heller Klang)",
+ "Scheppern",
+ "Quieken",
+ "Knarren",
+ "Rascheln",
+ "Surren",
+ "Klappern",
+ "Brutzeln",
+ "Klicken",
+ "Klackern",
+ "Grollen",
+ "Plopp (ins Wasser)",
+ "Klimpern",
+ "Brummen",
+ "Sirren",
+ "Boing (Sprungfeder)",
+ "Knirschen",
+ "Stille",
+ "Sinuston",
+ "Oberton",
+ "Gleitender Ton",
+ "Soundeffekt",
+ "Pulsierender Ton",
+ "Drinnen, kleiner Raum",
+ "Drinnen, großer Raum",
+ "Drinnen, öffentlicher Ort",
+ "Draußen, Stadt",
+ "Draußen, Natur",
+ "Nachhall",
+ "Echo",
+ "Rauschen",
+ "Umweltlärm",
+ "Statisches Rauschen",
+ "Brummen (Stromnetz)",
+ "Verzerrung",
+ "Mithörton (Telefon)",
+ "Lautes Durcheinander",
+ "Weißes Rauschen",
+ "Rosa Rauschen",
+ "Wummern",
+ "Vibrationsgeräusch",
+ "Fernseher",
+ "Radio",
+ "Tonaufnahme vor Ort"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/en.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/en.json
new file mode 100644
index 0000000..f11e96c
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/en.json
@@ -0,0 +1,523 @@
+[
+ "Speech",
+ "Child speech, kid speaking",
+ "Conversation",
+ "Narration, monologue",
+ "Babbling",
+ "Speech synthesizer",
+ "Shout",
+ "Bellow",
+ "Whoop",
+ "Yell",
+ "Children shouting",
+ "Screaming",
+ "Whispering",
+ "Laughter",
+ "Baby laughter",
+ "Giggle",
+ "Snicker",
+ "Belly laugh",
+ "Chuckle, chortle",
+ "Crying, sobbing",
+ "Baby cry, infant cry",
+ "Whimper",
+ "Wail, moan",
+ "Sigh",
+ "Singing",
+ "Choir",
+ "Yodeling",
+ "Chant",
+ "Mantra",
+ "Child singing",
+ "Synthetic singing",
+ "Rapping",
+ "Humming",
+ "Groan",
+ "Grunt",
+ "Whistling",
+ "Breathing",
+ "Wheeze",
+ "Snoring",
+ "Gasp",
+ "Pant",
+ "Snort",
+ "Cough",
+ "Throat clearing",
+ "Sneeze",
+ "Sniff",
+ "Run",
+ "Shuffle",
+ "Walk, footsteps",
+ "Chewing, mastication",
+ "Biting",
+ "Gargling",
+ "Stomach rumble",
+ "Burping, eructation",
+ "Hiccup",
+ "Fart",
+ "Hands",
+ "Finger snapping",
+ "Clapping",
+ "Heart sounds, heartbeat",
+ "Heart murmur",
+ "Cheering",
+ "Applause",
+ "Chatter",
+ "Crowd",
+ "Hubbub, speech noise, speech babble",
+ "Children playing",
+ "Animal",
+ "Domestic animals, pets",
+ "Dog",
+ "Bark",
+ "Yip",
+ "Howl",
+ "Bow-wow",
+ "Growling",
+ "Whimper (dog)",
+ "Cat",
+ "Purr",
+ "Meow",
+ "Hiss",
+ "Caterwaul",
+ "Livestock, farm animals, working animals",
+ "Horse",
+ "Clip-clop",
+ "Neigh, whinny",
+ "Cattle, bovinae",
+ "Moo",
+ "Cowbell",
+ "Pig",
+ "Oink",
+ "Goat",
+ "Bleat",
+ "Sheep",
+ "Fowl",
+ "Chicken, rooster",
+ "Cluck",
+ "Crowing, cock-a-doodle-doo",
+ "Turkey",
+ "Gobble",
+ "Duck",
+ "Quack",
+ "Goose",
+ "Honk",
+ "Wild animals",
+ "Roaring cats (lions, tigers)",
+ "Roar",
+ "Bird",
+ "Bird vocalization, bird call, bird song",
+ "Chirp, tweet",
+ "Squawk",
+ "Pigeon, dove",
+ "Coo",
+ "Crow",
+ "Caw",
+ "Owl",
+ "Hoot",
+ "Bird flight, flapping wings",
+ "Canidae, dogs, wolves",
+ "Rodents, rats, mice",
+ "Mouse",
+ "Patter",
+ "Insect",
+ "Cricket",
+ "Mosquito",
+ "Fly, housefly",
+ "Buzz",
+ "Bee, wasp, etc.",
+ "Frog",
+ "Croak",
+ "Snake",
+ "Rattle",
+ "Whale vocalization",
+ "Music",
+ "Musical instrument",
+ "Plucked string instrument",
+ "Guitar",
+ "Electric guitar",
+ "Bass guitar",
+ "Acoustic guitar",
+ "Steel guitar, slide guitar",
+ "Tapping (guitar technique)",
+ "Strum",
+ "Banjo",
+ "Sitar",
+ "Mandolin",
+ "Zither",
+ "Ukulele",
+ "Keyboard (musical)",
+ "Piano",
+ "Electric piano",
+ "Organ",
+ "Electronic organ",
+ "Hammond organ",
+ "Synthesizer",
+ "Sampler",
+ "Harpsichord",
+ "Percussion",
+ "Drum kit",
+ "Drum machine",
+ "Drum",
+ "Snare drum",
+ "Rimshot",
+ "Drum roll",
+ "Bass drum",
+ "Timpani",
+ "Tabla",
+ "Cymbal",
+ "Hi-hat",
+ "Wood block",
+ "Tambourine",
+ "Rattle (instrument)",
+ "Maraca",
+ "Gong",
+ "Tubular bells",
+ "Mallet percussion",
+ "Marimba, xylophone",
+ "Glockenspiel",
+ "Vibraphone",
+ "Steelpan",
+ "Orchestra",
+ "Brass instrument",
+ "French horn",
+ "Trumpet",
+ "Trombone",
+ "Bowed string instrument",
+ "String section",
+ "Violin, fiddle",
+ "Pizzicato",
+ "Cello",
+ "Double bass",
+ "Wind instrument, woodwind instrument",
+ "Flute",
+ "Saxophone",
+ "Clarinet",
+ "Harp",
+ "Bell",
+ "Church bell",
+ "Jingle bell",
+ "Bicycle bell",
+ "Tuning fork",
+ "Chime",
+ "Wind chime",
+ "Change ringing (campanology)",
+ "Harmonica",
+ "Accordion",
+ "Bagpipes",
+ "Didgeridoo",
+ "Shofar",
+ "Theremin",
+ "Singing bowl",
+ "Scratching (performance technique)",
+ "Pop music",
+ "Hip hop music",
+ "Beatboxing",
+ "Rock music",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Progressive rock",
+ "Rock and roll",
+ "Psychedelic rock",
+ "Rhythm and blues",
+ "Soul music",
+ "Reggae",
+ "Country",
+ "Swing music",
+ "Bluegrass",
+ "Funk",
+ "Folk music",
+ "Middle Eastern music",
+ "Jazz",
+ "Disco",
+ "Classical music",
+ "Opera",
+ "Electronic music",
+ "House music",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Electronic dance music",
+ "Ambient music",
+ "Trance music",
+ "Music of Latin America",
+ "Salsa music",
+ "Flamenco",
+ "Blues",
+ "Music for children",
+ "New-age music",
+ "Vocal music",
+ "A capella",
+ "Music of Africa",
+ "Afrobeat",
+ "Christian music",
+ "Gospel music",
+ "Music of Asia",
+ "Carnatic music",
+ "Music of Bollywood",
+ "Ska",
+ "Traditional music",
+ "Independent music",
+ "Song",
+ "Background music",
+ "Theme music",
+ "Jingle (music)",
+ "Soundtrack music",
+ "Lullaby",
+ "Video game music",
+ "Christmas music",
+ "Dance music",
+ "Wedding music",
+ "Happy music",
+ "Sad music",
+ "Tender music",
+ "Exciting music",
+ "Angry music",
+ "Scary music",
+ "Wind",
+ "Rustling leaves",
+ "Wind noise (microphone)",
+ "Thunderstorm",
+ "Thunder",
+ "Water",
+ "Rain",
+ "Raindrop",
+ "Rain on surface",
+ "Stream",
+ "Waterfall",
+ "Ocean",
+ "Waves, surf",
+ "Steam",
+ "Gurgling",
+ "Fire",
+ "Crackle",
+ "Vehicle",
+ "Boat, Water vehicle",
+ "Sailboat, sailing ship",
+ "Rowboat, canoe, kayak",
+ "Motorboat, speedboat",
+ "Ship",
+ "Motor vehicle (road)",
+ "Car",
+ "Vehicle horn, car horn, honking",
+ "Toot",
+ "Car alarm",
+ "Power windows, electric windows",
+ "Skidding",
+ "Tire squeal",
+ "Car passing by",
+ "Race car, auto racing",
+ "Truck",
+ "Air brake",
+ "Air horn, truck horn",
+ "Reversing beeps",
+ "Ice cream truck, ice cream van",
+ "Bus",
+ "Emergency vehicle",
+ "Police car (siren)",
+ "Ambulance (siren)",
+ "Fire engine, fire truck (siren)",
+ "Motorcycle",
+ "Traffic noise, roadway noise",
+ "Rail transport",
+ "Train",
+ "Train whistle",
+ "Train horn",
+ "Railroad car, train wagon",
+ "Train wheels squealing",
+ "Subway, metro, underground",
+ "Aircraft",
+ "Aircraft engine",
+ "Jet engine",
+ "Propeller, airscrew",
+ "Helicopter",
+ "Fixed-wing aircraft, airplane",
+ "Bicycle",
+ "Skateboard",
+ "Engine",
+ "Light engine (high frequency)",
+ "Dental drill, dentist's drill",
+ "Lawn mower",
+ "Chainsaw",
+ "Medium engine (mid frequency)",
+ "Heavy engine (low frequency)",
+ "Engine knocking",
+ "Engine starting",
+ "Idling",
+ "Accelerating, revving, vroom",
+ "Door",
+ "Doorbell",
+ "Ding-dong",
+ "Sliding door",
+ "Slam",
+ "Knock",
+ "Tap",
+ "Squeak",
+ "Cupboard open or close",
+ "Drawer open or close",
+ "Dishes, pots, and pans",
+ "Cutlery, silverware",
+ "Chopping (food)",
+ "Frying (food)",
+ "Microwave oven",
+ "Blender",
+ "Water tap, faucet",
+ "Sink (filling or washing)",
+ "Bathtub (filling or washing)",
+ "Hair dryer",
+ "Toilet flush",
+ "Toothbrush",
+ "Electric toothbrush",
+ "Vacuum cleaner",
+ "Zipper (clothing)",
+ "Keys jangling",
+ "Coin (dropping)",
+ "Scissors",
+ "Electric shaver, electric razor",
+ "Shuffling cards",
+ "Typing",
+ "Typewriter",
+ "Computer keyboard",
+ "Writing",
+ "Alarm",
+ "Telephone",
+ "Telephone bell ringing",
+ "Ringtone",
+ "Telephone dialing, DTMF",
+ "Dial tone",
+ "Busy signal",
+ "Alarm clock",
+ "Siren",
+ "Civil defense siren",
+ "Buzzer",
+ "Smoke detector, smoke alarm",
+ "Fire alarm",
+ "Foghorn",
+ "Whistle",
+ "Steam whistle",
+ "Mechanisms",
+ "Ratchet, pawl",
+ "Clock",
+ "Tick",
+ "Tick-tock",
+ "Gears",
+ "Pulleys",
+ "Sewing machine",
+ "Mechanical fan",
+ "Air conditioning",
+ "Cash register",
+ "Printer",
+ "Camera",
+ "Single-lens reflex camera",
+ "Tools",
+ "Hammer",
+ "Jackhammer",
+ "Sawing",
+ "Filing (rasp)",
+ "Sanding",
+ "Power tool",
+ "Drill",
+ "Explosion",
+ "Gunshot, gunfire",
+ "Machine gun",
+ "Fusillade",
+ "Artillery fire",
+ "Cap gun",
+ "Fireworks",
+ "Firecracker",
+ "Burst, pop",
+ "Eruption",
+ "Boom",
+ "Wood",
+ "Chop",
+ "Splinter",
+ "Crack",
+ "Glass",
+ "Chink, clink",
+ "Shatter",
+ "Liquid",
+ "Splash, splatter",
+ "Slosh",
+ "Squish",
+ "Drip",
+ "Pour",
+ "Trickle, dribble",
+ "Gush",
+ "Fill (with liquid)",
+ "Spray",
+ "Pump (liquid)",
+ "Stir",
+ "Boiling",
+ "Sonar",
+ "Arrow",
+ "Whoosh, swoosh, swish",
+ "Thump, thud",
+ "Thunk",
+ "Electronic tuner",
+ "Effects unit",
+ "Chorus effect",
+ "Basketball bounce",
+ "Bang",
+ "Slap, smack",
+ "Whack, thwack",
+ "Smash, crash",
+ "Breaking",
+ "Bouncing",
+ "Whip",
+ "Flap",
+ "Scratch",
+ "Scrape",
+ "Rub",
+ "Roll",
+ "Crushing",
+ "Crumpling, crinkling",
+ "Tearing",
+ "Beep, bleep",
+ "Ping",
+ "Ding",
+ "Clang",
+ "Squeal",
+ "Creak",
+ "Rustle",
+ "Whir",
+ "Clatter",
+ "Sizzle",
+ "Clicking",
+ "Clickety-clack",
+ "Rumble",
+ "Plop",
+ "Jingle, tinkle",
+ "Hum",
+ "Zing",
+ "Boing",
+ "Crunch",
+ "Silence",
+ "Sine wave",
+ "Harmonic",
+ "Chirp tone",
+ "Sound effect",
+ "Pulse",
+ "Inside, small room",
+ "Inside, large room or hall",
+ "Inside, public space",
+ "Outside, urban or manmade",
+ "Outside, rural or natural",
+ "Reverberation",
+ "Echo",
+ "Noise",
+ "Environmental noise",
+ "Static",
+ "Mains hum",
+ "Distortion",
+ "Sidetone",
+ "Cacophony",
+ "White noise",
+ "Pink noise",
+ "Throbbing",
+ "Vibration",
+ "Television",
+ "Radio",
+ "Field recording"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/es.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/es.json
new file mode 100644
index 0000000..3b85a69
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/es.json
@@ -0,0 +1,523 @@
+[
+ "Habla",
+ "Niño hablando",
+ "Conversación",
+ "Narración",
+ "Balbuceo",
+ "Voz sintética",
+ "Grito",
+ "Bramido",
+ "Grito de júbilo",
+ "Alarido",
+ "Niños gritando",
+ "Grito agudo",
+ "Susurro",
+ "Risa",
+ "Risa de bebé",
+ "Risita",
+ "Risa burlona",
+ "Carcajada",
+ "Risa contenida",
+ "Llanto",
+ "Llanto de bebé",
+ "Gimoteo",
+ "Gemido",
+ "Suspiro",
+ "Canto",
+ "Coro",
+ "Canto tirolés",
+ "Cántico",
+ "Mantra",
+ "Niño cantando",
+ "Canto sintético",
+ "Rap",
+ "Tarareo",
+ "Quejido",
+ "Gruñido de persona",
+ "Silbido",
+ "Respiración",
+ "Silbido al respirar",
+ "Ronquido",
+ "Inhalación brusca",
+ "Jadeo",
+ "Resoplido",
+ "Tos",
+ "Carraspeo",
+ "Estornudo",
+ "Sorber por la nariz",
+ "Correr",
+ "Arrastrar los pies",
+ "Pasos",
+ "Masticación",
+ "Mordisco",
+ "Gárgaras",
+ "Ruido de estómago",
+ "Eructo",
+ "Hipo",
+ "Flatulencia",
+ "Manos",
+ "Chasquido de dedos",
+ "Palmadas",
+ "Latido del corazón",
+ "Soplo cardíaco",
+ "Vítores",
+ "Aplausos",
+ "Gente charlando",
+ "Multitud",
+ "Vocerío",
+ "Niños jugando",
+ "Animal",
+ "Animales domésticos",
+ "Perro",
+ "Ladrido",
+ "Ladrido agudo",
+ "Aullido",
+ "Guau guau",
+ "Gruñido",
+ "Gimoteo de perro",
+ "Gato",
+ "Ronroneo",
+ "Maullido",
+ "Siseo",
+ "Maullido de celo",
+ "Animales de granja",
+ "Caballo",
+ "Pisadas de caballo",
+ "Relincho",
+ "Vaca",
+ "Mugido",
+ "Cencerro",
+ "Cerdo",
+ "Gruñido de cerdo",
+ "Cabra",
+ "Balido",
+ "Oveja",
+ "Aves de corral",
+ "Gallina y gallo",
+ "Cacareo",
+ "Canto del gallo",
+ "Pavo",
+ "Gluglú de pavo",
+ "Pato",
+ "Graznido de pato",
+ "Ganso",
+ "Graznido de ganso",
+ "Animales salvajes",
+ "Grandes felinos",
+ "Rugido",
+ "Pájaro",
+ "Canto de pájaros",
+ "Piar",
+ "Graznido",
+ "Paloma",
+ "Arrullo",
+ "Cuervo",
+ "Graznido de cuervo",
+ "Búho",
+ "Ululato de búho",
+ "Aleteo de pájaro",
+ "Lobos y otros cánidos",
+ "Roedores",
+ "Ratón",
+ "Correteo de patitas",
+ "Insecto",
+ "Grillo",
+ "Mosquito",
+ "Mosca",
+ "Zumbido",
+ "Abeja o avispa",
+ "Rana",
+ "Croar",
+ "Serpiente",
+ "Cascabel de serpiente",
+ "Canto de ballena",
+ "Música",
+ "Instrumento musical",
+ "Instrumento de cuerda pulsada",
+ "Guitarra",
+ "Guitarra eléctrica",
+ "Bajo eléctrico",
+ "Guitarra acústica",
+ "Guitarra slide",
+ "Tapping (guitarra)",
+ "Rasgueo",
+ "Banjo",
+ "Sitar",
+ "Mandolina",
+ "Cítara",
+ "Ukelele",
+ "Teclado musical",
+ "Piano",
+ "Piano eléctrico",
+ "Órgano",
+ "Órgano electrónico",
+ "Órgano Hammond",
+ "Sintetizador",
+ "Sampler",
+ "Clavecín",
+ "Percusión",
+ "Batería (instrumento)",
+ "Caja de ritmos",
+ "Tambor",
+ "Redoblante",
+ "Golpe de aro",
+ "Redoble",
+ "Bombo",
+ "Timbales de orquesta",
+ "Tabla (tambor indio)",
+ "Platillo",
+ "Hi-hat (platillos)",
+ "Bloque de madera",
+ "Pandereta",
+ "Sonajero",
+ "Maracas",
+ "Gong",
+ "Campanas tubulares",
+ "Percusión melódica",
+ "Marimba o xilófono",
+ "Metalófono",
+ "Vibráfono",
+ "Tambor de acero",
+ "Orquesta",
+ "Instrumento de viento metal",
+ "Trompa (corno francés)",
+ "Trompeta",
+ "Trombón",
+ "Instrumento de cuerda frotada",
+ "Sección de cuerdas",
+ "Violín",
+ "Pizzicato",
+ "Violonchelo",
+ "Contrabajo",
+ "Instrumento de viento madera",
+ "Flauta",
+ "Saxofón",
+ "Clarinete",
+ "Arpa",
+ "Campana",
+ "Campana de iglesia",
+ "Cascabel",
+ "Timbre de bicicleta",
+ "Diapasón",
+ "Carillón",
+ "Campanas de viento",
+ "Repique de campanas",
+ "Armónica",
+ "Acordeón",
+ "Gaita",
+ "Didgeridoo",
+ "Shofar",
+ "Theremín",
+ "Cuenco tibetano",
+ "Scratch de DJ",
+ "Música pop",
+ "Música hip hop",
+ "Beatbox",
+ "Música rock",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Rock progresivo",
+ "Rock and roll",
+ "Rock psicodélico",
+ "Rhythm and blues",
+ "Música soul",
+ "Reggae",
+ "Música country",
+ "Música swing",
+ "Bluegrass",
+ "Funk",
+ "Música folk",
+ "Música de Oriente Medio",
+ "Jazz",
+ "Música disco",
+ "Música clásica",
+ "Ópera",
+ "Música electrónica",
+ "Música house",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electrónica",
+ "Música electrónica de baile",
+ "Música ambient",
+ "Música trance",
+ "Música latinoamericana",
+ "Música salsa",
+ "Flamenco",
+ "Blues",
+ "Música infantil",
+ "Música new age",
+ "Música vocal",
+ "A capela",
+ "Música africana",
+ "Afrobeat",
+ "Música cristiana",
+ "Música góspel",
+ "Música asiática",
+ "Música carnática",
+ "Música de Bollywood",
+ "Ska",
+ "Música tradicional",
+ "Música independiente",
+ "Canción",
+ "Música de fondo",
+ "Tema principal",
+ "Jingle publicitario",
+ "Banda sonora",
+ "Canción de cuna",
+ "Música de videojuegos",
+ "Música navideña",
+ "Música de baile",
+ "Música de boda",
+ "Música alegre",
+ "Música triste",
+ "Música tierna",
+ "Música emocionante",
+ "Música furiosa",
+ "Música aterradora",
+ "Viento",
+ "Roce de hojas",
+ "Viento en el micrófono",
+ "Tormenta",
+ "Trueno",
+ "Agua",
+ "Lluvia",
+ "Gota de lluvia",
+ "Repiqueteo de lluvia",
+ "Arroyo",
+ "Cascada",
+ "Océano",
+ "Olas",
+ "Vapor",
+ "Gorgoteo",
+ "Fuego",
+ "Crepitar",
+ "Vehículo",
+ "Embarcación",
+ "Velero",
+ "Bote de remos o canoa",
+ "Lancha motora",
+ "Barco",
+ "Vehículo de motor",
+ "Automóvil",
+ "Bocina o claxon",
+ "Toque de bocina",
+ "Alarma de automóvil",
+ "Ventanillas eléctricas",
+ "Derrape",
+ "Chirrido de neumáticos",
+ "Automóvil que pasa",
+ "Carreras de automóviles",
+ "Camión",
+ "Freno de aire",
+ "Bocina de camión",
+ "Pitidos de marcha atrás",
+ "Camión de helados",
+ "Autobús",
+ "Vehículo de emergencia",
+ "Sirena de policía",
+ "Sirena de ambulancia",
+ "Sirena de bomberos",
+ "Motocicleta",
+ "Ruido de tráfico",
+ "Ferrocarril",
+ "Tren",
+ "Silbato de tren",
+ "Bocina de tren",
+ "Vagón de tren",
+ "Chirrido de ruedas de tren",
+ "Metro",
+ "Aeronave",
+ "Motor de aeronave",
+ "Motor a reacción",
+ "Hélice",
+ "Helicóptero",
+ "Avión",
+ "Bicicleta",
+ "Monopatín (skate)",
+ "Motor",
+ "Motor pequeño (tono agudo)",
+ "Taladro de dentista",
+ "Cortacésped",
+ "Motosierra",
+ "Motor mediano (tono medio)",
+ "Motor grande (tono grave)",
+ "Golpeteo del motor",
+ "Arranque del motor",
+ "Motor al ralentí",
+ "Motor acelerando",
+ "Puerta",
+ "Timbre de la puerta",
+ "Din don del timbre",
+ "Puerta deslizante",
+ "Portazo",
+ "Golpes en la puerta",
+ "Golpecito",
+ "Chirrido",
+ "Abrir o cerrar un armario",
+ "Abrir o cerrar un cajón",
+ "Platos y ollas",
+ "Cubiertos",
+ "Picar alimentos",
+ "Freír alimentos",
+ "Microondas",
+ "Licuadora o batidora",
+ "Grifo de agua",
+ "Lavabo o fregadero",
+ "Bañera o tina",
+ "Secador de pelo",
+ "Descarga del inodoro",
+ "Cepillo de dientes",
+ "Cepillo de dientes eléctrico",
+ "Aspiradora",
+ "Cremallera",
+ "Tintineo de llaves",
+ "Moneda cayendo",
+ "Tijeras",
+ "Afeitadora eléctrica",
+ "Barajar cartas",
+ "Tecleo",
+ "Máquina de escribir",
+ "Teclado de computadora",
+ "Escritura a mano",
+ "Alarma",
+ "Teléfono",
+ "Timbre de teléfono",
+ "Tono de llamada",
+ "Marcación telefónica",
+ "Tono de marcado",
+ "Tono de ocupado",
+ "Despertador",
+ "Sirena",
+ "Sirena de defensa civil",
+ "Zumbador",
+ "Detector de humo",
+ "Alarma de incendios",
+ "Sirena de niebla",
+ "Silbato",
+ "Silbato de vapor",
+ "Mecanismos",
+ "Trinquete",
+ "Reloj",
+ "Tic metálico",
+ "Tictac",
+ "Engranajes",
+ "Poleas",
+ "Máquina de coser",
+ "Ventilador",
+ "Aire acondicionado",
+ "Caja registradora",
+ "Impresora",
+ "Cámara",
+ "Cámara réflex",
+ "Herramientas",
+ "Martillo",
+ "Martillo neumático",
+ "Cortar con sierra",
+ "Limar",
+ "Lijar",
+ "Herramienta eléctrica",
+ "Taladro",
+ "Explosión",
+ "Disparo",
+ "Ametralladora",
+ "Descarga de fusilería",
+ "Fuego de artillería",
+ "Pistola de juguete",
+ "Fuegos artificiales",
+ "Petardo",
+ "Estallido",
+ "Erupción volcánica",
+ "Estruendo",
+ "Madera",
+ "Hachazo",
+ "Madera astillándose",
+ "Chasquido",
+ "Vidrio",
+ "Tintín de copas",
+ "Vidrio rompiéndose",
+ "Líquido",
+ "Salpicadura",
+ "Líquido agitándose",
+ "Estrujar algo mojado",
+ "Goteo",
+ "Verter líquido",
+ "Chorrito",
+ "Chorro fuerte",
+ "Llenado de líquido",
+ "Rociado",
+ "Bombeo de líquido",
+ "Revolver un líquido",
+ "Hervor",
+ "Señal de sonar",
+ "Flecha",
+ "Silbido en el aire",
+ "Golpe sordo",
+ "Golpe hueco",
+ "Afinador electrónico",
+ "Unidad de efectos",
+ "Efecto chorus",
+ "Balón de baloncesto rebotando",
+ "Estampido",
+ "Bofetada",
+ "Golpe seco",
+ "Rotura violenta",
+ "Rotura",
+ "Rebote",
+ "Latigazo",
+ "Tela ondeando",
+ "Arañazo",
+ "Raspado",
+ "Frotamiento",
+ "Objeto rodando",
+ "Aplastamiento",
+ "Arrugar papel",
+ "Desgarro",
+ "Pitido",
+ "Ping",
+ "Tilín",
+ "Golpe metálico",
+ "Chillido",
+ "Crujido de madera",
+ "Roce",
+ "Zumbido mecánico",
+ "Objetos entrechocando",
+ "Chisporroteo",
+ "Clic",
+ "Traqueteo rítmico",
+ "Retumbo",
+ "Caída suave en el agua",
+ "Tintineo",
+ "Zumbido sordo",
+ "Zumbido breve y agudo",
+ "Boing (resorte)",
+ "Crujido",
+ "Silencio",
+ "Tono puro",
+ "Tono armónico",
+ "Tono que sube o baja",
+ "Efecto de sonido",
+ "Pulsos regulares",
+ "Interior, sala pequeña",
+ "Interior, sala grande",
+ "Interior, espacio público",
+ "Exterior, entorno urbano",
+ "Exterior, entorno natural",
+ "Reverberación",
+ "Eco",
+ "Ruido",
+ "Ruido ambiental",
+ "Ruido de estática",
+ "Zumbido eléctrico",
+ "Distorsión",
+ "Retorno de voz (teléfono)",
+ "Cacofonía",
+ "Ruido blanco",
+ "Ruido rosa",
+ "Sonido palpitante",
+ "Vibración mecánica",
+ "Televisión",
+ "Radio",
+ "Grabación de campo"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/fr.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/fr.json
new file mode 100644
index 0000000..7bcca2b
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/fr.json
@@ -0,0 +1,523 @@
+[
+ "Parole",
+ "Parole d'enfant",
+ "Conversation",
+ "Narration",
+ "Babillage",
+ "Voix de synthèse",
+ "Cri",
+ "Cri bestial",
+ "Cri de joie",
+ "Hurlement",
+ "Cris d'enfants",
+ "Cri perçant",
+ "Chuchotement",
+ "Rire",
+ "Rire de bébé",
+ "Petit rire nerveux",
+ "Ricanement",
+ "Éclat de rire",
+ "Rire étouffé",
+ "Pleurs",
+ "Pleurs de bébé",
+ "Pleurnichement",
+ "Gémissement",
+ "Soupir",
+ "Chant",
+ "Chœur",
+ "Yodel",
+ "Psalmodie",
+ "Mantra",
+ "Chant d'enfant",
+ "Chant de synthèse",
+ "Rap",
+ "Fredonnement",
+ "Gémissement de douleur",
+ "Grognement",
+ "Sifflement",
+ "Respiration",
+ "Respiration sifflante",
+ "Ronflement",
+ "Inspiration brusque",
+ "Halètement",
+ "Souffle par le nez",
+ "Toux",
+ "Raclement de gorge",
+ "Éternuement",
+ "Reniflement",
+ "Course à pied",
+ "Pas traînants",
+ "Bruits de pas",
+ "Mastication",
+ "Morsure",
+ "Gargarisme",
+ "Ventre qui gargouille",
+ "Rot",
+ "Hoquet",
+ "Pet",
+ "Bruits de mains",
+ "Claquement de doigts",
+ "Claquement de mains",
+ "Battements de cœur",
+ "Souffle cardiaque",
+ "Acclamations",
+ "Applaudissements",
+ "Bavardages",
+ "Foule",
+ "Brouhaha",
+ "Enfants qui jouent",
+ "Animal",
+ "Animaux domestiques",
+ "Chien",
+ "Aboiement",
+ "Jappement",
+ "Hurlement (chien, loup)",
+ "Ouaf ouaf",
+ "Grognement animal",
+ "Gémissement de chien",
+ "Chat",
+ "Ronronnement",
+ "Miaulement",
+ "Sifflement (chat, serpent)",
+ "Chat en chaleur",
+ "Animaux de ferme",
+ "Cheval",
+ "Bruits de sabots",
+ "Hennissement",
+ "Vache",
+ "Meuglement",
+ "Cloche de vache",
+ "Cochon",
+ "Grognement de cochon",
+ "Chèvre",
+ "Bêlement",
+ "Mouton",
+ "Volaille",
+ "Poule, coq",
+ "Caquètement",
+ "Chant du coq",
+ "Dindon",
+ "Glouglou de dindon",
+ "Canard",
+ "Cri de canard",
+ "Oie",
+ "Cri d'oie",
+ "Animaux sauvages",
+ "Fauves (lions, tigres)",
+ "Rugissement",
+ "Oiseau",
+ "Chant d'oiseau",
+ "Pépiement",
+ "Criaillement",
+ "Pigeon, tourterelle",
+ "Roucoulement",
+ "Corbeau",
+ "Croassement",
+ "Hibou, chouette",
+ "Hululement",
+ "Battements d'ailes",
+ "Loups, chiens sauvages",
+ "Rongeurs",
+ "Souris",
+ "Trottinement",
+ "Insecte",
+ "Grillon",
+ "Moustique",
+ "Mouche",
+ "Bourdonnement",
+ "Abeille, guêpe",
+ "Grenouille",
+ "Coassement",
+ "Serpent",
+ "Bruit de crécelle",
+ "Chant de baleine",
+ "Musique",
+ "Instrument de musique",
+ "Cordes pincées",
+ "Guitare",
+ "Guitare électrique",
+ "Guitare basse",
+ "Guitare acoustique",
+ "Guitare slide",
+ "Tapping (guitare)",
+ "Guitare grattée",
+ "Banjo",
+ "Sitar",
+ "Mandoline",
+ "Cithare",
+ "Ukulélé",
+ "Instrument à clavier",
+ "Piano",
+ "Piano électrique",
+ "Orgue",
+ "Orgue électronique",
+ "Orgue Hammond",
+ "Synthétiseur",
+ "Échantillonneur",
+ "Clavecin",
+ "Percussions",
+ "Batterie (musique)",
+ "Boîte à rythmes",
+ "Tambour",
+ "Caisse claire",
+ "Rimshot",
+ "Roulement de tambour",
+ "Grosse caisse",
+ "Timbales",
+ "Tabla",
+ "Cymbale",
+ "Charleston (cymbale)",
+ "Wood-block",
+ "Tambourin",
+ "Hochet (instrument)",
+ "Maracas",
+ "Gong",
+ "Cloches tubulaires",
+ "Percussions mélodiques",
+ "Xylophone, marimba",
+ "Glockenspiel",
+ "Vibraphone",
+ "Steel drum",
+ "Orchestre",
+ "Cuivres",
+ "Cor d'harmonie",
+ "Trompette",
+ "Trombone",
+ "Cordes frottées",
+ "Ensemble à cordes",
+ "Violon",
+ "Pizzicato",
+ "Violoncelle",
+ "Contrebasse",
+ "Instrument à vent",
+ "Flûte",
+ "Saxophone",
+ "Clarinette",
+ "Harpe",
+ "Cloche",
+ "Cloche d'église",
+ "Grelot",
+ "Sonnette de vélo",
+ "Diapason",
+ "Carillon",
+ "Carillon à vent",
+ "Volée de cloches",
+ "Harmonica",
+ "Accordéon",
+ "Cornemuse",
+ "Didgeridoo",
+ "Chofar",
+ "Thérémine",
+ "Bol tibétain",
+ "Scratch (DJ)",
+ "Musique pop",
+ "Musique hip-hop",
+ "Beatbox",
+ "Musique rock",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Rock progressif",
+ "Rock and roll",
+ "Rock psychédélique",
+ "Rhythm and blues",
+ "Musique soul",
+ "Reggae",
+ "Country",
+ "Musique swing",
+ "Bluegrass",
+ "Funk",
+ "Musique folk",
+ "Musique du Moyen-Orient",
+ "Jazz",
+ "Disco",
+ "Musique classique",
+ "Opéra",
+ "Musique électronique",
+ "Musique house",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Électro dansante (EDM)",
+ "Musique ambient",
+ "Musique trance",
+ "Musique latino-américaine",
+ "Musique salsa",
+ "Flamenco",
+ "Blues",
+ "Musique pour enfants",
+ "Musique new age",
+ "Musique vocale",
+ "A cappella",
+ "Musique africaine",
+ "Afrobeat",
+ "Musique chrétienne",
+ "Musique gospel",
+ "Musique asiatique",
+ "Musique carnatique",
+ "Musique de Bollywood",
+ "Ska",
+ "Musique traditionnelle",
+ "Musique indépendante",
+ "Chanson",
+ "Musique de fond",
+ "Musique de générique",
+ "Jingle publicitaire",
+ "Bande originale",
+ "Berceuse",
+ "Musique de jeu vidéo",
+ "Musique de Noël",
+ "Musique de danse",
+ "Musique de mariage",
+ "Musique joyeuse",
+ "Musique triste",
+ "Musique tendre",
+ "Musique palpitante",
+ "Musique rageuse",
+ "Musique effrayante",
+ "Vent",
+ "Bruissement de feuilles",
+ "Vent dans le micro",
+ "Orage",
+ "Tonnerre",
+ "Eau",
+ "Pluie",
+ "Goutte de pluie",
+ "Pluie qui tambourine",
+ "Ruisseau",
+ "Cascade",
+ "Océan",
+ "Vagues",
+ "Vapeur",
+ "Gargouillis",
+ "Feu",
+ "Crépitement",
+ "Véhicule",
+ "Bateau",
+ "Voilier",
+ "Barque, canoë, kayak",
+ "Bateau à moteur",
+ "Navire",
+ "Véhicule à moteur",
+ "Voiture",
+ "Klaxon",
+ "Coup de klaxon",
+ "Alarme de voiture",
+ "Vitres électriques",
+ "Dérapage",
+ "Crissement de pneus",
+ "Voiture qui passe",
+ "Voiture de course",
+ "Camion",
+ "Frein pneumatique",
+ "Klaxon de camion",
+ "Bips de recul",
+ "Camion de glaces",
+ "Bus",
+ "Véhicule d'urgence",
+ "Voiture de police (sirène)",
+ "Ambulance (sirène)",
+ "Camion de pompiers (sirène)",
+ "Moto",
+ "Bruit de circulation",
+ "Chemin de fer",
+ "Train",
+ "Sifflet de train",
+ "Klaxon de train",
+ "Wagon",
+ "Crissement des rails",
+ "Métro",
+ "Aéronef",
+ "Moteur d'avion",
+ "Réacteur d'avion",
+ "Hélice",
+ "Hélicoptère",
+ "Avion",
+ "Vélo",
+ "Skateboard",
+ "Moteur",
+ "Petit moteur (aigu)",
+ "Fraise de dentiste",
+ "Tondeuse à gazon",
+ "Tronçonneuse",
+ "Moteur moyen",
+ "Gros moteur (grave)",
+ "Cliquetis de moteur",
+ "Démarrage de moteur",
+ "Moteur au ralenti",
+ "Moteur qui accélère",
+ "Porte",
+ "Sonnette",
+ "Ding-dong",
+ "Porte coulissante",
+ "Claquement de porte",
+ "Coups à la porte",
+ "Tapotement",
+ "Couinement",
+ "Porte de placard",
+ "Tiroir",
+ "Vaisselle",
+ "Couverts",
+ "Découpe d'aliments",
+ "Friture",
+ "Micro-ondes",
+ "Mixeur",
+ "Robinet",
+ "Évier, lavabo",
+ "Baignoire",
+ "Sèche-cheveux",
+ "Chasse d'eau",
+ "Brosse à dents",
+ "Brosse à dents électrique",
+ "Aspirateur",
+ "Fermeture éclair",
+ "Cliquetis de clés",
+ "Pièce qui tombe",
+ "Ciseaux",
+ "Rasoir électrique",
+ "Mélange de cartes",
+ "Frappe au clavier",
+ "Machine à écrire",
+ "Clavier d'ordinateur",
+ "Écriture à la main",
+ "Alarme",
+ "Téléphone",
+ "Sonnerie de téléphone",
+ "Sonnerie de portable",
+ "Composition d'un numéro",
+ "Tonalité téléphonique",
+ "Signal occupé",
+ "Réveil",
+ "Sirène",
+ "Sirène d'alerte",
+ "Buzzer",
+ "Détecteur de fumée",
+ "Alarme incendie",
+ "Corne de brume",
+ "Sifflet",
+ "Sifflet à vapeur",
+ "Mécanismes",
+ "Cliquet",
+ "Horloge",
+ "Tic métallique",
+ "Tic-tac",
+ "Engrenages",
+ "Poulies",
+ "Machine à coudre",
+ "Ventilateur",
+ "Climatisation",
+ "Caisse enregistreuse",
+ "Imprimante",
+ "Appareil photo",
+ "Appareil photo reflex",
+ "Outils",
+ "Marteau",
+ "Marteau-piqueur",
+ "Scie",
+ "Lime (outil)",
+ "Ponçage",
+ "Outil électrique",
+ "Perceuse",
+ "Explosion",
+ "Coup de feu",
+ "Mitrailleuse",
+ "Fusillade",
+ "Tir d'artillerie",
+ "Pistolet à amorces",
+ "Feux d'artifice",
+ "Pétard",
+ "Éclatement",
+ "Éruption volcanique",
+ "Boum",
+ "Bois",
+ "Coups de hache",
+ "Bois qui éclate",
+ "Craquement",
+ "Verre",
+ "Tintement",
+ "Bris de verre",
+ "Liquide",
+ "Éclaboussure",
+ "Clapotis",
+ "Écrasement mouillé",
+ "Goutte-à-goutte",
+ "Liquide versé",
+ "Filet d'eau",
+ "Jaillissement",
+ "Remplissage (liquide)",
+ "Pulvérisation",
+ "Pompe (liquide)",
+ "Liquide remué",
+ "Ébullition",
+ "Sonar",
+ "Flèche",
+ "Objet qui fend l'air",
+ "Choc sourd",
+ "Choc creux",
+ "Accordeur électronique",
+ "Unité d'effets",
+ "Effet chorus",
+ "Rebond de ballon (basket)",
+ "Bang",
+ "Claque",
+ "Coup sec",
+ "Fracas",
+ "Objet qui se casse",
+ "Rebond",
+ "Coup de fouet",
+ "Claquement au vent",
+ "Grattement",
+ "Raclement",
+ "Frottement",
+ "Roulement",
+ "Écrasement",
+ "Froissement",
+ "Déchirement",
+ "Bip",
+ "Ping",
+ "Ding",
+ "Choc métallique",
+ "Crissement",
+ "Grincement",
+ "Bruissement",
+ "Ronronnement mécanique",
+ "Entrechoquement",
+ "Grésillement",
+ "Clics",
+ "Cliquetis rythmé",
+ "Grondement",
+ "Petit plouf",
+ "Cliquetis",
+ "Bourdonnement sourd",
+ "Bref bourdonnement aigu",
+ "Boing (ressort)",
+ "Bruit croustillant",
+ "Silence",
+ "Son pur",
+ "Harmonique",
+ "Son montant ou descendant",
+ "Effet sonore",
+ "Impulsions régulières",
+ "Intérieur, petite pièce",
+ "Intérieur, grande salle",
+ "Intérieur, lieu public",
+ "Extérieur, milieu urbain",
+ "Extérieur, milieu naturel",
+ "Réverbération",
+ "Écho",
+ "Bruit",
+ "Bruit ambiant",
+ "Parasites",
+ "Bourdonnement électrique",
+ "Distorsion",
+ "Retour de voix (téléphone)",
+ "Cacophonie",
+ "Bruit blanc",
+ "Bruit rose",
+ "Pulsation sourde",
+ "Bruit de vibration",
+ "Télévision",
+ "Radio",
+ "Enregistrement de terrain"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/hi.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/hi.json
new file mode 100644
index 0000000..ee1fd8f
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/hi.json
@@ -0,0 +1,523 @@
+[
+ "बोलना",
+ "बच्चे का बोलना",
+ "लोगों की बातचीत",
+ "एक व्यक्ति का बोलना",
+ "शिशु का बड़बड़ाना",
+ "मशीनी आवाज़ में बोलना",
+ "चिल्लाना",
+ "इंसान की दहाड़",
+ "खुशी से चिल्लाना",
+ "गला फाड़कर चिल्लाना",
+ "बच्चों का चिल्लाना",
+ "चीखना",
+ "फुसफुसाना",
+ "हंसी",
+ "शिशु की हंसी",
+ "खिलखिलाना",
+ "मज़ाक उड़ाती हंसी",
+ "ठहाका",
+ "दबी हंसी",
+ "रोना",
+ "शिशु का रोना",
+ "सुबकना",
+ "कराहना",
+ "आह भरना",
+ "गायन",
+ "गायक मंडली",
+ "योडलिंग",
+ "लयबद्ध पाठ",
+ "मंत्र जाप",
+ "बच्चे का गाना",
+ "मशीनी आवाज़ में गाना",
+ "रैप",
+ "गुनगुनाना",
+ "गहरी कराह",
+ "इंसान का घुरघुराना",
+ "मुंह से सीटी बजाना",
+ "सांस लेना",
+ "सांस की घरघराहट",
+ "खर्राटे",
+ "अचानक सांस खींचना",
+ "हांफना",
+ "नाक से तेज़ सांस छोड़ना",
+ "खांसी",
+ "गला साफ़ करना",
+ "छींक",
+ "नाक सुड़कना",
+ "दौड़ना",
+ "पैर घसीटकर चलना",
+ "कदमों की आवाज़",
+ "चबाना",
+ "दांत से काटना",
+ "गरारे करना",
+ "पेट में गुड़गुड़",
+ "डकार",
+ "हिचकी",
+ "पाद",
+ "हाथों की आवाज़",
+ "चुटकी बजाना",
+ "ताली बजाना",
+ "दिल की धड़कन",
+ "दिल की असामान्य आवाज़",
+ "जयकार",
+ "तालियां",
+ "लोगों की गपशप",
+ "भीड़",
+ "लोगों का शोरगुल",
+ "बच्चों का खेलना",
+ "जानवर",
+ "पालतू जानवर",
+ "कुत्ता",
+ "भौंकना",
+ "छोटे कुत्ते का भौंकना",
+ "हुआं-हुआं",
+ "भौं-भौं",
+ "गुर्राना",
+ "कुत्ते की कूं-कूं",
+ "बिल्ली",
+ "बिल्ली का घुरघुराना",
+ "म्याऊं",
+ "फुफकारना",
+ "बिल्ली का रोना",
+ "खेत के जानवर",
+ "घोड़ा",
+ "घोड़े की टापें",
+ "हिनहिनाना",
+ "गाय-बैल",
+ "गाय का रंभाना",
+ "गाय की घंटी",
+ "सूअर",
+ "सूअर का घुरघुराना",
+ "बकरी",
+ "मिमियाना",
+ "भेड़",
+ "मुर्गी-बत्तख जैसे पक्षी",
+ "मुर्गा-मुर्गी",
+ "मुर्गी का कुड़कुड़ाना",
+ "मुर्गे की बांग",
+ "टर्की",
+ "टर्की की बोली",
+ "बत्तख",
+ "बत्तख की क्वैक-क्वैक",
+ "हंस",
+ "हंस की बोली",
+ "जंगली जानवर",
+ "शेर और बाघ",
+ "दहाड़",
+ "पक्षी",
+ "पक्षियों की चहचहाहट",
+ "चिड़िया की चीं-चीं",
+ "पक्षी की कर्कश चीख",
+ "कबूतर",
+ "कबूतर की गुटरगूं",
+ "कौआ",
+ "कांव-कांव",
+ "उल्लू",
+ "उल्लू की घू-घू",
+ "पक्षी के पंख फड़फड़ाना",
+ "भेड़िये और सियार",
+ "चूहे जैसे जीव",
+ "चूहा",
+ "नन्हे कदमों की आहट",
+ "कीड़े-मकोड़े",
+ "झींगुर",
+ "मच्छर",
+ "मक्खी",
+ "भिनभिनाना",
+ "मधुमक्खी और ततैया",
+ "मेंढक",
+ "टर्र-टर्र",
+ "सांप",
+ "खड़खड़ाहट",
+ "व्हेल की आवाज़",
+ "संगीत",
+ "वाद्य यंत्र",
+ "छेड़कर बजने वाले तार वाद्य",
+ "गिटार",
+ "इलेक्ट्रिक गिटार",
+ "बेस गिटार",
+ "अकूस्टिक गिटार",
+ "स्टील गिटार",
+ "गिटार पर टैपिंग",
+ "गिटार की झनकार",
+ "बैंजो",
+ "सितार",
+ "मैंडोलिन",
+ "ज़िथर",
+ "यूकुलेले",
+ "कीबोर्ड (वाद्य)",
+ "पियानो",
+ "इलेक्ट्रिक पियानो",
+ "ऑर्गन",
+ "इलेक्ट्रॉनिक ऑर्गन",
+ "हैमंड ऑर्गन",
+ "सिंथेसाइज़र",
+ "सैंपलर",
+ "हार्पसीकॉर्ड",
+ "ताल वाद्य",
+ "ड्रम सेट",
+ "ड्रम मशीन",
+ "ड्रम",
+ "स्नेयर ड्रम",
+ "ड्रम के किनारे पर थाप",
+ "ड्रम रोल",
+ "बेस ड्रम",
+ "टिंपनी",
+ "तबला",
+ "झांझ",
+ "हाई-हैट",
+ "लकड़ी का ब्लॉक (वाद्य)",
+ "डफली",
+ "झुनझुना",
+ "मराका",
+ "गोंग",
+ "नली वाली घंटियां",
+ "ज़ाइलोफ़ोन जैसे वाद्य",
+ "ज़ाइलोफ़ोन",
+ "ग्लॉकेनस्पील",
+ "वाइब्राफ़ोन",
+ "स्टीलपैन",
+ "ऑर्केस्ट्रा",
+ "पीतल के वाद्य",
+ "फ़्रेंच हॉर्न",
+ "ट्रंपेट",
+ "ट्रॉम्बोन",
+ "गज़ से बजने वाले तार वाद्य",
+ "तार वाद्यों का समूह",
+ "वायलिन",
+ "वायलिन के तार छेड़ना",
+ "चेलो",
+ "डबल बेस",
+ "फूंककर बजने वाले वाद्य",
+ "बांसुरी",
+ "सैक्सोफ़ोन",
+ "क्लैरिनेट",
+ "हार्प",
+ "घंटी",
+ "चर्च की घंटी",
+ "घुंघरू",
+ "साइकिल की घंटी",
+ "ट्यूनिंग फ़ोर्क",
+ "घंटियों की धुन",
+ "हवा से बजने वाली घंटियां",
+ "घंटियों का क्रमवार बजना",
+ "माउथ ऑर्गन",
+ "अकॉर्डियन",
+ "बैगपाइप",
+ "डिजेरिडू",
+ "शोफ़ार (सींग का बाजा)",
+ "थेरेमिन",
+ "सिंगिंग बाउल",
+ "डीजे स्क्रैचिंग",
+ "पॉप संगीत",
+ "हिप-हॉप संगीत",
+ "बीटबॉक्सिंग",
+ "रॉक संगीत",
+ "हेवी मेटल",
+ "पंक रॉक",
+ "ग्रंज",
+ "प्रोग्रेसिव रॉक",
+ "रॉक एंड रोल",
+ "साइकेडेलिक रॉक",
+ "रिदम एंड ब्लूज़",
+ "सोल संगीत",
+ "रेगे",
+ "कंट्री संगीत",
+ "स्विंग संगीत",
+ "ब्लूग्रास",
+ "फ़ंक",
+ "लोक संगीत",
+ "मध्य-पूर्वी संगीत",
+ "जैज़",
+ "डिस्को",
+ "पश्चिमी शास्त्रीय संगीत",
+ "ओपेरा",
+ "इलेक्ट्रॉनिक संगीत",
+ "हाउस संगीत",
+ "टेक्नो",
+ "डबस्टेप",
+ "ड्रम एंड बेस",
+ "इलेक्ट्रॉनिका",
+ "इलेक्ट्रॉनिक डांस संगीत",
+ "एंबिएंट संगीत",
+ "ट्रांस संगीत",
+ "लैटिन अमेरिकी संगीत",
+ "साल्सा संगीत",
+ "फ़्लामेंको",
+ "ब्लूज़",
+ "बच्चों के लिए संगीत",
+ "न्यू एज संगीत",
+ "गायन संगीत",
+ "बिना वाद्य के गायन",
+ "अफ़्रीकी संगीत",
+ "एफ़्रोबीट",
+ "ईसाई संगीत",
+ "गॉस्पेल संगीत",
+ "एशियाई संगीत",
+ "कर्नाटक संगीत",
+ "बॉलीवुड संगीत",
+ "स्का",
+ "पारंपरिक संगीत",
+ "इंडी संगीत",
+ "गीत",
+ "पृष्ठभूमि संगीत",
+ "थीम संगीत",
+ "विज्ञापन की धुन",
+ "साउंडट्रैक संगीत",
+ "लोरी",
+ "वीडियो गेम संगीत",
+ "क्रिसमस संगीत",
+ "डांस संगीत",
+ "शादी का संगीत",
+ "खुशी भरा संगीत",
+ "दुख भरा संगीत",
+ "कोमल संगीत",
+ "जोशीला संगीत",
+ "गुस्से भरा संगीत",
+ "डरावना संगीत",
+ "हवा",
+ "पत्तों की सरसराहट",
+ "माइक पर हवा का शोर",
+ "गरज के साथ तूफ़ान",
+ "बादलों की गरज",
+ "पानी",
+ "बारिश",
+ "बारिश की बूंद",
+ "छत या खिड़की पर बारिश",
+ "बहती नदी",
+ "झरना",
+ "समुद्र",
+ "समुद्र की लहरें",
+ "भाप",
+ "पानी की गुड़गुड़",
+ "आग",
+ "चटचटाहट",
+ "वाहन",
+ "नाव",
+ "पाल वाली नाव",
+ "चप्पू वाली नाव",
+ "मोटरबोट",
+ "पानी का जहाज़",
+ "मोटर वाहन",
+ "कार",
+ "वाहन का हॉर्न",
+ "हॉर्न की छोटी आवाज़",
+ "कार का अलार्म",
+ "कार की पावर विंडो",
+ "वाहन का फिसलना",
+ "टायरों की चीख",
+ "पास से गुज़रती कार",
+ "रेसिंग कार",
+ "ट्रक",
+ "ट्रक का एयर ब्रेक",
+ "ट्रक का प्रेशर हॉर्न",
+ "रिवर्स गियर की बीप",
+ "आइसक्रीम वैन",
+ "बस",
+ "आपातकालीन वाहन",
+ "पुलिस कार (सायरन)",
+ "एंबुलेंस (सायरन)",
+ "दमकल (सायरन)",
+ "मोटरसाइकिल",
+ "ट्रैफ़िक का शोर",
+ "रेल यातायात",
+ "ट्रेन",
+ "ट्रेन की सीटी",
+ "ट्रेन का हॉर्न",
+ "ट्रेन का डिब्बा",
+ "ट्रेन के पहियों की चीख",
+ "मेट्रो",
+ "विमान",
+ "विमान का इंजन",
+ "जेट इंजन",
+ "प्रोपेलर",
+ "हेलीकॉप्टर",
+ "हवाई जहाज़",
+ "साइकिल",
+ "स्केटबोर्ड",
+ "इंजन",
+ "हल्का इंजन",
+ "डेंटिस्ट की ड्रिल",
+ "घास काटने की मशीन",
+ "चेन वाली आरी",
+ "मध्यम इंजन",
+ "भारी इंजन",
+ "इंजन की खटखट",
+ "इंजन स्टार्ट होना",
+ "खड़े वाहन का चालू इंजन",
+ "इंजन को रेस देना",
+ "दरवाज़ा",
+ "दरवाज़े की घंटी",
+ "घंटी की टिंग-टोंग",
+ "स्लाइडिंग दरवाज़ा",
+ "दरवाज़ा ज़ोर से बंद होना",
+ "दरवाज़ा खटखटाना",
+ "हल्की ठक-ठक",
+ "चूं-चूं की आवाज़",
+ "अलमारी खुलना-बंद होना",
+ "दराज़ खुलना-बंद होना",
+ "बर्तनों की खटपट",
+ "छुरी-कांटे और चम्मच",
+ "सब्ज़ी काटना",
+ "तेल में तलना",
+ "माइक्रोवेव ओवन",
+ "मिक्सी",
+ "पानी का नल",
+ "सिंक (भरना या धोना)",
+ "बाथटब (भरना या नहाना)",
+ "हेयर ड्रायर",
+ "टॉयलेट फ़्लश",
+ "टूथब्रश",
+ "इलेक्ट्रिक टूथब्रश",
+ "वैक्यूम क्लीनर",
+ "ज़िप खींचना",
+ "चाबियों की खनखनाहट",
+ "सिक्का गिरना",
+ "कैंची",
+ "इलेक्ट्रिक शेवर",
+ "ताश फेंटना",
+ "टाइपिंग",
+ "टाइपराइटर",
+ "कंप्यूटर कीबोर्ड",
+ "कागज़ पर लिखना",
+ "अलार्म",
+ "फ़ोन",
+ "फ़ोन की घंटी",
+ "रिंगटोन",
+ "नंबर डायल करने की टोन",
+ "डायल टोन",
+ "बिज़ी टोन",
+ "अलार्म घड़ी",
+ "सायरन",
+ "आपदा चेतावनी सायरन",
+ "बज़र",
+ "धुएं का अलार्म",
+ "आग का अलार्म",
+ "कोहरे का हॉर्न",
+ "सीटी",
+ "भाप की सीटी",
+ "मशीनें और उपकरण",
+ "रैचेट की किट-किट",
+ "घड़ी",
+ "धातु की टिक",
+ "घड़ी की टिक-टिक",
+ "मशीन के गियर",
+ "घिरनी",
+ "सिलाई मशीन",
+ "पंखा",
+ "एसी",
+ "कैश रजिस्टर",
+ "प्रिंटर",
+ "कैमरा",
+ "एसएलआर कैमरा",
+ "औज़ार",
+ "हथौड़ा",
+ "कंक्रीट तोड़ने की मशीन",
+ "आरी चलाना",
+ "रेती से घिसना",
+ "रेगमाल से घिसना",
+ "बिजली के औज़ार",
+ "ड्रिल मशीन",
+ "विस्फोट",
+ "गोली चलने की आवाज़",
+ "मशीनगन",
+ "एक साथ गोलीबारी",
+ "तोपों की गोलाबारी",
+ "खिलौना बंदूक",
+ "आतिशबाज़ी",
+ "पटाखा",
+ "फट से फूटना",
+ "ज्वालामुखी फटना",
+ "गूंजता धमाका",
+ "लकड़ी",
+ "कुल्हाड़ी से काटना",
+ "लकड़ी चिरना",
+ "चटकने की आवाज़",
+ "कांच",
+ "खनक",
+ "चकनाचूर होना",
+ "तरल पदार्थ",
+ "छपाक",
+ "बर्तन में पानी छलकना",
+ "गीली चीज़ की पच-पच",
+ "टपकना",
+ "पानी उड़ेलना",
+ "पतली धार में बहना",
+ "तेज़ धार में बहना",
+ "बर्तन में तरल भरना",
+ "छिड़काव",
+ "पंप से तरल निकालना",
+ "चम्मच से हिलाना",
+ "उबलना",
+ "पनडुब्बी का सोनार",
+ "तीर",
+ "हवा चीरने की आवाज़",
+ "धम्म की आवाज़",
+ "खोखली ठक",
+ "सुर मिलाने वाला ट्यूनर",
+ "ऑडियो इफ़ेक्ट उपकरण",
+ "कोरस इफ़ेक्ट",
+ "बास्केटबॉल का टप्पा",
+ "अचानक तेज़ आवाज़",
+ "थप्पड़",
+ "ज़ोरदार प्रहार",
+ "धड़ाम से टूटना",
+ "टूटना",
+ "टप्पे खाना",
+ "चाबुक की फटकार",
+ "झंडे की फड़फड़ाहट",
+ "खरोंचना",
+ "खुरचना",
+ "रगड़ना",
+ "लुढ़कना",
+ "कुचलना",
+ "कागज़ मसलना",
+ "फाड़ना",
+ "बीप",
+ "पिंग",
+ "टन्न",
+ "धातु की झनझनाहट",
+ "चीं की तीखी आवाज़",
+ "चरमराहट",
+ "सरसराहट",
+ "हल्की घर्र-घर्र",
+ "खटर-पटर",
+ "तेल की छनछनाहट",
+ "क्लिक की आवाज़",
+ "लयबद्ध खटखट",
+ "गड़गड़ाहट",
+ "पानी में डुबुक",
+ "खनखनाहट",
+ "धीमी भनभनाहट",
+ "तीखी सनसनाहट",
+ "स्प्रिंग जैसी आवाज़",
+ "कुरकुराहट",
+ "सन्नाटा",
+ "शुद्ध टोन",
+ "हार्मोनिक टोन",
+ "चढ़ती या उतरती टोन",
+ "साउंड इफ़ेक्ट",
+ "रुक-रुककर आती आवाज़",
+ "छोटे कमरे के अंदर",
+ "बड़े कमरे या हॉल के अंदर",
+ "सार्वजनिक जगह के अंदर",
+ "बाहर शहरी इलाके में",
+ "बाहर गांव या प्रकृति में",
+ "गूंज",
+ "प्रतिध्वनि",
+ "शोर",
+ "आस-पास का शोर",
+ "सिग्नल की खरखराहट",
+ "बिजली की भनभनाहट",
+ "बिगड़ी हुई आवाज़",
+ "फ़ोन में अपनी आवाज़",
+ "कर्कश शोर-शराबा",
+ "व्हाइट नॉइज़",
+ "पिंक नॉइज़",
+ "धड़कती आवाज़",
+ "कंपन की आवाज़",
+ "टीवी",
+ "रेडियो",
+ "किसी जगह की रिकॉर्डिंग"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/id.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/id.json
new file mode 100644
index 0000000..8b3ff60
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/id.json
@@ -0,0 +1,523 @@
+[
+ "Orang berbicara",
+ "Anak berbicara",
+ "Percakapan",
+ "Narasi",
+ "Celoteh bayi",
+ "Suara bicara sintetis",
+ "Teriakan",
+ "Teriakan menggelegar",
+ "Pekik kegirangan",
+ "Teriakan keras",
+ "Anak-anak berteriak",
+ "Jeritan",
+ "Bisikan",
+ "Tawa",
+ "Tawa bayi",
+ "Cekikikan",
+ "Tawa mengejek",
+ "Tawa terbahak-bahak",
+ "Tawa kecil",
+ "Tangisan",
+ "Tangisan bayi",
+ "Rengekan",
+ "Rintihan",
+ "Helaan napas",
+ "Nyanyian",
+ "Paduan suara",
+ "Nyanyian yodel",
+ "Lantunan",
+ "Mantra",
+ "Anak bernyanyi",
+ "Nyanyian sintetis",
+ "Rap",
+ "Senandung",
+ "Erangan",
+ "Erangan pendek",
+ "Siulan",
+ "Napas",
+ "Mengi",
+ "Dengkuran",
+ "Tarikan napas kaget",
+ "Napas terengah-engah",
+ "Dengusan",
+ "Batuk",
+ "Berdeham",
+ "Bersin",
+ "Mengendus",
+ "Berlari",
+ "Langkah terseret",
+ "Langkah kaki",
+ "Mengunyah",
+ "Menggigit",
+ "Berkumur",
+ "Perut keroncongan",
+ "Sendawa",
+ "Cegukan",
+ "Kentut",
+ "Suara tangan",
+ "Jentikan jari",
+ "Tepukan tangan",
+ "Detak jantung",
+ "Murmur jantung",
+ "Sorak-sorai",
+ "Tepuk tangan penonton",
+ "Obrolan banyak orang",
+ "Kerumunan",
+ "Riuh suara orang",
+ "Anak-anak bermain",
+ "Hewan",
+ "Hewan peliharaan",
+ "Anjing",
+ "Gonggongan",
+ "Gonggongan anjing kecil",
+ "Lolongan",
+ "Guk-guk",
+ "Geraman",
+ "Rengekan anjing",
+ "Kucing",
+ "Dengkuran kucing",
+ "Meong",
+ "Desisan",
+ "Raungan kucing",
+ "Hewan ternak",
+ "Kuda",
+ "Derap kaki kuda",
+ "Ringkikan kuda",
+ "Sapi",
+ "Lenguhan sapi",
+ "Lonceng sapi",
+ "Babi",
+ "Dengusan babi",
+ "Kambing",
+ "Embikan",
+ "Domba",
+ "Unggas",
+ "Ayam",
+ "Ayam berkotek",
+ "Ayam jago berkokok",
+ "Kalkun",
+ "Suara kalkun jantan",
+ "Bebek",
+ "Suara bebek",
+ "Angsa",
+ "Suara angsa",
+ "Hewan liar",
+ "Singa dan harimau",
+ "Auman",
+ "Burung",
+ "Kicauan burung",
+ "Cicit burung",
+ "Pekik burung",
+ "Burung merpati",
+ "Suara merpati",
+ "Burung gagak",
+ "Suara gagak",
+ "Burung hantu",
+ "Suara burung hantu",
+ "Kepakan sayap",
+ "Serigala dan anjing liar",
+ "Hewan pengerat",
+ "Tikus",
+ "Derap langkah kecil",
+ "Serangga",
+ "Jangkrik",
+ "Nyamuk",
+ "Lalat",
+ "Dengung serangga",
+ "Lebah dan tawon",
+ "Katak",
+ "Suara katak",
+ "Ular",
+ "Derik ular",
+ "Suara paus",
+ "Musik",
+ "Alat musik",
+ "Alat musik petik",
+ "Gitar",
+ "Gitar listrik",
+ "Gitar bas",
+ "Gitar akustik",
+ "Gitar slide",
+ "Teknik tapping gitar",
+ "Genjrengan gitar",
+ "Banjo",
+ "Sitar",
+ "Mandolin",
+ "Siter",
+ "Ukulele",
+ "Keyboard (musik)",
+ "Piano",
+ "Piano listrik",
+ "Organ",
+ "Organ elektronik",
+ "Organ Hammond",
+ "Synthesizer",
+ "Sampler",
+ "Harpsikord",
+ "Perkusi",
+ "Drum set",
+ "Mesin drum",
+ "Drum",
+ "Snare drum",
+ "Rimshot",
+ "Pukulan drum beruntun",
+ "Drum bas",
+ "Timpani",
+ "Tabla",
+ "Simbal",
+ "Hi-hat",
+ "Balok kayu (perkusi)",
+ "Tamborin",
+ "Alat musik kocok",
+ "Marakas",
+ "Gong",
+ "Lonceng tabung",
+ "Perkusi bernada",
+ "Marimba dan xilofon",
+ "Glockenspiel",
+ "Vibrafon",
+ "Steelpan",
+ "Orkestra",
+ "Alat musik tiup logam",
+ "French horn",
+ "Terompet",
+ "Trombon",
+ "Alat musik gesek",
+ "Kelompok alat musik gesek",
+ "Biola",
+ "Petikan biola",
+ "Cello",
+ "Kontrabas",
+ "Alat musik tiup kayu",
+ "Seruling",
+ "Saksofon",
+ "Klarinet",
+ "Harpa",
+ "Lonceng",
+ "Lonceng gereja",
+ "Giring-giring",
+ "Bel sepeda",
+ "Garpu tala",
+ "Melodi lonceng",
+ "Lonceng angin",
+ "Lonceng bersahutan",
+ "Harmonika",
+ "Akordeon",
+ "Bagpipe",
+ "Didgeridoo",
+ "Shofar (terompet tanduk)",
+ "Theremin",
+ "Mangkuk bernyanyi",
+ "Scratch DJ",
+ "Musik pop",
+ "Musik hip hop",
+ "Beatbox",
+ "Musik rock",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Rock progresif",
+ "Rock and roll",
+ "Rock psikedelik",
+ "Rhythm and blues",
+ "Musik soul",
+ "Reggae",
+ "Musik country",
+ "Musik swing",
+ "Bluegrass",
+ "Funk",
+ "Musik folk",
+ "Musik Timur Tengah",
+ "Jazz",
+ "Disko",
+ "Musik klasik",
+ "Opera",
+ "Musik elektronik",
+ "Musik house",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Musik dansa elektronik",
+ "Musik ambient",
+ "Musik trance",
+ "Musik Amerika Latin",
+ "Musik salsa",
+ "Flamenco",
+ "Blues",
+ "Musik anak-anak",
+ "Musik new age",
+ "Musik vokal",
+ "Akapela",
+ "Musik Afrika",
+ "Afrobeat",
+ "Musik Kristen",
+ "Musik gospel",
+ "Musik Asia",
+ "Musik Karnatik",
+ "Musik Bollywood",
+ "Ska",
+ "Musik tradisional",
+ "Musik indie",
+ "Lagu",
+ "Musik latar",
+ "Musik tema",
+ "Jingle iklan",
+ "Musik pengiring film",
+ "Lagu nina bobo",
+ "Musik game",
+ "Musik Natal",
+ "Musik dansa",
+ "Musik pernikahan",
+ "Musik ceria",
+ "Musik sedih",
+ "Musik lembut",
+ "Musik bersemangat",
+ "Musik bernada marah",
+ "Musik seram",
+ "Angin",
+ "Gemeresik daun",
+ "Bising angin (mikrofon)",
+ "Badai petir",
+ "Guntur",
+ "Air",
+ "Hujan",
+ "Tetes hujan",
+ "Hujan di atap/jendela",
+ "Aliran sungai",
+ "Air terjun",
+ "Laut",
+ "Deburan ombak",
+ "Uap air",
+ "Air berdeguk",
+ "Api",
+ "Gemeretak api",
+ "Kendaraan",
+ "Kendaraan air",
+ "Perahu layar",
+ "Perahu dayung",
+ "Perahu motor",
+ "Kapal",
+ "Kendaraan bermotor",
+ "Mobil",
+ "Klakson",
+ "Klakson singkat",
+ "Alarm mobil",
+ "Power window mobil",
+ "Mobil tergelincir",
+ "Decit ban",
+ "Mobil melintas",
+ "Mobil balap",
+ "Truk",
+ "Rem angin",
+ "Klakson truk",
+ "Alarm mundur",
+ "Mobil es krim",
+ "Bus",
+ "Kendaraan darurat",
+ "Mobil polisi (sirene)",
+ "Ambulans (sirene)",
+ "Mobil pemadam (sirene)",
+ "Sepeda motor",
+ "Bising lalu lintas",
+ "Transportasi rel",
+ "Kereta api",
+ "Peluit kereta api",
+ "Klakson kereta api",
+ "Gerbong kereta",
+ "Decit roda kereta",
+ "Kereta bawah tanah",
+ "Pesawat udara",
+ "Mesin pesawat",
+ "Mesin jet",
+ "Baling-baling pesawat",
+ "Helikopter",
+ "Pesawat terbang",
+ "Sepeda",
+ "Skateboard",
+ "Mesin",
+ "Mesin kecil (nada tinggi)",
+ "Bor gigi",
+ "Mesin pemotong rumput",
+ "Gergaji mesin",
+ "Mesin sedang (nada sedang)",
+ "Mesin besar (nada rendah)",
+ "Ketukan mesin",
+ "Mesin dinyalakan",
+ "Mesin langsam",
+ "Mesin berakselerasi",
+ "Pintu",
+ "Bel pintu",
+ "Ding-dong (bel pintu)",
+ "Pintu geser",
+ "Pintu dibanting",
+ "Ketukan pintu",
+ "Ketukan ringan",
+ "Decit",
+ "Lemari dibuka/ditutup",
+ "Laci dibuka/ditutup",
+ "Piring dan panci",
+ "Sendok dan garpu",
+ "Mencincang makanan",
+ "Menggoreng",
+ "Microwave",
+ "Blender",
+ "Keran air",
+ "Air di wastafel",
+ "Air di bak mandi",
+ "Pengering rambut",
+ "Siraman toilet",
+ "Sikat gigi",
+ "Sikat gigi listrik",
+ "Penyedot debu",
+ "Ritsleting",
+ "Gemerincing kunci",
+ "Koin jatuh",
+ "Gunting",
+ "Alat cukur listrik",
+ "Mengocok kartu",
+ "Mengetik",
+ "Mesin tik",
+ "Keyboard komputer",
+ "Menulis",
+ "Alarm",
+ "Telepon",
+ "Dering bel telepon",
+ "Nada dering",
+ "Nada tombol telepon",
+ "Nada pilih telepon",
+ "Nada sibuk",
+ "Jam weker",
+ "Sirene",
+ "Sirene peringatan bahaya",
+ "Buzzer",
+ "Alarm asap",
+ "Alarm kebakaran",
+ "Klakson kapal (kabut)",
+ "Peluit",
+ "Peluit uap",
+ "Suara mekanis",
+ "Ratchet",
+ "Jam",
+ "Detak jam",
+ "Tik-tak jam",
+ "Roda gigi",
+ "Katrol",
+ "Mesin jahit",
+ "Kipas angin",
+ "AC (pendingin ruangan)",
+ "Mesin kasir",
+ "Printer",
+ "Kamera",
+ "Kamera SLR",
+ "Perkakas",
+ "Palu",
+ "Mesin pemecah beton",
+ "Menggergaji",
+ "Mengikir",
+ "Mengamplas",
+ "Perkakas listrik",
+ "Bor",
+ "Ledakan",
+ "Tembakan",
+ "Senapan mesin",
+ "Tembakan serentak",
+ "Tembakan artileri",
+ "Pistol mainan",
+ "Kembang api",
+ "Petasan",
+ "Letupan",
+ "Letusan gunung berapi",
+ "Dentuman",
+ "Kayu",
+ "Membelah kayu",
+ "Kayu menyerpih",
+ "Retakan",
+ "Kaca",
+ "Denting gelas",
+ "Kaca pecah",
+ "Cairan",
+ "Cipratan",
+ "Cairan terguncang",
+ "Benda basah diremas",
+ "Tetesan",
+ "Menuang cairan",
+ "Gemericik",
+ "Air mengucur deras",
+ "Wadah diisi cairan",
+ "Semprotan",
+ "Pompa cairan",
+ "Mengaduk",
+ "Air mendidih",
+ "Sonar",
+ "Anak panah",
+ "Benda melesat",
+ "Gedebuk",
+ "Ketukan tumpul",
+ "Tuner elektronik",
+ "Unit efek suara",
+ "Efek chorus",
+ "Pantulan bola basket",
+ "Suara keras mendadak",
+ "Tamparan",
+ "Pukulan keras",
+ "Pecah berantakan",
+ "Benda pecah",
+ "Benda memantul",
+ "Lecutan cambuk",
+ "Kibaran kain",
+ "Goresan",
+ "Gesekan benda keras",
+ "Gosokan",
+ "Benda menggelinding",
+ "Benda remuk",
+ "Kertas diremas",
+ "Merobek",
+ "Suara bip",
+ "Suara ting",
+ "Denting",
+ "Dentang logam",
+ "Decit melengking",
+ "Derit",
+ "Gemeresik",
+ "Deru halus",
+ "Benda berkelontangan",
+ "Desis minyak panas",
+ "Suara klik",
+ "Suara klik berirama",
+ "Gemuruh",
+ "Benda kecil tercebur",
+ "Gemerincing",
+ "Dengung",
+ "Desing",
+ "Suara pegas",
+ "Suara kriuk",
+ "Hening",
+ "Nada murni (sinus)",
+ "Nada harmonik",
+ "Nada naik/turun",
+ "Efek suara",
+ "Suara berdenyut",
+ "Dalam ruangan kecil",
+ "Dalam ruangan besar/aula",
+ "Dalam ruang publik",
+ "Luar ruangan (perkotaan)",
+ "Luar ruangan (alam)",
+ "Gaung ruangan",
+ "Gema",
+ "Bising",
+ "Bising lingkungan",
+ "Suara statis",
+ "Dengung listrik",
+ "Distorsi",
+ "Suara sendiri di telepon",
+ "Hiruk-pikuk",
+ "White noise",
+ "Pink noise",
+ "Dengung berdenyut",
+ "Suara getaran",
+ "Televisi",
+ "Radio",
+ "Rekaman suasana"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/it.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/it.json
new file mode 100644
index 0000000..6f6d37e
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/it.json
@@ -0,0 +1,523 @@
+[
+ "Parlato",
+ "Bambino che parla",
+ "Conversazione",
+ "Narrazione",
+ "Balbettio di bambino",
+ "Voce sintetica",
+ "Grido",
+ "Urlo animalesco",
+ "Grido di gioia",
+ "Urlo",
+ "Bambini che gridano",
+ "Strillo",
+ "Sussurro",
+ "Risata",
+ "Risata di neonato",
+ "Risatina",
+ "Risolino beffardo",
+ "Risata fragorosa",
+ "Risata sommessa",
+ "Pianto",
+ "Pianto di neonato",
+ "Piagnucolio",
+ "Lamento",
+ "Sospiro",
+ "Canto",
+ "Coro",
+ "Canto jodel",
+ "Cantilena",
+ "Mantra",
+ "Bambino che canta",
+ "Canto sintetico",
+ "Rap",
+ "Canto a bocca chiusa",
+ "Gemito",
+ "Grugnito",
+ "Fischio con la bocca",
+ "Respiro",
+ "Respiro sibilante",
+ "Russare",
+ "Respiro mozzato",
+ "Respiro affannoso",
+ "Sbuffo dal naso",
+ "Tosse",
+ "Schiarirsi la gola",
+ "Starnuto",
+ "Tirare su col naso",
+ "Passi di corsa",
+ "Passi strascicati",
+ "Passi",
+ "Masticazione",
+ "Morso",
+ "Gargarismi",
+ "Stomaco che brontola",
+ "Rutto",
+ "Singhiozzo",
+ "Peto",
+ "Suoni con le mani",
+ "Schiocco di dita",
+ "Battito di mani",
+ "Battito cardiaco",
+ "Soffio cardiaco",
+ "Acclamazioni",
+ "Applausi",
+ "Chiacchiericcio",
+ "Folla",
+ "Vocio confuso",
+ "Bambini che giocano",
+ "Animali",
+ "Animali domestici",
+ "Cane",
+ "Abbaio",
+ "Abbaio acuto",
+ "Ululato",
+ "Bau bau",
+ "Ringhio",
+ "Guaito",
+ "Gatto",
+ "Fusa",
+ "Miagolio",
+ "Sibilo",
+ "Gatto in calore",
+ "Animali da fattoria",
+ "Cavallo",
+ "Zoccoli di cavallo",
+ "Nitrito",
+ "Bovini",
+ "Muggito",
+ "Campanaccio",
+ "Maiale",
+ "Grugnito di maiale",
+ "Capra",
+ "Belato",
+ "Pecora",
+ "Pollame",
+ "Gallo e gallina",
+ "Coccodè",
+ "Chicchirichì",
+ "Tacchino",
+ "Glu glu del tacchino",
+ "Anatra",
+ "Qua qua",
+ "Oca",
+ "Verso dell'oca",
+ "Animali selvatici",
+ "Grandi felini",
+ "Ruggito",
+ "Uccelli",
+ "Canto degli uccelli",
+ "Cinguettio",
+ "Strido di uccello",
+ "Piccione",
+ "Tubare",
+ "Corvo",
+ "Gracchiare",
+ "Gufo",
+ "Verso del gufo",
+ "Battito d'ali",
+ "Lupi e canidi selvatici",
+ "Roditori",
+ "Topo",
+ "Zampettio",
+ "Insetti",
+ "Grillo",
+ "Zanzara",
+ "Mosca",
+ "Ronzio",
+ "Api e vespe",
+ "Rana",
+ "Gracidio",
+ "Serpente",
+ "Rumore di sonaglio",
+ "Canto delle balene",
+ "Musica",
+ "Strumento musicale",
+ "Strumento a corde pizzicate",
+ "Chitarra",
+ "Chitarra elettrica",
+ "Basso elettrico",
+ "Chitarra acustica",
+ "Chitarra slide",
+ "Tapping (chitarra)",
+ "Accordi di chitarra",
+ "Banjo",
+ "Sitar",
+ "Mandolino",
+ "Cetra",
+ "Ukulele",
+ "Tastiera musicale",
+ "Pianoforte",
+ "Piano elettrico",
+ "Organo",
+ "Organo elettronico",
+ "Organo Hammond",
+ "Sintetizzatore",
+ "Campionatore",
+ "Clavicembalo",
+ "Percussioni",
+ "Batteria (strumento)",
+ "Batteria elettronica",
+ "Tamburo",
+ "Rullante",
+ "Rimshot",
+ "Rullo di tamburo",
+ "Grancassa",
+ "Timpani",
+ "Tabla",
+ "Piatto (percussione)",
+ "Charleston (hi-hat)",
+ "Blocco di legno",
+ "Tamburello",
+ "Sonaglio (strumento)",
+ "Maracas",
+ "Gong",
+ "Campane tubolari",
+ "Percussioni melodiche",
+ "Xilofono",
+ "Glockenspiel",
+ "Vibrafono",
+ "Steel drum",
+ "Orchestra",
+ "Ottoni",
+ "Corno francese",
+ "Tromba",
+ "Trombone",
+ "Strumento ad arco",
+ "Sezione d'archi",
+ "Violino",
+ "Pizzicato",
+ "Violoncello",
+ "Contrabbasso",
+ "Strumento a fiato",
+ "Flauto",
+ "Sassofono",
+ "Clarinetto",
+ "Arpa",
+ "Campana",
+ "Campana di chiesa",
+ "Campanellini",
+ "Campanello della bicicletta",
+ "Diapason",
+ "Carillon",
+ "Campane a vento",
+ "Concerto di campane",
+ "Armonica a bocca",
+ "Fisarmonica",
+ "Cornamusa",
+ "Didgeridoo",
+ "Shofar",
+ "Theremin",
+ "Campana tibetana",
+ "Scratch da DJ",
+ "Musica pop",
+ "Musica hip hop",
+ "Beatbox",
+ "Musica rock",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Rock progressivo",
+ "Rock and roll",
+ "Rock psichedelico",
+ "Rhythm and blues",
+ "Musica soul",
+ "Reggae",
+ "Musica country",
+ "Musica swing",
+ "Bluegrass",
+ "Funk",
+ "Musica folk",
+ "Musica mediorientale",
+ "Jazz",
+ "Musica disco",
+ "Musica classica",
+ "Opera lirica",
+ "Musica elettronica",
+ "Musica house",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Musica dance elettronica",
+ "Musica ambient",
+ "Musica trance",
+ "Musica latinoamericana",
+ "Musica salsa",
+ "Flamenco",
+ "Blues",
+ "Musica per bambini",
+ "Musica new age",
+ "Musica vocale",
+ "A cappella",
+ "Musica africana",
+ "Afrobeat",
+ "Musica cristiana",
+ "Musica gospel",
+ "Musica asiatica",
+ "Musica carnatica",
+ "Musica di Bollywood",
+ "Ska",
+ "Musica tradizionale",
+ "Musica indie",
+ "Canzone",
+ "Musica di sottofondo",
+ "Sigla musicale",
+ "Jingle pubblicitario",
+ "Colonna sonora",
+ "Ninna nanna",
+ "Musica da videogioco",
+ "Musica natalizia",
+ "Musica da ballo",
+ "Musica nuziale",
+ "Musica allegra",
+ "Musica triste",
+ "Musica dolce",
+ "Musica emozionante",
+ "Musica rabbiosa",
+ "Musica che fa paura",
+ "Vento",
+ "Fruscio di foglie",
+ "Vento sul microfono",
+ "Temporale",
+ "Tuono",
+ "Acqua",
+ "Pioggia",
+ "Goccia di pioggia",
+ "Pioggia che batte",
+ "Ruscello",
+ "Cascata",
+ "Mare",
+ "Onde",
+ "Vapore",
+ "Gorgoglio",
+ "Fuoco",
+ "Crepitio",
+ "Veicolo",
+ "Barca",
+ "Barca a vela",
+ "Barca a remi",
+ "Motoscafo",
+ "Nave",
+ "Veicolo a motore",
+ "Auto",
+ "Clacson",
+ "Colpo di clacson",
+ "Allarme dell'auto",
+ "Finestrini elettrici",
+ "Sbandata",
+ "Stridio di gomme",
+ "Auto che passa",
+ "Auto da corsa",
+ "Camion",
+ "Freno ad aria compressa",
+ "Clacson del camion",
+ "Bip di retromarcia",
+ "Furgone dei gelati",
+ "Autobus",
+ "Veicolo di emergenza",
+ "Sirena della polizia",
+ "Sirena dell'ambulanza",
+ "Sirena dei pompieri",
+ "Moto",
+ "Rumore del traffico",
+ "Trasporto ferroviario",
+ "Treno",
+ "Fischio del treno",
+ "Tromba del treno",
+ "Vagone ferroviario",
+ "Stridio sui binari",
+ "Metropolitana",
+ "Velivolo",
+ "Motore d'aereo",
+ "Motore a reazione",
+ "Elica",
+ "Elicottero",
+ "Aereo",
+ "Bicicletta",
+ "Skateboard",
+ "Motore",
+ "Motore piccolo",
+ "Trapano del dentista",
+ "Tosaerba",
+ "Motosega",
+ "Motore medio",
+ "Motore grande",
+ "Motore che batte in testa",
+ "Avviamento del motore",
+ "Motore al minimo",
+ "Motore che accelera",
+ "Porta",
+ "Campanello della porta",
+ "Din don del campanello",
+ "Porta scorrevole",
+ "Porta sbattuta",
+ "Bussare",
+ "Colpetto",
+ "Cigolio",
+ "Anta dell'armadietto",
+ "Cassetto",
+ "Piatti e pentole",
+ "Posate",
+ "Coltello sul tagliere",
+ "Frittura",
+ "Forno a microonde",
+ "Frullatore",
+ "Rubinetto",
+ "Lavandino",
+ "Vasca da bagno",
+ "Asciugacapelli",
+ "Sciacquone",
+ "Spazzolino",
+ "Spazzolino elettrico",
+ "Aspirapolvere",
+ "Cerniera lampo",
+ "Tintinnio di chiavi",
+ "Moneta che cade",
+ "Forbici",
+ "Rasoio elettrico",
+ "Mescolare le carte",
+ "Digitazione",
+ "Macchina da scrivere",
+ "Tastiera del computer",
+ "Scrittura a mano",
+ "Allarme",
+ "Telefono",
+ "Squillo del telefono",
+ "Suoneria",
+ "Composizione del numero",
+ "Segnale di linea",
+ "Segnale di occupato",
+ "Sveglia",
+ "Sirena",
+ "Sirena di protezione civile",
+ "Cicalino",
+ "Rilevatore di fumo",
+ "Allarme antincendio",
+ "Sirena da nebbia",
+ "Fischietto",
+ "Fischio a vapore",
+ "Meccanismi",
+ "Cricchetto",
+ "Orologio",
+ "Tic metallico",
+ "Tic tac",
+ "Ingranaggi",
+ "Carrucole",
+ "Macchina da cucire",
+ "Ventilatore",
+ "Condizionatore",
+ "Registratore di cassa",
+ "Stampante",
+ "Fotocamera",
+ "Fotocamera reflex",
+ "Attrezzi",
+ "Martello",
+ "Martello pneumatico",
+ "Taglio con la sega",
+ "Limatura",
+ "Levigatura",
+ "Utensile elettrico",
+ "Trapano",
+ "Esplosione",
+ "Sparo",
+ "Mitragliatrice",
+ "Scarica di fucileria",
+ "Fuoco d'artiglieria",
+ "Pistola giocattolo",
+ "Fuochi d'artificio",
+ "Petardo",
+ "Scoppio",
+ "Eruzione",
+ "Boato",
+ "Legno",
+ "Colpi d'ascia",
+ "Legno che si scheggia",
+ "Schiocco",
+ "Vetro",
+ "Tintinnio di bicchieri",
+ "Vetri in frantumi",
+ "Liquido",
+ "Schizzi",
+ "Sciabordio",
+ "Spugna strizzata",
+ "Gocciolio",
+ "Versare un liquido",
+ "Liquido che cola",
+ "Liquido che sgorga",
+ "Riempire un contenitore",
+ "Spruzzo",
+ "Pompa per liquidi",
+ "Mescolare un liquido",
+ "Ebollizione",
+ "Sonar",
+ "Freccia",
+ "Sibilo nell'aria",
+ "Tonfo",
+ "Colpo cupo",
+ "Accordatore elettronico",
+ "Processore di effetti",
+ "Effetto chorus",
+ "Palleggio (basket)",
+ "Botto",
+ "Schiaffo",
+ "Colpo secco",
+ "Schianto",
+ "Rottura",
+ "Rimbalzo",
+ "Schiocco di frusta",
+ "Sventolio",
+ "Graffio",
+ "Raschiare",
+ "Sfregamento",
+ "Rotolamento",
+ "Schiacciamento",
+ "Accartocciamento",
+ "Strappo",
+ "Bip",
+ "Ping",
+ "Din",
+ "Fragore metallico",
+ "Stridio",
+ "Scricchiolio",
+ "Fruscio",
+ "Frullio",
+ "Oggetti che sbattono",
+ "Sfrigolio",
+ "Clic",
+ "Ticchettio",
+ "Rombo",
+ "Oggetto che cade in acqua",
+ "Tintinnio",
+ "Ronzio cupo",
+ "Breve ronzio acuto",
+ "Boing (molla)",
+ "Rumore croccante",
+ "Silenzio",
+ "Tono puro",
+ "Tono armonico",
+ "Tono che sale o scende",
+ "Effetto sonoro",
+ "Impulsi regolari",
+ "In una stanza piccola",
+ "In una sala grande",
+ "In un luogo pubblico",
+ "All'aperto, in città",
+ "All'aperto, in natura",
+ "Riverbero",
+ "Eco",
+ "Rumore",
+ "Rumore ambientale",
+ "Disturbo elettrico",
+ "Ronzio elettrico",
+ "Distorsione",
+ "Autoascolto al telefono",
+ "Frastuono",
+ "Rumore bianco",
+ "Rumore rosa",
+ "Rumore pulsante",
+ "Vibrazione meccanica",
+ "Televisione",
+ "Radio",
+ "Registrazione sul campo"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/ja.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/ja.json
new file mode 100644
index 0000000..d8946ba
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/ja.json
@@ -0,0 +1,523 @@
+[
+ "人の話し声",
+ "子どもの話し声",
+ "会話",
+ "ナレーション",
+ "赤ちゃんの喃語",
+ "合成音声",
+ "大声",
+ "雄たけび",
+ "喜びの叫び声",
+ "叫び声",
+ "子どもたちの大声",
+ "悲鳴",
+ "ささやき声",
+ "笑い声",
+ "赤ちゃんの笑い声",
+ "くすくす笑い",
+ "せせら笑い",
+ "大笑い",
+ "含み笑い",
+ "泣き声",
+ "赤ちゃんの泣き声",
+ "すすり泣き",
+ "うめき声",
+ "ため息",
+ "歌声",
+ "合唱",
+ "ヨーデル",
+ "詠唱",
+ "マントラ",
+ "子どもの歌声",
+ "合成された歌声",
+ "ラップ",
+ "鼻歌",
+ "うなり声",
+ "短いうなり声",
+ "口笛",
+ "呼吸音",
+ "ゼーゼーいう呼吸",
+ "いびき",
+ "息をのむ音",
+ "荒い息づかい",
+ "鼻を鳴らす音",
+ "せき",
+ "せき払い",
+ "くしゃみ",
+ "鼻をすする音",
+ "走る足音",
+ "足を引きずる音",
+ "足音",
+ "食べ物をかむ音",
+ "かじる音",
+ "うがい",
+ "おなかが鳴る音",
+ "げっぷ",
+ "しゃっくり",
+ "おなら",
+ "手や指の音",
+ "指を鳴らす音",
+ "手をたたく音",
+ "心臓の鼓動",
+ "心臓の雑音",
+ "歓声",
+ "拍手",
+ "大勢のおしゃべり",
+ "群衆",
+ "ざわめき",
+ "子どもが遊ぶ声",
+ "動物",
+ "ペット",
+ "犬",
+ "犬のほえ声",
+ "犬の甲高い鳴き声",
+ "遠吠え",
+ "犬の鳴き声(ワンワン)",
+ "動物のうなり声",
+ "犬の弱々しい鳴き声",
+ "猫",
+ "猫が喉を鳴らす音",
+ "猫の鳴き声",
+ "シャーという威嚇音",
+ "発情期の猫の鳴き声",
+ "家畜",
+ "馬",
+ "馬のひづめの音",
+ "馬のいななき",
+ "牛",
+ "牛の鳴き声",
+ "カウベル",
+ "豚",
+ "豚の鳴き声",
+ "ヤギ",
+ "ヤギや羊の鳴き声",
+ "羊",
+ "家禽(ニワトリなど)",
+ "ニワトリ",
+ "めんどりの鳴き声",
+ "おんどりの鳴き声",
+ "七面鳥",
+ "七面鳥の鳴き声",
+ "アヒル",
+ "アヒルの鳴き声",
+ "ガチョウ",
+ "ガチョウの鳴き声",
+ "野生動物",
+ "ライオン・トラなど",
+ "猛獣のほえ声",
+ "鳥",
+ "鳥の鳴き声",
+ "小鳥のさえずり",
+ "鳥のけたたましい声",
+ "ハト",
+ "ハトの鳴き声",
+ "カラス",
+ "カラスの鳴き声",
+ "フクロウ",
+ "フクロウの鳴き声",
+ "鳥の羽ばたき",
+ "野生のイヌ科動物",
+ "げっ歯類(ネズミなど)",
+ "ネズミ",
+ "小動物の足音",
+ "虫",
+ "コオロギ",
+ "蚊",
+ "ハエ",
+ "虫の羽音",
+ "ハチ",
+ "カエル",
+ "カエルの鳴き声",
+ "ヘビ",
+ "ガラガラヘビの音",
+ "クジラの鳴き声",
+ "音楽",
+ "楽器",
+ "弦をはじく楽器",
+ "ギター",
+ "エレキギター",
+ "ベースギター",
+ "アコースティックギター",
+ "スチールギター",
+ "ギターのタッピング",
+ "ギターのストローク",
+ "バンジョー",
+ "シタール",
+ "マンドリン",
+ "琴・ツィター",
+ "ウクレレ",
+ "鍵盤楽器",
+ "ピアノ",
+ "エレクトリックピアノ",
+ "オルガン",
+ "電子オルガン",
+ "ハモンドオルガン",
+ "シンセサイザー",
+ "サンプラー",
+ "チェンバロ",
+ "打楽器",
+ "ドラムセット",
+ "ドラムマシン",
+ "ドラム・太鼓",
+ "スネアドラム",
+ "リムショット",
+ "ドラムロール",
+ "バスドラム",
+ "ティンパニ",
+ "タブラ",
+ "シンバル",
+ "ハイハット",
+ "ウッドブロック",
+ "タンバリン",
+ "シェイカー(楽器)",
+ "マラカス",
+ "ゴング",
+ "チューブラーベル",
+ "鍵盤打楽器",
+ "マリンバ・木琴",
+ "鉄琴",
+ "ビブラフォン",
+ "スチールパン",
+ "オーケストラ",
+ "金管楽器",
+ "ホルン",
+ "トランペット",
+ "トロンボーン",
+ "弓で弾く弦楽器",
+ "弦楽合奏",
+ "バイオリン",
+ "ピチカート奏法",
+ "チェロ",
+ "コントラバス",
+ "木管楽器",
+ "フルート",
+ "サックス",
+ "クラリネット",
+ "ハープ",
+ "鐘・ベル",
+ "教会の鐘",
+ "鈴",
+ "自転車のベル",
+ "音叉",
+ "チャイム",
+ "風鈴",
+ "鐘を順に鳴らす音",
+ "ハーモニカ",
+ "アコーディオン",
+ "バグパイプ",
+ "ディジュリドゥ",
+ "ショファル(角笛)",
+ "テルミン",
+ "シンギングボウル",
+ "スクラッチ(DJ)",
+ "ポップス",
+ "ヒップホップ",
+ "ビートボックス",
+ "ロック",
+ "ヘビーメタル",
+ "パンク・ロック",
+ "グランジ",
+ "プログレッシブ・ロック",
+ "ロックンロール",
+ "サイケデリック・ロック",
+ "リズム・アンド・ブルース",
+ "ソウル・ミュージック",
+ "レゲエ",
+ "カントリー・ミュージック",
+ "スウィング・ジャズ",
+ "ブルーグラス",
+ "ファンク",
+ "フォークソング",
+ "中東の音楽",
+ "ジャズ",
+ "ディスコ・ミュージック",
+ "クラシック音楽",
+ "オペラ",
+ "電子音楽",
+ "ハウス・ミュージック",
+ "テクノ",
+ "ダブステップ",
+ "ドラムンベース",
+ "エレクトロニカ",
+ "EDM",
+ "アンビエント・ミュージック",
+ "トランス・ミュージック",
+ "ラテン音楽",
+ "サルサ",
+ "フラメンコ",
+ "ブルース",
+ "子ども向けの音楽",
+ "ニューエイジ・ミュージック",
+ "声楽",
+ "アカペラ",
+ "アフリカの音楽",
+ "アフロビート",
+ "キリスト教音楽",
+ "ゴスペル",
+ "アジアの音楽",
+ "カルナータカ音楽",
+ "ボリウッド音楽",
+ "スカ",
+ "伝統音楽",
+ "インディーズ音楽",
+ "歌",
+ "BGM",
+ "テーマ曲",
+ "CMソング",
+ "サウンドトラック",
+ "子守唄",
+ "ゲーム音楽",
+ "クリスマス音楽",
+ "ダンス音楽",
+ "結婚式の音楽",
+ "楽しい音楽",
+ "悲しい音楽",
+ "優しい音楽",
+ "わくわくする音楽",
+ "怒りの音楽",
+ "怖い音楽",
+ "風",
+ "木の葉のざわめき",
+ "マイクの風切り音",
+ "雷雨",
+ "雷",
+ "水",
+ "雨",
+ "雨粒",
+ "屋根や窓を打つ雨",
+ "川の流れ",
+ "滝",
+ "海",
+ "波",
+ "蒸気",
+ "ゴボゴボという水音",
+ "火が燃える音",
+ "パチパチという音",
+ "乗り物",
+ "船・ボート",
+ "帆船・ヨット",
+ "手こぎボート・カヌー",
+ "モーターボート",
+ "大型船",
+ "自動車・バイク",
+ "乗用車",
+ "車のクラクション",
+ "短いクラクション",
+ "車の警報音",
+ "パワーウィンドウ",
+ "車のスリップ音",
+ "タイヤのきしむ音",
+ "車が通り過ぎる音",
+ "レーシングカー",
+ "トラック",
+ "エアブレーキ",
+ "トラックのエアホーン",
+ "バック時の警告音",
+ "アイスクリーム販売車",
+ "バス",
+ "緊急車両",
+ "パトカー(サイレン)",
+ "救急車(サイレン)",
+ "消防車(サイレン)",
+ "バイク",
+ "交通騒音",
+ "鉄道",
+ "列車",
+ "機関車の汽笛",
+ "列車の警笛",
+ "鉄道車両",
+ "列車の車輪のきしむ音",
+ "地下鉄",
+ "航空機",
+ "航空機のエンジン",
+ "ジェットエンジン",
+ "プロペラ",
+ "ヘリコプター",
+ "飛行機",
+ "自転車",
+ "スケートボード",
+ "エンジン",
+ "小型エンジン(高音)",
+ "歯医者のドリル",
+ "芝刈り機",
+ "チェーンソー",
+ "中型エンジン(中音)",
+ "大型エンジン(低音)",
+ "エンジンのノッキング",
+ "エンジン始動",
+ "アイドリング",
+ "エンジンの加速音",
+ "ドア",
+ "玄関チャイム",
+ "ピンポーン(チャイム)",
+ "引き戸",
+ "ドアを強く閉める音",
+ "ノック",
+ "軽くたたく音",
+ "キュッという音",
+ "戸棚の開け閉め",
+ "引き出しの開け閉め",
+ "食器や鍋の音",
+ "ナイフやフォーク",
+ "包丁で刻む音",
+ "揚げ物・炒め物の音",
+ "電子レンジ",
+ "ミキサー(調理用)",
+ "蛇口の水音",
+ "シンクの水音",
+ "浴槽の水音",
+ "ドライヤー",
+ "トイレを流す音",
+ "歯ブラシ",
+ "電動歯ブラシ",
+ "掃除機",
+ "ファスナー",
+ "鍵のジャラジャラ",
+ "コインが落ちる音",
+ "はさみ",
+ "電気シェーバー",
+ "トランプを切る音",
+ "タイピング",
+ "タイプライター",
+ "パソコンのキーボード",
+ "ペンで書く音",
+ "警報音",
+ "電話",
+ "電話のベル",
+ "着信音",
+ "電話のプッシュ音",
+ "電話の発信音",
+ "電話の話し中音",
+ "目覚まし時計",
+ "サイレン",
+ "防災サイレン",
+ "ブザー",
+ "煙感知器の警報音",
+ "火災報知器",
+ "霧笛",
+ "ホイッスル",
+ "汽笛",
+ "機械",
+ "ラチェット",
+ "時計",
+ "カチッという金属音",
+ "時計のチクタク音",
+ "歯車",
+ "滑車",
+ "ミシン",
+ "扇風機・ファン",
+ "エアコン",
+ "レジ",
+ "プリンター",
+ "カメラ",
+ "一眼レフカメラ",
+ "工具",
+ "ハンマー",
+ "削岩機",
+ "のこぎり",
+ "やすりがけ",
+ "紙やすりがけ",
+ "電動工具",
+ "電動ドリル",
+ "爆発",
+ "銃声",
+ "機関銃",
+ "一斉射撃",
+ "砲撃",
+ "おもちゃの銃",
+ "花火",
+ "爆竹",
+ "破裂音",
+ "噴火",
+ "ドーンという音",
+ "木材",
+ "おので木を割る音",
+ "木が裂ける音",
+ "ピシッと割れる音",
+ "ガラス",
+ "カチンという音",
+ "ガラスが割れる音",
+ "液体",
+ "水がはねる音",
+ "液体が揺れる音",
+ "ぬれた物をつぶす音",
+ "しずくが落ちる音",
+ "液体を注ぐ音",
+ "ちょろちょろ流れる音",
+ "どっと流れ出る音",
+ "容器に液体を満たす音",
+ "スプレーの噴射音",
+ "液体のポンプ音",
+ "かき混ぜる音",
+ "沸騰する音",
+ "ソナー",
+ "矢",
+ "ヒュッと空を切る音",
+ "ドスンという音",
+ "ゴトンという音",
+ "楽器用チューナー",
+ "エフェクター",
+ "コーラス効果",
+ "バスケのドリブル音",
+ "バンという音",
+ "平手でたたく音",
+ "バシッと打つ音",
+ "ガシャンと壊れる音",
+ "物が割れる音",
+ "物が弾む音",
+ "むちの音",
+ "はためく音",
+ "ひっかく音",
+ "ガリガリこする音",
+ "こすれる音",
+ "転がる音",
+ "押しつぶす音",
+ "紙などを丸める音",
+ "引き裂く音",
+ "ピッという電子音",
+ "ピーンと響く音",
+ "チーンという音",
+ "ガーンという金属音",
+ "キーッという音",
+ "ギシギシきしむ音",
+ "カサカサという音",
+ "ウィーンという音",
+ "ガタガタという音",
+ "ジュージューという音",
+ "クリック音",
+ "カタカタという音",
+ "ゴーッという低い音",
+ "ポチャンという音",
+ "金属が触れ合う音",
+ "ブーンという音",
+ "ピュンという音",
+ "ビヨーンという音",
+ "バリッと砕ける音",
+ "無音",
+ "サイン波",
+ "倍音",
+ "音程が変わる電子音",
+ "効果音",
+ "規則的な断続音",
+ "室内(小さな部屋)",
+ "室内(広間・ホール)",
+ "室内(公共の場)",
+ "屋外(街中)",
+ "屋外(田舎・自然)",
+ "残響",
+ "こだま",
+ "ノイズ",
+ "環境騒音",
+ "ザーッという電気ノイズ",
+ "電源のハムノイズ",
+ "音の歪み",
+ "受話器に返る自分の声",
+ "やかましい騒音",
+ "ホワイトノイズ",
+ "ピンクノイズ",
+ "脈打つような音",
+ "振動音",
+ "テレビ",
+ "ラジオ",
+ "フィールド録音"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/ko.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/ko.json
new file mode 100644
index 0000000..e5b0eda
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/ko.json
@@ -0,0 +1,523 @@
+[
+ "말소리",
+ "아이 말소리",
+ "대화",
+ "내레이션",
+ "옹알이",
+ "합성 음성",
+ "외침",
+ "우렁찬 고함",
+ "환호성",
+ "고함",
+ "아이들 함성",
+ "비명",
+ "속삭임",
+ "웃음소리",
+ "아기 웃음소리",
+ "키득키득 웃음",
+ "비웃음",
+ "폭소",
+ "조용한 웃음",
+ "울음소리",
+ "아기 울음소리",
+ "흐느낌",
+ "신음",
+ "한숨",
+ "노랫소리",
+ "합창",
+ "요들",
+ "읊조림",
+ "만트라",
+ "아이 노랫소리",
+ "합성 노랫소리",
+ "랩",
+ "콧노래",
+ "앓는 소리",
+ "끙 소리",
+ "휘파람",
+ "숨소리",
+ "쌕쌕거림",
+ "코골이",
+ "헉 하는 소리",
+ "헐떡임",
+ "콧방귀",
+ "기침",
+ "헛기침",
+ "재채기",
+ "코 훌쩍임",
+ "달리는 발소리",
+ "발 끄는 소리",
+ "발소리",
+ "씹는 소리",
+ "깨무는 소리",
+ "가글하는 소리",
+ "배 꼬르륵 소리",
+ "트림",
+ "딸꾹질",
+ "방귀",
+ "손으로 내는 소리",
+ "손가락 튕기는 소리",
+ "손뼉 치는 소리",
+ "심장 박동",
+ "심장 잡음",
+ "응원 함성",
+ "박수갈채",
+ "사람들 잡담",
+ "군중",
+ "왁자지껄한 소리",
+ "아이들 노는 소리",
+ "동물",
+ "반려동물",
+ "개",
+ "개 짖는 소리",
+ "작은 개 짖는 소리",
+ "울부짖음 (개·늑대)",
+ "멍멍",
+ "으르렁거림",
+ "개 낑낑거림",
+ "고양이",
+ "고양이 골골거림",
+ "고양이 울음소리",
+ "쉬익 소리 (하악질)",
+ "고양이 발정 울음",
+ "가축",
+ "말 (동물)",
+ "말발굽 소리",
+ "말 울음소리",
+ "소",
+ "소 울음소리",
+ "카우벨",
+ "돼지",
+ "돼지 울음소리",
+ "염소",
+ "양·염소 울음소리",
+ "양",
+ "가금류",
+ "닭",
+ "닭 꼬꼬 소리",
+ "수탉 울음소리",
+ "칠면조",
+ "칠면조 울음소리",
+ "오리",
+ "오리 울음소리",
+ "거위",
+ "거위 울음소리",
+ "야생동물",
+ "사자·호랑이",
+ "맹수 포효",
+ "새",
+ "새 울음소리",
+ "새 지저귐",
+ "날카로운 새 울음소리",
+ "비둘기",
+ "비둘기 울음소리",
+ "까마귀",
+ "까마귀 울음소리",
+ "부엉이",
+ "부엉이 울음소리",
+ "새 날갯짓",
+ "늑대·들개",
+ "쥐 (설치류)",
+ "생쥐",
+ "작은 동물 발소리",
+ "곤충",
+ "귀뚜라미",
+ "모기",
+ "파리",
+ "윙윙 소리",
+ "벌",
+ "개구리",
+ "개구리 울음소리",
+ "뱀",
+ "덜거덕거리는 소리",
+ "고래 울음소리",
+ "음악",
+ "악기",
+ "뜯는 현악기",
+ "기타 (악기)",
+ "일렉트릭 기타",
+ "베이스 기타",
+ "어쿠스틱 기타",
+ "슬라이드 기타",
+ "기타 태핑",
+ "기타 스트로크",
+ "밴조",
+ "시타르",
+ "만돌린",
+ "치터",
+ "우쿨렐레",
+ "건반악기",
+ "피아노",
+ "일렉트릭 피아노",
+ "오르간",
+ "전자 오르간",
+ "해먼드 오르간",
+ "신시사이저",
+ "샘플러",
+ "하프시코드",
+ "타악기",
+ "드럼 세트",
+ "드럼 머신",
+ "북·드럼",
+ "스네어 드럼",
+ "림샷",
+ "드럼 롤",
+ "베이스 드럼",
+ "팀파니",
+ "타블라",
+ "심벌즈",
+ "하이햇",
+ "우드블록",
+ "탬버린",
+ "셰이커",
+ "마라카스",
+ "징",
+ "튜블러 벨",
+ "건반 타악기",
+ "실로폰·마림바",
+ "글로켄슈필",
+ "비브라폰",
+ "스틸팬",
+ "오케스트라",
+ "금관악기",
+ "호른",
+ "트럼펫",
+ "트롬본",
+ "켜는 현악기",
+ "현악 합주",
+ "바이올린",
+ "피치카토",
+ "첼로",
+ "콘트라베이스",
+ "목관악기",
+ "플루트",
+ "색소폰",
+ "클라리넷",
+ "하프",
+ "종소리",
+ "교회 종소리",
+ "방울 소리",
+ "자전거 벨",
+ "소리굽쇠",
+ "차임벨",
+ "윈드차임",
+ "교회 종 연주",
+ "하모니카",
+ "아코디언",
+ "백파이프",
+ "디저리두",
+ "쇼파르 (뿔나팔)",
+ "테레민",
+ "싱잉볼",
+ "DJ 스크래치",
+ "팝 음악",
+ "힙합",
+ "비트박스",
+ "록 음악",
+ "헤비메탈",
+ "펑크 록",
+ "그런지 록",
+ "프로그레시브 록",
+ "로큰롤",
+ "사이키델릭 록",
+ "R&B",
+ "소울 음악",
+ "레게",
+ "컨트리 음악",
+ "스윙 음악",
+ "블루그래스",
+ "펑크 (Funk)",
+ "포크 음악",
+ "중동 음악",
+ "재즈",
+ "디스코",
+ "클래식 음악",
+ "오페라",
+ "전자 음악",
+ "하우스 음악",
+ "테크노",
+ "덥스텝",
+ "드럼 앤 베이스",
+ "일렉트로니카",
+ "EDM",
+ "앰비언트 음악",
+ "트랜스 음악",
+ "라틴 음악",
+ "살사 음악",
+ "플라멩코",
+ "블루스",
+ "동요",
+ "뉴에이지 음악",
+ "보컬 음악",
+ "아카펠라",
+ "아프리카 음악",
+ "아프로비트",
+ "기독교 음악",
+ "가스펠",
+ "아시아 음악",
+ "남인도 고전 음악",
+ "발리우드 음악",
+ "스카 음악",
+ "전통 음악",
+ "인디 음악",
+ "노래",
+ "배경 음악",
+ "주제곡",
+ "광고 음악",
+ "OST",
+ "자장가",
+ "게임 음악",
+ "크리스마스 음악",
+ "댄스 음악",
+ "결혼식 음악",
+ "밝은 음악",
+ "슬픈 음악",
+ "부드러운 음악",
+ "신나는 음악",
+ "분노에 찬 음악",
+ "무서운 음악",
+ "바람",
+ "나뭇잎 바스락 소리",
+ "마이크 바람 잡음",
+ "천둥 번개",
+ "천둥",
+ "물",
+ "비",
+ "빗방울",
+ "빗소리 (지붕·창문)",
+ "시냇물",
+ "폭포",
+ "바다",
+ "파도",
+ "증기 뿜는 소리",
+ "꼬르륵 물소리",
+ "불타는 소리",
+ "타닥타닥 소리",
+ "탈것",
+ "배·보트",
+ "돛단배",
+ "노 젓는 배",
+ "모터보트",
+ "선박",
+ "도로 차량",
+ "자동차",
+ "자동차 경적",
+ "짧은 경적",
+ "자동차 경보음",
+ "자동차 전동 창문",
+ "차 미끄러지는 소리",
+ "타이어 끼익 소리",
+ "지나가는 자동차",
+ "자동차 경주",
+ "트럭",
+ "에어 브레이크",
+ "트럭 경적",
+ "후진 경고음",
+ "아이스크림 트럭",
+ "버스",
+ "긴급 차량",
+ "경찰차 사이렌",
+ "구급차 사이렌",
+ "소방차 사이렌",
+ "오토바이",
+ "교통 소음",
+ "철도",
+ "기차",
+ "기차 기적 소리",
+ "기차 경적",
+ "객차·화차",
+ "기차 바퀴 끼익 소리",
+ "지하철",
+ "항공기",
+ "항공기 엔진",
+ "제트 엔진",
+ "프로펠러",
+ "헬리콥터",
+ "비행기",
+ "자전거",
+ "스케이트보드",
+ "엔진",
+ "소형 엔진 (고음)",
+ "치과 드릴",
+ "잔디깎이",
+ "전기톱",
+ "중형 엔진 (중음)",
+ "대형 엔진 (저음)",
+ "엔진 노킹",
+ "엔진 시동",
+ "엔진 공회전",
+ "엔진 가속",
+ "문",
+ "초인종",
+ "딩동",
+ "미닫이문",
+ "문 쾅 닫는 소리",
+ "노크 소리",
+ "톡톡 두드리는 소리",
+ "끽 소리",
+ "찬장 여닫는 소리",
+ "서랍 여닫는 소리",
+ "그릇·냄비 소리",
+ "수저·포크 소리",
+ "칼질 소리",
+ "기름에 튀기는 소리",
+ "전자레인지",
+ "믹서기",
+ "수도꼭지 물소리",
+ "싱크대 물소리",
+ "욕조 물소리",
+ "헤어드라이어",
+ "변기 물 내리는 소리",
+ "칫솔질",
+ "전동 칫솔",
+ "진공청소기",
+ "지퍼",
+ "열쇠 짤랑이는 소리",
+ "동전 떨어지는 소리",
+ "가위",
+ "전기면도기",
+ "카드 섞는 소리",
+ "타자 치는 소리",
+ "타자기",
+ "컴퓨터 키보드",
+ "글씨 쓰는 소리",
+ "경보음",
+ "전화기",
+ "따르릉 전화벨",
+ "휴대폰 벨소리",
+ "전화 버튼음",
+ "전화 발신음",
+ "통화 중 신호음",
+ "알람 시계",
+ "사이렌",
+ "민방위 사이렌",
+ "버저",
+ "연기 감지기 경보음",
+ "화재 경보음",
+ "안개 경적",
+ "호루라기",
+ "증기 기적 소리",
+ "기계 장치",
+ "라쳇 드르륵 소리",
+ "시계",
+ "째깍 소리",
+ "시계 째깍째깍",
+ "톱니바퀴",
+ "도르래",
+ "재봉틀",
+ "선풍기",
+ "에어컨",
+ "금전 등록기",
+ "프린터",
+ "카메라",
+ "DSLR 카메라",
+ "공구",
+ "망치",
+ "착암기",
+ "톱질",
+ "줄칼질",
+ "사포질",
+ "전동 공구",
+ "드릴",
+ "폭발음",
+ "총소리",
+ "기관총",
+ "일제 사격",
+ "포격",
+ "장난감 총",
+ "불꽃놀이",
+ "폭죽",
+ "펑 터지는 소리",
+ "화산 폭발",
+ "굉음",
+ "나무",
+ "도끼질",
+ "나무 쪼개지는 소리",
+ "쩍 갈라지는 소리",
+ "유리",
+ "유리잔 부딪치는 소리",
+ "유리 깨지는 소리",
+ "액체",
+ "물 튀는 소리",
+ "물 출렁이는 소리",
+ "질퍽거리는 소리",
+ "물방울 소리",
+ "물 따르는 소리",
+ "졸졸 흐르는 소리",
+ "콸콸 쏟아지는 소리",
+ "물 받는 소리",
+ "칙칙 뿌리는 소리",
+ "물 펌프 소리",
+ "휘젓는 소리",
+ "끓는 소리",
+ "음파 탐지기",
+ "화살",
+ "휙 소리",
+ "쿵 소리",
+ "퉁 소리",
+ "악기 튜너",
+ "이펙터",
+ "코러스 효과",
+ "농구공 튀는 소리",
+ "쾅 소리",
+ "찰싹 소리",
+ "퍽 치는 소리",
+ "와장창 소리",
+ "깨지는 소리",
+ "통통 튀는 소리",
+ "채찍 소리",
+ "펄럭이는 소리",
+ "긁는 소리",
+ "긁어내는 소리",
+ "문지르는 소리",
+ "굴러가는 소리",
+ "으스러지는 소리",
+ "종이 구기는 소리",
+ "찢어지는 소리",
+ "삐 소리",
+ "핑 소리",
+ "띵 소리",
+ "요란한 쇳소리",
+ "끼익 소리",
+ "삐걱 소리",
+ "바스락 소리",
+ "윙 돌아가는 소리",
+ "달그락 소리",
+ "지글지글 소리",
+ "딸깍 소리",
+ "철컥철컥 소리",
+ "우르릉 소리",
+ "퐁당 소리",
+ "짤랑짤랑 소리",
+ "웅 하는 소리",
+ "짧고 높은 윙 소리",
+ "용수철 튕기는 소리",
+ "바삭 소리",
+ "무음",
+ "사인파",
+ "배음",
+ "음높이 변하는 신호음",
+ "효과음",
+ "규칙적인 펄스음",
+ "실내 (작은 방)",
+ "실내 (큰 방·홀)",
+ "실내 (공공장소)",
+ "실외 (도시)",
+ "실외 (자연)",
+ "잔향 (울림)",
+ "메아리",
+ "잡음",
+ "주변 소음",
+ "지지직 잡음",
+ "전기 웅웅 잡음",
+ "소리 왜곡",
+ "통화 되울림 (측음)",
+ "시끄러운 불협화음",
+ "백색 소음",
+ "핑크 노이즈",
+ "둥둥 울리는 소리",
+ "진동 소리",
+ "텔레비전",
+ "라디오",
+ "현장 녹음"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/pl.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/pl.json
new file mode 100644
index 0000000..2e6accb
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/pl.json
@@ -0,0 +1,523 @@
+[
+ "Mowa",
+ "Mowa dziecka",
+ "Rozmowa",
+ "Narracja, monolog",
+ "Gaworzenie",
+ "Syntezator mowy",
+ "Krzyk",
+ "Ryk człowieka",
+ "Okrzyk radości",
+ "Wrzask",
+ "Krzyki dzieci",
+ "Przeraźliwy krzyk",
+ "Szept",
+ "Śmiech",
+ "Śmiech niemowlęcia",
+ "Chichot",
+ "Złośliwy chichot",
+ "Gromki śmiech",
+ "Cichy śmiech",
+ "Płacz",
+ "Płacz niemowlęcia",
+ "Pochlipywanie",
+ "Jęk, zawodzenie",
+ "Westchnienie",
+ "Śpiew",
+ "Chór",
+ "Jodłowanie",
+ "Chorał, skandowanie",
+ "Mantra",
+ "Śpiew dziecka",
+ "Śpiew syntetyczny",
+ "Rapowanie",
+ "Nucenie",
+ "Stękanie",
+ "Mruknięcie",
+ "Gwizdanie",
+ "Oddech",
+ "Świszczący oddech",
+ "Chrapanie",
+ "Gwałtowny wdech",
+ "Dyszenie",
+ "Parsknięcie",
+ "Kaszel",
+ "Odchrząkiwanie",
+ "Kichanie",
+ "Pociąganie nosem",
+ "Bieganie",
+ "Szuranie nogami",
+ "Kroki",
+ "Żucie",
+ "Gryzienie",
+ "Płukanie gardła",
+ "Burczenie w brzuchu",
+ "Bekanie",
+ "Czkawka",
+ "Puszczanie bąków",
+ "Odgłosy rąk",
+ "Pstrykanie palcami",
+ "Klaskanie",
+ "Bicie serca",
+ "Szmer serca",
+ "Wiwaty",
+ "Oklaski",
+ "Gwar rozmów",
+ "Tłum",
+ "Zgiełk głosów",
+ "Bawiące się dzieci",
+ "Zwierzęta",
+ "Zwierzęta domowe",
+ "Pies",
+ "Szczekanie",
+ "Ujadanie",
+ "Wycie",
+ "Przeciągłe szczekanie",
+ "Warczenie",
+ "Skomlenie psa",
+ "Kot",
+ "Mruczenie",
+ "Miauczenie",
+ "Syczenie",
+ "Kocie zawodzenie",
+ "Zwierzęta gospodarskie",
+ "Koń",
+ "Stukot kopyt",
+ "Rżenie",
+ "Bydło",
+ "Muczenie",
+ "Krowi dzwonek",
+ "Świnia",
+ "Chrząkanie świni",
+ "Koza",
+ "Beczenie",
+ "Owca",
+ "Drób",
+ "Kura, kogut",
+ "Gdakanie",
+ "Pianie koguta",
+ "Indyk",
+ "Gulgotanie indyka",
+ "Kaczka",
+ "Kwakanie",
+ "Gęś",
+ "Gęganie",
+ "Dzikie zwierzęta",
+ "Lwy, tygrysy",
+ "Ryk",
+ "Ptak",
+ "Śpiew ptaków",
+ "Ćwierkanie",
+ "Skrzeczenie",
+ "Gołąb",
+ "Gruchanie",
+ "Wrona",
+ "Krakanie",
+ "Sowa",
+ "Pohukiwanie",
+ "Trzepot skrzydeł",
+ "Wilki i dzikie psy",
+ "Gryzonie",
+ "Mysz",
+ "Tupot łapek",
+ "Owad",
+ "Świerszcz",
+ "Komar",
+ "Mucha",
+ "Bzyczenie",
+ "Pszczoła, osa",
+ "Żaba",
+ "Kumkanie",
+ "Wąż",
+ "Grzechotanie",
+ "Głosy wielorybów",
+ "Muzyka",
+ "Instrument muzyczny",
+ "Instrument szarpany",
+ "Gitara",
+ "Gitara elektryczna",
+ "Gitara basowa",
+ "Gitara akustyczna",
+ "Gitara hawajska (slide)",
+ "Tapping na gitarze",
+ "Brzdąkanie na gitarze",
+ "Banjo",
+ "Sitar",
+ "Mandolina",
+ "Cytra",
+ "Ukulele",
+ "Instrument klawiszowy",
+ "Fortepian",
+ "Pianino elektryczne",
+ "Organy",
+ "Organy elektroniczne",
+ "Organy Hammonda",
+ "Syntezator",
+ "Sampler",
+ "Klawesyn",
+ "Instrument perkusyjny",
+ "Perkusja",
+ "Automat perkusyjny",
+ "Bęben",
+ "Werbel",
+ "Rimshot na werblu",
+ "Werble (tremolo)",
+ "Bęben basowy",
+ "Kotły (tympany)",
+ "Tabla",
+ "Talerz perkusyjny",
+ "Hi-hat",
+ "Drewniany klocek",
+ "Tamburyn",
+ "Grzechotka",
+ "Marakasy",
+ "Gong",
+ "Dzwony rurowe",
+ "Instrument sztabkowy",
+ "Ksylofon, marimba",
+ "Dzwonki (cymbałki)",
+ "Wibrafon",
+ "Steelpan (bęben stalowy)",
+ "Orkiestra",
+ "Instrument dęty blaszany",
+ "Waltornia",
+ "Trąbka",
+ "Puzon",
+ "Instrument smyczkowy",
+ "Sekcja smyczkowa",
+ "Skrzypce",
+ "Pizzicato",
+ "Wiolonczela",
+ "Kontrabas",
+ "Instrument dęty drewniany",
+ "Flet",
+ "Saksofon",
+ "Klarnet",
+ "Harfa",
+ "Dzwon",
+ "Dzwon kościelny",
+ "Dzwoneczki",
+ "Dzwonek rowerowy",
+ "Kamerton",
+ "Kuranty",
+ "Dzwonki wietrzne",
+ "Bicie w dzwony",
+ "Harmonijka ustna",
+ "Akordeon",
+ "Dudy",
+ "Didgeridoo",
+ "Szofar",
+ "Theremin",
+ "Misa tybetańska",
+ "Skreczowanie (DJ)",
+ "Muzyka pop",
+ "Muzyka hip-hopowa",
+ "Beatbox",
+ "Muzyka rockowa",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Rock progresywny",
+ "Rock and roll",
+ "Rock psychodeliczny",
+ "Rhythm and blues",
+ "Muzyka soul",
+ "Reggae",
+ "Muzyka country",
+ "Muzyka swingowa",
+ "Bluegrass",
+ "Funk",
+ "Muzyka folkowa",
+ "Muzyka Bliskiego Wschodu",
+ "Jazz",
+ "Disco",
+ "Muzyka klasyczna",
+ "Opera",
+ "Muzyka elektroniczna",
+ "Muzyka house",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Muzyka klubowa (EDM)",
+ "Muzyka ambient",
+ "Muzyka trance",
+ "Muzyka latynoamerykańska",
+ "Muzyka salsa",
+ "Flamenco",
+ "Blues",
+ "Muzyka dla dzieci",
+ "Muzyka new age",
+ "Muzyka wokalna",
+ "A cappella",
+ "Muzyka afrykańska",
+ "Afrobeat",
+ "Muzyka chrześcijańska",
+ "Muzyka gospel",
+ "Muzyka azjatycka",
+ "Muzyka karnatacka",
+ "Muzyka bollywoodzka",
+ "Ska",
+ "Muzyka tradycyjna",
+ "Muzyka niezależna",
+ "Piosenka",
+ "Muzyka w tle",
+ "Muzyka z czołówki",
+ "Dżingiel reklamowy",
+ "Muzyka filmowa",
+ "Kołysanka",
+ "Muzyka z gier",
+ "Muzyka świąteczna",
+ "Muzyka taneczna",
+ "Muzyka weselna",
+ "Wesoła muzyka",
+ "Smutna muzyka",
+ "Łagodna muzyka",
+ "Ekscytująca muzyka",
+ "Agresywna muzyka",
+ "Muzyka grozy",
+ "Wiatr",
+ "Szelest liści",
+ "Szum wiatru w mikrofonie",
+ "Burza",
+ "Grzmot",
+ "Woda",
+ "Deszcz",
+ "Kropla deszczu",
+ "Bębnienie deszczu",
+ "Strumień",
+ "Wodospad",
+ "Morze, ocean",
+ "Fale morskie",
+ "Para wodna",
+ "Bulgotanie",
+ "Ogień",
+ "Trzaskanie ognia",
+ "Pojazd",
+ "Łodzie i statki",
+ "Żaglówka, żaglowiec",
+ "Łódź wiosłowa, kajak",
+ "Motorówka",
+ "Statek",
+ "Pojazd silnikowy",
+ "Samochód",
+ "Klakson",
+ "Krótkie trąbnięcie",
+ "Alarm samochodowy",
+ "Elektryczne szyby",
+ "Poślizg",
+ "Pisk opon",
+ "Przejeżdżający samochód",
+ "Wyścigi samochodowe",
+ "Ciężarówka",
+ "Hamulec pneumatyczny",
+ "Klakson ciężarówki",
+ "Sygnał cofania",
+ "Samochód z lodami",
+ "Autobus",
+ "Pojazd uprzywilejowany",
+ "Radiowóz (syrena)",
+ "Karetka (syrena)",
+ "Wóz strażacki (syrena)",
+ "Motocykl",
+ "Szum ruchu ulicznego",
+ "Kolej",
+ "Pociąg",
+ "Gwizd pociągu",
+ "Syrena pociągu",
+ "Wagon kolejowy",
+ "Pisk kół pociągu",
+ "Metro",
+ "Statek powietrzny",
+ "Silnik lotniczy",
+ "Silnik odrzutowy",
+ "Śmigło",
+ "Helikopter",
+ "Samolot",
+ "Rower",
+ "Deskorolka",
+ "Silnik",
+ "Mały silnik",
+ "Wiertarka dentystyczna",
+ "Kosiarka",
+ "Piła łańcuchowa",
+ "Średni silnik",
+ "Duży silnik",
+ "Stukanie silnika",
+ "Uruchamianie silnika",
+ "Silnik na biegu jałowym",
+ "Przyspieszający pojazd",
+ "Drzwi",
+ "Dzwonek do drzwi",
+ "Ding-dong dzwonka",
+ "Drzwi przesuwne",
+ "Trzaśnięcie drzwiami",
+ "Pukanie",
+ "Postukiwanie",
+ "Piszczenie",
+ "Drzwiczki szafki",
+ "Szuflada",
+ "Stukot naczyń",
+ "Sztućce",
+ "Siekanie",
+ "Smażenie",
+ "Mikrofalówka",
+ "Blender",
+ "Kran",
+ "Woda w zlewie",
+ "Woda w wannie",
+ "Suszarka do włosów",
+ "Spłukiwanie toalety",
+ "Szczoteczka do zębów",
+ "Szczoteczka elektryczna",
+ "Odkurzacz",
+ "Zamek błyskawiczny",
+ "Brzęk kluczy",
+ "Upadająca moneta",
+ "Nożyczki",
+ "Golarka elektryczna",
+ "Tasowanie kart",
+ "Stukanie w klawisze",
+ "Maszyna do pisania",
+ "Klawiatura komputera",
+ "Pisanie odręczne",
+ "Alarm",
+ "Telefon",
+ "Dzwonienie telefonu",
+ "Dzwonek komórki",
+ "Wybieranie numeru",
+ "Sygnał zgłoszenia centrali",
+ "Sygnał zajętości",
+ "Budzik",
+ "Syrena",
+ "Syrena obrony cywilnej",
+ "Brzęczyk",
+ "Czujnik dymu",
+ "Alarm pożarowy",
+ "Syrena mgłowa",
+ "Gwizdek",
+ "Gwizdek parowy",
+ "Mechanizmy",
+ "Mechanizm zapadkowy",
+ "Zegar",
+ "Tyknięcie",
+ "Tykanie zegara",
+ "Koła zębate",
+ "Bloczki linowe",
+ "Maszyna do szycia",
+ "Wentylator",
+ "Klimatyzacja",
+ "Kasa fiskalna",
+ "Drukarka",
+ "Aparat fotograficzny",
+ "Lustrzanka",
+ "Narzędzia",
+ "Młotek",
+ "Młot pneumatyczny",
+ "Cięcie piłą",
+ "Piłowanie pilnikiem",
+ "Szlifowanie",
+ "Elektronarzędzie",
+ "Wiertarka",
+ "Wybuch",
+ "Strzał",
+ "Karabin maszynowy",
+ "Salwa karabinowa",
+ "Ostrzał artyleryjski",
+ "Pistolet na kapiszony",
+ "Fajerwerki",
+ "Petarda",
+ "Pęknięcie (np. balonu)",
+ "Erupcja wulkanu",
+ "Głęboki huk",
+ "Drewno",
+ "Rąbanie drewna",
+ "Łamanie drewna",
+ "Trzask",
+ "Szkło",
+ "Brzęk szkła",
+ "Tłuczenie szkła",
+ "Ciecz",
+ "Plusk",
+ "Chlupotanie",
+ "Ściskanie czegoś mokrego",
+ "Kapanie",
+ "Nalewanie",
+ "Ciurkanie",
+ "Chlustanie",
+ "Napełnianie naczynia",
+ "Rozpylanie",
+ "Pompowanie cieczy",
+ "Mieszanie",
+ "Wrzenie",
+ "Sonar",
+ "Strzała z łuku",
+ "Świst",
+ "Głuche uderzenie",
+ "Głuchy stuk",
+ "Stroik elektroniczny",
+ "Procesor efektów",
+ "Efekt chorus",
+ "Kozłowanie piłki",
+ "Huk",
+ "Plaśnięcie",
+ "Mocne uderzenie",
+ "Roztrzaskanie",
+ "Łamanie",
+ "Odbijanie się",
+ "Trzask bicza",
+ "Łopotanie",
+ "Drapanie",
+ "Skrobanie",
+ "Pocieranie",
+ "Toczenie się",
+ "Miażdżenie",
+ "Gniecenie papieru, folii",
+ "Rozdzieranie",
+ "Piknięcie",
+ "Ping",
+ "Dzyń",
+ "Szczęk metalu",
+ "Pisk",
+ "Skrzypienie",
+ "Szelest",
+ "Furkot",
+ "Klekot",
+ "Skwierczenie",
+ "Klikanie",
+ "Rytmiczny stukot",
+ "Dudnienie",
+ "Wpadnięcie do wody",
+ "Pobrzękiwanie",
+ "Buczenie",
+ "Krótkie brzęczenie",
+ "Dźwięk sprężyny",
+ "Chrzęst",
+ "Cisza",
+ "Czysty ton (sinusoida)",
+ "Ton harmoniczny",
+ "Ton o zmiennej wysokości",
+ "Efekt dźwiękowy",
+ "Pulsowanie",
+ "W małym pomieszczeniu",
+ "W dużej sali",
+ "W budynku publicznym",
+ "Na zewnątrz, w mieście",
+ "Na zewnątrz, poza miastem",
+ "Pogłos",
+ "Echo",
+ "Szum",
+ "Szum otoczenia",
+ "Trzaski zakłóceń",
+ "Buczenie elektryczne",
+ "Zniekształcenie",
+ "Własny głos w słuchawce",
+ "Kakofonia",
+ "Biały szum",
+ "Różowy szum",
+ "Rytmiczne dudnienie",
+ "Drgania",
+ "Telewizor",
+ "Radio",
+ "Nagranie terenowe"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/pt-BR.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/pt-BR.json
new file mode 100644
index 0000000..b9ed1a1
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/pt-BR.json
@@ -0,0 +1,523 @@
+[
+ "Fala",
+ "Criança falando",
+ "Conversa",
+ "Narração",
+ "Balbucio",
+ "Voz sintetizada",
+ "Grito",
+ "Urro",
+ "Grito de euforia",
+ "Berro",
+ "Crianças gritando",
+ "Grito agudo",
+ "Sussurro",
+ "Risada",
+ "Risada de bebê",
+ "Risadinha",
+ "Risinho de deboche",
+ "Gargalhada",
+ "Riso contido",
+ "Choro",
+ "Choro de bebê",
+ "Choramingo",
+ "Lamento",
+ "Suspiro",
+ "Canto",
+ "Coral",
+ "Canto tirolês",
+ "Cântico",
+ "Mantra",
+ "Criança cantando",
+ "Canto sintetizado",
+ "Rap",
+ "Cantarolar",
+ "Gemido",
+ "Grunhido",
+ "Assobio",
+ "Respiração",
+ "Chiado no peito",
+ "Ronco",
+ "Suspiro de susto",
+ "Respiração ofegante",
+ "Bufada",
+ "Tosse",
+ "Pigarro",
+ "Espirro",
+ "Fungada",
+ "Passos de corrida",
+ "Passos arrastados",
+ "Passos",
+ "Mastigação",
+ "Mordida",
+ "Gargarejo",
+ "Barriga roncando",
+ "Arroto",
+ "Soluço",
+ "Pum",
+ "Mãos",
+ "Estalo de dedos",
+ "Palmas",
+ "Batimentos cardíacos",
+ "Sopro cardíaco",
+ "Torcida",
+ "Aplausos",
+ "Burburinho",
+ "Multidão",
+ "Falatório",
+ "Crianças brincando",
+ "Animal",
+ "Animais de estimação",
+ "Cachorro",
+ "Latido",
+ "Latido agudo",
+ "Uivo",
+ "Au-au",
+ "Rosnado",
+ "Ganido",
+ "Gato",
+ "Ronronar",
+ "Miado",
+ "Chiado",
+ "Miado de gato no cio",
+ "Animais de fazenda",
+ "Cavalo",
+ "Cascos de cavalo",
+ "Relincho",
+ "Gado",
+ "Mugido",
+ "Sino de vaca",
+ "Porco",
+ "Grunhido de porco",
+ "Cabra",
+ "Balido",
+ "Ovelha",
+ "Aves domésticas",
+ "Galinha e galo",
+ "Cacarejo",
+ "Canto do galo",
+ "Peru",
+ "Glu-glu de peru",
+ "Pato",
+ "Grasnado de pato",
+ "Ganso",
+ "Grasnado de ganso",
+ "Animais selvagens",
+ "Grandes felinos",
+ "Rugido",
+ "Pássaro",
+ "Canto de pássaro",
+ "Piado",
+ "Grasnado",
+ "Pombo",
+ "Arrulho de pombo",
+ "Corvo",
+ "Grasnado de corvo",
+ "Coruja",
+ "Piado de coruja",
+ "Bater de asas",
+ "Lobos e cães selvagens",
+ "Roedores",
+ "Camundongo",
+ "Passinhos de animal",
+ "Inseto",
+ "Grilo",
+ "Mosquito",
+ "Mosca",
+ "Zumbido de inseto",
+ "Abelha ou vespa",
+ "Sapo",
+ "Coaxar",
+ "Cobra",
+ "Chocalho de cascavel",
+ "Canto de baleia",
+ "Música",
+ "Instrumento musical",
+ "Cordas dedilhadas",
+ "Violão ou guitarra",
+ "Guitarra elétrica",
+ "Baixo elétrico",
+ "Violão",
+ "Guitarra havaiana (slide)",
+ "Tapping na guitarra",
+ "Batida de violão",
+ "Banjo",
+ "Sitar",
+ "Bandolim",
+ "Cítara",
+ "Ukulele",
+ "Teclado musical",
+ "Piano",
+ "Piano elétrico",
+ "Órgão",
+ "Órgão eletrônico",
+ "Órgão Hammond",
+ "Sintetizador",
+ "Sampler",
+ "Cravo (instrumento)",
+ "Percussão",
+ "Bateria",
+ "Bateria eletrônica",
+ "Tambor",
+ "Caixa-clara",
+ "Batida no aro",
+ "Rufar de tambor",
+ "Bumbo",
+ "Tímpano (instrumento)",
+ "Tabla",
+ "Prato de bateria",
+ "Chimbal",
+ "Bloco de madeira",
+ "Pandeiro",
+ "Chocalho",
+ "Maraca",
+ "Gongo",
+ "Sinos tubulares",
+ "Percussão melódica",
+ "Xilofone ou marimba",
+ "Glockenspiel",
+ "Vibrafone",
+ "Tambor de aço",
+ "Orquestra",
+ "Instrumento de sopro de metal",
+ "Trompa",
+ "Trompete",
+ "Trombone",
+ "Cordas com arco",
+ "Seção de cordas",
+ "Violino",
+ "Pizzicato",
+ "Violoncelo",
+ "Contrabaixo acústico",
+ "Instrumento de sopro",
+ "Flauta",
+ "Saxofone",
+ "Clarinete",
+ "Harpa",
+ "Sino",
+ "Sino de igreja",
+ "Guizo",
+ "Campainha de bicicleta",
+ "Diapasão",
+ "Carrilhão",
+ "Mensageiro dos ventos",
+ "Repique de sinos",
+ "Gaita de boca",
+ "Acordeão (sanfona)",
+ "Gaita de foles",
+ "Didgeridoo",
+ "Shofar",
+ "Teremim",
+ "Tigela tibetana",
+ "Scratch de DJ",
+ "Música pop",
+ "Hip-hop",
+ "Beatbox",
+ "Rock",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Rock progressivo",
+ "Rock and roll",
+ "Rock psicodélico",
+ "Rhythm and blues",
+ "Soul",
+ "Reggae",
+ "Música country",
+ "Swing",
+ "Bluegrass",
+ "Funk",
+ "Música folk",
+ "Música do Oriente Médio",
+ "Jazz",
+ "Música disco",
+ "Música clássica",
+ "Ópera",
+ "Música eletrônica",
+ "Música house",
+ "Techno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Música eletrônica dançante",
+ "Música ambiente (gênero)",
+ "Trance",
+ "Música latino-americana",
+ "Música salsa",
+ "Flamenco",
+ "Blues",
+ "Música infantil",
+ "Música new age",
+ "Música vocal",
+ "A cappella",
+ "Música africana",
+ "Afrobeat",
+ "Música cristã",
+ "Gospel",
+ "Música asiática",
+ "Música carnática",
+ "Música de Bollywood",
+ "Ska",
+ "Música tradicional",
+ "Música independente",
+ "Canção",
+ "Música de fundo",
+ "Música-tema",
+ "Jingle",
+ "Trilha sonora",
+ "Canção de ninar",
+ "Música de videogame",
+ "Música de Natal",
+ "Música para dançar",
+ "Música de casamento",
+ "Música alegre",
+ "Música triste",
+ "Música terna",
+ "Música empolgante",
+ "Música raivosa",
+ "Música assustadora",
+ "Vento",
+ "Farfalhar de folhas",
+ "Vento no microfone",
+ "Tempestade com trovões",
+ "Trovão",
+ "Água",
+ "Chuva",
+ "Gota de chuva",
+ "Chuva batendo",
+ "Riacho",
+ "Cachoeira",
+ "Mar",
+ "Ondas",
+ "Vapor",
+ "Gorgolejo",
+ "Fogo",
+ "Crepitar",
+ "Veículo",
+ "Barco",
+ "Veleiro",
+ "Barco a remo",
+ "Lancha",
+ "Navio",
+ "Veículo motorizado",
+ "Carro",
+ "Buzina",
+ "Buzinada",
+ "Alarme de carro",
+ "Vidro elétrico",
+ "Derrapagem",
+ "Pneu cantando",
+ "Carro passando",
+ "Carro de corrida",
+ "Caminhão",
+ "Freio a ar",
+ "Buzina de caminhão",
+ "Bipe de ré",
+ "Caminhão de sorvete",
+ "Ônibus",
+ "Veículo de emergência",
+ "Carro de polícia (sirene)",
+ "Ambulância (sirene)",
+ "Carro de bombeiros (sirene)",
+ "Moto",
+ "Ruído de trânsito",
+ "Transporte ferroviário",
+ "Trem",
+ "Apito de trem",
+ "Buzina de trem",
+ "Vagão de trem",
+ "Rodas de trem rangendo",
+ "Metrô",
+ "Aeronave",
+ "Motor de aeronave",
+ "Motor a jato",
+ "Hélice",
+ "Helicóptero",
+ "Avião",
+ "Bicicleta",
+ "Skate",
+ "Motor",
+ "Motor leve (agudo)",
+ "Broca de dentista",
+ "Cortador de grama",
+ "Motosserra",
+ "Motor médio",
+ "Motor pesado (grave)",
+ "Motor batendo pino",
+ "Partida de motor",
+ "Motor em marcha lenta",
+ "Motor acelerando",
+ "Porta",
+ "Campainha",
+ "Dim-dom da campainha",
+ "Porta de correr",
+ "Porta batendo com força",
+ "Batida na porta",
+ "Batidinha",
+ "Rangido",
+ "Porta de armário",
+ "Gaveta",
+ "Louça e panelas",
+ "Talheres",
+ "Picando alimentos",
+ "Fritura",
+ "Micro-ondas",
+ "Liquidificador",
+ "Torneira",
+ "Água na pia",
+ "Água na banheira",
+ "Secador de cabelo",
+ "Descarga do vaso",
+ "Escova de dentes",
+ "Escova de dentes elétrica",
+ "Aspirador de pó",
+ "Zíper",
+ "Chaves tilintando",
+ "Moeda caindo",
+ "Tesoura",
+ "Barbeador elétrico",
+ "Embaralhando cartas",
+ "Digitação",
+ "Máquina de escrever",
+ "Teclado de computador",
+ "Escrita à mão",
+ "Alarme",
+ "Telefone",
+ "Campainha de telefone",
+ "Toque de celular",
+ "Discagem telefônica",
+ "Tom de discagem",
+ "Sinal de ocupado",
+ "Despertador",
+ "Sirene",
+ "Sirene da Defesa Civil",
+ "Campainha elétrica",
+ "Detector de fumaça",
+ "Alarme de incêndio",
+ "Buzina de nevoeiro",
+ "Apito",
+ "Apito a vapor",
+ "Mecanismos",
+ "Catraca",
+ "Relógio",
+ "Tique de relógio",
+ "Tique-taque",
+ "Engrenagens",
+ "Roldanas",
+ "Máquina de costura",
+ "Ventilador",
+ "Ar-condicionado",
+ "Caixa registradora",
+ "Impressora",
+ "Câmera",
+ "Câmera reflex",
+ "Ferramentas",
+ "Martelo",
+ "Britadeira",
+ "Serrando",
+ "Limando",
+ "Lixando",
+ "Ferramenta elétrica",
+ "Furadeira",
+ "Explosão",
+ "Tiro",
+ "Metralhadora",
+ "Tiroteio",
+ "Fogo de artilharia",
+ "Revólver de espoleta",
+ "Fogos de artifício",
+ "Bombinha",
+ "Estouro",
+ "Erupção vulcânica",
+ "Estrondo",
+ "Madeira",
+ "Golpe de machado",
+ "Madeira lascando",
+ "Estalo",
+ "Vidro",
+ "Tilintar de vidro",
+ "Vidro estilhaçando",
+ "Líquido",
+ "Respingo",
+ "Líquido balançando",
+ "Espremendo algo molhado",
+ "Gotejamento",
+ "Despejando líquido",
+ "Líquido escorrendo",
+ "Jorro",
+ "Enchendo um recipiente",
+ "Borrifo",
+ "Bombeando líquido",
+ "Mexendo líquido",
+ "Fervura",
+ "Sonar",
+ "Flecha",
+ "Algo cortando o ar",
+ "Baque",
+ "Baque oco",
+ "Afinador eletrônico",
+ "Pedal de efeitos",
+ "Efeito chorus",
+ "Bola de basquete quicando",
+ "Pancada",
+ "Tapa",
+ "Golpe forte",
+ "Algo se espatifando",
+ "Algo quebrando",
+ "Algo quicando",
+ "Estalo de chicote",
+ "Pano batendo ao vento",
+ "Arranhão",
+ "Raspagem",
+ "Fricção",
+ "Algo rolando",
+ "Esmagamento",
+ "Amassando papel",
+ "Algo rasgando",
+ "Bipe",
+ "Ping",
+ "Toque metálico agudo",
+ "Estrondo metálico",
+ "Guincho",
+ "Rangido de madeira",
+ "Farfalhar",
+ "Zumbido mecânico",
+ "Objetos chacoalhando",
+ "Chiado de fritura",
+ "Cliques",
+ "Cliques rítmicos",
+ "Ruído grave contínuo",
+ "Algo caindo na água",
+ "Tilintar",
+ "Zumbido contínuo",
+ "Zumbido curto e agudo",
+ "Som de mola",
+ "Som crocante",
+ "Silêncio",
+ "Tom puro",
+ "Harmônico",
+ "Tom deslizante",
+ "Efeito sonoro",
+ "Pulsos regulares",
+ "Dentro de sala pequena",
+ "Dentro de salão grande",
+ "Dentro de local público",
+ "Ao ar livre, na cidade",
+ "Ao ar livre, na natureza",
+ "Reverberação",
+ "Eco",
+ "Ruído",
+ "Ruído ambiental",
+ "Estática",
+ "Zumbido elétrico",
+ "Distorção",
+ "Retorno de voz (telefone)",
+ "Barulheira",
+ "Ruído branco",
+ "Ruído rosa",
+ "Ruído pulsante",
+ "Vibração mecânica",
+ "Televisão",
+ "Rádio",
+ "Gravação de campo"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/ru.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/ru.json
new file mode 100644
index 0000000..08712b4
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/ru.json
@@ -0,0 +1,523 @@
+[
+ "Речь",
+ "Детская речь",
+ "Разговор",
+ "Монолог, рассказ",
+ "Детский лепет",
+ "Синтезированная речь",
+ "Крик",
+ "Рёв человека",
+ "Радостный возглас",
+ "Вопль",
+ "Крики детей",
+ "Пронзительный крик",
+ "Шёпот",
+ "Смех",
+ "Смех младенца",
+ "Хихиканье",
+ "Ехидный смешок",
+ "Хохот",
+ "Тихий смех",
+ "Плач",
+ "Плач младенца",
+ "Хныканье",
+ "Стон",
+ "Вздох",
+ "Пение",
+ "Хор",
+ "Йодль",
+ "Песнопение",
+ "Мантра",
+ "Детское пение",
+ "Синтезированное пение",
+ "Рэп",
+ "Напевание без слов",
+ "Оханье",
+ "Кряхтение",
+ "Свист",
+ "Дыхание",
+ "Свистящее дыхание",
+ "Храп",
+ "Резкий вдох",
+ "Одышка",
+ "Фырканье",
+ "Кашель",
+ "Откашливание",
+ "Чихание",
+ "Шмыганье носом",
+ "Бег",
+ "Шарканье",
+ "Шаги",
+ "Жевание",
+ "Откусывание",
+ "Полоскание горла",
+ "Урчание в животе",
+ "Отрыжка",
+ "Икота",
+ "Кишечные газы",
+ "Звуки рук",
+ "Щелчок пальцами",
+ "Хлопки в ладоши",
+ "Сердцебиение",
+ "Шум в сердце",
+ "Ликование",
+ "Аплодисменты",
+ "Болтовня",
+ "Толпа",
+ "Гул голосов",
+ "Играющие дети",
+ "Животные",
+ "Домашние животные",
+ "Собака",
+ "Лай",
+ "Тявканье",
+ "Вой",
+ "Протяжный лай",
+ "Рычание",
+ "Скуление собаки",
+ "Кошка",
+ "Мурлыканье",
+ "Мяуканье",
+ "Шипение",
+ "Кошачий вой",
+ "Животные на ферме",
+ "Лошадь",
+ "Цокот копыт",
+ "Ржание",
+ "Корова, бык",
+ "Мычание",
+ "Коровий колокольчик",
+ "Свинья",
+ "Хрюканье",
+ "Коза",
+ "Блеяние",
+ "Овца",
+ "Домашняя птица",
+ "Курица, петух",
+ "Кудахтанье",
+ "Кукареканье",
+ "Индейка",
+ "Крик индюка",
+ "Утка",
+ "Кряканье",
+ "Гусь",
+ "Гоготание",
+ "Дикие животные",
+ "Львы, тигры",
+ "Рык",
+ "Птица",
+ "Пение птиц",
+ "Чириканье",
+ "Резкий птичий крик",
+ "Голубь",
+ "Воркование",
+ "Ворона",
+ "Карканье",
+ "Сова",
+ "Уханье совы",
+ "Хлопанье крыльев",
+ "Волки, дикие собаки",
+ "Грызуны",
+ "Мышь",
+ "Топот лапок",
+ "Насекомые",
+ "Сверчок",
+ "Комар",
+ "Муха",
+ "Жужжание",
+ "Пчела, оса",
+ "Лягушка",
+ "Кваканье",
+ "Змея",
+ "Тарахтение",
+ "Пение китов",
+ "Музыка",
+ "Музыкальный инструмент",
+ "Щипковый инструмент",
+ "Гитара",
+ "Электрогитара",
+ "Бас-гитара",
+ "Акустическая гитара",
+ "Слайд-гитара",
+ "Тэппинг на гитаре",
+ "Бой по струнам",
+ "Банджо",
+ "Ситар",
+ "Мандолина",
+ "Цитра",
+ "Укулеле",
+ "Клавишный инструмент",
+ "Фортепиано",
+ "Электропиано",
+ "Орган",
+ "Электронный орган",
+ "Орган Хаммонда",
+ "Синтезатор",
+ "Сэмплер",
+ "Клавесин",
+ "Ударный инструмент",
+ "Ударная установка",
+ "Драм-машина",
+ "Барабан",
+ "Малый барабан",
+ "Удар по ободу барабана",
+ "Барабанная дробь",
+ "Большой барабан",
+ "Литавры",
+ "Табла",
+ "Тарелки (ударные)",
+ "Хай-хэт",
+ "Вуд-блок",
+ "Бубен",
+ "Погремушка",
+ "Маракасы",
+ "Гонг",
+ "Трубчатые колокола",
+ "Мелодические ударные",
+ "Ксилофон, маримба",
+ "Металлофон",
+ "Вибрафон",
+ "Стальной барабан",
+ "Оркестр",
+ "Медный духовой инструмент",
+ "Валторна",
+ "Труба",
+ "Тромбон",
+ "Смычковый инструмент",
+ "Струнная группа оркестра",
+ "Скрипка",
+ "Пиццикато",
+ "Виолончель",
+ "Контрабас",
+ "Духовой инструмент",
+ "Флейта",
+ "Саксофон",
+ "Кларнет",
+ "Арфа",
+ "Колокол",
+ "Церковный колокол",
+ "Бубенчики",
+ "Велосипедный звонок",
+ "Камертон",
+ "Перезвон",
+ "Музыка ветра",
+ "Колокольный звон",
+ "Губная гармошка",
+ "Аккордеон",
+ "Волынка",
+ "Диджериду",
+ "Шофар",
+ "Терменвокс",
+ "Поющая чаша",
+ "Диджейский скретч",
+ "Поп-музыка",
+ "Хип-хоп",
+ "Битбокс",
+ "Рок-музыка",
+ "Хеви-метал",
+ "Панк-рок",
+ "Гранж",
+ "Прогрессивный рок",
+ "Рок-н-ролл",
+ "Психоделический рок",
+ "Ритм-н-блюз",
+ "Соул",
+ "Регги",
+ "Кантри",
+ "Свинг",
+ "Блюграсс",
+ "Фанк",
+ "Фолк-музыка",
+ "Ближневосточная музыка",
+ "Джаз",
+ "Диско",
+ "Классическая музыка",
+ "Опера",
+ "Электронная музыка",
+ "Хаус-музыка",
+ "Техно",
+ "Дабстеп",
+ "Драм-н-бейс",
+ "Электроника (музыка)",
+ "Танцевальная электроника",
+ "Эмбиент",
+ "Транс-музыка",
+ "Латиноамериканская музыка",
+ "Сальса",
+ "Фламенко",
+ "Блюз",
+ "Детская музыка",
+ "Нью-эйдж",
+ "Вокальная музыка",
+ "А капелла",
+ "Африканская музыка",
+ "Афробит",
+ "Христианская музыка",
+ "Госпел",
+ "Азиатская музыка",
+ "Карнатическая музыка",
+ "Музыка Болливуда",
+ "Ска",
+ "Народная музыка",
+ "Инди-музыка",
+ "Песня",
+ "Фоновая музыка",
+ "Музыкальная заставка",
+ "Рекламный джингл",
+ "Саундтрек",
+ "Колыбельная",
+ "Музыка из видеоигр",
+ "Рождественская музыка",
+ "Танцевальная музыка",
+ "Свадебная музыка",
+ "Весёлая музыка",
+ "Грустная музыка",
+ "Нежная музыка",
+ "Захватывающая музыка",
+ "Агрессивная музыка",
+ "Пугающая музыка",
+ "Ветер",
+ "Шелест листьев",
+ "Ветер в микрофоне",
+ "Гроза",
+ "Гром",
+ "Вода",
+ "Дождь",
+ "Капля дождя",
+ "Стук дождя",
+ "Ручей",
+ "Водопад",
+ "Океан",
+ "Волны, прибой",
+ "Шипение пара",
+ "Бульканье",
+ "Огонь",
+ "Потрескивание",
+ "Транспорт",
+ "Водный транспорт",
+ "Парусник",
+ "Гребная лодка",
+ "Моторная лодка, катер",
+ "Корабль",
+ "Автотранспорт",
+ "Автомобиль",
+ "Автомобильный гудок",
+ "Короткий гудок",
+ "Автосигнализация",
+ "Стеклоподъёмник",
+ "Занос автомобиля",
+ "Визг шин",
+ "Проезжающий автомобиль",
+ "Автогонки",
+ "Грузовик",
+ "Пневматический тормоз",
+ "Гудок грузовика",
+ "Сигнал заднего хода",
+ "Фургон с мороженым",
+ "Автобус",
+ "Машина экстренных служб",
+ "Полицейская машина (сирена)",
+ "Скорая помощь (сирена)",
+ "Пожарная машина (сирена)",
+ "Мотоцикл",
+ "Шум дорожного движения",
+ "Рельсовый транспорт",
+ "Поезд",
+ "Свисток паровоза",
+ "Гудок поезда",
+ "Вагон поезда",
+ "Визг колёс поезда",
+ "Метро",
+ "Воздушный транспорт",
+ "Двигатель самолёта",
+ "Реактивный двигатель",
+ "Пропеллер",
+ "Вертолёт",
+ "Самолёт",
+ "Велосипед",
+ "Скейтборд",
+ "Двигатель",
+ "Малый двигатель",
+ "Бормашина",
+ "Газонокосилка",
+ "Бензопила",
+ "Средний двигатель",
+ "Большой двигатель",
+ "Стук в двигателе",
+ "Запуск двигателя",
+ "Холостой ход",
+ "Рёв двигателя при разгоне",
+ "Дверь",
+ "Дверной звонок",
+ "Динь-дон (дверной звонок)",
+ "Раздвижная дверь",
+ "Хлопок двери",
+ "Стук в дверь",
+ "Постукивание",
+ "Писк",
+ "Дверца шкафа",
+ "Выдвижной ящик",
+ "Посуда",
+ "Столовые приборы",
+ "Нарезка продуктов",
+ "Жарка на сковороде",
+ "Микроволновая печь",
+ "Блендер",
+ "Вода из крана",
+ "Вода в раковине",
+ "Вода в ванне",
+ "Фен",
+ "Смыв унитаза",
+ "Зубная щётка",
+ "Электрическая зубная щётка",
+ "Пылесос",
+ "Застёжка-молния",
+ "Звон ключей",
+ "Падение монеты",
+ "Ножницы",
+ "Электробритва",
+ "Тасование карт",
+ "Набор текста",
+ "Пишущая машинка",
+ "Компьютерная клавиатура",
+ "Письмо от руки",
+ "Сигнал тревоги",
+ "Телефон",
+ "Звонок телефона",
+ "Мелодия звонка",
+ "Тональный набор номера",
+ "Гудок в трубке",
+ "Сигнал занятой линии",
+ "Будильник",
+ "Сирена",
+ "Сирена гражданской обороны",
+ "Зуммер",
+ "Датчик дыма",
+ "Пожарная сигнализация",
+ "Туманный горн",
+ "Свисток",
+ "Паровой свисток",
+ "Механизмы",
+ "Трещотка (храповик)",
+ "Часы",
+ "Тиканье",
+ "Ход часов (тик-так)",
+ "Шестерёнки",
+ "Шкивы",
+ "Швейная машинка",
+ "Вентилятор",
+ "Кондиционер",
+ "Кассовый аппарат",
+ "Принтер",
+ "Фотоаппарат",
+ "Зеркальный фотоаппарат",
+ "Рабочие инструменты",
+ "Молоток",
+ "Отбойный молоток",
+ "Пила",
+ "Напильник",
+ "Шлифовка",
+ "Электроинструмент",
+ "Дрель",
+ "Взрыв",
+ "Выстрел",
+ "Пулемёт",
+ "Ружейный залп",
+ "Артиллерийский огонь",
+ "Игрушечный пистолет",
+ "Фейерверк",
+ "Петарда",
+ "Хлопок",
+ "Извержение вулкана",
+ "Грохот",
+ "Древесина",
+ "Рубка дров",
+ "Расщепление древесины",
+ "Треск",
+ "Стекло",
+ "Звяканье",
+ "Бьющееся стекло",
+ "Жидкость",
+ "Всплеск",
+ "Бултыхание",
+ "Хлюпанье",
+ "Капанье",
+ "Наливание жидкости",
+ "Тонкая струйка",
+ "Мощная струя",
+ "Наполнение ёмкости",
+ "Распыление",
+ "Перекачка жидкости",
+ "Помешивание",
+ "Кипение",
+ "Сонар",
+ "Стрела",
+ "Рассекание воздуха",
+ "Глухой удар",
+ "Глухой стук",
+ "Электронный тюнер",
+ "Процессор эффектов",
+ "Эффект хорус",
+ "Стук баскетбольного мяча",
+ "Громкий удар",
+ "Шлепок",
+ "Хлёсткий удар",
+ "Разбивание вдребезги",
+ "Разламывание",
+ "Отскок",
+ "Щелчок кнута",
+ "Хлопанье на ветру",
+ "Царапание",
+ "Скрежет",
+ "Трение",
+ "Катящийся предмет",
+ "Раздавливание",
+ "Сминание",
+ "Разрывание",
+ "Пиканье",
+ "Звонкий затухающий звук",
+ "Звон",
+ "Лязг",
+ "Визг",
+ "Скрип",
+ "Шорох",
+ "Механическое жужжание",
+ "Дребезжание",
+ "Шкворчание",
+ "Щёлканье",
+ "Перестук",
+ "Низкий гул",
+ "Бульк (падение в воду)",
+ "Позвякивание",
+ "Гудение",
+ "Короткое жужжание",
+ "Пружинящий звук",
+ "Хруст",
+ "Тишина",
+ "Чистый тон",
+ "Обертон",
+ "Скользящий тон",
+ "Звуковой эффект",
+ "Ритмичные импульсы",
+ "В маленькой комнате",
+ "В большом помещении",
+ "В общественном помещении",
+ "На улице в городе",
+ "На природе",
+ "Гулкое помещение",
+ "Эхо",
+ "Шум",
+ "Шум окружающей среды",
+ "Помехи",
+ "Гудение электросети",
+ "Искажение звука",
+ "Свой голос в трубке",
+ "Какофония",
+ "Белый шум",
+ "Розовый шум",
+ "Пульсирующий гул",
+ "Звук вибрации",
+ "Телевизор",
+ "Радио",
+ "Полевая запись"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/th.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/th.json
new file mode 100644
index 0000000..af46896
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/th.json
@@ -0,0 +1,523 @@
+[
+ "เสียงพูด",
+ "เสียงเด็กพูด",
+ "เสียงสนทนา",
+ "เสียงบรรยาย",
+ "เสียงเด็กอ้อแอ้",
+ "เสียงพูดสังเคราะห์",
+ "เสียงตะโกน",
+ "เสียงคนคำราม",
+ "เสียงโห่ร้องดีใจ",
+ "เสียงตะโกนลั่น",
+ "เสียงเด็กตะโกน",
+ "เสียงกรีดร้อง",
+ "เสียงกระซิบ",
+ "เสียงหัวเราะ",
+ "เสียงทารกหัวเราะ",
+ "เสียงหัวเราะคิกคัก",
+ "เสียงหัวเราะเยาะ",
+ "เสียงหัวเราะลั่น",
+ "เสียงหัวเราะหึๆ",
+ "เสียงร้องไห้",
+ "เสียงทารกร้องไห้",
+ "เสียงร้องครางหงิงๆ",
+ "เสียงครวญคราง",
+ "เสียงถอนหายใจ",
+ "เสียงร้องเพลง",
+ "คณะประสานเสียง",
+ "เสียงร้องโยเดล",
+ "เสียงสวด",
+ "เสียงสวดคาถา",
+ "เสียงเด็กร้องเพลง",
+ "เสียงร้องเพลงสังเคราะห์",
+ "เสียงแร็ป",
+ "เสียงฮัมเพลง",
+ "เสียงโอดโอย",
+ "เสียงฮึดฮัด",
+ "เสียงผิวปาก",
+ "เสียงหายใจ",
+ "เสียงหายใจหวีด",
+ "เสียงกรน",
+ "เสียงสูดหายใจเฮือก",
+ "เสียงหายใจหอบ",
+ "เสียงพ่นลมทางจมูก",
+ "เสียงไอ",
+ "เสียงกระแอม",
+ "เสียงจาม",
+ "เสียงสูดจมูก",
+ "เสียงวิ่ง",
+ "เสียงเดินลากเท้า",
+ "เสียงฝีเท้า",
+ "เสียงเคี้ยว",
+ "เสียงกัด",
+ "เสียงกลั้วคอ",
+ "เสียงท้องร้อง",
+ "เสียงเรอ",
+ "เสียงสะอึก",
+ "เสียงผายลม",
+ "เสียงจากมือ",
+ "เสียงดีดนิ้ว",
+ "เสียงตบมือ",
+ "เสียงหัวใจเต้น",
+ "เสียงหัวใจผิดปกติ",
+ "เสียงเชียร์",
+ "เสียงปรบมือ (หลายคน)",
+ "เสียงพูดคุยจอแจ",
+ "เสียงฝูงชน",
+ "เสียงพูดอื้ออึง",
+ "เสียงเด็กเล่น",
+ "สัตว์",
+ "สัตว์เลี้ยง",
+ "สุนัข",
+ "สุนัขเห่า",
+ "สุนัขเล็กเห่าแหลม",
+ "สุนัขหอน",
+ "สุนัขเห่าโฮ่งๆ",
+ "สัตว์ขู่คำราม",
+ "สุนัขร้องหงิงๆ",
+ "แมว",
+ "แมวครางครืดๆ",
+ "แมวร้องเหมียว",
+ "เสียงขู่ฟ่อ",
+ "แมวร้องหาคู่",
+ "สัตว์ในฟาร์ม",
+ "ม้า",
+ "เสียงกีบม้า",
+ "ม้าร้อง",
+ "วัว",
+ "วัวร้อง",
+ "กระดิ่งวัว",
+ "หมู",
+ "หมูร้อง",
+ "แพะ",
+ "แพะแกะร้อง",
+ "แกะ",
+ "สัตว์ปีก",
+ "ไก่",
+ "ไก่ร้องกุ๊กๆ",
+ "ไก่ขัน",
+ "ไก่งวง",
+ "ไก่งวงร้อง",
+ "เป็ด",
+ "เป็ดร้อง",
+ "ห่าน",
+ "ห่านร้อง",
+ "สัตว์ป่า",
+ "สิงโตและเสือ",
+ "สิงโตคำราม",
+ "นก",
+ "นกร้อง",
+ "นกร้องจิ๊บๆ",
+ "นกร้องแว้กๆ",
+ "นกพิราบและนกเขา",
+ "นกพิราบร้องกุ๊กกู",
+ "อีกา",
+ "อีการ้อง",
+ "นกฮูก",
+ "นกฮูกร้อง",
+ "นกกระพือปีก",
+ "สุนัขป่า",
+ "สัตว์ฟันแทะ",
+ "หนู",
+ "สัตว์เล็กวิ่งกุกกัก",
+ "แมลง",
+ "จิ้งหรีด",
+ "ยุง",
+ "แมลงวัน",
+ "แมลงบินหึ่งๆ",
+ "ผึ้งและตัวต่อ",
+ "กบ",
+ "กบร้อง",
+ "งู",
+ "เสียงเขย่า (งูหางกระดิ่ง)",
+ "วาฬร้อง",
+ "ดนตรี",
+ "เครื่องดนตรี",
+ "เครื่องสายแบบดีด",
+ "กีตาร์",
+ "กีตาร์ไฟฟ้า",
+ "กีตาร์เบส",
+ "กีตาร์โปร่ง",
+ "กีตาร์สไลด์",
+ "เคาะสายกีตาร์ (แท็ปปิ้ง)",
+ "ตีคอร์ดกีตาร์",
+ "แบนโจ",
+ "ซีตาร์",
+ "แมนโดลิน",
+ "พิณซิเธอร์",
+ "อูคูเลเล่",
+ "คีย์บอร์ดดนตรี",
+ "เปียโน",
+ "เปียโนไฟฟ้า",
+ "ออร์แกน",
+ "ออร์แกนอิเล็กทรอนิกส์",
+ "ออร์แกนแฮมมอนด์",
+ "ซินธิไซเซอร์",
+ "แซมเพลอร์ (ดนตรี)",
+ "ฮาร์ปซิคอร์ด",
+ "เครื่องเคาะจังหวะ",
+ "กลองชุด",
+ "ดรัมแมชชีน",
+ "กลอง",
+ "กลองสแนร์",
+ "ริมช็อต (ตีขอบกลอง)",
+ "รัวกลอง",
+ "กลองเบส",
+ "กลองทิมปานี",
+ "กลองตับลา",
+ "ฉาบ",
+ "ไฮแฮต",
+ "วู้ดบล็อก",
+ "แทมบูรีน",
+ "ลูกเขย่า",
+ "ลูกแซ็ก (มาราคัส)",
+ "ฆ้อง",
+ "ระฆังราว",
+ "เครื่องตีแบบระนาด",
+ "ระนาดฝรั่ง (มาริมบา)",
+ "กล็อกเกนสปีล",
+ "ไวบราโฟน",
+ "สตีลแพน",
+ "วงออร์เคสตรา",
+ "เครื่องเป่าทองเหลือง",
+ "เฟรนช์ฮอร์น",
+ "ทรัมเป็ต",
+ "ทรอมโบน",
+ "เครื่องสายแบบสี",
+ "กลุ่มเครื่องสาย",
+ "ไวโอลิน",
+ "พิซซิคาโต (ดีดสาย)",
+ "เชลโล",
+ "ดับเบิลเบส",
+ "เครื่องเป่าลมไม้",
+ "ฟลุต",
+ "แซกโซโฟน",
+ "คลาริเน็ต",
+ "ฮาร์ป",
+ "ระฆัง",
+ "ระฆังโบสถ์",
+ "กระพรวน",
+ "กริ่งจักรยาน",
+ "ส้อมเสียง",
+ "ระฆังเป็นทำนอง",
+ "กระดิ่งลม",
+ "ตีระฆังสลับลำดับ",
+ "หีบเพลงปาก",
+ "หีบเพลงชัก",
+ "ปี่สก๊อต",
+ "ดิดเจอรีดู",
+ "แตรเขาแกะ (โชฟาร์)",
+ "เทอร์มิน",
+ "ขันทิเบต",
+ "ดีเจสแครชแผ่นเสียง",
+ "เพลงป๊อป",
+ "เพลงฮิปฮอป",
+ "บีทบ็อกซ์",
+ "เพลงร็อก",
+ "เพลงเฮฟวีเมทัล",
+ "เพลงพังก์ร็อก",
+ "เพลงกรันจ์",
+ "เพลงโปรเกรสซีฟร็อก",
+ "เพลงร็อกแอนด์โรล",
+ "เพลงไซคีเดลิกร็อก",
+ "เพลงอาร์แอนด์บี",
+ "เพลงโซล",
+ "เพลงเร็กเก้",
+ "เพลงคันทรี",
+ "เพลงสวิง",
+ "เพลงบลูแกรส",
+ "เพลงฟังก์",
+ "เพลงโฟล์ก",
+ "เพลงตะวันออกกลาง",
+ "เพลงแจ๊ส",
+ "เพลงดิสโก้",
+ "เพลงคลาสสิก",
+ "โอเปร่า",
+ "เพลงอิเล็กทรอนิกส์",
+ "เพลงเฮาส์",
+ "เพลงเทคโน",
+ "เพลงดับสเต็ป",
+ "เพลงดรัมแอนด์เบส",
+ "เพลงอิเล็กทรอนิกา",
+ "เพลง EDM",
+ "เพลงแอมเบียนต์",
+ "เพลงทรานซ์",
+ "เพลงละตินอเมริกา",
+ "เพลงซัลซ่า",
+ "เพลงฟลาเมงโก",
+ "เพลงบลูส์",
+ "เพลงเด็ก",
+ "เพลงนิวเอจ",
+ "เพลงขับร้อง",
+ "เพลงอะแคปเปลลา",
+ "เพลงแอฟริกา",
+ "เพลงแอโฟรบีท",
+ "เพลงคริสเตียน",
+ "เพลงกอสเปล",
+ "เพลงเอเชีย",
+ "เพลงคาร์นาติก",
+ "เพลงบอลลีวูด",
+ "เพลงสกา",
+ "เพลงพื้นเมือง",
+ "เพลงอินดี้",
+ "เพลง",
+ "เพลงพื้นหลัง",
+ "เพลงธีม",
+ "เพลงโฆษณา",
+ "เพลงประกอบภาพยนตร์",
+ "เพลงกล่อมเด็ก",
+ "เพลงประกอบเกม",
+ "เพลงคริสต์มาส",
+ "เพลงเต้นรำ",
+ "เพลงงานแต่งงาน",
+ "เพลงร่าเริง",
+ "เพลงเศร้า",
+ "เพลงอ่อนโยน",
+ "เพลงเร้าใจ",
+ "เพลงเกรี้ยวกราด",
+ "เพลงน่ากลัว",
+ "ลมพัด",
+ "ใบไม้เสียดสี",
+ "เสียงลมเข้าไมโครโฟน",
+ "พายุฝนฟ้าคะนอง",
+ "ฟ้าร้อง",
+ "เสียงน้ำ",
+ "ฝนตก",
+ "หยดน้ำฝน",
+ "ฝนตกกระทบพื้นผิว",
+ "ลำธาร",
+ "น้ำตก",
+ "ทะเล",
+ "คลื่นซัดฝั่ง",
+ "ไอน้ำ",
+ "น้ำไหลโครกๆ",
+ "ไฟลุกไหม้",
+ "เสียงปะทุเปรี๊ยะๆ",
+ "ยานพาหนะ",
+ "เรือ",
+ "เรือใบ",
+ "เรือพาย",
+ "เรือยนต์",
+ "เรือขนาดใหญ่",
+ "ยานยนต์บนถนน",
+ "รถยนต์",
+ "แตรรถ",
+ "บีบแตรปี๊น",
+ "สัญญาณกันขโมยรถ",
+ "กระจกไฟฟ้ารถยนต์",
+ "รถลื่นไถล",
+ "ยางรถดังเอี๊ยด",
+ "รถวิ่งผ่าน",
+ "รถแข่ง",
+ "รถบรรทุก",
+ "เบรกลม",
+ "แตรลมรถบรรทุก",
+ "เสียงเตือนรถถอยหลัง",
+ "รถขายไอศกรีม",
+ "รถบัส",
+ "รถฉุกเฉิน",
+ "ไซเรนรถตำรวจ",
+ "ไซเรนรถพยาบาล",
+ "ไซเรนรถดับเพลิง",
+ "รถจักรยานยนต์",
+ "เสียงจราจร",
+ "ยานพาหนะบนราง",
+ "รถไฟ",
+ "หวูดรถไฟ",
+ "แตรรถไฟ",
+ "ตู้รถไฟ",
+ "ล้อรถไฟเสียดราง",
+ "รถไฟใต้ดิน",
+ "อากาศยาน",
+ "เครื่องยนต์เครื่องบิน",
+ "เครื่องยนต์ไอพ่น",
+ "ใบพัดเครื่องบิน",
+ "เฮลิคอปเตอร์",
+ "เครื่องบิน",
+ "จักรยาน",
+ "สเก็ตบอร์ด",
+ "เครื่องยนต์",
+ "เครื่องยนต์ขนาดเล็ก",
+ "เครื่องกรอฟัน",
+ "เครื่องตัดหญ้า",
+ "เลื่อยยนต์",
+ "เครื่องยนต์ขนาดกลาง",
+ "เครื่องยนต์ขนาดใหญ่",
+ "เครื่องยนต์น็อก",
+ "สตาร์ทเครื่องยนต์",
+ "เครื่องยนต์เดินเบา",
+ "เร่งเครื่องยนต์",
+ "ประตู",
+ "กริ่งประตู",
+ "กริ่งดังติ๊งต่อง",
+ "ประตูบานเลื่อน",
+ "ปิดประตูดังปัง",
+ "เคาะประตู",
+ "เคาะเบาๆ",
+ "เสียงเอี๊ยด",
+ "เปิดปิดตู้",
+ "เปิดปิดลิ้นชัก",
+ "จานชามหม้อกระทะ",
+ "ช้อนส้อม",
+ "หั่นอาหาร",
+ "ทอดอาหาร",
+ "เตาไมโครเวฟ",
+ "เครื่องปั่น",
+ "ก๊อกน้ำ",
+ "อ่างล้างมือหรือล้างจาน",
+ "อ่างอาบน้ำ",
+ "ไดร์เป่าผม",
+ "กดชักโครก",
+ "แปรงฟัน",
+ "แปรงสีฟันไฟฟ้า",
+ "เครื่องดูดฝุ่น",
+ "รูดซิป",
+ "พวงกุญแจกระทบกัน",
+ "เหรียญตก",
+ "กรรไกร",
+ "เครื่องโกนหนวดไฟฟ้า",
+ "สับไพ่",
+ "กดแป้นพิมพ์",
+ "เครื่องพิมพ์ดีด",
+ "คีย์บอร์ดคอมพิวเตอร์",
+ "เขียนหนังสือ",
+ "สัญญาณเตือนภัย",
+ "โทรศัพท์",
+ "กริ่งโทรศัพท์",
+ "เสียงเรียกเข้า",
+ "เสียงกดเบอร์โทรศัพท์",
+ "สัญญาณพร้อมโทรออก",
+ "สัญญาณสายไม่ว่าง",
+ "นาฬิกาปลุก",
+ "ไซเรน",
+ "ไซเรนเตือนภัยสาธารณะ",
+ "ออดไฟฟ้า",
+ "เครื่องตรวจจับควัน",
+ "สัญญาณเตือนไฟไหม้",
+ "แตรหมอก",
+ "นกหวีด",
+ "หวูดไอน้ำ",
+ "เครื่องกลไก",
+ "เฟืองกรอกแกรก (แรทเชท)",
+ "นาฬิกา",
+ "เสียงติ๊ก",
+ "นาฬิกาเดินติ๊กต็อก",
+ "เฟืองหมุน",
+ "รอก",
+ "จักรเย็บผ้า",
+ "พัดลม",
+ "เครื่องปรับอากาศ",
+ "เครื่องคิดเงิน",
+ "เครื่องพิมพ์",
+ "กล้องถ่ายรูป",
+ "กล้อง SLR",
+ "เครื่องมือช่าง",
+ "ตอกด้วยค้อน",
+ "เครื่องเจาะถนน",
+ "เลื่อยไม้",
+ "ตะไบ",
+ "ขัดกระดาษทราย",
+ "เครื่องมือไฟฟ้า",
+ "สว่าน",
+ "เสียงระเบิด",
+ "เสียงปืน",
+ "ปืนกล",
+ "ระดมยิง",
+ "ยิงปืนใหญ่",
+ "ปืนแก๊ป (ของเล่น)",
+ "ดอกไม้ไฟ",
+ "ประทัด",
+ "เสียงแตกโพละ",
+ "ภูเขาไฟระเบิด",
+ "เสียงดังตูม",
+ "เสียงไม้",
+ "ผ่าไม้",
+ "ไม้แตกเป็นเสี่ยง",
+ "เสียงแตกร้าว",
+ "แก้วและกระจก",
+ "แก้วกระทบกัน",
+ "กระจกแตก",
+ "ของเหลว",
+ "น้ำกระเซ็น",
+ "น้ำกระฉอก",
+ "เสียงบีบของเปียก",
+ "น้ำหยด",
+ "เทน้ำ",
+ "น้ำไหลริน",
+ "น้ำพุ่ง",
+ "เติมน้ำใส่ภาชนะ",
+ "ฉีดพ่น",
+ "ปั๊มน้ำ",
+ "กวนของเหลว",
+ "น้ำเดือด",
+ "โซนาร์",
+ "ลูกธนู",
+ "เสียงหวือ",
+ "เสียงตุบ",
+ "เสียงโป๊ก",
+ "เครื่องจูนเสียง",
+ "เครื่องเอฟเฟกต์",
+ "เอฟเฟกต์คอรัส",
+ "ลูกบาสเด้ง",
+ "เสียงดังปัง",
+ "เสียงตบฉาด",
+ "เสียงฟาดผัวะ",
+ "เสียงพังโครม",
+ "เสียงแตกหัก",
+ "เสียงเด้งกระดอน",
+ "เสียงแส้ฟาด",
+ "เสียงสะบัดพึ่บพั่บ",
+ "เสียงขีดข่วน",
+ "เสียงขูดครูด",
+ "เสียงถู",
+ "เสียงกลิ้ง",
+ "เสียงบดขยี้",
+ "เสียงขยำกรอบแกรบ",
+ "เสียงฉีก",
+ "เสียงบี๊บ",
+ "เสียงปิ๊ง",
+ "เสียงติ๊ง",
+ "เสียงเคร้ง",
+ "เสียงแหลมวี้ด",
+ "เสียงเอี๊ยดอ๊าด",
+ "เสียงสวบสาบ",
+ "เสียงหมุนหวิวๆ",
+ "เสียงโครมคราม",
+ "เสียงฉ่า",
+ "เสียงคลิก",
+ "เสียงกึกกักเป็นจังหวะ",
+ "เสียงครืนๆ",
+ "เสียงจ๋อม",
+ "เสียงกรุ๊งกริ๊ง",
+ "เสียงหึ่งต่อเนื่อง",
+ "เสียงวิ้ง",
+ "เสียงดึ๋ง (สปริง)",
+ "เสียงกรุบกรับ",
+ "ความเงียบ",
+ "คลื่นไซน์",
+ "เสียงฮาร์มอนิก",
+ "เสียงโทนเลื่อนระดับ",
+ "เสียงเอฟเฟกต์",
+ "เสียงดังเป็นห้วงๆ",
+ "ในห้องขนาดเล็ก",
+ "ในห้องโถงหรือห้องใหญ่",
+ "ในอาคารสาธารณะ",
+ "กลางแจ้งในเมือง",
+ "กลางแจ้งในธรรมชาติ",
+ "เสียงก้อง",
+ "เสียงสะท้อน",
+ "เสียงรบกวน",
+ "เสียงรบกวนรอบข้าง",
+ "เสียงซ่าจากคลื่นรบกวน",
+ "เสียงหึ่งจากไฟบ้าน",
+ "เสียงแตกพร่า",
+ "เสียงตัวเองในหูโทรศัพท์",
+ "เสียงอึกทึกครึกโครม",
+ "ไวท์นอยส์",
+ "พิงก์นอยส์",
+ "เสียงทุ้มเต้นเป็นจังหวะ",
+ "เสียงสั่นสะเทือน",
+ "โทรทัศน์",
+ "วิทยุ",
+ "บันทึกเสียงภาคสนาม"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/tr.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/tr.json
new file mode 100644
index 0000000..29d3582
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/tr.json
@@ -0,0 +1,523 @@
+[
+ "Konuşma",
+ "Çocuk konuşması",
+ "Sohbet",
+ "Anlatım, monolog",
+ "Bebek agulaması",
+ "Sentetik konuşma",
+ "Bağırma",
+ "Gür sesle bağırma",
+ "Sevinç çığlığı",
+ "Haykırma",
+ "Bağıran çocuklar",
+ "Çığlık",
+ "Fısıltı",
+ "Gülüş",
+ "Bebek gülüşü",
+ "Kıkırdama",
+ "Alaycı gülüş",
+ "Kahkaha",
+ "Kıs kıs gülme",
+ "Ağlama",
+ "Bebek ağlaması",
+ "Sızlanma",
+ "İnleme",
+ "İç çekme",
+ "Şarkı söyleme",
+ "Koro",
+ "Yodel",
+ "İlahi, ezgili okuma",
+ "Mantra",
+ "Şarkı söyleyen çocuk",
+ "Sentetik şarkı söyleme",
+ "Rap yapma",
+ "Mırıldanma",
+ "İnilti",
+ "Homurtu",
+ "Islık çalma",
+ "Nefes alıp verme",
+ "Hırıltılı nefes",
+ "Horlama",
+ "Ani nefes çekme",
+ "Nefes nefese kalma",
+ "Burundan sert nefes",
+ "Öksürük",
+ "Boğaz temizleme",
+ "Hapşırık",
+ "Burun çekme",
+ "Koşu",
+ "Ayak sürüyerek yürüme",
+ "Ayak sesleri",
+ "Çiğneme",
+ "Isırma",
+ "Gargara",
+ "Mide guruldaması",
+ "Geğirme",
+ "Hıçkırık",
+ "Osuruk",
+ "El sesleri",
+ "Parmak şıklatma",
+ "El çırpma",
+ "Kalp atışı",
+ "Kalp üfürümü",
+ "Tezahürat",
+ "Alkış",
+ "Karışık konuşmalar",
+ "Kalabalık",
+ "Konuşma uğultusu",
+ "Oynayan çocuklar",
+ "Hayvan",
+ "Evcil hayvanlar",
+ "Köpek",
+ "Havlama",
+ "Küçük köpek havlaması",
+ "Uluma",
+ "Hav hav (köpek)",
+ "Hırlama",
+ "Sızlanma (köpek)",
+ "Kedi",
+ "Mırlama",
+ "Miyavlama",
+ "Tıslama",
+ "Kedi uluması",
+ "Çiftlik hayvanları",
+ "At",
+ "Nal sesleri",
+ "Kişneme",
+ "Sığır",
+ "İnek böğürmesi",
+ "İnek çanı",
+ "Domuz",
+ "Domuz homurtusu",
+ "Keçi",
+ "Meleme",
+ "Koyun",
+ "Kümes hayvanları",
+ "Tavuk, horoz",
+ "Gıdaklama",
+ "Horoz ötüşü",
+ "Hindi",
+ "Glu glu (hindi)",
+ "Ördek",
+ "Vaklama",
+ "Kaz",
+ "Kaz ötüşü",
+ "Yabani hayvanlar",
+ "Aslan, kaplan",
+ "Kükreme",
+ "Kuş",
+ "Kuş ötüşü",
+ "Cıvıltı",
+ "Kuş ciyaklaması",
+ "Güvercin",
+ "Güvercin guruldaması",
+ "Karga",
+ "Karga gaklaması",
+ "Baykuş",
+ "Baykuş ötüşü",
+ "Kuş kanat çırpışı",
+ "Kurt, yabani köpek",
+ "Kemirgenler",
+ "Fare",
+ "Tıpırtı",
+ "Böcek",
+ "Cırcır böceği",
+ "Sivrisinek",
+ "Sinek",
+ "Vızıltı",
+ "Arı, eşek arısı",
+ "Kurbağa",
+ "Vıraklama",
+ "Yılan",
+ "Şıkırtı",
+ "Balina sesi",
+ "Müzik",
+ "Müzik aleti",
+ "Telli çalgı",
+ "Gitar",
+ "Elektro gitar",
+ "Bas gitar",
+ "Akustik gitar",
+ "Slide gitar",
+ "Tapping (gitar tekniği)",
+ "Gitar tıngırdatma",
+ "Banjo",
+ "Sitar",
+ "Mandolin",
+ "Kanun, zither",
+ "Ukulele",
+ "Klavyeli çalgı",
+ "Piyano",
+ "Elektrikli piyano",
+ "Org",
+ "Elektronik org",
+ "Hammond org",
+ "Synthesizer",
+ "Sampler",
+ "Klavsen",
+ "Vurmalı çalgı",
+ "Bateri",
+ "Davul makinesi",
+ "Davul",
+ "Trampet",
+ "Rimshot (trampet)",
+ "Trampet rulosu",
+ "Bas davul",
+ "Timpani",
+ "Tabla",
+ "Zil (çalgı)",
+ "Hi-hat",
+ "Tahta blok",
+ "Tef",
+ "Sallamalı çalgı",
+ "Marakas",
+ "Gong",
+ "Borulu çanlar",
+ "Melodik vurmalı çalgı",
+ "Marimba, ksilofon",
+ "Metalofon",
+ "Vibrafon",
+ "Çelik davul",
+ "Orkestra",
+ "Bakır üflemeli çalgı",
+ "Korno",
+ "Trompet",
+ "Trombon",
+ "Yaylı çalgı",
+ "Yaylı çalgılar grubu",
+ "Keman",
+ "Pizzicato (parmakla çalma)",
+ "Viyolonsel",
+ "Kontrbas",
+ "Üflemeli çalgı",
+ "Flüt",
+ "Saksofon",
+ "Klarnet",
+ "Arp",
+ "Çan",
+ "Kilise çanı",
+ "Çıngırak",
+ "Bisiklet zili",
+ "Diyapazon",
+ "Çan melodisi",
+ "Rüzgâr çanı",
+ "Sıralı çan çalma",
+ "Mızıka",
+ "Akordeon",
+ "Gayda, tulum",
+ "Didgeridoo",
+ "Şofar",
+ "Theremin",
+ "Tibet çanağı",
+ "Scratch (DJ tekniği)",
+ "Pop müzik",
+ "Hip hop müzik",
+ "Beatbox",
+ "Rock müzik",
+ "Heavy metal",
+ "Punk rock",
+ "Grunge",
+ "Progresif rock",
+ "Rock and roll",
+ "Psikedelik rock",
+ "Rhythm and blues",
+ "Soul müzik",
+ "Reggae",
+ "Country müzik",
+ "Swing müzik",
+ "Bluegrass",
+ "Funk",
+ "Folk müzik",
+ "Orta Doğu müziği",
+ "Caz",
+ "Disko",
+ "Klasik müzik",
+ "Opera",
+ "Elektronik müzik",
+ "House müzik",
+ "Tekno",
+ "Dubstep",
+ "Drum and bass",
+ "Electronica",
+ "Elektronik dans müziği",
+ "Ambient müzik",
+ "Trance müzik",
+ "Latin Amerika müziği",
+ "Salsa müzik",
+ "Flamenko",
+ "Blues",
+ "Çocuk şarkıları",
+ "New age müzik",
+ "Vokal müzik",
+ "A cappella",
+ "Afrika müziği",
+ "Afrobeat",
+ "Hristiyan müziği",
+ "Gospel müzik",
+ "Asya müziği",
+ "Karnatik müzik",
+ "Bollywood müziği",
+ "Ska",
+ "Geleneksel müzik",
+ "Bağımsız müzik",
+ "Şarkı",
+ "Fon müziği",
+ "Jenerik müziği",
+ "Reklam müziği",
+ "Film müziği",
+ "Ninni",
+ "Video oyunu müziği",
+ "Noel müziği",
+ "Dans müziği",
+ "Düğün müziği",
+ "Neşeli müzik",
+ "Hüzünlü müzik",
+ "Yumuşak müzik",
+ "Heyecanlı müzik",
+ "Öfkeli müzik",
+ "Korkutucu müzik",
+ "Rüzgâr",
+ "Yaprak hışırtısı",
+ "Rüzgâr gürültüsü (mikrofon)",
+ "Gök gürültülü fırtına",
+ "Gök gürültüsü",
+ "Su",
+ "Yağmur",
+ "Yağmur damlası",
+ "Yüzeye vuran yağmur",
+ "Dere",
+ "Şelale",
+ "Okyanus",
+ "Deniz dalgaları",
+ "Buhar",
+ "Lıkırtı",
+ "Ateş",
+ "Çıtırtı",
+ "Araç",
+ "Tekne, deniz aracı",
+ "Yelkenli",
+ "Kürekli tekne, kano",
+ "Motorbot",
+ "Gemi",
+ "Motorlu kara aracı",
+ "Araba",
+ "Korna",
+ "Kısa korna",
+ "Araba alarmı",
+ "Elektrikli cam (araba)",
+ "Patinaj",
+ "Lastik gıcırtısı",
+ "Geçen araba",
+ "Yarış arabası",
+ "Kamyon",
+ "Havalı fren",
+ "Havalı korna",
+ "Geri vites bip sesi",
+ "Dondurma arabası",
+ "Otobüs",
+ "Acil durum aracı",
+ "Polis arabası (siren)",
+ "Ambulans (siren)",
+ "İtfaiye aracı (siren)",
+ "Motosiklet",
+ "Trafik gürültüsü",
+ "Raylı ulaşım",
+ "Tren",
+ "Tren düdüğü",
+ "Tren kornası",
+ "Tren vagonu",
+ "Tren tekerleği gıcırtısı",
+ "Metro",
+ "Hava aracı",
+ "Uçak motoru",
+ "Jet motoru",
+ "Pervane",
+ "Helikopter",
+ "Uçak",
+ "Bisiklet",
+ "Kaykay",
+ "Motor",
+ "Küçük motor",
+ "Dişçi matkabı",
+ "Çim biçme makinesi",
+ "Motorlu testere",
+ "Orta boy motor",
+ "Büyük motor",
+ "Motor vuruntusu",
+ "Motor çalıştırma",
+ "Rölanti",
+ "Hızlanan motor",
+ "Kapı",
+ "Kapı zili",
+ "Ding dong (kapı zili)",
+ "Sürgülü kapı",
+ "Kapı çarpması",
+ "Kapıya vurma",
+ "Hafif vuruş",
+ "Gıcırtı",
+ "Dolap açıp kapama",
+ "Çekmece açıp kapama",
+ "Tabak, tencere, tava",
+ "Çatal bıçak",
+ "Yiyecek doğrama",
+ "Yağda kızartma",
+ "Mikrodalga fırın",
+ "Blender",
+ "Musluk",
+ "Lavabo (doldurma, yıkama)",
+ "Küvet (doldurma, yıkama)",
+ "Saç kurutma makinesi",
+ "Tuvalet sifonu",
+ "Diş fırçası",
+ "Elektrikli diş fırçası",
+ "Elektrikli süpürge",
+ "Fermuar",
+ "Anahtar şıngırtısı",
+ "Düşen bozuk para",
+ "Makas",
+ "Tıraş makinesi",
+ "İskambil karıştırma",
+ "Tuşlarla yazma",
+ "Daktilo",
+ "Bilgisayar klavyesi",
+ "Kalemle yazma",
+ "Alarm",
+ "Telefon",
+ "Klasik telefon zili",
+ "Telefon zil sesi",
+ "Telefon tuş sesleri",
+ "Çevir sesi",
+ "Meşgul sesi",
+ "Çalar saat",
+ "Siren",
+ "Sivil savunma sireni",
+ "Buzzer (uyarı sesi)",
+ "Duman alarmı",
+ "Yangın alarmı",
+ "Sis düdüğü",
+ "Düdük",
+ "Buhar düdüğü",
+ "Mekanizmalar",
+ "Cırcır mekanizması",
+ "Saat",
+ "Tıkırtı",
+ "Tik tak",
+ "Dişliler",
+ "Makaralar",
+ "Dikiş makinesi",
+ "Vantilatör",
+ "Klima",
+ "Yazar kasa",
+ "Yazıcı",
+ "Fotoğraf makinesi",
+ "SLR fotoğraf makinesi",
+ "Aletler",
+ "Çekiç",
+ "Beton kırıcı",
+ "Testereyle kesme",
+ "Eğeleme",
+ "Zımparalama",
+ "Elektrikli alet",
+ "Matkap",
+ "Patlama",
+ "Silah sesi",
+ "Makineli tüfek",
+ "Yaylım ateşi",
+ "Top atışı",
+ "Oyuncak tabanca",
+ "Havai fişek",
+ "Kestane fişeği",
+ "Pat sesi (balon vb.)",
+ "Yanardağ patlaması",
+ "Gümleme",
+ "Ahşap",
+ "Baltayla kesme",
+ "Ahşap kırılması",
+ "Çatırtı",
+ "Cam",
+ "Tıngırtı",
+ "Cam kırılması",
+ "Sıvı",
+ "Sıvı sıçraması",
+ "Çalkalanma",
+ "Islak ezilme",
+ "Damlama",
+ "Sıvı dökme",
+ "İnce akış",
+ "Fışkırma",
+ "Sıvı doldurma",
+ "Püskürtme",
+ "Sıvı pompalama",
+ "Kaşıkla karıştırma",
+ "Kaynama",
+ "Sonar",
+ "Ok",
+ "Vınlama",
+ "Boğuk vuruş",
+ "Kof vuruş",
+ "Akort cihazı",
+ "Efekt cihazı",
+ "Chorus efekti",
+ "Basketbol topu sekmesi",
+ "Ani gürültü",
+ "Tokat",
+ "Sert vuruş",
+ "Parçalanma",
+ "Kırılma",
+ "Sekme (top vb.)",
+ "Kırbaç şaklaması",
+ "Çırpınma (bayrak vb.)",
+ "Yüzeyi çizme",
+ "Kazıma",
+ "Sürtme",
+ "Yuvarlanma",
+ "Ezilme",
+ "Buruşturma",
+ "Yırtma",
+ "Bip",
+ "Çınlama",
+ "Ding",
+ "Tangırtı",
+ "Ciyaklama",
+ "Gıcırdama (ahşap)",
+ "Hışırtı",
+ "Hızla dönme sesi",
+ "Takırtı",
+ "Cızırtı",
+ "Tıklama",
+ "Ritmik takırtı",
+ "Gümbürtü",
+ "Suya düşme",
+ "Şıngırtı",
+ "Uğultu",
+ "Kısa tiz vızıltı",
+ "Yay sesi (boing)",
+ "Kıtırtı",
+ "Sessizlik",
+ "Sinüs dalgası (saf ton)",
+ "Harmonik ton",
+ "Perdesi değişen ton",
+ "Ses efekti",
+ "Düzenli aralıklı ses",
+ "İç mekân, küçük oda",
+ "İç mekân, büyük salon",
+ "İç mekân, kamusal alan",
+ "Dış mekân, şehir",
+ "Dış mekân, kır ve doğa",
+ "Yankılanma",
+ "Yankı",
+ "Gürültü",
+ "Çevre gürültüsü",
+ "Parazit",
+ "Elektrik uğultusu",
+ "Ses bozulması",
+ "Telefonda kendi sesi",
+ "Ses karmaşası",
+ "Beyaz gürültü",
+ "Pembe gürültü",
+ "Zonklayan uğultu",
+ "Titreşim sesi",
+ "Televizyon",
+ "Radyo",
+ "Ortam kaydı"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/vi.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/vi.json
new file mode 100644
index 0000000..104dd2d
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/vi.json
@@ -0,0 +1,523 @@
+[
+ "Giọng nói",
+ "Trẻ em nói",
+ "Trò chuyện",
+ "Lời dẫn, độc thoại",
+ "Trẻ bập bẹ",
+ "Giọng nói tổng hợp",
+ "Nói to",
+ "Gào rống",
+ "Reo mừng",
+ "La hét",
+ "Trẻ em la hét",
+ "Tiếng thét",
+ "Thì thầm",
+ "Tiếng cười",
+ "Em bé cười",
+ "Cười khúc khích",
+ "Cười khẩy",
+ "Cười ha hả",
+ "Cười khẽ",
+ "Tiếng khóc",
+ "Em bé khóc",
+ "Khóc thút thít",
+ "Rên rỉ",
+ "Thở dài",
+ "Tiếng hát",
+ "Hợp xướng",
+ "Hát yodel",
+ "Ngâm tụng",
+ "Tụng thần chú",
+ "Trẻ em hát",
+ "Giọng hát tổng hợp",
+ "Đọc rap",
+ "Ngân nga",
+ "Kêu rên",
+ "Tiếng hự",
+ "Huýt sáo",
+ "Tiếng thở",
+ "Thở khò khè",
+ "Ngáy",
+ "Hít hơi gấp",
+ "Thở hổn hển",
+ "Khịt mũi",
+ "Ho",
+ "Hắng giọng",
+ "Hắt hơi",
+ "Hít mũi",
+ "Tiếng chân chạy",
+ "Tiếng lê chân",
+ "Tiếng bước chân",
+ "Nhai thức ăn",
+ "Tiếng cắn",
+ "Súc họng",
+ "Bụng sôi",
+ "Ợ hơi",
+ "Nấc cụt",
+ "Đánh rắm",
+ "Âm thanh bàn tay",
+ "Búng tay",
+ "Vỗ tay",
+ "Tiếng tim đập",
+ "Tiếng thổi tim",
+ "Reo hò cổ vũ",
+ "Tràng pháo tay",
+ "Nhiều người trò chuyện",
+ "Đám đông",
+ "Tiếng người ồn ào",
+ "Trẻ em nô đùa",
+ "Động vật",
+ "Thú cưng",
+ "Chó",
+ "Chó sủa",
+ "Chó sủa ăng ẳng",
+ "Chó tru",
+ "Chó sủa gâu gâu",
+ "Chó gầm gừ",
+ "Chó rên ư ử",
+ "Mèo",
+ "Mèo kêu rừ rừ",
+ "Mèo kêu meo meo",
+ "Mèo khè, tiếng xì",
+ "Mèo gào",
+ "Vật nuôi trang trại",
+ "Ngựa",
+ "Tiếng vó ngựa",
+ "Ngựa hí",
+ "Trâu bò",
+ "Bò rống",
+ "Chuông bò",
+ "Lợn",
+ "Lợn kêu ụt ịt",
+ "Dê",
+ "Dê cừu kêu be be",
+ "Cừu",
+ "Gia cầm",
+ "Gà",
+ "Gà cục tác",
+ "Gà trống gáy",
+ "Gà tây",
+ "Gà tây kêu",
+ "Vịt",
+ "Vịt kêu quạc quạc",
+ "Ngỗng",
+ "Ngỗng kêu",
+ "Động vật hoang dã",
+ "Sư tử, hổ",
+ "Sư tử, hổ gầm",
+ "Chim",
+ "Chim hót",
+ "Chim kêu chíp chíp",
+ "Chim kêu quang quác",
+ "Chim bồ câu",
+ "Bồ câu gù",
+ "Quạ",
+ "Quạ kêu",
+ "Cú mèo",
+ "Cú mèo kêu",
+ "Chim vỗ cánh",
+ "Sói, chó hoang",
+ "Loài gặm nhấm",
+ "Chuột",
+ "Tiếng chân thú nhỏ",
+ "Côn trùng",
+ "Dế",
+ "Muỗi",
+ "Ruồi",
+ "Tiếng vo ve",
+ "Ong",
+ "Ếch",
+ "Ếch kêu ộp ộp",
+ "Rắn",
+ "Tiếng lắc lọc xọc",
+ "Cá voi kêu",
+ "Âm nhạc",
+ "Nhạc cụ",
+ "Đàn dây gảy",
+ "Đàn guitar",
+ "Đàn guitar điện",
+ "Đàn guitar bass",
+ "Đàn guitar thùng",
+ "Đàn guitar slide",
+ "Tapping (kỹ thuật guitar)",
+ "Quạt dây đàn guitar",
+ "Đàn banjo",
+ "Đàn sitar",
+ "Đàn mandolin",
+ "Đàn tranh",
+ "Đàn ukulele",
+ "Đàn phím",
+ "Đàn piano",
+ "Đàn piano điện",
+ "Đàn organ ống",
+ "Đàn organ điện tử",
+ "Đàn organ Hammond",
+ "Đàn synthesizer",
+ "Máy sampler",
+ "Đàn harpsichord",
+ "Nhạc cụ gõ",
+ "Dàn trống",
+ "Máy tạo nhịp trống",
+ "Trống",
+ "Trống snare",
+ "Gõ vành trống",
+ "Trống dồn",
+ "Trống bass",
+ "Trống định âm",
+ "Trống tabla",
+ "Chũm chọe",
+ "Chũm chọe hi-hat",
+ "Mõ gỗ",
+ "Trống lắc tay",
+ "Nhạc cụ lắc",
+ "Nhạc cụ lắc maracas",
+ "Cồng chiêng",
+ "Chuông ống",
+ "Nhạc cụ gõ giai điệu",
+ "Đàn xylophone",
+ "Đàn glockenspiel",
+ "Đàn vibraphone",
+ "Trống thép",
+ "Dàn nhạc giao hưởng",
+ "Kèn đồng",
+ "Kèn cor",
+ "Kèn trumpet",
+ "Kèn trombone",
+ "Đàn dây kéo vĩ",
+ "Bộ dây dàn nhạc",
+ "Đàn violin",
+ "Gảy dây (pizzicato)",
+ "Đàn cello",
+ "Đàn contrabass",
+ "Nhạc cụ hơi",
+ "Sáo",
+ "Kèn saxophone",
+ "Kèn clarinet",
+ "Đàn hạc",
+ "Chuông",
+ "Chuông nhà thờ",
+ "Chuông lục lạc",
+ "Chuông xe đạp",
+ "Âm thoa",
+ "Chuông giai điệu",
+ "Chuông gió",
+ "Chuông đổ hồi",
+ "Kèn harmonica",
+ "Đàn accordion",
+ "Kèn túi",
+ "Kèn didgeridoo",
+ "Kèn sừng shofar",
+ "Đàn theremin",
+ "Chuông xoay",
+ "Scratch đĩa than (DJ)",
+ "Nhạc pop",
+ "Nhạc hip hop",
+ "Beatbox",
+ "Nhạc rock",
+ "Nhạc heavy metal",
+ "Nhạc punk rock",
+ "Nhạc grunge",
+ "Nhạc progressive rock",
+ "Nhạc rock and roll",
+ "Nhạc psychedelic rock",
+ "Nhạc R&B",
+ "Nhạc soul",
+ "Nhạc reggae",
+ "Nhạc đồng quê",
+ "Nhạc swing",
+ "Nhạc bluegrass",
+ "Nhạc funk",
+ "Nhạc dân gian",
+ "Nhạc Trung Đông",
+ "Nhạc jazz",
+ "Nhạc disco",
+ "Nhạc cổ điển",
+ "Nhạc opera",
+ "Nhạc điện tử",
+ "Nhạc house",
+ "Nhạc techno",
+ "Nhạc dubstep",
+ "Nhạc drum and bass",
+ "Nhạc electronica",
+ "Nhạc EDM",
+ "Nhạc ambient",
+ "Nhạc trance",
+ "Nhạc Mỹ Latinh",
+ "Nhạc salsa",
+ "Nhạc flamenco",
+ "Nhạc blues",
+ "Nhạc thiếu nhi",
+ "Nhạc new age",
+ "Thanh nhạc",
+ "Hát không nhạc đệm",
+ "Nhạc châu Phi",
+ "Nhạc afrobeat",
+ "Nhạc Cơ Đốc giáo",
+ "Nhạc phúc âm",
+ "Nhạc châu Á",
+ "Nhạc cổ điển Nam Ấn",
+ "Nhạc Bollywood",
+ "Nhạc ska",
+ "Nhạc truyền thống",
+ "Nhạc indie",
+ "Bài hát",
+ "Nhạc nền",
+ "Nhạc chủ đề",
+ "Nhạc quảng cáo",
+ "Nhạc phim",
+ "Hát ru",
+ "Nhạc trò chơi điện tử",
+ "Nhạc Giáng sinh",
+ "Nhạc khiêu vũ",
+ "Nhạc đám cưới",
+ "Nhạc vui tươi",
+ "Nhạc buồn",
+ "Nhạc dịu dàng",
+ "Nhạc sôi động",
+ "Nhạc giận dữ",
+ "Nhạc rùng rợn",
+ "Gió",
+ "Lá xào xạc",
+ "Gió thổi vào micrô",
+ "Mưa giông",
+ "Sấm",
+ "Nước",
+ "Mưa",
+ "Giọt mưa",
+ "Mưa rơi lộp độp",
+ "Suối chảy",
+ "Thác nước",
+ "Biển",
+ "Sóng vỗ",
+ "Hơi nước",
+ "Nước chảy ọc ọc",
+ "Lửa",
+ "Lửa nổ lách tách",
+ "Phương tiện giao thông",
+ "Tàu thuyền",
+ "Thuyền buồm",
+ "Thuyền chèo",
+ "Xuồng máy",
+ "Tàu thủy",
+ "Xe cơ giới (đường bộ)",
+ "Ô tô",
+ "Còi xe",
+ "Tiếng còi ngắn",
+ "Báo động ô tô",
+ "Cửa kính điện ô tô",
+ "Xe trượt bánh",
+ "Lốp xe rít",
+ "Ô tô chạy qua",
+ "Xe đua",
+ "Xe tải",
+ "Phanh hơi",
+ "Còi hơi xe tải",
+ "Còi báo lùi xe",
+ "Xe bán kem",
+ "Xe buýt",
+ "Xe ưu tiên",
+ "Xe cảnh sát (còi hụ)",
+ "Xe cứu thương (còi hụ)",
+ "Xe cứu hỏa (còi hụ)",
+ "Xe máy",
+ "Tiếng ồn giao thông",
+ "Phương tiện đường sắt",
+ "Tàu hỏa",
+ "Còi tàu hơi nước",
+ "Còi tàu hỏa",
+ "Toa tàu",
+ "Bánh tàu rít trên ray",
+ "Tàu điện ngầm",
+ "Máy bay",
+ "Động cơ máy bay",
+ "Động cơ phản lực",
+ "Cánh quạt máy bay",
+ "Trực thăng",
+ "Máy bay cánh cố định",
+ "Xe đạp",
+ "Ván trượt",
+ "Động cơ",
+ "Động cơ nhỏ (âm cao)",
+ "Máy khoan nha khoa",
+ "Máy cắt cỏ",
+ "Cưa máy",
+ "Động cơ vừa (âm trung)",
+ "Động cơ lớn (âm trầm)",
+ "Tiếng gõ động cơ",
+ "Động cơ khởi động",
+ "Động cơ chạy không tải",
+ "Rồ ga tăng tốc",
+ "Cửa",
+ "Chuông cửa",
+ "Chuông cửa kính coong",
+ "Cửa trượt",
+ "Đóng sầm cửa",
+ "Gõ cửa",
+ "Gõ nhẹ",
+ "Tiếng kin kít",
+ "Đóng mở tủ",
+ "Đóng mở ngăn kéo",
+ "Bát đĩa, xoong nồi",
+ "Dao nĩa",
+ "Băm thái thức ăn",
+ "Chiên rán thức ăn",
+ "Lò vi sóng",
+ "Máy xay sinh tố",
+ "Vòi nước",
+ "Bồn rửa (xả nước, rửa)",
+ "Bồn tắm (xả nước, tắm)",
+ "Máy sấy tóc",
+ "Xả bồn cầu",
+ "Bàn chải đánh răng",
+ "Bàn chải đánh răng điện",
+ "Máy hút bụi",
+ "Khóa kéo",
+ "Chìa khóa leng keng",
+ "Đồng xu rơi",
+ "Cắt bằng kéo",
+ "Máy cạo râu",
+ "Tráo bài",
+ "Gõ phím",
+ "Máy đánh chữ",
+ "Bàn phím máy tính",
+ "Viết trên giấy",
+ "Báo động",
+ "Điện thoại",
+ "Chuông điện thoại reo",
+ "Nhạc chuông",
+ "Tiếng bấm số điện thoại",
+ "Âm mời quay số",
+ "Âm báo bận",
+ "Đồng hồ báo thức",
+ "Còi hụ",
+ "Còi hụ cảnh báo khẩn cấp",
+ "Còi báo điện tử",
+ "Chuông báo khói",
+ "Chuông báo cháy",
+ "Còi báo sương mù",
+ "Thổi còi",
+ "Còi hơi nước",
+ "Máy móc",
+ "Cơ cấu bánh cóc",
+ "Đồng hồ",
+ "Tiếng tích",
+ "Đồng hồ tích tắc",
+ "Bánh răng",
+ "Ròng rọc",
+ "Máy khâu",
+ "Quạt điện",
+ "Máy điều hòa",
+ "Máy tính tiền",
+ "Máy in",
+ "Máy ảnh",
+ "Máy ảnh SLR",
+ "Dụng cụ",
+ "Búa",
+ "Máy đục bê tông",
+ "Cưa gỗ",
+ "Giũa",
+ "Chà nhám",
+ "Dụng cụ điện",
+ "Máy khoan",
+ "Tiếng nổ",
+ "Tiếng súng",
+ "Súng máy",
+ "Bắn đồng loạt",
+ "Pháo kích",
+ "Súng đồ chơi",
+ "Pháo hoa",
+ "Pháo nổ",
+ "Tiếng nổ bụp",
+ "Núi lửa phun trào",
+ "Tiếng nổ ầm",
+ "Gỗ",
+ "Chặt gỗ",
+ "Gỗ vỡ vụn",
+ "Tiếng nứt rắc",
+ "Thủy tinh",
+ "Tiếng lanh canh",
+ "Vỡ tan tành",
+ "Chất lỏng",
+ "Nước bắn tung tóe",
+ "Nước sóng sánh",
+ "Tiếng bóp vật ướt",
+ "Nước nhỏ giọt",
+ "Rót nước",
+ "Nước chảy róc rách",
+ "Nước tuôn xối xả",
+ "Đổ đầy nước",
+ "Phun xịt",
+ "Bơm nước",
+ "Tiếng khuấy",
+ "Nước sôi",
+ "Tiếng sonar",
+ "Mũi tên",
+ "Tiếng vút",
+ "Tiếng thịch",
+ "Tiếng cộp",
+ "Máy lên dây đàn",
+ "Bộ hiệu ứng âm thanh",
+ "Hiệu ứng chorus",
+ "Bóng rổ nảy",
+ "Tiếng đùng",
+ "Tiếng tát",
+ "Tiếng đập mạnh",
+ "Tiếng đổ vỡ",
+ "Tiếng gãy vỡ",
+ "Tiếng vật nảy",
+ "Tiếng roi quất",
+ "Tiếng phần phật",
+ "Tiếng cào xước",
+ "Tiếng cạo",
+ "Tiếng cọ xát",
+ "Tiếng lăn",
+ "Tiếng nghiền nát",
+ "Tiếng vò nhàu",
+ "Tiếng xé rách",
+ "Tiếng bíp",
+ "Tiếng ting",
+ "Tiếng keng",
+ "Tiếng kim loại chát chúa",
+ "Tiếng rít",
+ "Tiếng kẽo kẹt",
+ "Tiếng sột soạt",
+ "Tiếng quay vù vù",
+ "Tiếng loảng xoảng",
+ "Tiếng xèo xèo",
+ "Tiếng lách cách",
+ "Tiếng lạch cạch đều đều",
+ "Tiếng ì ầm",
+ "Tiếng tõm",
+ "Tiếng leng keng",
+ "Tiếng ù ù",
+ "Tiếng chíu",
+ "Tiếng lò xo nảy",
+ "Tiếng rôm rốp",
+ "Im lặng",
+ "Sóng sin (âm thuần)",
+ "Họa âm",
+ "Âm quét tần số",
+ "Hiệu ứng âm thanh",
+ "Âm xung đều nhịp",
+ "Trong nhà, phòng nhỏ",
+ "Trong nhà, phòng lớn",
+ "Trong nhà, nơi công cộng",
+ "Ngoài trời, đô thị",
+ "Ngoài trời, thiên nhiên",
+ "Âm vang",
+ "Tiếng vọng",
+ "Tiếng ồn",
+ "Tiếng ồn môi trường",
+ "Tiếng rè nhiễu",
+ "Tiếng ù điện lưới",
+ "Méo tiếng",
+ "Âm phản hồi điện thoại",
+ "Âm thanh hỗn loạn",
+ "Tiếng ồn trắng",
+ "Tiếng ồn hồng",
+ "Tiếng đập âm ỉ",
+ "Tiếng rung cơ học",
+ "Tivi",
+ "Đài radio",
+ "Ghi âm hiện trường"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/zh-Hans.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/zh-Hans.json
new file mode 100644
index 0000000..4ad9243
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/zh-Hans.json
@@ -0,0 +1,523 @@
+[
+ "说话声",
+ "儿童说话声",
+ "对话",
+ "旁白、独白",
+ "婴儿咿呀声",
+ "合成语音",
+ "喊叫",
+ "吼叫",
+ "高声欢叫",
+ "大叫",
+ "儿童喊叫",
+ "尖叫",
+ "耳语",
+ "笑声",
+ "婴儿笑声",
+ "咯咯笑",
+ "窃笑",
+ "捧腹大笑",
+ "轻笑",
+ "哭声",
+ "婴儿哭声",
+ "呜咽",
+ "呻吟",
+ "叹气",
+ "歌声",
+ "合唱",
+ "约德尔唱法",
+ "吟诵",
+ "念咒",
+ "儿童歌声",
+ "合成歌声",
+ "说唱",
+ "哼歌",
+ "痛苦呻吟",
+ "闷哼",
+ "吹口哨",
+ "呼吸声",
+ "喘鸣",
+ "打鼾",
+ "倒吸一口气",
+ "喘气",
+ "鼻子喷气",
+ "咳嗽",
+ "清嗓子",
+ "打喷嚏",
+ "吸鼻子",
+ "跑步声",
+ "拖着脚走路",
+ "脚步声",
+ "咀嚼声",
+ "咬东西",
+ "漱口",
+ "肚子咕咕叫",
+ "打饱嗝",
+ "打嗝",
+ "放屁",
+ "手部声音",
+ "打响指",
+ "拍手",
+ "心跳声",
+ "心脏杂音",
+ "欢呼",
+ "掌声",
+ "多人闲聊",
+ "人群",
+ "嘈杂人声",
+ "儿童玩耍",
+ "动物",
+ "宠物",
+ "狗",
+ "狗吠",
+ "小狗尖吠",
+ "嚎叫",
+ "汪汪叫",
+ "低吼",
+ "狗呜咽",
+ "猫",
+ "猫打呼噜",
+ "猫叫声",
+ "嘶嘶声",
+ "猫叫春",
+ "家畜",
+ "马",
+ "马蹄声",
+ "马嘶鸣",
+ "牛",
+ "牛叫声",
+ "牛铃",
+ "猪",
+ "猪叫声",
+ "山羊",
+ "羊叫声",
+ "绵羊",
+ "家禽",
+ "鸡",
+ "母鸡咯咯叫",
+ "公鸡打鸣",
+ "火鸡",
+ "火鸡叫声",
+ "鸭子",
+ "鸭子叫声",
+ "鹅",
+ "鹅叫声",
+ "野生动物",
+ "狮子、老虎",
+ "猛兽咆哮",
+ "鸟",
+ "鸟叫声",
+ "小鸟啾啾叫",
+ "鸟的刺耳叫声",
+ "鸽子",
+ "鸽子咕咕叫",
+ "乌鸦",
+ "乌鸦叫声",
+ "猫头鹰",
+ "猫头鹰叫声",
+ "鸟拍打翅膀",
+ "狼等犬科动物",
+ "啮齿动物",
+ "老鼠",
+ "小动物跑动声",
+ "昆虫",
+ "蟋蟀",
+ "蚊子",
+ "苍蝇",
+ "嗡嗡声",
+ "蜜蜂、黄蜂",
+ "青蛙",
+ "青蛙叫声",
+ "蛇",
+ "响尾蛇嘎啦声",
+ "鲸鱼叫声",
+ "音乐",
+ "乐器",
+ "弹拨乐器",
+ "吉他",
+ "电吉他",
+ "贝斯",
+ "木吉他",
+ "滑棒吉他",
+ "吉他点弦",
+ "吉他扫弦",
+ "班卓琴",
+ "西塔琴",
+ "曼陀林",
+ "筝类乐器",
+ "尤克里里",
+ "键盘乐器",
+ "钢琴",
+ "电钢琴",
+ "管风琴",
+ "电子风琴",
+ "哈蒙德风琴",
+ "合成器",
+ "采样器",
+ "羽管键琴",
+ "打击乐器",
+ "架子鼓",
+ "鼓机",
+ "鼓",
+ "小军鼓",
+ "军鼓边击",
+ "军鼓滚奏",
+ "大鼓",
+ "定音鼓",
+ "塔布拉鼓",
+ "钹",
+ "踩镲",
+ "木鱼",
+ "铃鼓",
+ "摇响乐器",
+ "沙锤",
+ "锣",
+ "管钟",
+ "旋律打击乐器",
+ "木琴",
+ "钟琴",
+ "颤音琴",
+ "钢鼓",
+ "管弦乐队",
+ "铜管乐器",
+ "圆号",
+ "小号",
+ "长号",
+ "弓弦乐器",
+ "弦乐合奏",
+ "小提琴",
+ "提琴拨弦",
+ "大提琴",
+ "低音提琴",
+ "木管乐器",
+ "笛子",
+ "萨克斯",
+ "单簧管",
+ "竖琴",
+ "钟声",
+ "教堂钟声",
+ "铃铛",
+ "自行车铃",
+ "音叉",
+ "报时钟声",
+ "风铃",
+ "组钟轮鸣",
+ "口琴",
+ "手风琴",
+ "风笛",
+ "迪吉里杜管",
+ "羊角号",
+ "特雷门琴",
+ "颂钵",
+ "DJ 搓碟",
+ "流行音乐",
+ "嘻哈音乐",
+ "节奏口技",
+ "摇滚乐",
+ "重金属音乐",
+ "朋克摇滚",
+ "垃圾摇滚",
+ "前卫摇滚",
+ "早期摇滚乐",
+ "迷幻摇滚",
+ "节奏布鲁斯",
+ "灵魂乐",
+ "雷鬼音乐",
+ "乡村音乐",
+ "摇摆乐",
+ "蓝草音乐",
+ "放克音乐",
+ "民谣",
+ "中东音乐",
+ "爵士乐",
+ "迪斯科",
+ "古典音乐",
+ "歌剧",
+ "电子音乐",
+ "浩室音乐",
+ "铁克诺音乐",
+ "回响贝斯",
+ "鼓打贝斯",
+ "电子乐(Electronica)",
+ "电子舞曲",
+ "氛围音乐",
+ "迷幻舞曲",
+ "拉丁美洲音乐",
+ "萨尔萨音乐",
+ "弗拉门戈",
+ "布鲁斯",
+ "儿童音乐",
+ "新世纪音乐",
+ "声乐",
+ "无伴奏演唱",
+ "非洲音乐",
+ "非洲节拍音乐",
+ "基督教音乐",
+ "福音音乐",
+ "亚洲音乐",
+ "南印度古典音乐",
+ "宝莱坞音乐",
+ "斯卡音乐",
+ "传统音乐",
+ "独立音乐",
+ "歌曲",
+ "背景音乐",
+ "主题音乐",
+ "广告歌曲",
+ "配乐",
+ "摇篮曲",
+ "游戏音乐",
+ "圣诞音乐",
+ "舞曲",
+ "婚礼音乐",
+ "欢快音乐",
+ "悲伤音乐",
+ "温柔音乐",
+ "激昂音乐",
+ "愤怒音乐",
+ "恐怖音乐",
+ "风声",
+ "树叶沙沙声",
+ "麦克风风噪",
+ "雷雨",
+ "雷声",
+ "水声",
+ "雨声",
+ "雨滴声",
+ "雨点敲打声",
+ "溪流",
+ "瀑布",
+ "大海",
+ "海浪",
+ "蒸汽",
+ "咕嘟水声",
+ "火焰燃烧",
+ "噼啪声",
+ "交通工具",
+ "船只",
+ "帆船",
+ "划艇",
+ "快艇",
+ "轮船",
+ "机动车",
+ "汽车",
+ "汽车喇叭",
+ "喇叭短鸣",
+ "汽车警报",
+ "电动车窗",
+ "车辆打滑",
+ "轮胎尖啸",
+ "汽车驶过",
+ "赛车",
+ "卡车",
+ "气刹放气声",
+ "卡车气喇叭",
+ "倒车提示音",
+ "冰淇淋车",
+ "公交车",
+ "应急车辆",
+ "警车警笛",
+ "救护车警笛",
+ "消防车警笛",
+ "摩托车",
+ "交通噪音",
+ "轨道交通",
+ "火车",
+ "火车汽笛",
+ "火车喇叭",
+ "火车车厢",
+ "火车车轮尖啸",
+ "地铁",
+ "飞行器",
+ "飞机发动机",
+ "喷气发动机",
+ "螺旋桨",
+ "直升机",
+ "飞机",
+ "自行车",
+ "滑板",
+ "发动机",
+ "小型发动机",
+ "牙钻",
+ "割草机",
+ "链锯",
+ "中型发动机",
+ "大型发动机",
+ "发动机爆震",
+ "发动机启动",
+ "发动机怠速",
+ "发动机加速",
+ "门",
+ "门铃",
+ "门铃叮咚声",
+ "推拉门",
+ "摔门声",
+ "敲门声",
+ "轻敲声",
+ "吱吱声",
+ "开关橱柜",
+ "开关抽屉",
+ "锅碗瓢盆",
+ "刀叉餐具",
+ "切菜",
+ "煎炸食物",
+ "微波炉",
+ "搅拌机",
+ "水龙头",
+ "水槽放水、冲洗",
+ "浴缸放水、洗澡",
+ "吹风机",
+ "马桶冲水",
+ "刷牙",
+ "电动牙刷",
+ "吸尘器",
+ "拉链",
+ "钥匙叮当响",
+ "硬币掉落",
+ "剪刀",
+ "电动剃须刀",
+ "洗牌",
+ "打字",
+ "打字机",
+ "电脑键盘",
+ "写字",
+ "警报",
+ "电话",
+ "电话铃声",
+ "手机铃声",
+ "电话按键音",
+ "电话拨号音",
+ "电话忙音",
+ "闹钟",
+ "警笛",
+ "防空警笛",
+ "蜂鸣器",
+ "烟雾报警器",
+ "火灾警报",
+ "雾笛",
+ "哨声",
+ "蒸汽汽笛",
+ "机械装置",
+ "棘轮",
+ "钟表",
+ "嘀嗒声",
+ "钟表嘀嗒声",
+ "齿轮",
+ "滑轮",
+ "缝纫机",
+ "风扇",
+ "空调",
+ "收银机",
+ "打印机",
+ "相机",
+ "单反相机",
+ "工具",
+ "锤子",
+ "风镐",
+ "锯木头",
+ "锉刀",
+ "砂纸打磨",
+ "电动工具",
+ "电钻",
+ "爆炸",
+ "枪声",
+ "机枪",
+ "排枪齐射",
+ "炮火",
+ "玩具火药枪",
+ "烟花",
+ "鞭炮",
+ "爆裂声",
+ "火山喷发",
+ "轰隆巨响",
+ "木头",
+ "劈木头",
+ "木头碎裂",
+ "断裂声",
+ "玻璃",
+ "清脆碰撞声",
+ "玻璃破碎",
+ "液体",
+ "水花飞溅",
+ "液体晃荡",
+ "挤压湿物声",
+ "滴水声",
+ "倒水",
+ "涓涓细流",
+ "水流喷涌",
+ "注入液体",
+ "喷洒",
+ "水泵抽水",
+ "搅拌",
+ "沸腾",
+ "声呐",
+ "射箭",
+ "嗖嗖声",
+ "闷响",
+ "空洞闷响",
+ "电子调音器",
+ "效果器",
+ "合唱效果",
+ "篮球弹地",
+ "砰的一声",
+ "拍打声",
+ "重击声",
+ "砸碎声",
+ "破碎声",
+ "弹跳声",
+ "鞭子抽打",
+ "旗帜拍动",
+ "划刻声",
+ "刮擦声",
+ "摩擦声",
+ "滚动声",
+ "压碎声",
+ "揉皱声",
+ "撕裂声",
+ "哔哔声",
+ "铮的一声",
+ "叮的一声",
+ "金属哐当声",
+ "尖啸声",
+ "嘎吱声",
+ "沙沙声",
+ "呼呼转动声",
+ "杂乱碰撞声",
+ "滋滋声",
+ "咔嗒声",
+ "有节奏的咔嗒声",
+ "隆隆声",
+ "小物体落水",
+ "叮当声",
+ "低沉嗡鸣",
+ "咻的一声",
+ "弹簧嘣声",
+ "咔嚓脆响",
+ "寂静无声",
+ "正弦波",
+ "谐波",
+ "扫频音",
+ "音效",
+ "脉冲音",
+ "室内(小房间)",
+ "室内(大空间)",
+ "室内(公共场所)",
+ "户外(城市)",
+ "户外(乡村、野外)",
+ "混响",
+ "回声",
+ "噪音",
+ "环境噪音",
+ "静电噪音",
+ "电流嗡鸣",
+ "声音失真",
+ "通话侧音",
+ "刺耳嘈杂声",
+ "白噪音",
+ "粉红噪音",
+ "低沉脉动声",
+ "振动声",
+ "电视",
+ "收音机",
+ "实地录音"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/SoundNames/zh-Hant.json b/SoundVisualizer.Core/Sounds/Data/SoundNames/zh-Hant.json
new file mode 100644
index 0000000..87db2ea
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/SoundNames/zh-Hant.json
@@ -0,0 +1,523 @@
+[
+ "說話聲",
+ "兒童說話聲",
+ "交談聲",
+ "旁白、獨白",
+ "牙牙學語",
+ "合成語音",
+ "大喊",
+ "咆哮",
+ "興奮高呼",
+ "大叫",
+ "兒童大喊",
+ "尖叫",
+ "悄悄話",
+ "笑聲",
+ "嬰兒笑聲",
+ "咯咯笑",
+ "竊笑",
+ "捧腹大笑",
+ "輕笑",
+ "哭泣聲",
+ "嬰兒哭聲",
+ "嗚咽",
+ "呻吟",
+ "嘆氣",
+ "歌聲",
+ "合唱團",
+ "約德爾唱法",
+ "吟誦",
+ "唸咒",
+ "兒童歌聲",
+ "合成歌聲",
+ "饒舌",
+ "哼歌",
+ "悶哼",
+ "哼聲",
+ "吹口哨",
+ "呼吸聲",
+ "喘鳴聲",
+ "打呼聲",
+ "倒抽一口氣",
+ "喘氣聲",
+ "鼻子噴氣聲",
+ "咳嗽聲",
+ "清喉嚨",
+ "打噴嚏",
+ "吸鼻子",
+ "跑步聲",
+ "拖著腳走路",
+ "腳步聲",
+ "咀嚼聲",
+ "啃咬聲",
+ "漱口聲",
+ "肚子咕嚕叫",
+ "打飽嗝",
+ "打嗝",
+ "放屁聲",
+ "手部聲音",
+ "彈手指",
+ "拍手聲",
+ "心跳聲",
+ "心雜音",
+ "歡呼聲",
+ "掌聲",
+ "眾人閒聊聲",
+ "人群",
+ "嘈雜人聲",
+ "兒童嬉戲聲",
+ "動物",
+ "寵物",
+ "狗",
+ "狗吠聲",
+ "小型犬尖吠",
+ "嚎叫",
+ "汪汪叫",
+ "低吼",
+ "狗哀鳴",
+ "貓",
+ "貓呼嚕聲",
+ "貓叫聲",
+ "嘶嘶聲",
+ "貓叫春",
+ "家畜",
+ "馬",
+ "馬蹄聲",
+ "馬嘶聲",
+ "牛",
+ "牛叫聲",
+ "牛鈴",
+ "豬",
+ "豬叫聲",
+ "山羊",
+ "羊叫聲",
+ "綿羊",
+ "家禽",
+ "雞",
+ "雞咯咯叫",
+ "公雞啼叫",
+ "火雞",
+ "火雞叫聲",
+ "鴨子",
+ "鴨叫聲",
+ "鵝",
+ "鵝叫聲",
+ "野生動物",
+ "獅子、老虎",
+ "獅虎吼叫",
+ "鳥類",
+ "鳥叫聲",
+ "小鳥啾啾叫",
+ "鳥尖叫聲",
+ "鴿子",
+ "鴿子咕咕叫",
+ "烏鴉",
+ "烏鴉叫聲",
+ "貓頭鷹",
+ "貓頭鷹叫聲",
+ "鳥拍翅聲",
+ "狼、野犬",
+ "鼠類",
+ "老鼠",
+ "小動物跑動聲",
+ "昆蟲",
+ "蟋蟀",
+ "蚊子",
+ "蒼蠅",
+ "嗡嗡聲",
+ "蜜蜂、黃蜂",
+ "青蛙",
+ "青蛙叫聲",
+ "蛇",
+ "喀啦喀啦聲",
+ "鯨魚叫聲",
+ "音樂",
+ "樂器",
+ "撥弦樂器",
+ "吉他",
+ "電吉他",
+ "電貝斯",
+ "木吉他",
+ "滑棒吉他",
+ "吉他點弦",
+ "吉他刷弦",
+ "斑鳩琴",
+ "西塔琴",
+ "曼陀林",
+ "齊特琴 (箏類)",
+ "烏克麗麗",
+ "鍵盤樂器",
+ "鋼琴",
+ "電鋼琴",
+ "管風琴",
+ "電子風琴",
+ "哈蒙德風琴",
+ "合成器",
+ "取樣器",
+ "大鍵琴",
+ "打擊樂器",
+ "爵士鼓",
+ "鼓機",
+ "鼓",
+ "小鼓",
+ "鼓邊重擊",
+ "擊鼓滾奏",
+ "大鼓",
+ "定音鼓",
+ "塔布拉鼓",
+ "銅鈸",
+ "腳踏鈸",
+ "木魚",
+ "鈴鼓",
+ "搖鈴 (樂器)",
+ "沙鈴",
+ "鑼",
+ "管鐘",
+ "鍵盤打擊樂器",
+ "木琴",
+ "鐵琴",
+ "顫音琴",
+ "鋼鼓",
+ "管弦樂團",
+ "銅管樂器",
+ "法國號",
+ "小號",
+ "長號",
+ "弓弦樂器",
+ "弦樂合奏",
+ "小提琴",
+ "弦樂撥奏",
+ "大提琴",
+ "低音大提琴",
+ "木管樂器",
+ "笛子",
+ "薩克斯風",
+ "單簧管",
+ "豎琴",
+ "鐘聲",
+ "教堂鐘聲",
+ "鈴鐺",
+ "腳踏車鈴",
+ "音叉",
+ "報時鐘聲",
+ "風鈴",
+ "組鐘輪鳴",
+ "口琴",
+ "手風琴",
+ "風笛",
+ "迪吉里杜管",
+ "羊角號",
+ "特雷門琴",
+ "頌缽",
+ "DJ 刷碟",
+ "流行音樂",
+ "嘻哈音樂",
+ "口技 (Beatbox)",
+ "搖滾樂",
+ "重金屬",
+ "龐克搖滾",
+ "油漬搖滾",
+ "前衛搖滾",
+ "早期搖滾樂",
+ "迷幻搖滾",
+ "節奏藍調",
+ "靈魂樂",
+ "雷鬼樂",
+ "鄉村音樂",
+ "搖擺樂",
+ "藍草音樂",
+ "放克",
+ "民謠",
+ "中東音樂",
+ "爵士樂",
+ "迪斯可",
+ "古典音樂",
+ "歌劇",
+ "電子音樂",
+ "浩室音樂 (House)",
+ "鐵克諾 (Techno)",
+ "回響貝斯 (Dubstep)",
+ "鼓打貝斯",
+ "電子樂 (Electronica)",
+ "電子舞曲",
+ "氛圍音樂",
+ "迷幻舞曲 (Trance)",
+ "拉丁美洲音樂",
+ "騷莎音樂",
+ "佛朗明哥",
+ "藍調",
+ "兒童音樂",
+ "新世紀音樂",
+ "聲樂",
+ "阿卡貝拉",
+ "非洲音樂",
+ "非洲節拍",
+ "基督教音樂",
+ "福音音樂",
+ "亞洲音樂",
+ "南印度古典音樂",
+ "寶萊塢音樂",
+ "斯卡 (Ska)",
+ "傳統音樂",
+ "獨立音樂",
+ "歌曲",
+ "背景音樂",
+ "主題曲",
+ "廣告歌曲",
+ "配樂",
+ "搖籃曲",
+ "遊戲音樂",
+ "聖誕音樂",
+ "舞曲",
+ "婚禮音樂",
+ "歡樂音樂",
+ "悲傷音樂",
+ "溫柔音樂",
+ "激昂音樂",
+ "憤怒音樂",
+ "恐怖音樂",
+ "風聲",
+ "樹葉沙沙聲",
+ "麥克風風切聲",
+ "雷雨聲",
+ "雷聲",
+ "水聲",
+ "雨聲",
+ "雨滴聲",
+ "雨打屋頂或窗戶",
+ "溪流聲",
+ "瀑布聲",
+ "海洋",
+ "海浪聲",
+ "蒸汽聲",
+ "咕嚕水聲",
+ "火焰燃燒",
+ "劈啪聲",
+ "交通工具",
+ "船隻",
+ "帆船",
+ "划船、獨木舟",
+ "快艇",
+ "輪船",
+ "道路車輛",
+ "汽車",
+ "汽車喇叭",
+ "喇叭短鳴",
+ "汽車防盜警報",
+ "電動車窗",
+ "車輛打滑",
+ "輪胎尖嘯聲",
+ "汽車駛過",
+ "賽車",
+ "卡車",
+ "氣壓煞車",
+ "卡車喇叭",
+ "倒車警示音",
+ "冰淇淋車",
+ "公車",
+ "緊急車輛",
+ "警車 (警笛)",
+ "救護車 (警笛)",
+ "消防車 (警笛)",
+ "機車",
+ "交通噪音",
+ "軌道車輛",
+ "火車",
+ "火車汽笛",
+ "火車喇叭",
+ "火車車廂",
+ "火車車輪尖嘯聲",
+ "捷運、地鐵",
+ "飛行器",
+ "飛機引擎",
+ "噴射引擎",
+ "螺旋槳",
+ "直升機",
+ "飛機",
+ "腳踏車",
+ "滑板",
+ "引擎",
+ "輕型引擎 (高頻)",
+ "牙醫鑽牙聲",
+ "割草機",
+ "電鋸",
+ "中型引擎 (中頻)",
+ "重型引擎 (低頻)",
+ "引擎爆震",
+ "引擎啟動",
+ "引擎怠速",
+ "引擎加速",
+ "門",
+ "門鈴",
+ "門鈴叮咚聲",
+ "拉門",
+ "甩門聲",
+ "敲門聲",
+ "輕敲聲",
+ "吱吱聲",
+ "櫥櫃開關聲",
+ "抽屜開關聲",
+ "碗盤鍋具碰撞",
+ "刀叉餐具",
+ "切菜聲",
+ "煎炸聲",
+ "微波爐",
+ "果汁機",
+ "水龍頭",
+ "水槽 (放水或清洗)",
+ "浴缸 (放水或清洗)",
+ "吹風機",
+ "馬桶沖水",
+ "刷牙聲",
+ "電動牙刷",
+ "吸塵器",
+ "拉鍊",
+ "鑰匙叮噹聲",
+ "硬幣掉落",
+ "剪刀",
+ "電動刮鬍刀",
+ "洗牌聲",
+ "打字聲",
+ "打字機",
+ "電腦鍵盤",
+ "寫字聲",
+ "警報",
+ "電話",
+ "電話鈴聲",
+ "手機鈴聲",
+ "撥號按鍵音",
+ "撥號音",
+ "忙線音",
+ "鬧鐘",
+ "警笛",
+ "民防警笛",
+ "蜂鳴器",
+ "煙霧警報",
+ "火警警報",
+ "霧笛",
+ "哨子聲",
+ "蒸汽汽笛",
+ "機械裝置",
+ "棘輪",
+ "時鐘",
+ "滴答聲",
+ "時鐘滴答聲",
+ "齒輪",
+ "滑輪",
+ "縫紉機",
+ "風扇",
+ "冷氣",
+ "收銀機",
+ "印表機",
+ "相機",
+ "單眼相機",
+ "工具",
+ "鐵鎚",
+ "路面破碎機",
+ "鋸切聲",
+ "銼刀聲",
+ "砂紙打磨",
+ "電動工具",
+ "電鑽",
+ "爆炸聲",
+ "槍聲",
+ "機關槍",
+ "排槍齊射",
+ "砲擊",
+ "玩具火藥槍",
+ "煙火",
+ "鞭炮",
+ "爆裂聲",
+ "火山爆發",
+ "轟隆巨響",
+ "木頭",
+ "劈柴聲",
+ "木頭碎裂",
+ "斷裂聲",
+ "玻璃",
+ "清脆碰撞聲",
+ "玻璃碎裂",
+ "液體",
+ "潑濺聲",
+ "液體晃動",
+ "濕軟物擠壓聲",
+ "滴水聲",
+ "倒水聲",
+ "細流聲",
+ "液體湧出",
+ "裝水聲",
+ "噴灑聲",
+ "幫浦抽水",
+ "攪拌聲",
+ "沸騰聲",
+ "聲納",
+ "弓箭",
+ "咻的破風聲",
+ "沉悶撞擊聲",
+ "空洞悶響",
+ "電子調音器",
+ "效果器",
+ "合唱效果",
+ "籃球彈跳聲",
+ "砰的一聲",
+ "拍打聲",
+ "重擊聲",
+ "砸碎聲",
+ "碎裂聲",
+ "彈跳聲",
+ "揮鞭聲",
+ "旗幟拍動聲",
+ "刮劃聲",
+ "刮擦聲",
+ "摩擦聲",
+ "滾動聲",
+ "壓碎聲",
+ "揉皺聲",
+ "撕裂聲",
+ "嗶聲",
+ "清脆鳴響",
+ "叮的一聲",
+ "金屬匡噹聲",
+ "尖嘯聲",
+ "嘎吱聲",
+ "沙沙聲",
+ "機件轉動聲",
+ "雜亂碰撞聲",
+ "滋滋聲",
+ "喀噠聲",
+ "連續喀噠聲",
+ "隆隆聲",
+ "小物落水聲",
+ "叮噹聲",
+ "低鳴聲",
+ "短促尖鳴",
+ "彈簧聲",
+ "喀滋脆響",
+ "安靜無聲",
+ "正弦波",
+ "泛音",
+ "掃頻音",
+ "音效",
+ "脈衝聲",
+ "室內 (小房間)",
+ "室內 (大廳)",
+ "室內 (公共場所)",
+ "戶外 (都市)",
+ "戶外 (大自然)",
+ "殘響",
+ "回音",
+ "噪音",
+ "環境噪音",
+ "靜電雜音",
+ "電流低鳴聲",
+ "聲音失真",
+ "電話側音",
+ "混亂嘈雜聲",
+ "白噪音",
+ "粉紅噪音",
+ "陣陣悶響",
+ "機械振動聲",
+ "電視",
+ "收音機",
+ "實地錄音"
+]
diff --git a/SoundVisualizer.Core/Sounds/Data/sound_groups.tsv b/SoundVisualizer.Core/Sounds/Data/sound_groups.tsv
new file mode 100644
index 0000000..a6a6668
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/Data/sound_groups.tsv
@@ -0,0 +1,534 @@
+# SPDX-License-Identifier: CC-BY-SA-4.0
+#
+# SoundVisualizer sound groups: the group of each of the 521 YAMNet sound names in the Sounds tab.
+# Adapted from the AudioSet Ontology by Google Inc.
+# (https://github.com/audioset/ontology, commit d417d32), licensed under
+# CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
+# Changes: the 521 YAMNet sound names were regrouped into everyday groups by SoundVisualizer contributors (revised 2026-10: laughs, yells, cries and cheering moved to the speech groups).
+# This file is licensed under CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
+# Provided as is, without warranties of any kind (Section 5 of the licence: Disclaimer of Warranties
+# and Limitation of Liability).
+#
+# Format: group_iddisplay_name, one sound per line. Groups and sounds are in display order.
+# group_id is a SoundGroup entry (lower case); display_name is from assets/ai/yamnet_class_map.csv.
+alarms Alarm
+alarms Smoke detector, smoke alarm
+alarms Fire alarm
+sirens Siren
+sirens Ambulance (siren)
+sirens Fire engine, fire truck (siren)
+sirens Police car (siren)
+sirens Civil defense siren
+horns Vehicle horn, car horn, honking
+horns Air horn, truck horn
+horns Car alarm
+vehicle_warnings Reversing beeps
+vehicle_warnings Bicycle bell
+vehicle_warnings Tire squeal
+vehicle_warnings Skidding
+vehicle_warnings Emergency vehicle
+vehicle_warnings Train horn
+vehicle_warnings Train whistle
+vehicle_warnings Foghorn
+breaking Shatter
+breaking Breaking
+breaking Smash, crash
+breaking Bang
+slam Slam
+gunshots Gunshot, gunfire
+gunshots Machine gun
+gunshots Fusillade
+gunshots Artillery fire
+gunshots Cap gun
+explosions Explosion
+explosions Boom
+explosions Burst, pop
+fireworks Fireworks
+fireworks Firecracker
+thunder Thunder
+thunder Thunderstorm
+chainsaw Chainsaw
+talking Speech
+talking Conversation
+talking Child speech, kid speaking
+talking Narration, monologue
+talking Whispering
+talking Babbling
+talking Speech synthesizer
+distress Shout
+distress Screaming
+distress Crying, sobbing
+distress Children shouting
+distress Yell
+distress Bellow
+distress Whoop
+distress Wail, moan
+baby Baby cry, infant cry
+baby Whimper
+crowd Crowd
+crowd Chatter
+crowd Hubbub, speech noise, speech babble
+crowd Children playing
+crowd Cheering
+laughter Laughter
+laughter Baby laughter
+laughter Giggle
+laughter Belly laugh
+laughter Chuckle, chortle
+laughter Snicker
+singing Singing
+singing Choir
+singing Child singing
+singing Rapping
+singing Synthetic singing
+doorbell Doorbell
+doorbell Ding-dong
+doorbell Knock
+doorbell Tap
+phone Telephone
+phone Telephone bell ringing
+phone Ringtone
+phone Telephone dialing, DTMF
+phone Dial tone
+phone Busy signal
+signals Alarm clock
+signals Beep, bleep
+signals Buzzer
+signals Ding
+signals Ping
+signals Whistle
+signals Steam whistle
+fire Fire
+fire Crackle
+dog Dog
+dog Bark
+dog Bow-wow
+dog Yip
+dog Growling
+dog Howl
+dog Whimper (dog)
+voices Applause
+voices Clapping
+voices Humming
+voices Whistling
+voices Yodeling
+voices Chant
+voices Mantra
+voices Finger snapping
+voices Hands
+footsteps Walk, footsteps
+footsteps Run
+footsteps Shuffle
+door Door
+door Sliding door
+door Squeak
+door Creak
+door Cupboard open or close
+door Drawer open or close
+water Water tap, faucet
+water Toilet flush
+water Drip
+water Sink (filling or washing)
+water Bathtub (filling or washing)
+water Fill (with liquid)
+water Gush
+water Pour
+water Trickle, dribble
+water Spray
+water Splash, splatter
+water Slosh
+water Squish
+water Pump (liquid)
+water Liquid
+kitchen Microwave oven
+kitchen Frying (food)
+kitchen Dishes, pots, and pans
+kitchen Blender
+kitchen Boiling
+kitchen Sizzle
+kitchen Chopping (food)
+kitchen Cutlery, silverware
+kitchen Stir
+kitchen Glass
+kitchen Chink, clink
+appliances Vacuum cleaner
+appliances Hair dryer
+appliances Clock
+appliances Mechanical fan
+appliances Air conditioning
+appliances Sewing machine
+appliances Electric shaver, electric razor
+appliances Electric toothbrush
+appliances Toothbrush
+appliances Printer
+appliances Camera
+appliances Single-lens reflex camera
+appliances Tick-tock
+appliances Tick
+tools Drill
+tools Hammer
+tools Lawn mower
+tools Power tool
+tools Jackhammer
+tools Sawing
+tools Sanding
+tools Filing (rasp)
+tools Tools
+tools Dental drill, dentist's drill
+tools Mechanisms
+tools Gears
+tools Pulleys
+tools Ratchet, pawl
+tools Whir
+tools Cash register
+vehicle Vehicle
+vehicle Car
+vehicle Train
+vehicle Fixed-wing aircraft, airplane
+vehicle Car passing by
+vehicle Traffic noise, roadway noise
+vehicle Bus
+vehicle Truck
+vehicle Motorcycle
+vehicle Motor vehicle (road)
+vehicle Air brake
+vehicle Power windows, electric windows
+vehicle Race car, auto racing
+vehicle Ice cream truck, ice cream van
+vehicle Bicycle
+vehicle Skateboard
+vehicle Engine
+vehicle Engine starting
+vehicle Idling
+vehicle Accelerating, revving, vroom
+vehicle Engine knocking
+vehicle Light engine (high frequency)
+vehicle Medium engine (mid frequency)
+vehicle Heavy engine (low frequency)
+vehicle Subway, metro, underground
+vehicle Rail transport
+vehicle Railroad car, train wagon
+vehicle Train wheels squealing
+vehicle Helicopter
+vehicle Aircraft
+vehicle Aircraft engine
+vehicle Jet engine
+vehicle Propeller, airscrew
+vehicle Boat, Water vehicle
+vehicle Ship
+vehicle Motorboat, speedboat
+vehicle Sailboat, sailing ship
+vehicle Rowboat, canoe, kayak
+vehicle Toot
+bell Bell
+bell Church bell
+bell Chime
+bell Wind chime
+bell Jingle bell
+bell Cowbell
+bell Change ringing (campanology)
+bell Tuning fork
+animal Animal
+animal Cat
+animal Bird
+animal Chicken, rooster
+animal Meow
+animal Purr
+animal Caterwaul
+animal Domestic animals, pets
+animal Bird vocalization, bird call, bird song
+animal Chirp, tweet
+animal Squawk
+animal Pigeon, dove
+animal Coo
+animal Crow
+animal Caw
+animal Owl
+animal Hoot
+animal Bird flight, flapping wings
+animal Livestock, farm animals, working animals
+animal Crowing, cock-a-doodle-doo
+animal Cluck
+animal Fowl
+animal Duck
+animal Quack
+animal Goose
+animal Honk
+animal Turkey
+animal Gobble
+animal Horse
+animal Neigh, whinny
+animal Clip-clop
+animal Cattle, bovinae
+animal Moo
+animal Pig
+animal Oink
+animal Sheep
+animal Goat
+animal Bleat
+animal Wild animals
+animal Roaring cats (lions, tigers)
+animal Roar
+animal Canidae, dogs, wolves
+animal Rodents, rats, mice
+animal Mouse
+animal Patter
+animal Insect
+animal Mosquito
+animal Fly, housefly
+animal Buzz
+animal Bee, wasp, etc.
+animal Cricket
+animal Frog
+animal Croak
+animal Snake
+animal Whale vocalization
+nature Rain
+nature Wind
+nature Waves, surf
+nature Rain on surface
+nature Raindrop
+nature Rustling leaves
+nature Water
+nature Stream
+nature Waterfall
+nature Ocean
+nature Steam
+nature Gurgling
+nature Eruption
+body Cough
+body Snoring
+body Breathing
+body Sneeze
+body Gasp
+body Wheeze
+body Pant
+body Throat clearing
+body Sniff
+body Snort
+body Sigh
+body Groan
+body Grunt
+body Chewing, mastication
+body Biting
+body Gargling
+body Hiccup
+body Burping, eructation
+body Stomach rumble
+body Fart
+body Heart sounds, heartbeat
+body Heart murmur
+objects Thump, thud
+objects Typing
+objects Keys jangling
+objects Thunk
+objects Clatter
+objects Clang
+objects Slap, smack
+objects Whack, thwack
+objects Crunch
+objects Crushing
+objects Crumpling, crinkling
+objects Tearing
+objects Rattle
+objects Hiss
+objects Jingle, tinkle
+objects Coin (dropping)
+objects Zipper (clothing)
+objects Scissors
+objects Shuffling cards
+objects Computer keyboard
+objects Typewriter
+objects Writing
+objects Clicking
+objects Clickety-clack
+objects Squeal
+objects Rustle
+objects Scratch
+objects Scrape
+objects Rub
+objects Roll
+objects Bouncing
+objects Basketball bounce
+objects Plop
+objects Whoosh, swoosh, swish
+objects Whip
+objects Flap
+objects Boing
+objects Zing
+objects Arrow
+objects Wood
+objects Chop
+objects Splinter
+objects Crack
+music Music
+music Musical instrument
+music Pop music
+music Piano
+music Classical music
+music Guitar
+music Background music
+music Drum
+music Song
+music Theme music
+music Soundtrack music
+music Video game music
+music Jingle (music)
+music Lullaby
+music Music for children
+music Christmas music
+music Wedding music
+music Dance music
+music Happy music
+music Sad music
+music Tender music
+music Exciting music
+music Angry music
+music Scary music
+music Rock music
+music Heavy metal
+music Punk rock
+music Grunge
+music Progressive rock
+music Rock and roll
+music Psychedelic rock
+music Hip hop music
+music Beatboxing
+music Rhythm and blues
+music Soul music
+music Funk
+music Disco
+music Reggae
+music Ska
+music Jazz
+music Blues
+music Swing music
+music Country
+music Bluegrass
+music Folk music
+music Opera
+music Electronic music
+music House music
+music Techno
+music Dubstep
+music Drum and bass
+music Electronica
+music Electronic dance music
+music Ambient music
+music Trance music
+music New-age music
+music Vocal music
+music A capella
+music Gospel music
+music Christian music
+music Independent music
+music Traditional music
+music Middle Eastern music
+music Music of Africa
+music Afrobeat
+music Music of Asia
+music Carnatic music
+music Music of Bollywood
+music Music of Latin America
+music Salsa music
+music Flamenco
+music Electric piano
+music Keyboard (musical)
+music Organ
+music Electronic organ
+music Hammond organ
+music Synthesizer
+music Sampler
+music Harpsichord
+music Acoustic guitar
+music Electric guitar
+music Bass guitar
+music Steel guitar, slide guitar
+music Tapping (guitar technique)
+music Strum
+music Plucked string instrument
+music Banjo
+music Sitar
+music Mandolin
+music Zither
+music Ukulele
+music Harp
+music Bowed string instrument
+music Violin, fiddle
+music Pizzicato
+music Cello
+music Double bass
+music String section
+music Orchestra
+music Brass instrument
+music Trumpet
+music Trombone
+music French horn
+music Wind instrument, woodwind instrument
+music Flute
+music Saxophone
+music Clarinet
+music Harmonica
+music Accordion
+music Bagpipes
+music Didgeridoo
+music Shofar
+music Theremin
+music Percussion
+music Drum kit
+music Drum machine
+music Snare drum
+music Rimshot
+music Drum roll
+music Bass drum
+music Timpani
+music Tabla
+music Cymbal
+music Hi-hat
+music Wood block
+music Tambourine
+music Rattle (instrument)
+music Maraca
+music Gong
+music Tubular bells
+music Mallet percussion
+music Marimba, xylophone
+music Glockenspiel
+music Vibraphone
+music Steelpan
+music Scratching (performance technique)
+music Electronic tuner
+music Effects unit
+music Chorus effect
+music Singing bowl
+background Television
+background Radio
+background Noise
+background Field recording
+background Environmental noise
+background Hum
+background Mains hum
+background Rumble
+background Static
+background White noise
+background Pink noise
+background Throbbing
+background Vibration
+background Distortion
+background Cacophony
+background Sidetone
+background Wind noise (microphone)
+background Echo
+background Reverberation
+background Inside, small room
+background Inside, large room or hall
+background Inside, public space
+background Outside, urban or manmade
+background Outside, rural or natural
+background Silence
+background Sound effect
+background Sine wave
+background Harmonic
+background Chirp tone
+background Pulse
+background Sonar
diff --git a/SoundVisualizer.Core/Sounds/KeptSounds.cs b/SoundVisualizer.Core/Sounds/KeptSounds.cs
new file mode 100644
index 0000000..c5ce652
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/KeptSounds.cs
@@ -0,0 +1,48 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+
+namespace SoundVisualizer.Sounds
+{
+ ///
+ /// 거르기를 켠 채로 방금 바꾼 소리(모델 이름, #374). 모바일 KeptSounds 와 같다. 바꾼 소리가 거르기에서 빠져도 찾는 말이나 거르기를
+ /// 바꿀 때까지 목록에 남겨, 누른 카드(고급 모드는 그 줄)가 눈앞에서 사라지지 않게 한다.
+ ///
+ /// 카드가 아니라 소리를 남긴다. 기본 모드는 남은 소리가 있는 카드가, 고급 모드는 그 줄과 그 위의 머리가 남고, "바꾼 소리" 거르기도
+ /// 같은 방법으로 된다. · 는 남기기 시작할 때의 것이고, 지금 것과 다르면 아무것도 남기지 않는다.
+ /// 화면은 찾는 말이나 거르기를 바꿀 때와 모두 되돌리기를 할 때 로 비운다.
+ ///
+ public sealed record KeptSounds(string Query, SoundFilter Filter, IReadOnlySet Names)
+ {
+ private static readonly IReadOnlySet NoNames = FrozenSet.Empty;
+
+ /// 남길 것이 없다.
+ public static KeptSounds Empty { get; } = new("", SoundFilter.All, NoNames);
+
+ /// 지금의 찾는 말과 거르기에서 남길 소리. 남기기 시작할 때와 다르면 빈 것이다.
+ public IReadOnlySet NamesFor(string query, SoundFilter filter) =>
+ string.Equals(query, Query, StringComparison.Ordinal) && filter == Filter ? Names : NoNames;
+
+ ///
+ /// 를 더한 것. 찾는 말이나 거르기가 남기기 시작할 때와 다르면 앞의 것은 버리고 새로 시작한다.
+ /// 전체에서는 모든 소리가 거르기에 맞아 남길 것이 없으니 빈 것이다.
+ ///
+ public KeptSounds Plus(string query, SoundFilter filter, IEnumerable added)
+ {
+ if (filter == SoundFilter.All) return Empty;
+ var names = new HashSet(NamesFor(query, filter), StringComparer.Ordinal);
+ names.UnionWith(added);
+ return new KeptSounds(query, filter, names.ToFrozenSet(StringComparer.Ordinal));
+ }
+
+ public bool Equals(KeptSounds? other) =>
+ other is not null &&
+ string.Equals(Query, other.Query, StringComparison.Ordinal) &&
+ Filter == other.Filter &&
+ Names.SetEquals(other.Names);
+
+ // 집합의 순서와 상관없이 같은 값이어야 해서 수만 넣는다.
+ public override int GetHashCode() => HashCode.Combine(Query, Filter, Names.Count);
+ }
+}
diff --git a/SoundVisualizer.Core/Sounds/SoundCards.cs b/SoundVisualizer.Core/Sounds/SoundCards.cs
new file mode 100644
index 0000000..20e1ecd
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/SoundCards.cs
@@ -0,0 +1,220 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Globalization;
+using System.Text;
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Sounds
+{
+ // 분류 탭의 묶음 화면이 다루는 값들. 모바일 SoundGroups.kt 의 데이터 클래스와 같다.
+ // 목록을 든 레코드는 같음을 목록의 내용으로 본다(모바일 data class 처럼). 레코드가 스스로 만드는 같음은 목록의 참조만 본다.
+
+ /// 묶음 표의 한 줄. 는 머리글을 뺀 줄 순서(0부터)이고, 묶음 화면에서 소리가 보이는 순서다.
+ public sealed record SoundGroupRow(SoundGroup Group, int Order);
+
+ ///
+ /// 분류 탭의 카드 하나. 같은 묶음이면서 기본 종류가 같은 소리들이다(카드 = (묶음, 기본 종류)).
+ ///
+ /// 지금은 묶음마다 기본 종류가 하나라 카드 하나가 곧 묶음 하나다. AI 의 매핑이 바뀌어 한 소리의 기본 종류가 달라지면, 그 소리는
+ /// 표를 고치지 않아도 새 종류의 구역에 같은 묶음 이름의 카드로 따로 보인다. 그래서 한 카드의 소리는 늘 기본 종류가 같고,
+ /// 카드에서 고르는 "(기본)" 이 모든 소리에 맞다. 섞인 카드()도 하나씩 바꾼 소리가 있을 때만 생긴다.
+ ///
+ /// 표에 없는 소리면 null 이고, 그 소리 하나가 카드가 된다.
+ /// 카드의 소리들의 기본 종류. 카드가 놓이는 구역이다.
+ /// 표의 순서대로.
+ ///
+ /// 이름(소리 이름)이 곧 카드 이름인 소리. 새로 지은 이름을 쓰는 묶음이면 null 이다. 매핑이 바뀌어 묶음이 두 구역에 나뉘면 다른
+ /// 구역의 카드에 들어 있을 수 있다.
+ ///
+ public sealed record SoundCard(SoundGroup? Group, SoundType DefaultType, IReadOnlyList Members, SoundEntry? TitleEntry)
+ {
+ /// 새로 지은 카드 이름을 쓰면 그 묶음(모바일 titleRes). 카드 이름이 소리 이름()이면 null.
+ public SoundGroup? TitleGroup => TitleEntry is null ? Group : null;
+
+ /// 목록에서 카드를 가리키는 이름("sirens:danger"). 한 묶음이 두 구역에 나뉘어도 겹치지 않게 기본 종류를 붙인다.
+ public string Id =>
+ (Group?.Id() ?? "sound_" + (Members.Count > 0 ? Members[0].Index : -1).ToString(CultureInfo.InvariantCulture)) +
+ ":" + DefaultType.ToLabel();
+
+ public bool Equals(SoundCard? other) =>
+ other is not null && Group == other.Group && DefaultType == other.DefaultType &&
+ Equals(TitleEntry, other.TitleEntry) && ValueLists.SameItems(Members, other.Members);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(Group);
+ hash.Add(DefaultType);
+ hash.Add(TitleEntry);
+ ValueLists.AddItems(ref hash, Members);
+ return hash.ToHashCode();
+ }
+
+ private bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append("Id = ").Append(Id).Append(", Members = ").Append(ValueLists.Names(Members));
+ builder.Append(", TitleEntry = ").Append(TitleEntry?.Name);
+ return true;
+ }
+ }
+
+ /// 구역 하나. 이 기본 종류인 카드들이다.
+ public sealed record SoundSection(SoundType Type, IReadOnlyList Cards)
+ {
+ public bool Equals(SoundSection? other) =>
+ other is not null && Type == other.Type && ValueLists.SameItems(Cards, other.Cards);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(Type);
+ ValueLists.AddItems(ref hash, Cards);
+ return hash.ToHashCode();
+ }
+
+ private bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append("Type = ").Append(Type).Append(", Cards = [").AppendJoin(", ", Cards.Select(card => card.Id)).Append(']');
+ return true;
+ }
+ }
+
+ ///
+ /// 카드의 지금 종류. 저장하지 않고 소리마다의 종류에서 그때그때 정한다().
+ ///
+ public abstract record GroupState
+ {
+ private GroupState()
+ {
+ }
+
+ /// 모든 소리가 이다. 카드의 기본 종류와 다르면 묶음째 바꾼 것이다.
+ public sealed record Uniform(SoundType Type) : GroupState;
+
+ /// 소리마다 종류가 다르다("일부 다름"). 카드의 기본 종류는 하나라 하나씩 바꾼 소리가 있다는 뜻이다.
+ public sealed record Mixed : GroupState
+ {
+ private Mixed()
+ {
+ }
+
+ public static Mixed Instance { get; } = new();
+ }
+ }
+
+ ///
+ /// 카드 이름 밑의 작은 글씨. 는 개까지의 소리이고,
+ /// 는 못 보인 소리의 수("+N")다.
+ ///
+ public sealed record GroupPreview(IReadOnlyList Names, int More)
+ {
+ public bool Equals(GroupPreview? other) =>
+ other is not null && More == other.More && ValueLists.SameItems(Names, other.Names);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(More);
+ ValueLists.AddItems(ref hash, Names);
+ return hash.ToHashCode();
+ }
+
+ private bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append("Names = ").Append(ValueLists.Names(Names)).Append(", More = ").Append(More);
+ return true;
+ }
+ }
+
+ /// 찾는 말과 거르기에 남은 카드.
+ /// 남은 카드.
+ ///
+ /// 찾는 말과 거르기에 맞는 소리(표 순서). 기본 모드는 이 소리를 미리보기 앞에 두고(),
+ /// 고급 모드는 이 소리만 줄로 보인다.
+ ///
+ public sealed record ShownCard(SoundCard Card, IReadOnlyList Shown)
+ {
+ public bool Equals(ShownCard? other) =>
+ other is not null && Card.Equals(other.Card) && ValueLists.SameItems(Shown, other.Shown);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(Card);
+ ValueLists.AddItems(ref hash, Shown);
+ return hash.ToHashCode();
+ }
+
+ private bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append("Card = ").Append(Card.Id).Append(", Shown = ").Append(ValueLists.Names(Shown));
+ return true;
+ }
+ }
+
+ /// 남은 카드가 있는 구역. 남은 카드가 없는 구역은 목록에 넣지 않는다.
+ public sealed record ShownSection(SoundType Type, IReadOnlyList Cards)
+ {
+ public bool Equals(ShownSection? other) =>
+ other is not null && Type == other.Type && ValueLists.SameItems(Cards, other.Cards);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(Type);
+ ValueLists.AddItems(ref hash, Cards);
+ return hash.ToHashCode();
+ }
+
+ private bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append("Type = ").Append(Type).Append(", Cards = [").AppendJoin(", ", Cards).Append(']');
+ return true;
+ }
+ }
+
+ ///
+ /// 고급 모드 목록의 한 칸. 는 목록의 키로, 칸의 종류마다 앞 글자를 달리해 서로 겹치지 않는다.
+ ///
+ public abstract record AdvancedItem
+ {
+ private AdvancedItem()
+ {
+ }
+
+ public abstract string Key { get; }
+
+ /// 구역 머리. 그 아래 묶음들의 기본 종류다.
+ public sealed record SectionHeading(SoundType Type) : AdvancedItem
+ {
+ public override string Key => "s:" + Type.ToLabel();
+ }
+
+ /// 묶음 머리. 그 아래 줄들이 이 카드의 소리다.
+ public sealed record GroupHeading(SoundCard Card) : AdvancedItem
+ {
+ public override string Key => "g:" + Card.Id;
+ }
+
+ /// 소리 한 줄.
+ public sealed record Row(SoundEntry Entry) : AdvancedItem
+ {
+ public override string Key => "r:" + Entry.Index.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ /// 목록을 든 레코드의 같음과 해시, 표시.
+ internal static class ValueLists
+ {
+ public static bool SameItems(IReadOnlyList a, IReadOnlyList b) => ReferenceEquals(a, b) || a.SequenceEqual(b);
+
+ public static void AddItems(ref HashCode hash, IReadOnlyList items)
+ {
+ hash.Add(items.Count);
+ foreach (T item in items) hash.Add(item);
+ }
+
+ public static string Names(IEnumerable entries) => "[" + string.Join(", ", entries.Select(entry => entry.Name)) + "]";
+ }
+}
diff --git a/SoundVisualizer.Core/Sounds/SoundCatalog.cs b/SoundVisualizer.Core/Sounds/SoundCatalog.cs
new file mode 100644
index 0000000..332f6bc
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/SoundCatalog.cs
@@ -0,0 +1,317 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Frozen;
+using System.Globalization;
+using System.Text;
+using SoundVisualizer.Ai;
+
+namespace SoundVisualizer.Sounds
+{
+ ///
+ /// 분류 탭의 한 줄. AI 가 알아듣는 소리 하나와 그 소리의 기본 종류. 모바일 SoundEntry(SoundCatalog.kt)와 같다.
+ ///
+ /// YAMNet 클래스 번호. 언어마다의 소리 이름(Data/SoundNames)도 이 순서다.
+ ///
+ /// 모델이 쓰는 영어 이름. 사용자가 고른 종류를 저장하는 키이고, AI()가 받는 이름과 같아야 해서
+ /// 번역된 이름이 아니라 이것을 쓴다.
+ ///
+ /// 키워드 규칙()만으로 정한 종류.
+ /// 이 소리가 든 묶음(Data/sound_groups.tsv, #349). 표에 없으면 null 이고, 그 소리 하나가 카드가 된다.
+ /// 묶음 화면에서 보이는 자리. 표의 줄 순서이고, 표에 없는 소리는 표 뒤에 번호 순서로 온다.
+ public sealed record SoundEntry(int Index, string Name, SoundType DefaultType, SoundGroup? Group, int Order);
+
+ /// 분류 탭의 거르기. 종류는 사용자가 바꾼 것까지 반영한 지금 종류로 본다.
+ public enum SoundFilter
+ {
+ All,
+ Ambient,
+ Speech,
+ Danger,
+
+ /// 바꾼 소리. 지금 종류가 기본 종류와 다른 소리다.
+ Changed,
+ }
+
+ ///
+ /// 분류 탭의 소리 목록과 찾기·거르기. 모바일 SoundCatalog.kt 와 같다. 화면을 모르므로 WPF 없이 테스트한다.
+ ///
+ /// 사용자가 바꾼 종류(overrides)는 설정에 저장하는 그대로 받는다. 키는 모델의 영어 이름, 값은 의
+ /// 문자열이고, 이 받는 것과 같은 맵이다.
+ ///
+ public static class SoundCatalog
+ {
+ ///
+ /// 모델의 소리 이름(번호 순서)과 묶음 표(Data/sound_groups.tsv)로 목록을 만든다. 기본 종류는 AI 의 매핑이다.
+ /// 는 AI 가 읽는 클래스 목록()을 그대로 넘긴다.
+ ///
+ public static IReadOnlyList Load(IReadOnlyList classNames) => FromNames(classNames, SoundData.GroupTable);
+
+ ///
+ /// 모델의 소리 이름(번호 순서)으로 목록을 만든다.
+ ///
+ /// 모델의 소리 이름. 번호 순서다.
+ /// 소리 이름별 묶음(). 표에 없는 소리는 묶음이 없다.
+ /// 기본 종류. AI 의 매핑이 바뀐 경우를 테스트가 흉내 낼 때만 바꾼다.
+ public static IReadOnlyList FromNames(
+ IReadOnlyList names,
+ IReadOnlyDictionary? groups = null,
+ Func? defaultOf = null)
+ {
+ ArgumentNullException.ThrowIfNull(names);
+ defaultOf ??= YamnetThreeClassMapper.MapDisplayNameToCoarse;
+ int afterTable = (groups is { Count: > 0 } ? groups.Values.Max(row => row.Order) : -1) + 1;
+ var entries = new SoundEntry[names.Count];
+ for (int index = 0; index < entries.Length; index++)
+ {
+ string name = names[index];
+ SoundGroupRow? row = groups is not null && groups.TryGetValue(name, out var found) ? found : null;
+ entries[index] = new SoundEntry(index, name, defaultOf(name), row?.Group, row?.Order ?? afterTable + index);
+ }
+ return Array.AsReadOnly(entries);
+ }
+
+ ///
+ /// 분류 탭의 색인을 넣어 둔 데이터로 만든다. 이름은 의 것이고, 없는 언어나 빠진 이름은 영어다.
+ /// 이름을 1,042번 다듬으므로 화면 스레드에서 부르지 않는다( 가 스레드 풀에서 부른다).
+ ///
+ public static SoundIndex BuildIndex(string? language, IReadOnlyList entries)
+ {
+ string code = SoundLanguages.Resolve(language);
+ return BuildIndex(code, entries, SoundData.Names(code), SoundData.GroupTitles(code), SoundData.GroupTitles(SoundLanguages.English));
+ }
+
+ ///
+ /// 색인 하나를 만든다. 시간은 거의 찾기 값을 만드는 데(소리마다 두 이름을 ) 든다.
+ ///
+ /// 색인의 언어. 캐시의 키다.
+ /// 소리 목록().
+ /// 그 언어의 소리 이름. 클래스 번호 순서다. 모자라면 모델 이름을 쓴다.
+ /// 그 언어의 새로 지은 묶음 이름.
+ /// 영어의 새로 지은 묶음 이름. 그 이름으로도 찾게 하고, 그 언어에 빠진 이름을 채운다.
+ public static SoundIndex BuildIndex(
+ string language,
+ IReadOnlyList entries,
+ IReadOnlyList labels,
+ IReadOnlyDictionary titles,
+ IReadOnlyDictionary englishTitles)
+ {
+ ArgumentNullException.ThrowIfNull(language);
+ ArgumentNullException.ThrowIfNull(entries);
+ ArgumentNullException.ThrowIfNull(labels);
+ ArgumentNullException.ThrowIfNull(titles);
+ ArgumentNullException.ThrowIfNull(englishTitles);
+
+ string English(SoundGroup group) => englishTitles.TryGetValue(group, out string? title) ? title : group.Id();
+ string Local(SoundGroup group) => titles.TryGetValue(group, out string? title) ? title : English(group);
+
+ var localTitles = new Dictionary();
+ foreach (SoundGroup group in SoundGroupInfo.All)
+ {
+ if (group.HasOwnTitle()) localTitles[group] = Local(group);
+ }
+
+ var searchKeys = new string[entries.Count];
+ for (int i = 0; i < searchKeys.Length; i++)
+ {
+ SoundEntry entry = entries[i];
+ searchKeys[i] = SearchKey(SoundIndex.LabelOf(labels, entry), entry.Name);
+ }
+
+ return new SoundIndex(
+ language,
+ entries,
+ labels,
+ Array.AsReadOnly(searchKeys),
+ SoundGroups.Sections(entries),
+ localTitles.ToFrozenDictionary(),
+ SoundGroups.TitleKeys(Local, English));
+ }
+
+ /// 사용자가 바꾼 것까지 반영한 지금 종류. 세 라벨이 아닌 값은 AI 처럼() 없는 것으로 본다.
+ public static SoundType TypeOf(SoundEntry entry, IReadOnlyDictionary overrides) =>
+ overrides.TryGetValue(entry.Name, out string? label) && SoundTypeLabels.TryParse(label, out SoundType type)
+ ? type
+ : entry.DefaultType;
+
+ ///
+ /// 모델 이름의 클래스 번호. 모델 이름은 서로 겹치지 않는다. 글자 그대로(대소문자 포함) 맞춰 보고, 목록에 없으면 -1.
+ ///
+ public static int IndexOf(IReadOnlyList entries, string? name)
+ {
+ foreach (SoundEntry entry in entries)
+ {
+ if (string.Equals(entry.Name, name, StringComparison.Ordinal)) return entry.Index;
+ }
+ return -1;
+ }
+
+ ///
+ /// 찾는 말과 거르기에 맞는 줄만 남긴다. 순서는 그대로다.
+ ///
+ /// 줄마다 로 만든 값. 입력할 때마다 521개 이름을 다시 다듬지 않게 미리 만든다.
+ public static IReadOnlyList Filter(
+ IReadOnlyList entries,
+ IReadOnlyList searchKeys,
+ IReadOnlyDictionary overrides,
+ string query,
+ SoundFilter filter)
+ {
+ string needle = Normalize(query);
+ var result = new List();
+ for (int i = 0; i < entries.Count; i++)
+ {
+ SoundEntry entry = entries[i];
+ if (Passes(entry, overrides, filter) &&
+ (needle.Length == 0 || searchKeys[i].Contains(needle, StringComparison.Ordinal)))
+ {
+ result.Add(entry);
+ }
+ }
+ return result.AsReadOnly();
+ }
+
+ ///
+ /// "바꾼 소리" 수. 묶음이 아니라 소리를 센다(묶음 하나를 바꾸면 그 소리 수만큼 는다, #374). 저장값에는 지금 목록에 없는
+ /// 이름이나 기본 종류와 같아진 값이 남아 있을 수 있어, "바꾼 소리" 거르기와 같은 기준으로 목록에서 센다.
+ ///
+ public static int CountChanged(IReadOnlyList entries, IReadOnlyDictionary overrides)
+ {
+ int count = 0;
+ foreach (SoundEntry entry in entries)
+ {
+ if (TypeOf(entry, overrides) != entry.DefaultType) count++;
+ }
+ return count;
+ }
+
+ /// 가 거르기에 맞는지. 묶음 화면()도 소리마다 같은 기준으로 본다.
+ internal static bool Passes(SoundEntry entry, IReadOnlyDictionary overrides, SoundFilter filter)
+ {
+ SoundType type = TypeOf(entry, overrides);
+ return filter switch
+ {
+ SoundFilter.All => true,
+ SoundFilter.Ambient => type == SoundType.Ambient,
+ SoundFilter.Speech => type == SoundType.Speech,
+ SoundFilter.Danger => type == SoundType.Danger,
+ SoundFilter.Changed => type != entry.DefaultType,
+ _ => false,
+ };
+ }
+
+ ///
+ /// 찾기에 쓰는 값. 보이는 이름과 모델 이름 어느 쪽으로 찾아도 맞게 둘을 잇는다. 줄바꿈으로 이어서, 찾는 말(줄바꿈이 없다)이
+ /// 둘이 맞닿은 곳에서 맞지 않는다. 영어 이름을 함께 넣는 것은 어느 화면 언어에서도 개발자 모드(영어)에서 본 이름으로 찾게 하려는 것이다.
+ ///
+ public static string SearchKey(string label, string name) => Normalize(label) + "\n" + Normalize(name);
+
+ ///
+ /// 대소문자, 띄어쓰기, 악센트 같은 결합 부호를 가리지 않는다. "fire truck" 은 "Firetruck" 으로도, "sirène" 은 "sirene" 으로도 찾힌다.
+ ///
+ /// 문장 부호(쉼표, 가운뎃점 '·'·'・', 붙임표, 괄호)도 띄어쓰기처럼 뺀다. 묶음 이름은 낱말을 이런 부호로 잇는데("부엌·요리",
+ /// "TV, radio and …"), 사람은 띄어 쳐서 찾는다. 부호를 남겨 두면 "부엌 요리" 처럼 이어진 두 낱말로는 아무것도 찾지 못했다(#349).
+ ///
+ /// 한글은 낱자로 풀어 첫소리와 받침을 같은 글자로 본다. 글자를 치는 중에는 입력 칸에 "ㅅ", "상"(다음 글자의 첫소리가 받침으로
+ /// 붙은 모양), "홪" 같은 모양이 잠깐 들어오는데, 음절째 비교하면 한 글자 칠 때마다 결과가 사라졌다 나타난다. 겹받침·겹모음(ㄺ, ㅘ)도
+ /// 낱자 둘로 푼다.
+ ///
+ /// 모바일과 같은 차례다(NFD → 결합 부호(Mn) 빼기 → 띄어쓰기·문장 부호(P) 빼기 → 소문자 → 한글 낱자). 띄어쓰기는 유니코드의 것
+ /// 모두(모바일 앱의 ICU 정규식과 같다)이고, 소문자는 문화권을 타지 않는다.
+ ///
+ internal static string Normalize(string text)
+ {
+ string decomposed = Decompose(text);
+ var builder = new StringBuilder(decomposed.Length);
+ Span buffer = stackalloc char[2];
+ foreach (Rune rune in decomposed.EnumerateRunes())
+ {
+ UnicodeCategory category = Rune.GetUnicodeCategory(rune);
+ if (category == UnicodeCategory.NonSpacingMark || IsPunctuation(category) || Rune.IsWhiteSpace(rune)) continue;
+
+ Rune lower = Rune.ToLowerInvariant(rune);
+ if (lower.IsBmp && HangulLetters.TryGetValue((char)lower.Value, out string? letters))
+ {
+ builder.Append(letters);
+ }
+ else
+ {
+ builder.Append(buffer[..lower.EncodeToUtf16(buffer)]);
+ }
+ }
+ return builder.ToString();
+ }
+
+ ///
+ /// NFD 로 푼다. 짝 없는 서로게이트가 든 글(붙여 넣은 글 등)은 .NET 의 정규화가 받지 않으므로 대체 문자(U+FFFD)로 바꿔 다시 한다.
+ /// 모바일(Java)은 그 글자를 그대로 두지만, 어느 쪽이든 실제 이름과는 맞지 않는다.
+ ///
+ private static string Decompose(string text)
+ {
+ try
+ {
+ return text.Normalize(NormalizationForm.FormD);
+ }
+ catch (ArgumentException)
+ {
+ var sanitized = new StringBuilder(text.Length);
+ foreach (Rune rune in text.EnumerateRunes()) sanitized.Append(rune.ToString());
+ return sanitized.ToString().Normalize(NormalizationForm.FormD);
+ }
+ }
+
+ private static bool IsPunctuation(UnicodeCategory category) => category is
+ UnicodeCategory.ConnectorPunctuation or
+ UnicodeCategory.DashPunctuation or
+ UnicodeCategory.OpenPunctuation or
+ UnicodeCategory.ClosePunctuation or
+ UnicodeCategory.InitialQuotePunctuation or
+ UnicodeCategory.FinalQuotePunctuation or
+ UnicodeCategory.OtherPunctuation;
+
+ /// 한글 낱자(첫소리·가운뎃소리·받침, 겹자모)를 입력 칸에 보이는 호환용 낱자로. NFD 가 음절을 이 낱자들로 푼다.
+ private static readonly FrozenDictionary HangulLetters = BuildHangulLetters();
+
+ private static FrozenDictionary BuildHangulLetters()
+ {
+ var map = new Dictionary();
+ const string initials = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ";
+ for (int i = 0; i < initials.Length; i++) map[(char)('ᄀ' + i)] = initials[i].ToString();
+
+ string[] vowels =
+ {
+ "ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅗㅏ", "ㅗㅐ", "ㅗㅣ", "ㅛ", "ㅜ", "ㅜㅓ", "ㅜㅔ", "ㅜㅣ",
+ "ㅠ", "ㅡ", "ㅡㅣ", "ㅣ",
+ };
+ for (int i = 0; i < vowels.Length; i++) map[(char)('ᅡ' + i)] = vowels[i];
+
+ string[] finals =
+ {
+ "ㄱ", "ㄲ", "ㄱㅅ", "ㄴ", "ㄴㅈ", "ㄴㅎ", "ㄷ", "ㄹ", "ㄹㄱ", "ㄹㅁ", "ㄹㅂ", "ㄹㅅ", "ㄹㅌ", "ㄹㅍ", "ㄹㅎ",
+ "ㅁ", "ㅂ", "ㅂㅅ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ",
+ };
+ for (int i = 0; i < finals.Length; i++) map[(char)('ᆨ' + i)] = finals[i];
+
+ // 치는 중에 입력 칸에 보이는 호환용 겹자모
+ map['ㄳ'] = "ㄱㅅ";
+ map['ㄵ'] = "ㄴㅈ";
+ map['ㄶ'] = "ㄴㅎ";
+ map['ㄺ'] = "ㄹㄱ";
+ map['ㄻ'] = "ㄹㅁ";
+ map['ㄼ'] = "ㄹㅂ";
+ map['ㄽ'] = "ㄹㅅ";
+ map['ㄾ'] = "ㄹㅌ";
+ map['ㄿ'] = "ㄹㅍ";
+ map['ㅀ'] = "ㄹㅎ";
+ map['ㅄ'] = "ㅂㅅ";
+ map['ㅘ'] = "ㅗㅏ";
+ map['ㅙ'] = "ㅗㅐ";
+ map['ㅚ'] = "ㅗㅣ";
+ map['ㅝ'] = "ㅜㅓ";
+ map['ㅞ'] = "ㅜㅔ";
+ map['ㅟ'] = "ㅜㅣ";
+ map['ㅢ'] = "ㅡㅣ";
+ return map.ToFrozenDictionary();
+ }
+ }
+}
diff --git a/SoundVisualizer.Core/Sounds/SoundData.cs b/SoundVisualizer.Core/Sounds/SoundData.cs
new file mode 100644
index 0000000..13eb694
--- /dev/null
+++ b/SoundVisualizer.Core/Sounds/SoundData.cs
@@ -0,0 +1,136 @@
+// Copyright (C) 2026 amophi (SoundVisualizer Contributors)
+// This file is part of SoundVisualizer.
+// SoundVisualizer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
+using System.Collections.Concurrent;
+using System.Collections.Frozen;
+using System.Text.Json;
+
+namespace SoundVisualizer.Sounds
+{
+ ///
+ /// 분류 탭의 데이터. Core 에 리소스로 넣어 둔 파일(Sounds/Data)을 읽는다. 모바일 판의 res/values*/sound_names.xml,
+ /// strings.xml 의 sound_group_* 문구, assets/sound_groups.tsv 를 옮긴 것이다.
+ ///
+ /// - SoundNames/{언어}.json: 소리 이름 521개의 배열(YAMNet 번호 순서). 안드로이드 이스케이프(\' 와 &)는 풀어 두었다.
+ /// AudioSet 온톨로지의 각색물이라 CC BY-SA 4.0 이다(Data/NOTICE.txt).
+ /// - GroupTitles/{언어}.json: 새로 지은 묶음 이름 15개(묶음 id → 이름).
+ /// - sound_groups.tsv: 소리마다의 묶음. CC BY-SA 4.0 머리글을 그대로 둔다.
+ ///
+ /// 언어마다 한 번 읽어 두고 같은(바꿀 수 없는) 목록을 돌려준다. 어느 스레드에서 불러도 된다.
+ ///
+ public static class SoundData
+ {
+ private const string Prefix = "SoundVisualizer.Sounds.Data.";
+ private const string NamesFolder = "SoundNames.";
+ private const string TitlesFolder = "GroupTitles.";
+ private const string Extension = ".json";
+
+ private static readonly ConcurrentDictionary> NamesByLanguage = new(StringComparer.Ordinal);
+
+ private static readonly ConcurrentDictionary> TitlesByLanguage =
+ new(StringComparer.Ordinal);
+
+ private static readonly Lazy> Table = new(() =>
+ {
+ using Stream stream = OpenGroupTable();
+ return SoundGroups.ParseTable(stream);
+ });
+
+ ///
+ ///