diff --git a/README.ko.md b/README.ko.md index a8988d9..8f1050d 100644 --- a/README.ko.md +++ b/README.ko.md @@ -8,7 +8,7 @@ SoundVisualizer -SoundVisualizer 오버레이: 환경음, 대화음, 위협음, 왼쪽에서 나는 소리, 오른쪽에서 나는 소리 +SoundVisualizer 오버레이: 환경음, 사람 소리, 위협음, 왼쪽에서 나는 소리, 오른쪽에서 나는 소리 [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![WPF](https://img.shields.io/badge/WPF-blue?style=for-the-badge&logo=windows)](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/) @@ -23,9 +23,9 @@
> **SoundVisualizer**는 컴퓨터의 시스템 오디오를 실시간으로 캡처하여 방향·크기·의미를 그래픽 오버레이로 변환하는 데스크톱 애플리케이션입니다. -단순한 파형 시각화를 넘어, **YAMNet**과 **ONNX Runtime** 기반의 AI 사운드 분류기를 내장하여 현재 재생 중인 소리의 종류(배경음, 대화음, 위협음 등)를 실시간으로 인지하고 피드백을 제공합니다. +단순한 파형 시각화를 넘어, **YAMNet**과 **ONNX Runtime** 기반의 AI 사운드 분류기를 내장하여 현재 재생 중인 소리의 종류(환경음, 사람 소리, 위협음)를 실시간으로 인지하고 피드백을 제공합니다. -> 📱 안드로이드 버전도 있습니다: [SoundVisualizer for Android](https://github.com/bibibic76/SoundVisualizer) +> 📱 안드로이드 버전도 있습니다: [SoundVisualizer for Android](https://github.com/SOVIS-Team/SoundVisualizer-Android) --- @@ -59,6 +59,7 @@ - **패드 모드 (Pad)**: 지정된 공간 그리드(방향)에 고정된 패드 형태로 소리를 표현합니다. - **외곽선 모드 (Outline)**: 파도 모드를 기반으로, 파도의 상한 외곽선 테두리만 표현하여 화면 가림을 최소화합니다. - 30 FPS부터 모니터 주사율까지, 최대 프레임을 얼마로 두어도 그래픽이 같은 빠르기로 움직입니다. +- **쉬운 설정**: 설정 탭은 그림으로 선택하는 화면으로 열립니다. 모양, 크기(작게·보통·크게), 보여 줄 소리 종류를 선택하고, 나머지 설정은 ‘자세한 설정’에서 바꿉니다. ### 2. 온스크린 실시간 제어 및 에디터 전체 화면 게임을 내리지 않고도 오버레이 상태에서 즉각적인 수정이 가능합니다. @@ -71,14 +72,15 @@ - 스테레오 출력에서는 좌우에 함께 있는 소리를 화면 위쪽에, 좌우 차이만 옆쪽에 그립니다. 그래서 가운데에서 나는 소리가 양쪽에서 나는 소리처럼 보이지 않습니다. ### 4. AI 실시간 사운드 분류 (ONNX & YAMNet) -- **3-Class 분류 체계**: 모든 소리를 `Ambient(환경음)`, `Speech(대화음)`, `Danger(위협음)` 3가지로 분석하여 카테고리별로 지정된 UI 색상과 투명도로 동적 시각 피드백을 제공합니다. 색상과 투명도는 커스터마이징이 가능합니다. +- **3-Class 분류 체계**: 모든 소리를 `Ambient(환경음)`, `Human sounds(사람 소리)`, `Danger(위협음)` 3가지로 분석하여 카테고리별로 지정된 UI 색상과 투명도로 동적 시각 피드백을 제공합니다. 색상과 투명도는 커스터마이징이 가능합니다. - **위협 단서**: 사이렌, 경보, 폭발음, 폭죽 같은 경고음은 모델의 상위 후보 다섯 개 안에(5% 이상) 들기만 해도 `위협음`으로 봅니다. 음악이나 게임 소리에 묻혀도 놓치지 않기 위해서입니다. 총소리 이름은 오경보를 막기 위해 이렇게 끌어올리지 않습니다. -- **흔들리지 않는 색**: `위협음`은 조건을 넘는 첫 결과에 바로 바뀌고, `대화음`과 `환경음`은 두 번 연달아 같아야 바뀌어 색이 깜빡이지 않습니다. 조용한 동안에는 AI 가 쉬고, 소리가 나면 바로 다시 듣습니다. +- **흔들리지 않는 색**: `위협음`은 조건을 넘는 첫 결과에 바로 바뀌고, `사람 소리`와 `환경음`은 두 번 연달아 같아야 바뀌어 색이 깜빡이지 않습니다. 조용한 동안에는 AI 가 쉬고, 소리가 나면 바로 다시 듣습니다. +- **더 빠른 사람 소리**: 말소리가 시작되면 작은 음성 감지 모델(Silero VAD)이 새로 들어온 소리를 확인해 `사람 소리`를 한 박자(약 0.25초) 먼저 보여 줍니다. 화면 표시만 바꾸며, `위협음`과 분류 탭에서 정한 종류가 늘 우선합니다. - **모든 채널을 듣습니다**: 5.1/7.1 환경에서는 앞쪽뿐 아니라 뒤쪽·옆쪽 채널의 소리도 AI 가 듣습니다. - **늦게 온 위협음**: `위협음` 판정이 소리보다 조금 늦게 와도, 그 소리를 나던 때의 크기로 그립니다. 앞서 숨겨 둔 종류였던 소리도 그렇습니다. ### 5. 소리마다 종류 정하기 (분류 탭) -- AI 가 알아듣는 소리 521개를 안드로이드 버전과 같은 묶음으로 보여 주고, 묶음마다(고급 모드에서는 소리마다) `환경음`, `대화음`, `위협음` 가운데 무엇으로 볼지 선택합니다. +- AI 가 알아듣는 소리 521개를 안드로이드 버전과 같은 묶음으로 보여 주고, 묶음마다(고급 모드에서는 소리마다) `환경음`, `사람 소리`, `위협음` 가운데 무엇으로 볼지 선택합니다. - 이름으로 찾고, 종류나 바꾼 소리로 거르고, 한 번에 모두 되돌릴 수 있습니다. 바꾼 것은 오버레이의 AI 에 바로 반영됩니다. ### 6. 도움말, 안내, 트레이 아이콘 @@ -111,7 +113,7 @@ SoundVisualizer는 설치가 필요 없는 가벼운 **포터블(Portable)** 패키지로 제공됩니다. Windows 10/11 (64비트)에서 돌아가며, .NET 을 따로 설치하지 않아도 됩니다. -1. **[GitHub Releases](https://github.com/amophi/SoundVisualizer/releases)** 페이지에 접속합니다. +1. **[GitHub Releases](https://github.com/SOVIS-Team/SoundVisualizer-Windows/releases)** 페이지에 접속합니다. 2. 최신 버전의 `SoundVisualizer_vX.Y.zip` 파일을 다운로드해 원하는 곳에 압축 해제합니다. 3. 폴더 내의 **`SoundVisualizer.exe`**를 실행합니다. 4. 런처에서 언어 및 초기 설정을 마친 후, **실행**을 눌러 오버레이를 켭니다. @@ -127,7 +129,7 @@ SoundVisualizer는 설치가 필요 없는 가벼운 **포터블(Portable)** 패 1. **요구 사항**: Windows 10/11, .NET 10 SDK. Visual Studio 2026 (.NET 데스크톱 개발 워크로드)은 있어도 되고 없어도 됩니다. 2. **리포지토리 클론**: ```bash - git clone https://github.com/amophi/SoundVisualizer.git + git clone https://github.com/SOVIS-Team/SoundVisualizer-Windows.git ``` 3. 리포지토리 폴더에서 빌드하고 테스트합니다. ```bash diff --git a/README.md b/README.md index c6f08de..9f37e2f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ SoundVisualizer -SoundVisualizer overlay: ambient, speech, danger, sound from the left, and sound from the right +SoundVisualizer overlay: ambient, human sounds, danger, sound from the left, and sound from the right [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![WPF](https://img.shields.io/badge/WPF-blue?style=for-the-badge&logo=windows)](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/) @@ -23,9 +23,9 @@
> **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. +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, human sounds, danger) and provide adaptive visual feedback. -> 📱 There is also an Android version: [SoundVisualizer for Android](https://github.com/bibibic76/SoundVisualizer). +> 📱 There is also an Android version: [SoundVisualizer for Android](https://github.com/SOVIS-Team/SoundVisualizer-Android). --- @@ -59,6 +59,7 @@ We provide 4 unique rendering modes that intuitively map sound intensity and dir - **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. +- **Easy settings**: The Settings tab opens with a picture-based view where you choose the shape, the size (Small, Medium, Large), and which sound types to show. Switch to “Detailed” for every other setting. ### 2. On-Screen Real-Time Editor (F4) Modify settings instantly while in a full-screen application or game without minimizing the window. @@ -71,14 +72,15 @@ Modify settings instantly while in a full-screen application or game without min - 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. +- **3-Class Detection**: Analyzes all incoming audio into `Ambient`, `Human sounds`, and `Danger` categories. Each category triggers independent, customizable UI colors and dynamic opacity changes. - **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. +- **Stable colors**: `Danger` appears on the first qualifying result; `Human sounds` 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. +- **Faster human sounds**: When a voice starts, a small voice detector (Silero VAD) checks the newest audio and can show `Human sounds` one step (about 0.25 s) earlier. It only changes what you see: `Danger` and your choices in the Sounds tab always win. - **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`. +- 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`, `Human sounds`, 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 @@ -111,7 +113,7 @@ The project is rigorously engineered for high-performance, real-time background 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. +1. Go to the **[GitHub Releases](https://github.com/SOVIS-Team/SoundVisualizer-Windows/releases)** page. 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. @@ -127,7 +129,7 @@ To build from source to modify the code or contribute to the project: 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 + git clone https://github.com/SOVIS-Team/SoundVisualizer-Windows.git ``` 3. Build and test from the repository folder: ```bash diff --git a/SoundVisualizer.Core/Ai/AiAudioBuffer.cs b/SoundVisualizer.Core/Ai/AiAudioBuffer.cs index e9e9937..2937276 100644 --- a/SoundVisualizer.Core/Ai/AiAudioBuffer.cs +++ b/SoundVisualizer.Core/Ai/AiAudioBuffer.cs @@ -30,6 +30,10 @@ public sealed class AiAudioBuffer private readonly int _windowSamples; private int _tail; private int _count; + // 말소리 미리보기(SileroStreamWorker)가 링의 어디까지 읽었는지 맞춰 보는 값. 프레임은 이 세대에 들어온 모노 프레임 수이고, + // 세대는 Reset 마다 하나씩 올라 이전 캡처의 소리를 새 캡처의 소리로 읽지 않게 한다(Android #422). + private long _totalFrames; + private long _generation; /// 섞은 모노를 링에 넣기 전에 잠깐 두는 곳. 버퍼마다 할당하지 않으려고 다시 쓴다. private readonly float[] _monoScratch = new float[MaxRingSize / 2]; @@ -73,6 +77,43 @@ public void Reset() { _tail = 0; _count = 0; + _totalFrames = 0; + _generation++; + } + } + + /// 링의 세대와 이 세대에 들어온 모노 프레임 수. 어느 스레드에서나 부른다. + internal readonly record struct StreamCursor(long Generation, long Frames); + + /// 지금의 세대와 들어온 프레임 수(Android 의 cursor()). + internal StreamCursor Cursor() + { + lock (_lock) return new StreamCursor(_generation, _totalFrames); + } + + /// + /// 이 세대의 절대 프레임 [, + .Length) 를 복사한다 + /// (Android 의 copyRange). 0 보다 앞의 프레임은 0 이다. 세대가 다르거나, 아직 다 들어오지 않았거나, 이미 2초 밖으로 밀려났으면 + /// 복사하지 않고 false 를 돌려준다. 그래서 미래의 소리나 덮어쓴 소리, 이전 캡처의 소리를 새 소리로 읽지 않는다. + /// 모델을 기다리지 않고 잠금 안에서 복사만 한다. + /// + internal bool CopyRange(long generation, long first, Span destination) + { + lock (_lock) + { + long size = destination.Length; + if (generation != _generation || first + size > _totalFrames || Math.Max(first, 0L) < _totalFrames - _count) return false; + + int leading = (int)Math.Clamp(-first, 0L, size); + destination[..leading].Clear(); + int remaining = destination.Length - leading; + if (remaining == 0) return true; + // 링 끝(_tail)이 프레임 _totalFrames 다. 2의 거듭제곱 마스크라 음수도 링 안의 자리로 돈다. + int ringStart = (int)((_tail - (_totalFrames - (first + leading))) & _ringMask); + int firstPart = Math.Min(remaining, _ring.Length - ringStart); + _ring.AsSpan(ringStart, firstPart).CopyTo(destination[leading..]); + _ring.AsSpan(0, remaining - firstPart).CopyTo(destination[(leading + firstPart)..]); + return true; } } @@ -108,6 +149,7 @@ private void AppendMono(ReadOnlySpan monoChunk) { lock (_lock) { + _totalFrames += monoChunk.Length; for (int i = 0; i < monoChunk.Length; i++) { _ring[_tail] = monoChunk[i]; diff --git a/SoundVisualizer.Core/Ai/AiClassificationResult.cs b/SoundVisualizer.Core/Ai/AiClassificationResult.cs index 05fd2b9..0f43dd2 100644 --- a/SoundVisualizer.Core/Ai/AiClassificationResult.cs +++ b/SoundVisualizer.Core/Ai/AiClassificationResult.cs @@ -53,5 +53,33 @@ public sealed class AiClassificationResult /// (). /// internal FrozenDictionary MappingInputs { get; init; } = FrozenDictionary.Empty; + + /// + /// 말소리 미리보기로 한 박자 먼저 보이는 화면 전용 결과인지(Android #422). 확정 결과()는 + /// 늘 false 이고, 화면용 만 true 인 결과를 줄 수 있다. + /// + public bool SpeechPreview { get; init; } + + /// + /// 이 확정 결과(환경음)를 부모로 한 미리보기(Android 의 parent.copy(coarse = "speech", ..., speechPreview = true)). 종류는 대화음, + /// 이름과 확신도는 부모 프레임의 대화음 후보 값, 시각은 미리보기를 만든 때다. 상위 5개·소요시간·매핑은 부모 그대로 둔다. + /// + internal AiClassificationResult AsSpeechPreview(string display, float confidence, long timestampMs) => new() + { + Coarse = SoundType.Speech, + Display = display, + Confidence = confidence, + Top5 = Top5, + DangerCuePromoted = DangerCuePromoted, + MeetsThreshold = MeetsThreshold, + TimestampMs = timestampMs, + PreprocessMs = PreprocessMs, + YamnetMs = YamnetMs, + TotalMs = TotalMs, + MappingOverrideCount = MappingOverrideCount, + MappingSignature = MappingSignature, + MappingInputs = MappingInputs, + SpeechPreview = true, + }; } } diff --git a/SoundVisualizer.Core/Ai/AiClassificationStage.cs b/SoundVisualizer.Core/Ai/AiClassificationStage.cs index 69f28e2..e876101 100644 --- a/SoundVisualizer.Core/Ai/AiClassificationStage.cs +++ b/SoundVisualizer.Core/Ai/AiClassificationStage.cs @@ -33,6 +33,14 @@ public AiClassificationStage(YamnetClassMap classes) /// 지금 쓰는 매핑 스냅샷. public YamnetMappingPolicy Mapping => _mapping; + /// + /// 마지막 의 프레임이 첫 대화음 후보였으면 그 이름과 확신도, 아니면 null. + /// 첫 후보란 이번 프레임이 기준을 넘은 대화음이고, 확정은 아직 환경음이며, 대화음으로 바뀌려고 한 프레임째 기다리는 것이다 + /// (다음 틱에서 같은 대화음이 오면 확정된다). 사용자 지정이 하나라도 있으면 늘 null 이다. 말소리 미리보기가 이 후보를 확인한다 + /// (Android RealtimeAiPipeline 의 Silero 후보 조건과 같다, #422). + /// + internal SpeechOnsetFrame? SpeechOnset { get; private set; } + /// /// 설정의 사용자 지정(영어 클래스 이름 → "ambient"/"speech"/"danger")을 반영한다. 추론 틱마다 먼저 부르고, 무음이라 추론을 /// 건너뛰는 틱에도 부른다. 그래야 이전 매핑에서 확정한 위협음이 소리가 다시 날 때 새 매핑으로 넘어오지 않는다. @@ -50,6 +58,7 @@ public bool UpdateMapping(IReadOnlyDictionary overrides) if (restarted) { _postProcessor.Reset(); + SpeechOnset = null; Volatile.Write(ref _lastResult, null); } // 입력을 복사해 둔다. 설정 쪽 맵이 나중에 바뀌어도 이 결과가 어떤 입력으로 만들어졌는지는 그대로 남는다. @@ -100,6 +109,13 @@ public AiClassificationResult Process(ReadOnlySpan probabilities, long st MappingSignature = mapping.Signature, MappingInputs = _previousOverrides, }; + SpeechOnset = decision.PostCoarse == SoundType.Speech && + post.MeetsThreshold && + post.UiCoarse == SoundType.Ambient && + post.CandidateStreak == 1 && + _previousOverrides.Count == 0 + ? new SpeechOnsetFrame(decision.PostDisplay, decision.PostConfidence) + : null; Volatile.Write(ref _lastResult, result); return result; } @@ -119,6 +135,7 @@ public AiClassificationResult Process(ReadOnlySpan probabilities, long st public void Reset() { _postProcessor.Reset(); + SpeechOnset = null; Volatile.Write(ref _lastResult, null); } @@ -134,4 +151,7 @@ private static bool SameEntries(FrozenDictionary snapshot, IRead return true; } } + + /// 첫 대화음 후보 프레임의 이름(영어 클래스 이름)과 확신도. 말소리 미리보기가 받아들여지면 화면에 이 값이 보인다. + internal readonly record struct SpeechOnsetFrame(string Display, float Confidence); } diff --git a/SoundVisualizer.Core/Ai/CaptureAudioMath.cs b/SoundVisualizer.Core/Ai/CaptureAudioMath.cs index 8959651..fbc59f9 100644 --- a/SoundVisualizer.Core/Ai/CaptureAudioMath.cs +++ b/SoundVisualizer.Core/Ai/CaptureAudioMath.cs @@ -224,6 +224,85 @@ public static void ResampleMonoFloatTo16kCustom(ReadOnlySpan source, int } } + /// + /// 말소리 미리보기(Silero)의 스트림 리샘플(Android 의 resampleStreamBlock, #422). 16kHz 출력 샘플 + /// [, + .Length) 를 만든다. + /// 는 캡처 프레임 부터의 모노이고 의 범위를 + /// 모두 담아야 한다(0 보다 앞의 프레임은 0). 정규 경로()와 같은 계수·같은 곱셈과 덧셈 순서라 + /// 같은 소리를 0 번째 출력부터 차례로 넣으면 정규 경로와 비트까지 같다. 정규 YAMNet 경로는 바꾸지 않는다. + /// 위상은 창이 아니라 스트림 처음부터의 위치로 고른다. PC 레이트(88.2~192kHz)는 정규 경로처럼 늘린 탭을 쓴다. + /// + /// 16kHz 보다 낮은 레이트(AI 가 받지 않는다). + internal static void ResampleStreamBlock(ReadOnlySpan source, long sourceFirst, long outputStart, int sourceSampleRate, Span destination) + { + if (sourceSampleRate == TargetSampleRate) + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = source[(int)(outputStart + i - sourceFirst)]; + } + return; + } + if (sourceSampleRate < TargetSampleRate) + throw new ArgumentOutOfRangeException(nameof(sourceSampleRate), sourceSampleRate, "stream resampling needs a rate of at least 16 kHz"); + + 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 = (outputStart + i) * sourceSampleRate; + long center = positionNumerator / TargetSampleRate; + int phaseStart = PhaseIndex(positionNumerator % TargetSampleRate, table.PhaseCount) * taps; + int @base = (int)(center - sourceFirst) - halfTaps + 1; + double sum = 0.0; + for (int tap = 0; tap < taps; tap++) + { + sum += (double)coefficients[phaseStart + tap] * source[@base + tap]; + } + destination[i] = (float)sum; + } + } + + /// + /// 16kHz 출력 [, + ) 을 FIR 로 만드는 데 드는 + /// 캡처 프레임 범위 [First, End). 출력 샘플마다 중심 c 에서 c − (탭/2 − 1) 부터 c + 탭/2 까지 쓴다(44.1·48kHz 는 47 과 48 로 Android 와 같다). + /// 16kHz 는 같은 번호의 프레임 하나다. First 는 음수일 수 있다(스트림 처음 앞은 0 으로 본다). + /// + internal static (long First, long End) StreamSourceRange(long outputStart, int count, int sourceSampleRate) + { + long first = outputStart * sourceSampleRate / TargetSampleRate - StreamLeftContext(sourceSampleRate); + long end = (outputStart + count - 1) * sourceSampleRate / TargetSampleRate + StreamRightContext(sourceSampleRate) + 1; + return (first, end); + } + + /// 가 어느 위치에서든 넘지 않는 길이. 작업 버퍼를 한 번만 잡는 데 쓴다. + internal static int StreamSourceCapacity(int count, int sourceSampleRate) + { + long span = ((long)(count - 1) * sourceSampleRate + TargetSampleRate - 1) / TargetSampleRate; + return (int)span + StreamLeftContext(sourceSampleRate) + StreamRightContext(sourceSampleRate) + 1; + } + + /// + /// 캡처 프레임 개가 들어왔을 때 밀린 것을 버리고 다시 시작할 덩어리의 시작(16kHz 샘플). 오른쪽 탭까지 다 받은 + /// 마지막 덩어리의 바로 앞 덩어리다(Android SileroStreamWorker 의 newest 와 같은 식, 오른쪽 탭 수만 레이트를 따른다). + /// + internal static long NewestStreamBlockStart(long frames, int blockSize, int sourceSampleRate) + { + long usable = Math.Max(0L, frames - StreamRightContext(sourceSampleRate)); + long blocks = usable * TargetSampleRate / sourceSampleRate / blockSize; + return Math.Max(0L, blocks - 1) * blockSize; + } + + /// 출력 샘플 하나가 중심보다 앞에서 쓰는 프레임 수. + private static int StreamLeftContext(int sourceSampleRate) => + sourceSampleRate > TargetSampleRate ? FirTapsFor(sourceSampleRate) / 2 - 1 : 0; + + /// 출력 샘플 하나가 중심보다 뒤에서 쓰는 프레임 수. 이만큼 더 들어와야 그 샘플을 만들 수 있다. + private static int StreamRightContext(int sourceSampleRate) => + sourceSampleRate > TargetSampleRate ? FirTapsFor(sourceSampleRate) / 2 : 0; + /// 16kHz 보다 낮은 레이트(AI 는 받지 않는다)에서 예전 보간을 그대로 둔다. private static void LinearResample(ReadOnlySpan source, int sourceSampleRate, Span destination) { diff --git a/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs b/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs index 160d9d6..df0fb4d 100644 --- a/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs +++ b/SoundVisualizer.Core/Ai/ModelFileIntegrity.cs @@ -53,6 +53,36 @@ public static void Verify(string directory, IReadOnlyList files) } } + /// + /// 파일을 한 번 읽어 크기와 SHA-256 을 확인하고 그 바이트를 돌려준다. 확인한 바이트로 바로 세션을 만들면 확인과 불러오기 사이에 + /// 파일이 바뀌어도 확인하지 않은 모델을 쓰지 않는다(Android SileroInference 가 assets 를 읽어 해시한 바이트로 세션을 만드는 것과 같다). + /// 통째로 메모리에 올리므로 몇 MB 인 작은 모델에만 쓴다. + /// + /// 파일이 없다. + /// 크기나 SHA-256 이 배포한 파일과 다르다. + public static byte[] ReadVerified(string directory, ExpectedFile expected) + { + ArgumentNullException.ThrowIfNull(directory); + ArgumentNullException.ThrowIfNull(expected); + 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}"); + byte[] bytes = File.ReadAllBytes(path); + // 크기를 본 뒤에 파일이 바뀌었을 수 있으므로 읽은 바이트로 다시 본다. + if (bytes.LongLength != expected.ByteCount) + throw new InvalidDataException( + $"Model file '{expected.Name}' is {bytes.LongLength} bytes, expected {expected.ByteCount} (truncated or a different model): {path}"); + string actual = Convert.ToHexStringLower(SHA256.HashData(bytes)); + if (actual != expected.Sha256) + throw new InvalidDataException( + $"Model file '{expected.Name}' has SHA-256 {actual}, expected {expected.Sha256} (damaged or a different model): {path}"); + return bytes; + } + internal static string Sha256Hex(string path) { using var stream = File.OpenRead(path); diff --git a/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs b/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs index 3f5ea49..d81b329 100644 --- a/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs +++ b/SoundVisualizer.Core/Ai/RealtimeAiPipeline.cs @@ -17,10 +17,19 @@ namespace SoundVisualizer.Ai /// - 틱은 이 클래스가 띄우는 배경 스레드 "SV-AiTick" 에서 돈다(Android 는 Dispatchers.Default 코루틴). /// 시각은 모두 단조 증가하는 밀리초 하나()로 잰다. /// + /// 말소리 미리보기(Android #422, ): 정규 추론이 첫 대화음 후보(확정까지 한 틱 남음)를 내면, 그 뒤에 + /// 새로 들어온 소리를 배경 스레드 "SV-SileroPreview" 의 Silero VAD 가 확인해 화면에만 대화음을 한 박자 먼저 보인다 + /// (). 정규 결과()·위협음 판단·사용자 지정은 바꾸지 않는다. + /// 정규 위협음이나 다음 정규 결과가 늘 이기고, 사용자 지정이 하나라도 있으면 미리보기를 쓰지 않는다. Silero 를 불러오거나 돌리다 + /// 실패해도 정규 분류는 그대로 돈다. + /// /// PC 에서 바꾼 것: /// - 이 돌고 있는 틱을 기다리고 마지막 결과를 바로 숨기며, 는 앞 틱이 끝난 뒤에 비운다. /// Android 서비스는 늦게 끝난 추론이 비운 뒤의 결과를 덮지 않도록 읽는 쪽을 떼고 AI 재시작을 미뤘는데, 여기서는 그럴 필요가 없다. /// - 가 1초 안에 추론을 기다리지 못하면 Android 는 세션을 버리지만(누수), 여기서는 그 추론이 끝나면서 닫는다. + /// Silero 스레드도 같은 1초 안에서 끝나기를 기다린다(Android 는 기다리지 않는다). 못 기다리면 그 스레드가 끝나며 세션을 닫는다. + /// - 미리보기 시각은 의 단조 나노초로 잰다(Android 의 elapsedRealtimeNanos). 틱의 밀리초 시계(15.6ms 단위)로는 + /// 250·275ms 창이 한 프레임씩 흔들린다. /// public sealed class RealtimeAiPipeline : IDisposable { @@ -33,6 +42,18 @@ public sealed class RealtimeAiPipeline : IDisposable /// Stop·Dispose 가 돌고 있는 추론을 기다리는 한도. UI 스레드를 오래 붙잡지 않는다(Android 는 메인 스레드 ANR). internal const int CloseWaitMs = 1000; + /// + /// 말소리 미리보기를 켤지(Android 의 BuildConfig.AI_SILERO_PREVIEW_ENABLED, v1.8.0 부터 기본 켬). 사용자 설정은 없다. + /// 의 기본값이다. + /// + public const bool SpeechPreviewEnabled = true; + + /// 정규 추론이 소리를 가져간 때부터 이 시간 안에(다음 틱 무렵까지) Silero 가 확인해야 미리보기를 받아들인다. + internal const long SpeechPreviewAcceptNs = 250_000_000L; + + /// 정규 추론이 소리를 가져간 때부터 이 시간이 지나면 미리보기가 사라진다. 다음 정규 결과가 오면 그 전에라도 사라진다. + internal const long SpeechPreviewExpiryNs = 275_000_000L; + private const string Tag = nameof(RealtimeAiPipeline); private const long FailureLogIntervalMs = 2000; private const long NoTimestamp = long.MinValue; @@ -50,6 +71,12 @@ public sealed class RealtimeAiPipeline : IDisposable private readonly Func _waitForNextTick; private readonly bool _runScheduler; + // 말소리 미리보기. 스레드가 없으면(끔) 후보도 만들지 않고 화면용 결과는 늘 정규 결과다. + private readonly Func _nanoClock; + private readonly SileroPreviewGate _speechGate = new(); + private readonly SileroStreamWorker? _speechWorker; + private DisplayPreview? _displayPreview; + // 추론을 한 번에 하나로 줄 세운다. 모델·후처리·작업 버퍼는 이 잠금 안에서만 쓴다. private readonly object _inferLock = new(); // Start·Stop·Dispose 끼리의 순서를 지킨다. 추론 잠금을 기다리는 동안에도 잡고 있으므로 틱은 이 잠금을 잡지 않는다. @@ -80,6 +107,11 @@ public sealed class RealtimeAiPipeline : IDisposable /// 단조 밀리초. 테스트가 시각을 정한다. 기본은 . /// 다음 틱까지 쉰다. 멈추라는 신호를 받으면 false. 테스트가 기다리지 않고 한 걸음씩 돌린다. /// false 면 가 상태만 비우고 스레드를 띄우지 않는다. 테스트가 를 직접 부른다. + /// + /// 말소리 미리보기의 Silero 를 만든다. 있으면 미리보기 스레드를 띄우고 그 스레드 안에서 한 번 부른다. null 이면 미리보기를 끈다 + /// (이 생성자로 만드는 테스트의 기본). 앱은 가 넣는다. + /// + /// 미리보기 시각을 재는 단조 나노초. 테스트가 시각을 정한다. 기본은 . internal RealtimeAiPipeline( int captureSampleRate, int channels, @@ -89,7 +121,9 @@ internal RealtimeAiPipeline( Action? tickFailed = null, Func? clock = null, Func? waitForNextTick = null, - bool runScheduler = true) + bool runScheduler = true, + Func? speechModel = null, + Func? nanoClock = null) { ValidateArguments(captureSampleRate, channels, classes); ArgumentNullException.ThrowIfNull(model); @@ -103,6 +137,12 @@ internal RealtimeAiPipeline( _waitForNextTick = waitForNextTick ?? WaitForNextTick; _runScheduler = runScheduler; _captureScratch = new float[CaptureAudioMath.CaptureSamplesForOneYamnetWindow(captureSampleRate)]; + _nanoClock = nanoClock ?? MonotonicNanoseconds; + // 스레드는 맨 끝에 띄운다. 앞에서 실패하면 띄운 스레드가 남지 않는다. 덩어리를 보는 것은 Start 뒤에 소리가 들어온 다음이다. + if (speechModel is not null) + { + _speechWorker = new SileroStreamWorker(_buffer, speechModel, ClearSpeechPreview, OnSpeechBlock, _nanoClock); + } } /// @@ -119,6 +159,10 @@ internal RealtimeAiPipeline( /// 예외를 던지면 안 된다. null 을 돌려주면 사용자 지정이 없는 것으로 본다. /// /// 틱 실패를 틱 스레드에서 받는다. null 이면 으로 2초에 한 번까지 남긴다. + /// + /// 말소리 미리보기를 켤지. 켜면 같은 폴더의 silero_vad.onnx 를 미리보기 스레드가 불러온다. 그 파일이 없거나 다르면 미리보기만 꺼지고 + /// () 정규 분류는 돈다. + /// /// AI 가 받지 않는 캡처 레이트이거나 채널 수가 1 보다 작다. /// 모델 파일이 없다. /// 모델 파일이 배포한 것과 다르거나 입출력이 YAMNet 계약과 다르다. @@ -129,14 +173,17 @@ public static RealtimeAiPipeline Create( string modelDirectory, YamnetClassMap classes, Func>? mappingOverrides = null, - Action? tickFailed = null) + Action? tickFailed = null, + bool speechPreview = SpeechPreviewEnabled) { // 모델을 읽기 전에 거른다. 화면만 돌 레이트라면 15MB 를 해시하고 세션을 만들 까닭이 없다. ValidateArguments(captureSampleRate, channels, classes); var model = YamnetInference.Create(modelDirectory); try { - return new RealtimeAiPipeline(captureSampleRate, channels, model, classes, mappingOverrides, tickFailed); + string directory = Path.GetFullPath(modelDirectory); + return new RealtimeAiPipeline(captureSampleRate, channels, model, classes, mappingOverrides, tickFailed, + speechModel: speechPreview ? () => SileroInference.Create(directory) : null); } catch { @@ -164,6 +211,9 @@ public void Ingest(ReadOnlySpan interleaved) if (!Volatile.Read(ref _running) || IsClosed) return; // 섞기 전의 원래 채널로 무음을 판단하고, 게이트가 닫혀 있어도 늘 링에 남긴다. 다시 열릴 때 그 앞의 소리까지 한 창으로 본다. _gate.IngestInterleaved(interleaved, _clock()); + // 링에 넣은 뒤 미리보기 스레드를 깨운다. Android 처럼 무음 게이트와 상관없이 소리가 들어오는 동안은 따라 읽는다. + // Windows 루프백은 아무것도 재생하지 않으면 버퍼를 보내지 않으므로 그때는 깨우지 않는다. + _speechWorker?.Signal(); } /// @@ -177,6 +227,30 @@ public void Ingest(ReadOnlySpan interleaved) return _stage.LastClassification(CurrentOverrides()); } + /// + /// 화면과 개발자 HUD 가 읽는 결과(Android 의 lastDisplayClassification). 보통은 과 같다. + /// 말소리 미리보기를 받아들였고 정규 결과가 아직 그 부모(환경음) 그대로이며 사용자 지정이 없을 때만, 만료 전까지 + /// 대화음 미리보기()를 준다. 정규 위협음이나 다음 정규 결과, 사용자 지정이 + /// 늘 이긴다. 진동·알림처럼 확정 판단이 필요한 곳은 쓰지 않는다(PC 에는 아직 없다). 어느 스레드에서나 부르고 잠그지 않는다. + /// + public AiClassificationResult? LastDisplayClassification() + { + var regular = LastClassification(); + if (regular is null) return null; + var view = Volatile.Read(ref _displayPreview); + if (view is null) return regular; + if (_nanoClock() >= view.ExpiresNs) + { + Interlocked.CompareExchange(ref _displayPreview, null, view); + return regular; + } + return ReferenceEquals(view.Parent, regular) && + regular.Coarse == SoundType.Ambient && + CurrentOverrides().Count == 0 + ? view.Result + : regular; + } + /// /// 추론을 시작한다. 링·무음 게이트·히스테리시스·마지막 결과를 비우고 틱 스레드를 띄운다. 이미 돌고 있거나 닫혔으면 아무것도 /// 하지 않는다. 앞선 실행의 추론이 아직 돌고 있으면 그 추론이 끝난 뒤에 비운다. 그래야 그 결과가 비운 뒤에 되살아나지 않는다. @@ -228,7 +302,11 @@ public void Start() /// 추론을 멈춘다. 마지막 결과는 바로 숨기고( 이 null), 다음 틱을 막고, 돌고 있는 틱이 끝나기를 /// 최대 1초 기다린다. 다시 할 수 있다. 여러 번 불러도 된다. /// - public void Stop() => StopScheduler(Environment.TickCount64 + CloseWaitMs); + public void Stop() + { + StopScheduler(Environment.TickCount64 + CloseWaitMs); + ClearSpeechPreview(); + } /// /// 멈추고 모델을 닫는다. 여러 번 불러도 된다. 닫은 뒤에는 추론하지 않고, 는 버리고, @@ -239,7 +317,10 @@ public void Dispose() { if (Interlocked.Exchange(ref _closed, 1) != 0) return; long deadlineMs = Environment.TickCount64 + CloseWaitMs; + // 미리보기 스레드에 먼저 알려 정규 추론을 기다리는 동안 함께 멈추게 한다. 끝나기를 기다리는 것은 맨 뒤에서 한다. + _speechWorker?.RequestClose(); StopScheduler(deadlineMs); + ClearSpeechPreview(); // 멈추기는 다음 틱만 막는다. 이미 모델 안에 들어간 추론이 있으면 끝나기를 기다린 뒤에 세션을 닫아야 해제한 메모리를 쓰지 않는다. if (Monitor.TryEnter(_inferLock, RemainingMs(deadlineMs))) @@ -257,6 +338,12 @@ public void Dispose() { AppLog.Warn(Tag, "Inference still running after 1 s; the running tick releases the YAMNet session when it finishes"); } + + // Silero 세션은 미리보기 스레드가 끝나며 닫는다. 남은 시간 안에 끝나지 않으면 기다리지 않는다(그 스레드가 덩어리를 마치고 닫는다). + if (_speechWorker is not null && !_speechWorker.Join(RemainingMs(deadlineMs))) + { + AppLog.Warn(Tag, "Silero preview still running after 1 s; it releases its session when the current block finishes"); + } } /// @@ -314,15 +401,21 @@ internal AiTickOutcome RunTickOnce(long nowMs) /// 닫기 경합 테스트가 틱을 링 검사에 세워 두는 데 쓴다. internal AiAudioBuffer AudioBufferForTest => _buffer; + /// 말소리 미리보기 스레드. 끄면 null. + internal SileroStreamWorker? SpeechWorkerForTest => _speechWorker; + private (AiTickOutcome Outcome, Exception? Failure) RunTickLocked(long nowMs) { + // 정규 추론이 시작되면 미리보기를 새로 받아들이지 않는다. 이미 받아들인 것은 만료나 다음 정규 결과까지 둔다(Android 와 같다). + _speechGate.RegularStarted(); // Dispose 가 잠금을 잡기 직전에 위의 검사를 지났을 수 있으므로 잠금 안에서 다시 본다. if (IsClosed) return (AiTickOutcome.Closed, null); try { // 무음이라 건너뛰는 틱에도 먼저 반영한다. 이전 매핑에서 확정한 위협음이 새 매핑으로 넘어가지 않게 한다. - _stage.UpdateMapping(CurrentOverrides()); + // 매핑이 바뀌어 마지막 결과를 지웠으면 그 결과의 미리보기도 지운다. + if (_stage.UpdateMapping(CurrentOverrides())) ClearSpeechPreview(); } catch (Exception ex) { @@ -353,6 +446,8 @@ internal AiTickOutcome RunTickOnce(long nowMs) private void InferLocked() { Interlocked.Increment(ref _inferenceExecutions); + // 미리보기 창(250·275ms)은 정규 추론이 소리를 가져가기 직전부터 잰다(Android 의 snapshotBootNs). + long snapshotNs = _speechWorker is null ? 0L : _nanoClock(); long started = Stopwatch.GetTimestamp(); _buffer.CopyTailRightPadded(_captureScratch); CaptureAudioMath.ResampleMonoFloatTo16kCustom(_captureScratch, _buffer.SampleRate, _mono16k); @@ -360,13 +455,66 @@ private void InferLocked() long preprocessed = Stopwatch.GetTimestamp(); _model.Infer(_logMel, _logits, _probabilities); long inferred = Stopwatch.GetTimestamp(); - _stage.Process( + var result = _stage.Process( _probabilities, started, preprocessMs: Stopwatch.GetElapsedTime(started, preprocessed).TotalMilliseconds, yamnetMs: Stopwatch.GetElapsedTime(preprocessed, inferred).TotalMilliseconds); + OnRegularResultPublished(result, snapshotNs); + } + + /// + /// 새 정규 결과를 게시한 직후(추론 잠금 안). 앞 결과의 미리보기와 후보를 버리고, 이번 프레임이 첫 대화음 후보면 새 후보를 건다. + /// 후보는 지금까지 링에 들어온 소리 뒤의 새 소리만 본다. Android 는 결과를 저장하기 전에 비우지만, 여기서는 저장한 뒤에 비운다. + /// 미리보기는 부모가 지금 정규 결과일 때만 보이므로() 그 사이에도 옛 미리보기가 보이지 않고, + /// 저장하기 전에 비우면 그 사이 한 프레임 동안 미리보기 대신 부모(환경음)가 보일 수 있다. + /// + private void OnRegularResultPublished(AiClassificationResult result, long snapshotNs) + { + if (_speechWorker is null) return; + SileroPreviewGate.Candidate? candidate = null; + if (_stage.SpeechOnset is { } onset) + { + var cursor = _buffer.Cursor(); + candidate = new SileroPreviewGate.Candidate( + new SpeechParent(result, onset.Display, onset.Confidence), + FreshAfterFrame: cursor.Frames, + AcceptBeforeNs: snapshotNs + SpeechPreviewAcceptNs, + ExpiresNs: snapshotNs + SpeechPreviewExpiryNs, + Generation: cursor.Generation); + } + _speechGate.Regular(candidate); + Volatile.Write(ref _displayPreview, null); } + /// + /// Silero 덩어리 하나의 결과(미리보기 스레드). 받아들였고, 그 사이 새 정규 결과나 사용자 지정이 생기지 않았고, 받아들임 마감 전이면 + /// 화면용 미리보기를 게시한다. + /// + private void OnSpeechBlock(long generation, long startFrame, long endFrame, float probability, long nowNs) + { + var accepted = _speechGate.Block(generation, startFrame, endFrame, probability, nowNs); + if (accepted is null || IsClosed) return; + var parent = accepted.Parent; + if (!ReferenceEquals(LastClassification(), parent.Result) || CurrentOverrides().Count != 0) return; + var preview = parent.Result.AsSpeechPreview(parent.Display, parent.Confidence, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + if (_nanoClock() < accepted.AcceptBeforeNs) + { + Volatile.Write(ref _displayPreview, new DisplayPreview(parent.Result, preview, accepted.ExpiresNs)); + } + } + + /// 후보·받아들인 것·게시한 미리보기를 모두 버린다. 멈춤·초기화·매핑 변경·닫기, 미리보기 스레드가 밀린 소리를 버릴 때. + private void ClearSpeechPreview() + { + _speechGate.Regular(null); + Volatile.Write(ref _displayPreview, null); + } + + private static readonly double NanosecondsPerTimestampTick = 1_000_000_000.0 / Stopwatch.Frequency; + + private static long MonotonicNanoseconds() => (long)(Stopwatch.GetTimestamp() * NanosecondsPerTimestampTick); + private void RunScheduler(CancellationToken stop) { try @@ -430,6 +578,7 @@ private void ResetStateLocked() _buffer.Reset(); _gate.Reset(); _stage.Reset(); + ClearSpeechPreview(); Interlocked.Exchange(ref _inferenceExecutions, 0); Interlocked.Exchange(ref _inferenceCompletions, 0); Interlocked.Exchange(ref _silenceSkippedTicks, 0); @@ -491,6 +640,12 @@ private static void ValidateArguments(int captureSampleRate, int channels, Yamne /// 추론 횟수. Executed 는 시작한 추론, Completed 는 결과를 게시하고 끝난 추론이다. internal readonly record struct InferenceStats(long Executed, long Completed, long SkippedForSilence); + + /// 미리보기 후보의 부모: 그 정규 결과(환경음)와, 받아들이면 보일 대화음 이름·확신도. + private sealed record SpeechParent(AiClassificationResult Result, string Display, float Confidence); + + /// 게시한 미리보기. 가 지금 정규 결과일 때만, 전까지 보인다. + private sealed record DisplayPreview(AiClassificationResult Parent, AiClassificationResult Result, long ExpiresNs); } /// 틱 한 번이 어떻게 끝났는지. 테스트와 진단용이다. diff --git a/SoundVisualizer.Core/Ai/SileroInference.cs b/SoundVisualizer.Core/Ai/SileroInference.cs new file mode 100644 index 0000000..68f6245 --- /dev/null +++ b/SoundVisualizer.Core/Ai/SileroInference.cs @@ -0,0 +1,237 @@ +// 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 +{ + /// + /// Silero VAD 실행(Android 의 SileroInference.kt, #422). 16kHz 512샘플(32ms) 덩어리 하나마다 말소리일 확률 하나를 낸다. + /// 소리 종류를 가르는 분류기가 아니다. 말소리 미리보기()가 YAMNet 의 첫 대화음 후보를 확인하는 데만 쓴다. + /// + /// 모델은 v6.2.3(MIT)로 고정했고 불러올 때 크기와 SHA-256 을 Android 와 같은 값으로 확인한다(). + /// 순환 상태(2 × 1 × 128)를 덩어리마다 이어 받고, 입력 앞에 바로 앞 덩어리의 끝 64샘플을 붙인다(576샘플). 그래서 덩어리를 + /// 끊김 없이 차례로 넣어야 하고, 끊기면 한다. 세션 설정은 YAMNet 과 같다(연산 스레드 하나). + /// + /// PC 에서 바꾼 것: YAMNet 처럼 버퍼를 한 번만 잡아 IoBinding 으로 묶어 두어 덩어리마다 할당하지 않는다(Android 는 덩어리마다 + /// 텐서를 새로 만든다). 대신 스레드 안전하지 않으므로 한 스레드()만 부른다. + /// + public sealed class SileroInference : ISileroModel + { + public const string InputName = "input"; + public const string StateName = "state"; + public const string RateName = "sr"; + public const string OutputName = "output"; + public const string NextStateName = "stateN"; + + /// 모델이 받는 레이트. 덩어리 크기 512 는 이 레이트에서만 맞다. + public const int SampleRate = 16000; + + /// 덩어리 하나(32ms). + public const int BlockSize = 512; + + /// 입력 앞에 붙이는 바로 앞 덩어리의 끝. + public const int ContextSize = 64; + + private const int StateLength = 2 * 1 * 128; + + private static readonly long[] InputShape = { 1, ContextSize + BlockSize }; + private static readonly long[] StateShape = { 2, 1, 128 }; + private static readonly long[] OutputShape = { 1, 1 }; + + private readonly InferenceSession _session; + private readonly float[] _input; + private readonly float[] _state; + private readonly float[] _nextState; + private readonly float[] _output; + private readonly long[] _rate; + private readonly float[] _contextTail = new float[ContextSize]; + private readonly OrtValue _inputValue; + private readonly OrtValue _stateValue; + private readonly OrtValue _rateValue; + private readonly OrtValue _outputValue; + private readonly OrtValue _nextStateValue; + private readonly RunOptions _runOptions; + private readonly OrtIoBinding _binding; + private bool _disposed; + + private SileroInference(InferenceSession session) + { + ValidateContract(session); + _session = session; + // 고정 힙에 둔다. ONNX Runtime 이 배열의 주소를 들고 있는 동안 GC 가 옮기지 않게 한다. + _input = GC.AllocateArray(ContextSize + BlockSize, pinned: true); + _state = GC.AllocateArray(StateLength, pinned: true); + _nextState = GC.AllocateArray(StateLength, pinned: true); + _output = GC.AllocateArray(1, pinned: true); + _rate = GC.AllocateArray(1, pinned: true); + _rate[0] = SampleRate; + try + { + _inputValue = OrtValue.CreateTensorValueFromMemory(_input, InputShape); + _stateValue = OrtValue.CreateTensorValueFromMemory(_state, StateShape); + _rateValue = OrtValue.CreateTensorValueFromMemory(_rate, Array.Empty()); // 모양 [] 의 스칼라 + _outputValue = OrtValue.CreateTensorValueFromMemory(_output, OutputShape); + _nextStateValue = OrtValue.CreateTensorValueFromMemory(_nextState, StateShape); + _runOptions = new RunOptions(); + // 다음 상태는 입력 상태와 다른 칸에 받는다. 같은 칸을 입력과 출력에 함께 묶으면 계산 도중에 입력이 바뀔 수 있다. + _binding = session.CreateIoBinding(); + _binding.BindInput(InputName, _inputValue); + _binding.BindInput(StateName, _stateValue); + _binding.BindInput(RateName, _rateValue); + _binding.BindOutput(OutputName, _outputValue); + _binding.BindOutput(NextStateName, _nextStateValue); + } + catch + { + ReleaseBuffers(); + throw; + } + } + + /// + /// 의 silero_vad.onnx 로 세션을 만든다. 파일을 한 번 읽어 확인한 바이트로 만든다. + /// 실패해도 정규 분류는 그대로 돌고 미리보기만 꺼진다(). + /// + /// 모델 파일이 없다. + /// 모델 파일이 배포한 것과 다르거나(크기·SHA-256), 입출력이 Silero 계약과 다르다. + /// ONNX Runtime 이 세션을 만들지 못했다. + public static SileroInference Create(string modelDirectory) + { + ArgumentException.ThrowIfNullOrEmpty(modelDirectory); + byte[] model = ModelFileIntegrity.ReadVerified(Path.GetFullPath(modelDirectory), SileroModelFile.Onnx); + + InferenceSession session; + using (var options = new SessionOptions + { + IntraOpNumThreads = 1, + InterOpNumThreads = 1, + ExecutionMode = ExecutionMode.ORT_SEQUENTIAL, + GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL, + }) + { + session = new InferenceSession(model, options); + } + + try + { + return new SileroInference(session); + } + catch + { + try + { + session.Dispose(); + } + catch (Exception cleanupFailure) + { + AppLog.Warn(nameof(SileroInference), "Failed to release a rejected Silero session", cleanupFailure); + } + throw; + } + } + + /// 순환 상태와 앞 덩어리의 끝을 비운다. 덩어리가 끊기거나(밀린 것을 버림) 캡처를 다시 시작했을 때 부른다. + public void Reset() + { + ObjectDisposedException.ThrowIf(_disposed, this); + Array.Clear(_state); + Array.Clear(_contextTail); + } + + /// + /// 16kHz 512샘플로 말소리일 확률(0~1)을 낸다. 상태와 앞 덩어리의 끝을 다음 덩어리로 넘긴다. 할당하지 않는다. + /// + /// 닫은 뒤에 불렀다. + /// 모델이 0~1 밖이나 유한하지 않은 값을 냈다(Android 의 require 와 같다). + public float Score(ReadOnlySpan block) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (block.Length != BlockSize) + throw new ArgumentException($"Expected {BlockSize} samples, got {block.Length}", nameof(block)); + + _contextTail.CopyTo(_input, 0); + block.CopyTo(_input.AsSpan(ContextSize)); + _session.RunWithBinding(_runOptions, _binding); + float probability = _output[0]; + _nextState.CopyTo(_state, 0); + block[(BlockSize - ContextSize)..].CopyTo(_contextTail); + if (!float.IsFinite(probability) || probability < 0f || probability > 1f) + throw new InvalidDataException($"Silero returned an invalid speech probability {probability}"); + return probability; + } + + /// 세션과 버퍼를 놓는다. 여러 번 불러도 된다. 와 겹쳐 부르면 안 된다(같은 스레드에서만 닫는다). + public void Dispose() + { + if (_disposed) return; + _disposed = true; + ReleaseBuffers(); + _session.Dispose(); + } + + /// 입력 세 개·출력 두 개의 이름과 자료형, 차원 수를 본다. 길이는 동적이라(-1) 실행할 때 정해진다. + private static void ValidateContract(InferenceSession session) + { + string[] inputs = { InputName, StateName, RateName }; + string[] outputs = { OutputName, NextStateName }; + if (!session.InputNames.SequenceEqual(inputs)) + throw new InvalidDataException($"Expected inputs [{string.Join(", ", inputs)}], got [{string.Join(", ", session.InputNames)}]"); + if (!session.OutputNames.SequenceEqual(outputs)) + throw new InvalidDataException($"Expected outputs [{string.Join(", ", outputs)}], got [{string.Join(", ", session.OutputNames)}]"); + RequireTensor(session.InputMetadata[InputName], InputName, TensorElementType.Float, InputShape.Length); + RequireTensor(session.InputMetadata[StateName], StateName, TensorElementType.Float, StateShape.Length); + RequireTensor(session.InputMetadata[RateName], RateName, TensorElementType.Int64, 0); + RequireTensor(session.OutputMetadata[OutputName], OutputName, TensorElementType.Float, OutputShape.Length); + RequireTensor(session.OutputMetadata[NextStateName], NextStateName, TensorElementType.Float, StateShape.Length); + } + + private static void RequireTensor(NodeMetadata metadata, string name, TensorElementType type, int rank) + { + if (!metadata.IsTensor || metadata.ElementDataType != type || metadata.Dimensions.Length != rank) + { + throw new InvalidDataException( + $"Expected '{name}' as {type} of rank {rank}, got {metadata.ElementDataType}[{string.Join(",", metadata.Dimensions)}]"); + } + } + + private void ReleaseBuffers() + { + // 생성자 도중에 실패하면 일부는 아직 없다. + _binding?.Dispose(); + _runOptions?.Dispose(); + _nextStateValue?.Dispose(); + _outputValue?.Dispose(); + _rateValue?.Dispose(); + _stateValue?.Dispose(); + _inputValue?.Dispose(); + } + } + + /// + /// 말소리 미리보기가 쓰는 모델 단계. 실제로는 이고, 테스트는 확률을 정해 주거나 덩어리 앞에서 붙잡아 + /// 두는 가짜로 바꿔 끼운다. + /// + internal interface ISileroModel : IDisposable + { + /// + void Reset(); + + /// + float Score(ReadOnlySpan block); + } + + /// + /// 배포하는 Silero VAD 파일의 크기와 SHA-256. Android 의 SileroInference.SHA256 과 글자까지 같아야 하고, 모델을 바꾸면 두 앱에서 함께 고친다. + /// 원본: snakers4/silero-vad v6.2.3, 커밋 5cd7945676eb32225748052e2e6a0580e4686a08 의 src/silero_vad/data/silero_vad.onnx + /// (Android tools/prepare_silero_model.py 가 같은 해시로 확인해 받는다). + /// + internal static class SileroModelFile + { + public static readonly ModelFileIntegrity.ExpectedFile Onnx = new( + "silero_vad.onnx", + 2_327_524L, + "1a153a22f4509e292a94e67d6f9b85e8deb25b4988682b7e174c65279d8788e3"); + } +} diff --git a/SoundVisualizer.Core/Ai/SileroPreviewGate.cs b/SoundVisualizer.Core/Ai/SileroPreviewGate.cs new file mode 100644 index 0000000..93cf0a4 --- /dev/null +++ b/SoundVisualizer.Core/Ai/SileroPreviewGate.cs @@ -0,0 +1,105 @@ +// 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 의 SileroPreviewGate.kt, #422). + /// + /// 정규 추론이 첫 대화음 후보(확정까지 한 프레임 남음)를 내면 후보를 건다(). 그 뒤에 새로 들어온 소리로 + /// Silero 가 연달아 이어진 덩어리 세 개를 모두 확률 0.9 이상으로 보면 받아들인다. 받아들이는 것은 정해 둔 시각(다음 정규 틱 무렵) + /// 전까지만이고, 받아들인 미리보기도 만료 시각까지만 보인다. 다음 정규 추론이 시작되면 새로 받아들이지 않지만, 이미 받아들인 것은 + /// 만료나 다음 정규 결과까지 둔다(). + /// + /// 상태는 잠금 하나로 지킨다. 잠금 안에서는 모델·PCM·입출력·대기를 하지 않는다. 시각 단위는 부르는 쪽이 정한다(파이프라인은 나노초). + /// + /// 후보의 부모(파이프라인은 그 정규 결과와 대화음 이름·확신도). + internal sealed class SileroPreviewGate where T : class + { + /// 덩어리 하나를 말소리로 보는 확률. + public const float SpeechProbability = 0.9f; + + /// 받아들이는 데 필요한 연달아 이어진 덩어리 수(32ms × 3). + public const int RequiredBlocks = 3; + + /// 이 후보를 낸 정규 결과 쪽 정보. + /// 이 캡처 프레임부터 들어온 소리만 본다(정규 추론이 본 소리는 다시 세지 않는다). + /// 이 시각 전에만 받아들인다. + /// 받아들인 미리보기가 사라지는 시각. + /// 링의 세대(). 캡처를 다시 시작한 뒤의 소리는 세지 않는다. + internal sealed record Candidate(T Parent, long FreshAfterFrame, long AcceptBeforeNs, long ExpiresNs, long Generation); + + /// 받아들인 후보의 부모. + /// 미리보기가 사라지는 시각. + /// 받아들인 시각. + /// 후보의 받아들임 마감. 게시하기 직전에 한 번 더 본다. + internal sealed record Accepted(T Parent, long ExpiresNs, long AtNs, long AcceptBeforeNs); + + private readonly object _lock = new(); + private Candidate? _candidate; + private Accepted? _accepted; + private int _streak; + private long _previousEnd = -1; + + /// + /// 새 정규 결과가 나왔다(또는 멈춤·초기화·매핑 변경·밀림으로 비운다). 후보를 로 바꾸고 받아들인 것과 + /// 모아 둔 증거를 모두 버린다. 새 후보는 이전 후보의 증거를 물려받지 않는다. + /// + public void Regular(Candidate? candidate) + { + lock (_lock) + { + _candidate = candidate; + _accepted = null; + _streak = 0; + _previousEnd = -1; + } + } + + /// 정규 추론이 시작됐다. 새로 받아들이지 않되, 이미 받아들인 미리보기는 만료까지 둔다(일찍 거두지 않는다). + public void RegularStarted() + { + lock (_lock) + { + _candidate = null; + _streak = 0; + _previousEnd = -1; + } + } + + /// + /// Silero 덩어리 하나의 결과. 캡처 프레임 [, ) 를 본 확률이다. + /// 이번 덩어리로 받아들였으면 그 결과를, 아니면 null 을 돌려준다(한 후보에 한 번만 받아들인다). + /// + public Accepted? Block(long generation, long start, long end, float probability, long nowNs) + { + lock (_lock) + { + var candidate = _candidate; + if (candidate is null) return null; + if (_accepted is not null || candidate.Generation != generation || start < candidate.FreshAfterFrame) return null; + if (nowNs >= candidate.AcceptBeforeNs) + { + _candidate = null; + return null; + } + // 덩어리가 이어지지 않으면(밀려 건너뛰었으면) 처음부터 다시 센다. + if (_previousEnd != -1 && _previousEnd != start) _streak = 0; + _previousEnd = end; + _streak = float.IsFinite(probability) && probability >= SpeechProbability ? _streak + 1 : 0; + if (_streak < RequiredBlocks) return null; + _accepted = new Accepted(candidate.Parent, candidate.ExpiresNs, nowNs, candidate.AcceptBeforeNs); + return _accepted; + } + } + + /// 지금 보일 미리보기. 받아들인 것이 없거나 만료됐으면 null. + public Accepted? View(long nowNs) + { + lock (_lock) + { + return _accepted is { } accepted && nowNs < accepted.ExpiresNs ? accepted : null; + } + } + } +} diff --git a/SoundVisualizer.Core/Ai/SileroStreamWorker.cs b/SoundVisualizer.Core/Ai/SileroStreamWorker.cs new file mode 100644 index 0000000..ed4c35f --- /dev/null +++ b/SoundVisualizer.Core/Ai/SileroStreamWorker.cs @@ -0,0 +1,202 @@ +// 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 +{ + /// Silero 덩어리 하나의 결과. 캡처 프레임 [, ) 를 본 확률이다. + internal delegate void SileroBlockHandler(long generation, long startFrame, long endFrame, float probability, long nowNs); + + /// + /// AI 링의 소리를 16kHz 512샘플 덩어리로 차례로 Silero 에 넣는 배경 스레드 하나(Android 의 SileroStreamWorker.kt, #422). + /// + /// - 캡처 스레드는 링에 넣은 뒤 만 부른다. 깨우기는 하나로 합쳐지고, 캡처 PCM 을 따로 복사하거나 큐에 쌓지 않는다. + /// 이 스레드가 링에서 덩어리에 필요한 범위만 꺼내() 정규 리샘플러와 같은 계수로 16kHz 로 바꾼다. + /// - 한 번 깰 때 덩어리를 다섯 개까지만 본다. 밀린 것이 5덩어리(160ms)를 넘으면 버리고 가장 최근 쪽에서 다시 시작하며, 이어지지 않으므로 + /// 모델 상태를 비우고 discontinuity 를 부른다. 정규 추론은 이 스레드를 기다리지 않는다. + /// - 링의 세대가 바뀌면(캡처를 다시 시작) 처음부터 다시 센다. 덩어리를 본 뒤에도 세대가 그대로일 때만 결과를 넘긴다. + /// - 모델은 이 스레드가 만들고 쓰고 닫는다. 만들거나 돌리다 실패하면 미리보기만 꺼지고() 정규 분류는 그대로 돈다. + /// + /// PC 에서 바꾼 것: 닫을 때 스레드가 끝나기를 기다린다(, 파이프라인이 1초까지). Android 는 기다리지 않고 스레드가 + /// 스스로 끝나며 세션을 놓게 둔다. 여기서도 기다리다 시간이 다 되면 그렇게 둔다. + /// 88.2·96·176.4·192kHz 는 정규 리샘플러처럼 FIR 탭을 레이트에 비례해 늘린 범위를 꺼낸다(). + /// + internal sealed class SileroStreamWorker : IDisposable + { + /// 한 번 깰 때 보는 덩어리 수의 한도. + internal const int MaxBlocksPerWake = 5; + + /// 이만큼(16kHz 샘플, 5덩어리 = 160ms) 넘게 밀리면 버리고 가장 최근 쪽에서 다시 시작한다. + internal const long MaxBacklogSamples = SileroInference.BlockSize * 5L; + + private const string Tag = nameof(SileroStreamWorker); + + private readonly AiAudioBuffer _buffer; + private readonly Func _modelFactory; + private readonly Action _discontinuity; + private readonly SileroBlockHandler _block; + private readonly Func _nanoClock; + // 깨우기. AvailableWaitHandle 을 쓰지 않아 놓을 네이티브 자원이 없다. 캡처 스레드가 닫기와 엇갈려 Release 해도 터지지 않게 + // 버리지(Dispose) 않는다. + private readonly SemaphoreSlim _wake = new(0); + private readonly Thread _thread; + private int _closed; + private int _queued; + private int _failed; + private long _calls; + private long _resets; + private long _next; + + /// 읽을 AI 링. 레이트는 만들 때 정해진다. + /// 이 스레드 안에서 한 번 부른다. 만든 모델은 이 스레드가 닫는다. + /// 밀린 것을 버리고 건너뛸 때 이 스레드에서 부른다. 걸린 후보를 비우는 데 쓴다. + /// 덩어리 하나를 본 뒤 이 스레드에서 부른다. 오래 붙잡지 않는다. + /// 단조 나노초. 덩어리를 본 시각을 잰다. + public SileroStreamWorker( + AiAudioBuffer buffer, + Func modelFactory, + Action discontinuity, + SileroBlockHandler block, + Func nanoClock) + { + _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + _modelFactory = modelFactory ?? throw new ArgumentNullException(nameof(modelFactory)); + _discontinuity = discontinuity ?? throw new ArgumentNullException(nameof(discontinuity)); + _block = block ?? throw new ArgumentNullException(nameof(block)); + _nanoClock = nanoClock ?? throw new ArgumentNullException(nameof(nanoClock)); + _thread = new Thread(Run) + { + IsBackground = true, + Name = "SV-SileroPreview", + // Android 의 THREAD_PRIORITY_BACKGROUND. 캡처·화면·정규 추론보다 뒤에 선다. + Priority = ThreadPriority.BelowNormal, + }; + _thread.Start(); + } + + /// Silero 를 돌린 덩어리 수(테스트와 진단용). + public long Calls => Interlocked.Read(ref _calls); + + /// 모델 상태를 비운 횟수. 세대가 바뀌거나(처음 한 번 포함) 밀린 것을 버릴 때 센다. + public long Resets => Interlocked.Read(ref _resets); + + /// 모델을 만들거나 돌리다 실패해 미리보기를 껐는지. 정규 분류는 상관없이 돈다. + public bool Failed => Volatile.Read(ref _failed) != 0; + + /// 다음에 볼 덩어리의 시작(지금 세대의 16kHz 샘플 번호). 테스트가 따라잡았는지 기다리는 데 쓴다. + internal long NextOutputSample => Interlocked.Read(ref _next); + + /// 스레드가 아직 도는지(테스트용). + internal bool IsAlive => _thread.IsAlive; + + /// 덩어리를 Silero 에 넣기 직전에 이 스레드에서 부른다. 테스트가 스레드를 붙잡아 둔다. + internal Action? BeforeScoreForTest { get; set; } + + /// 링에 새 소리가 들어왔다. 캡처 스레드에서 부른다. 잠그지 않고, 이미 깨워 두었으면 아무것도 하지 않는다. + public void Signal() + { + if (Volatile.Read(ref _closed) != 0 || Volatile.Read(ref _failed) != 0) return; + if (Interlocked.CompareExchange(ref _queued, 1, 0) == 0) _wake.Release(); + } + + /// 멈추라고 알린다. 기다리지 않는다. 지금 보고 있는 덩어리를 마치면 끝나고 모델을 닫는다. 여러 번 불러도 된다. + public void RequestClose() + { + if (Interlocked.Exchange(ref _closed, 1) == 0) _wake.Release(); + } + + /// 스레드가 끝나기를 까지 기다린다. 끝났으면 true. 이 스레드 안에서 부르면 기다리지 않는다. + public bool Join(int timeoutMs) + { + if (Thread.CurrentThread == _thread) return false; + return _thread.Join(Math.Max(0, timeoutMs)); + } + + /// 멈추고 1초까지 기다린다(테스트용 using). + public void Dispose() + { + RequestClose(); + Join(RealtimeAiPipeline.CloseWaitMs); + } + + private bool IsClosed => Volatile.Read(ref _closed) != 0; + + private void Run() + { + ISileroModel? model = null; + try + { + model = _modelFactory(); + int rate = _buffer.SampleRate; + long epoch = -1; + long next = 0; + var pcm = new float[CaptureAudioMath.StreamSourceCapacity(SileroInference.BlockSize, rate)]; + var output = new float[SileroInference.BlockSize]; + while (!IsClosed) + { + _wake.Wait(); + Volatile.Write(ref _queued, 0); + if (IsClosed) break; + + var cursor = _buffer.Cursor(); + if (epoch != cursor.Generation) + { + epoch = cursor.Generation; + next = 0; + model.Reset(); + Interlocked.Increment(ref _resets); + } + // CPU 가 따라가지 못하면 밀린 것을 버리고 순환 상태도 비운다. 이어지지 않은 소리를 이어진 것처럼 세지 않는다. + long newest = CaptureAudioMath.NewestStreamBlockStart(cursor.Frames, SileroInference.BlockSize, rate); + if (newest - next > MaxBacklogSamples) + { + next = newest; + model.Reset(); + Interlocked.Increment(ref _resets); + _discontinuity(); + } + Interlocked.Exchange(ref _next, next); + + int processed = 0; + while (!IsClosed && processed++ < MaxBlocksPerWake) + { + var (first, end) = CaptureAudioMath.StreamSourceRange(next, SileroInference.BlockSize, rate); + var source = pcm.AsSpan(0, (int)(end - first)); + if (!_buffer.CopyRange(epoch, first, source)) break; // 아직 다 오지 않았거나 이미 밀려났다 + CaptureAudioMath.ResampleStreamBlock(source, first, next, rate, output); + BeforeScoreForTest?.Invoke(); + float probability = model.Score(output); + Interlocked.Increment(ref _calls); + // 보는 사이 캡처를 다시 시작했으면 옛 세대의 결과는 넘기지 않는다. + if (_buffer.Cursor().Generation == epoch && !IsClosed) + { + _block(epoch, + next * rate / CaptureAudioMath.TargetSampleRate, + (next + SileroInference.BlockSize) * rate / CaptureAudioMath.TargetSampleRate, + probability, + _nanoClock()); + } + next += SileroInference.BlockSize; + Interlocked.Exchange(ref _next, next); + } + } + } + catch (Exception ex) + { + Volatile.Write(ref _failed, 1); + AppLog.Warn(Tag, "Silero speech preview disabled; the regular AI keeps running", ex); + } + finally + { + // 모델은 이 스레드만 쓰므로 여기서 닫는다. 닫는 쪽이 기다리다 포기해도 이 스레드가 끝나며 놓는다. + try + { + model?.Dispose(); + } + catch (Exception ex) + { + AppLog.Warn(Tag, "Failed to release the Silero session", ex); + } + } + } + } +} diff --git a/SoundVisualizer.Core/Ai/SoundType.cs b/SoundVisualizer.Core/Ai/SoundType.cs index f86249e..f680878 100644 --- a/SoundVisualizer.Core/Ai/SoundType.cs +++ b/SoundVisualizer.Core/Ai/SoundType.cs @@ -14,7 +14,7 @@ public enum SoundType /// 환경음. Ambient = 0, - /// 대화음. 알아듣는 말만이 아니라 사람이 내는 소리 전반(웃음, 울음, 고함 등)이다. + /// 사람 소리(옛 이름 대화음). 알아듣는 말만이 아니라 사람이 내는 소리 전반(웃음, 울음, 고함, 재채기·기침·숨소리 등)이다. Speech = 1, /// 위협음. diff --git a/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs b/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs index a3eb31e..0355fae 100644 --- a/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs +++ b/SoundVisualizer.Core/Ai/YamnetThreeClassMapper.cs @@ -8,8 +8,8 @@ namespace SoundVisualizer.Ai /// /// YAMNet 클래스 이름(영어 display_name) → 기본 소리 종류. 모바일 YamnetThreeClassMapper.kt 와 규칙이 같다. /// - /// 위협음 → 대화음 → 환경음 순서로 보고 어디에도 걸리지 않으면 환경음이다. 실제 521개 클래스는 위협음 37개, - /// 대화음 33개, 환경음 451개로 나뉜다. 옛 PC 판이 위협음으로 보던 총소리 대리 라벨(Plop, Gargling, 비·폭포 소리)과 + /// 위협음 → 사람 소리 → 환경음 순서로 보고 어디에도 걸리지 않으면 환경음이다. 실제 521개 클래스는 위협음 37개, + /// 사람 소리 36개, 환경음 448개로 나뉜다. 옛 PC 판이 위협음으로 보던 총소리 대리 라벨(Plop, Gargling, 비·폭포 소리)과 /// 발소리는 모바일에서 원래 뜻대로 환경음으로 돌렸으므로(#319) 여기에도 없다. /// /// 이름을 소문자로 바꿔 비교하므로 부를 때마다 문자열을 하나 만든다. 추론 중에는 부르지 않고, @@ -71,8 +71,8 @@ private static bool MatchesSpeech(string s) { // 악기 이름이다. 사람 목소리가 아니다. if (s == "singing bowl") return false; - // 대화음은 알아듣는 말만이 아니라 사람이 내는 소리 전체다. 개의 낑낑거림 같은 비슷한 이름이 따라오지 않게 - // 정확히 같은 이름만 넣는다(#345). + // 사람 소리는 알아듣는 말만이 아니라 사람이 내는 소리 전체다. 재채기·기침·숨소리도 들어간다(모바일 #421, 사용자 승인). + // 개의 낑낑거림 같은 비슷한 이름이 따라오지 않게 정확히 같은 이름만 넣는다(#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; @@ -104,12 +104,15 @@ private static bool MatchesAmbient(string s) "emergency vehicle", "skidding", "tire squeal", "slam", }.ToFrozenSet(StringComparer.Ordinal); - /// 대화음으로 옮긴 사람 소리 11개(#345). 소문자. 테스트가 실제 클래스 이름과 맞는지 확인한다. + /// + /// 사람 소리로 옮긴 이름 14개: 사람 발성 11개(#345)와 재채기·기침·숨소리(모바일 #421). 소문자. 테스트가 실제 클래스 이름과 맞는지 확인한다. + /// internal static readonly FrozenSet HumanVocalizationLabels = new[] { "bellow", "whoop", "yell", "cheering", "giggle", "snicker", "belly laugh", "chuckle, chortle", "baby cry, infant cry", "whimper", "wail, moan", + "sneeze", "cough", "breathing", }.ToFrozenSet(StringComparer.Ordinal); } } diff --git a/SoundVisualizer.Core/AppSettings.cs b/SoundVisualizer.Core/AppSettings.cs index c60e1ec..d2350bd 100644 --- a/SoundVisualizer.Core/AppSettings.cs +++ b/SoundVisualizer.Core/AppSettings.cs @@ -23,6 +23,25 @@ public class VisualModeSettings public bool IntensityAsOpacity { get; set; } = false; public double OpacityFixedSize { get; set; } = 30.0; public double OpacityFixedMaxOpacity { get; set; } = 100.0; + + /// 모든 값이 처음 값(이 클래스의 초기값)과 같은지. 쉬운 설정으로 열지 정할 때 쓴다(). + public bool HasDefaultValues() => HasSameValues(new VisualModeSettings()); + + /// + /// 모든 값이 와 같은지. 속성을 하나씩 적지 않고 모두 훑는다. 나중에 값을 더해도 빠뜨리지 않는다 + /// (안드로이드는 data class 의 == 로 같은 일을 한다). + /// + public bool HasSameValues(VisualModeSettings other) + { + foreach (var property in typeof(VisualModeSettings).GetProperties()) + { + if (!Equals(property.GetValue(this), property.GetValue(other))) return false; + } + return true; + } + + /// 값을 그대로 옮긴 새 객체. 오버레이가 읽는 객체와 섞이지 않게 고쳐 쓸 때 쓴다(모드 그림의 설정 등). + public VisualModeSettings Copy() => (VisualModeSettings)MemberwiseClone(); } public static class AppSettings @@ -171,6 +190,12 @@ public static IReadOnlyDictionary SoundTypes // 보는 사람에 대한 값이라 전체 초기화(ResetToDefaults)가 건드리지 않는다. 모양 설정을 되돌렸다고 튜토리얼이 다시 뜨면 안 된다. public static bool TutorialSeen { get; set; } = false; + // 14. 설정 탭을 쉬운 설정(그림으로 선택하는 화면)으로 열지(모바일 easy_settings, #408). 새로 설치하면 쉬운 설정이다. + // 저장한 적이 없으면 읽을 때 정한다(Load, DefaultEasySettingsFor). 화면 설정이라 전체 초기화(ResetToDefaults)가 건드리지 않는다. + // 고급 모드(ClassifyAdvanced)와 같은 까닭이고, 자세한 설정 맨 아래의 초기화 버튼을 누르자마자 화면이 쉬운 설정으로 바뀌면 헷갈린다. + public const bool DefaultEasySettings = true; + public static bool EasySettings { get; set; } = DefaultEasySettings; + /// 설정 파일 위치. 포터블 배포라 실행 파일 옆에 둔다. 테스트는 임시 파일로 바꾼다. public static string SettingsFilePath { get; set; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); @@ -202,6 +227,9 @@ public static void Load() // 모바일은 정한 값을 곧바로 저장한다(기기 표시가 먼저 저장되면 새 설치를 쓰던 사람으로 잘못 친다). PC 는 저장할 때마다 모든 // 항목을 쓰므로, 무엇을 먼저 저장하든 정한 값이 함께 남는다. 파일이 없는 동안은 늘 새 설치로 읽힌다. TutorialSeen = File.Exists(SettingsFilePath); + // 쉬운 설정: 파일이 없으면 새로 설치한 것이라 쉬운 설정이다. 파일을 읽지 못하면(깨진 파일) 모든 값이 처음 값으로 시작하므로 + // 손대지 않은 사람과 같이 쉬운 설정으로 연다. 읽으면 아래(모드 설정을 읽은 뒤)에서 정한다. + EasySettings = DefaultEasySettings; try { if (File.Exists(SettingsFilePath)) @@ -255,6 +283,10 @@ public static void Load() 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 }; + // 쉬운 설정 항목이 없는 파일(이 기능 전의 파일)은 모드별 상세 값을 바꿔 둔 사람인지로 정한다(모바일 loadEasySettings). + // 모드 설정을 읽은 뒤라야 한다. + EasySettings = data.EasySettings ?? DefaultEasySettingsFor(WaveMode, PadMode, CircleMode, OutlineMode); + int maxRate = GetMonitorRefreshRate(); if (TargetFps > maxRate) TargetFps = maxRate; } @@ -384,6 +416,7 @@ private static void WriteFile() SoundTypes = WriteSoundTypes(SoundTypes), ClassifyAdvanced = ClassifyAdvanced, TutorialSeen = TutorialSeen, + EasySettings = EasySettings, WaveMode = WaveMode, PadMode = PadMode, @@ -449,6 +482,15 @@ private static JsonElement WriteSoundTypes(IReadOnlyDictionary t /// private static bool DefaultClassifyAdvanced(IReadOnlyDictionary soundTypes) => soundTypes.Count > 0; + /// + /// 쉬운 설정을 저장한 적이 없을 때의 값(모바일 loadEasySettings). 네 모드의 설정이 모두 처음 값이면(손대지 않았으면) 쉬운 설정으로, + /// 하나라도 바꿨으면 손에 익은 자세한 설정으로 연다. 표현 모드·색·표시처럼 쉬운 설정에도 보이는 값과, 최대 프레임·단축키처럼 + /// 모드 카드 밖의 값은 따지지 않는다(모바일과 같다). 모바일은 정한 값을 곧바로 저장하지만 PC 는 저장할 때마다 모든 항목을 쓰므로 + /// 다음 저장에서 함께 남는다. 그래서 쉬운 설정에서 크기를 바꾼 사람이 다음에 열 때 자세한 설정으로 바뀌는 일이 없다. + /// + private static bool DefaultEasySettingsFor(params VisualModeSettings[] modes) => + DefaultEasySettings && modes.All(mode => mode.HasDefaultValues()); + [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern IntPtr GetDC(IntPtr hWnd); @@ -520,6 +562,9 @@ private class SettingsData // 튜토리얼을 본 적이 있는지. 예전 설정 파일에는 없어 null 로 읽힌다(그때는 본 것으로 친다). public bool? TutorialSeen { get; set; } + // 설정 탭을 쉬운 설정으로 열지. 예전 설정 파일에는 없어 null 로 읽힌다(그때는 모드 설정을 보고 정한다). + public bool? EasySettings { get; set; } + public VisualModeSettings WaveMode { get; set; } = new VisualModeSettings(); public VisualModeSettings PadMode { get; set; } = new VisualModeSettings(); public VisualModeSettings CircleMode { get; set; } = new VisualModeSettings(); diff --git a/SoundVisualizer.Core/Diagnostics/AiDebugText.cs b/SoundVisualizer.Core/Diagnostics/AiDebugText.cs index dcd4f60..94c718e 100644 --- a/SoundVisualizer.Core/Diagnostics/AiDebugText.cs +++ b/SoundVisualizer.Core/Diagnostics/AiDebugText.cs @@ -71,6 +71,12 @@ public static class AiDebugText public const string Ellipsis = "…"; + /// + /// 말소리 미리보기()일 때 종류 뒤에 붙인다(모바일과 같은 글자, #422). 아직 확정이 아닌 + /// 화면 전용 표시라는 것을 정규 확정과 구분해 채점한다. + /// + public const string PreviewMarker = " ~PREVIEW"; + /// /// HUD 한 줄에 들어가는 글자 수. 이보다 길면 폰에서 줄이 두 줄로 꺾인다(#165). 폭 411dp 폰에서 고정폭 11sp 글자(6.6dp)가 /// 383dp 에 58자다. PC 화면은 훨씬 넓지만 두 판이 같은 줄을 만들도록 같은 한도를 지킨다(나이를 짧게 쓰는 규칙이 여기서 나온다). @@ -110,7 +116,7 @@ public static AiDebugLines Format(AiClassificationResult? result, long nowMs, fl return new AiDebugLines( ColorType: result.Coarse, - Coarse: result.Coarse.ToLabel().ToUpperInvariant(), + Coarse: result.Coarse.ToLabel().ToUpperInvariant() + (result.SpeechPreview ? PreviewMarker : ""), // 확정된 이름이 아직 없으면 빈 문자열이 온다. 임계값을 한 번도 넘지 못한 상태다. Label: result.Display.Length == 0 ? "(none)" : result.Display, Confidence: TwoDecimals(result.Confidence), diff --git a/SoundVisualizer.Core/EasySettings.cs b/SoundVisualizer.Core/EasySettings.cs new file mode 100644 index 0000000..bda17dc --- /dev/null +++ b/SoundVisualizer.Core/EasySettings.cs @@ -0,0 +1,138 @@ +// 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 +{ + /// + /// 쉬운 설정의 크기 세 단계(안드로이드 EasySize, #408). 자세한 설정의 크기·진하기 슬라이더 대신 그림 칸 셋으로 선택한다. + /// 값과 규칙은 에 있다. 선언 순서가 화면의 칸 순서다. + /// + public enum EasySize + { + Small, + Medium, + Large, + } + + /// + /// 쉬운 설정의 '자세한 설정' 칸을 눌렀을 때 자세한 설정에서 보여 줄 칸(안드로이드 #427 의 DetailTarget.Size 를 PC 에 맞춘 것). + /// 안드로이드는 그 모드의 카드 맨 위로 옮기기만 하지만, PC 는 키보드로 쓰는 사람을 위해 그 칸에 초점까지 옮기므로 칸 하나를 고른다. + /// 모드 카드 안의 순서(크기 고정 → 크기 → 진하기)와 같다. + /// + public enum EasySizeControl + { + /// 크기 고정 스위치. 켜 두면 크기·진하기 슬라이더가 접혀 있다. + SizeLock, + + /// 크기 슬라이더. + Size, + + /// 진하기 슬라이더. + Opacity, + } + + /// + /// 쉬운 설정의 크기 단계 규칙(안드로이드 EasySize). 한 단계가 크기와 진하기를 함께 정한다. 작게는 덜 눈에 띄게, 크게는 더 눈에 띄게 그린다. + /// 보통은 처음 값( 의 기본값)을 그대로 가리켜, 새로 설치한 사람에게는 보통이 선택되어 보인다. + /// + /// 값은 안드로이드와 똑같다. PC 엔진(Rendering/VisualizerEngine)도 크기를 "화면 짧은 변의 절반에서 가장자리 여백을 뺀 깊이의 몇 %" + /// (BaseDepthFor), 진하기를 "불투명도 %"(Alpha)로 안드로이드와 같은 식으로 쓰므로, 같은 값이 같은 크기·진하기로 보인다. + /// 네 모드 모두 같은 두 값(크기·진하기)을 쓰고 모드마다 뜻이 다르지 않아(원형의 반지름은 따로다) 모드별로 값을 달리 두지 않는다. + /// + /// 크기는 모드마다 따로다(안드로이드 사용자 결정 2026-10-22). 쉬운 설정의 크기 칸은 지금 표현 모드의 크기를 보여 주고 바꾼다. + /// + public static class EasySizes + { + /// + /// 슬라이더 값이 단계 값과 이만큼 안쪽이면 같은 것으로 본다. 자세한 설정의 슬라이더는 끌면 소수 값(49.8)이 되지만 화면에는 + /// 정수("50")로 보인다. 안드로이드처럼 꼭 같은 값만 치면, 자세한 설정에서 50 으로 보이는데 쉬운 설정은 "자세한 설정에서 맞춘 + /// 크기"라고 하는 엇갈림이 생긴다. 그래서 화면에 같은 정수로 보이는 값은 같은 단계로 본다. + /// + public const double Tolerance = 0.5; + + private static readonly VisualModeSettings Defaults = new(); + + /// 화면에 놓는 차례(작게, 보통, 크게). + public static IReadOnlyList All { get; } = Enum.GetValues(); + + /// 이 단계의 크기(0~100). + public static double Intensity(this EasySize step) => step switch + { + EasySize.Small => 30, + EasySize.Large => 75, + _ => Defaults.Intensity, + }; + + /// 이 단계의 진하기(0~100). + public static double Opacity(this EasySize step) => step switch + { + EasySize.Small => 40, + EasySize.Large => 70, + _ => Defaults.VisualOpacity, + }; + + /// + /// 이 단계를 에 쓴다. 크기 고정을 켜 둔 모드는 크기가 움직이지 않으므로 끈다. 누른 크기가 실제로 보여야 한다. + /// 나머지 값(속도·민감도·광원·반지름, 고정 크기·최대 진하기)은 그대로 둔다. 오버레이가 프레임마다 읽는 객체를 그 자리에서 고친다 + /// (자세한 설정의 슬라이더와 같다). + /// + public static void ApplyTo(this EasySize step, VisualModeSettings settings) + { + settings.Intensity = step.Intensity(); + settings.VisualOpacity = step.Opacity(); + settings.IntensityAsOpacity = false; + } + + /// + /// 가 한 단계에 맞으면 그 단계. 자세한 설정에서 크기나 진하기를 단계와 다르게 맞췄거나 크기 고정을 켰으면 + /// 어느 단계도 아니다(null). 그때는 쉬운 설정에서 어느 칸도 선택해 두지 않는다. 가장 가까운 단계로 보이면 실제 크기와 다르고, + /// 그 칸을 누르면 오히려 크기가 바뀐다(안드로이드 #424). + /// + public static EasySize? Of(VisualModeSettings settings) + { + if (settings.IntensityAsOpacity) return null; + foreach (var step in All) + { + if (Near(settings.Intensity, step.Intensity()) && Near(settings.VisualOpacity, step.Opacity())) return step; + } + return null; + } + + /// + /// '자세한 설정' 칸을 눌렀을 때 보여 줄 칸. 단계에 맞지 않게 만든 첫 칸을 모드 카드의 차례로 고른다: 크기 고정을 켰으면 그 스위치, + /// 크기가 어느 단계와도 다르면 크기, 크기는 맞는데 진하기가 그 단계와 다르면 진하기. 단계에 맞으면 크기다. + /// + public static EasySizeControl DetailControl(VisualModeSettings settings) + { + if (settings.IntensityAsOpacity) return EasySizeControl.SizeLock; + foreach (var step in All) + { + if (Near(settings.Intensity, step.Intensity())) + { + return Near(settings.VisualOpacity, step.Opacity()) ? EasySizeControl.Size : EasySizeControl.Opacity; + } + } + return EasySizeControl.Size; + } + + private static bool Near(double value, double step) => Math.Abs(value - step) < Tolerance; + } + + /// 쉬운 설정에서 화면에 보이지 않는 이름(화면 읽어주기)을 짓는 규칙. + public static class EasyNames + { + /// + /// 쉬운 설정의 소리 종류 줄을 화면 읽어주기에 알리는 이름(안드로이드 typeRowName, #434). 자세한 설정의 스위치와 같은 이름 + /// (, "위협음 표시")을 쓰고, 그 안에 화면에 보이는 종류 이름()이 없으면 앞에 붙인다. + /// 보이는 글자를 말해 누르는 음성 조작도 이 줄을 찾을 수 있어야 한다(WCAG 2.5.3). 번역에 따라 "Mostrar sons de perigo" 에는 + /// 보이는 "Som de perigo" 가 들어 있지 않다. + /// + /// 두 이름을 잇는 꼴("{0}, {1}"). 언어마다 쉼표가 달라(아랍어 "،", 일본어 "、") 문자열 표에서 받는다. + public static string TypeRow(string typeName, string showLabel, string pairFormat) => + showLabel.Contains(typeName, StringComparison.OrdinalIgnoreCase) + ? showLabel + : string.Format(CultureInfo.InvariantCulture, pairFormat, typeName, showLabel); + } +} diff --git a/SoundVisualizer.Core/Help/ReportLink.cs b/SoundVisualizer.Core/Help/ReportLink.cs index c8325b4..12e82bd 100644 --- a/SoundVisualizer.Core/Help/ReportLink.cs +++ b/SoundVisualizer.Core/Help/ReportLink.cs @@ -12,8 +12,8 @@ namespace SoundVisualizer.Help /// public static class ReportLink { - /// 새 이슈 화면. PC 판 저장소다(안드로이드 판과 다르다). - public const string IssuesUrl = "https://github.com/amophi/SoundVisualizer/issues/new"; + /// 새 이슈 화면. PC 판 저장소다(안드로이드 판과 다르다). 2026-11 에 SOVIS-Team 조직으로 옮겼다(옛 amophi 주소는 넘겨 준다). + public const string IssuesUrl = "https://github.com/SOVIS-Team/SoundVisualizer-Windows/issues/new"; /// 메일 제보를 받는 곳. 안드로이드 판과 같은 주소다. public const string ReportEmail = "nawwhod@gmail.com"; diff --git a/SoundVisualizer.Core/Localization/Strings/ar.json b/SoundVisualizer.Core/Localization/Strings/ar.json index 7ed2d1e..b3a021f 100644 --- a/SoundVisualizer.Core/Localization/Strings/ar.json +++ b/SoundVisualizer.Core/Localization/Strings/ar.json @@ -75,7 +75,7 @@ "settings.radius.desc": "يضبط نصف قطر الحلقة التي تتوسط الشاشة في وضع الدائرة.", "settings.section.sound_types": "أنواع الأصوات (AI)", "settings.show_ambient": "إظهار الأصوات المحيطة", - "settings.show_speech": "إظهار الكلام", + "settings.show_speech": "إظهار الأصوات البشرية", "settings.show_danger": "إظهار أصوات الخطر", "settings.set_color": "تعيين اللون", "settings.set_color_for": "تعيين اللون: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "تصغير نافذة التشغيل عند البدء", "settings.auto_minimize.desc": "يصغّر نافذة التشغيل هذه تلقائيًا عند بدء التراكب.", + "settings.view.easy": "إعدادات سهلة", + "settings.view.detailed": "إعدادات مفصّلة", + + "easy.section.shape": "الشكل", + "easy.section.size": "الحجم", + "easy.section.types": "أنواع الأصوات", + "easy.size.small": "صغير", + "easy.size.medium": "متوسط", + "easy.size.large": "كبير", + "easy.size.custom": "هذا الحجم مضبوط في «إعدادات مفصّلة». اختر أحد الخيارات أعلاه لتغييره.", + "easy.name_pair": "{0}، {1}", + "sounds.title": "اختيار أنواع الأصوات", "sounds.desc": "تُجمع الأصوات التي يتعرّف عليها الذكاء الاصطناعي مع الأصوات المشابهة لها في مجموعات، وتُعرض حسب النوع الذي تُصنَّف ضمنه تلقائيًا. إذا غيّرت نوع مجموعة، فستأخذ أصواتها فورًا لون النوع الجديد.", "sounds.sibling_note": "كثيرًا ما يسمع الذكاء الاصطناعي الصوت الواحد بعدة أسماء معًا. على سبيل المثال، تُسمع «صفارة إنذار» أيضًا على أنها «إنذار»، وهو في مجموعة أخرى. إذا ظل صوت يظهر بنوعه القديم بعد التغيير، فغيّر أيضًا المجموعات أو الأصوات المرتبطة به.", @@ -106,7 +118,7 @@ "sounds.filter.all": "الكل", "sounds.filter.changed": "المعدّلة", "sounds.type.ambient": "الأصوات المحيطة", - "sounds.type.speech": "الكلام", + "sounds.type.speech": "أصوات بشرية", "sounds.type.danger": "أصوات الخطر", "sounds.type.default": "{0} (تلقائي)", "sounds.type.mixed": "مختلط", @@ -148,19 +160,19 @@ "help.mode.pad": "تضيء أشرطة في 8 مواضع حول حافة الشاشة، وهو أوضح وضع لإظهار الاتجاه.", "help.mode.circle": "تنتفخ حلقة في وسط الشاشة باتجاه مصدر الصوت.", "help.mode.outline": "يرسم خطًا رفيعًا يمثّل حدود الموجة فقط، لذا يغطي مساحة أقل من شاشة اللعبة.", - "help.modes.note": "تُحفظ إعدادات مثل «الحجم» و«العتامة» بشكل منفصل لكل وضع، ويمكنك تغييرها من علامة تبويب «الإعدادات».", + "help.modes.note": "تُحفظ إعدادات مثل «الحجم» و«العتامة» بشكل منفصل لكل وضع، ويمكنك تغييرها من «إعدادات مفصّلة» في علامة تبويب «الإعدادات».", "help.sound.title": "أنواع الأصوات", - "help.sound.kinds": "يصنّف الذكاء الاصطناعي ما تسمعه إلى أصوات محيطة وكلام وأصوات خطر. الألوان التلقائية هي: الأبيض للأصوات المحيطة، والأصفر للكلام، والأحمر لأصوات الخطر. يمكنك تغيير لون كل نوع واختيار إظهاره أو إخفائه من علامة تبويب «الإعدادات».", + "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.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.body": "في «إعدادات مفصّلة» ضمن علامة تبويب «الإعدادات»، انقر على زر مفتاح الاختصار، ثم اضغط على مفتاح واحد أو مجموعة مفاتيح (مثل Ctrl + Shift + A) لتغيير مفتاح الاختصار كما تريد. ويؤدي الضغط على مفتاح الاختصار إلى تبديل الوضع فورًا، حتى عندما يعمل التطبيق في الخلفية.", "help.hotkeys.defaults": "افتراضيًا، يبدّل F2 وضع الصوت، ويبدّل F3 وضع العرض، ويفتح F4 إعدادات التراكب.", "help.size_lock.title": "وضع تثبيت الحجم", "help.size_lock.body": "يُبقي وضع تثبيت الحجم الرسومات بحجم ثابت، ويستخدم شفافية تتناسب مع شدة الصوت. وهو مفيد عندما تريد إبقاء الرسومات بالحجم نفسه مع رؤية تفاعلها مع الصوت.", @@ -172,15 +184,15 @@ "help.note.direction_surround": "عند الإخراج بقنوات 5.1 أو 7.1، يعرض أعلى الشاشة الصوت القادم من الأمام، ويعرض أسفلها الصوت القادم من الخلف. ويعرض الجانبان الأيسر والأيمن الصوت القادم من اليسار واليمين، أما الرسومات التي تظهر من كل جوانب الشاشة في الوقت نفسه فهي أصوات محيطة أو أصوات بلا اتجاه.", "help.note.direction_stereo": "عند الإخراج بقناتين (ستيريو)، يميّز التطبيق بين اليسار واليمين، لكنه لا يميّز بين الأمام والخلف. وعندها تُمثّل الرسومات في أعلى الشاشة وأسفلها الأصوات المسموعة بالتساوي من الجانبين، كما تعبّر عن الإحساس المكاني.", "help.note.sources": "يعرض التطبيق صوت كل التطبيقات التي تعمل عبر جهاز التشغيل الافتراضي في Windows. ولا يُعرض الصوت الذي يرسله تطبيق إلى جهاز آخر، مثل سماعة رأس تستخدمها للدردشة الصوتية فقط.", - "help.note.volume": "إذا كان مستوى صوت تطبيق ما منخفضًا جدًا أو مكتومًا، داخل التطبيق نفسه أو في خالط مستوى الصوت في 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.no_graphic_a": "تحقّق من أن الصوت يعمل عبر جهاز التشغيل الافتراضي في Windows وأنه غير مكتوم، ومن عدم وجود تنبيه في «الرئيسية». وإذا كانت لعبة تعمل في وضع ملء الشاشة الحصري، فجرّب وضع النافذة بلا حدود أو وضع النافذة. تأكّد أيضًا من أن إظهار نوع الصوت الحالي مفعّل في علامة تبويب «الإعدادات»، وأن «العتامة» ليست منخفضة جدًا في «إعدادات مفصّلة».", "help.faq.performance_q": "هل يستهلك الكثير من المعالج أو بطاقة الرسومات؟", - "help.faq.performance_a": "لتخفيف الحمل، خفّض «{0}» في علامة تبويب «الإعدادات» أو أوقف «{1}».", + "help.faq.performance_a": "لتخفيف الحمل، خفّض «{0}» من «إعدادات مفصّلة» في علامة تبويب «الإعدادات» أو أوقف «{1}».", "help.faq.language_q": "كيف أغيّر لغة التطبيق؟", "help.faq.language_a": "اختر لغة من قائمة «{0}» في أعلى هذه النافذة. ويمكنك أثناء التشغيل تغييرها أيضًا من إعدادات التراكب. عند اختيار «{1}» يستخدم التطبيق لغة العرض في Windows.", "help.report.title": "الإبلاغ عن مشكلة", @@ -285,7 +297,7 @@ "tutorial.direction.title": "يُظهر من أين يأتي الصوت", "tutorial.direction.body": "عند الإخراج بقناتين (ستيريو)، يُرسم الصوت القادم من اليسار أكبر على يسار الشاشة، والصوت القادم من اليمين أكبر على يمينها. وعند الإخراج بقنوات 5.1 أو 7.1، يميّز التطبيق أيضًا بين الأمام والخلف: يُرسم الصوت القادم من الأمام في أعلى الشاشة، والصوت القادم من الخلف في أسفلها.", "tutorial.types.title": "لكل نوع من الأصوات لونه الخاص", - "tutorial.types.body": "يصنّف الذكاء الاصطناعي الأصوات إلى ثلاثة أنواع، ويرسم كل نوع بلونه الخاص. يمكنك تغيير الألوان من علامة تبويب «الإعدادات».", + "tutorial.types.body": "يصنّف الذكاء الاصطناعي الأصوات إلى ثلاثة أنواع، ويرسم كل نوع بلونه الخاص. يمكنك تغيير الألوان من «إعدادات مفصّلة» في علامة تبويب «الإعدادات».", "tutorial.type.ambient_examples": "الموسيقى والمطر وكل الأصوات الأخرى", "tutorial.type.speech_examples": "أصوات الناس", "tutorial.type.danger_examples": "طلقات نارية، انفجارات، صفارات إنذار", diff --git a/SoundVisualizer.Core/Localization/Strings/de.json b/SoundVisualizer.Core/Localization/Strings/de.json index 5a0ed4a..e0a28b2 100644 --- a/SoundVisualizer.Core/Localization/Strings/de.json +++ b/SoundVisualizer.Core/Localization/Strings/de.json @@ -75,7 +75,7 @@ "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_speech": "Menschliche Laute", "settings.show_danger": "Gefahrengeräusche", "settings.set_color": "Farbe wählen", "settings.set_color_for": "Farbe wählen: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "Launcher beim Start minimieren", "settings.auto_minimize.desc": "Minimiert dieses Launcher-Fenster automatisch, wenn das Overlay startet.", + "settings.view.easy": "Einfach", + "settings.view.detailed": "Detailliert", + + "easy.section.shape": "Form", + "easy.section.size": "Größe", + "easy.section.types": "Geräuscharten", + "easy.size.small": "Klein", + "easy.size.medium": "Mittel", + "easy.size.large": "Groß", + "easy.size.custom": "Diese Größe wurde in der Ansicht „Detailliert“ eingestellt. Wählen Sie oben eine Option, um sie zu ändern.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Alle", "sounds.filter.changed": "Geändert", "sounds.type.ambient": "Umgebungsgeräusche", - "sounds.type.speech": "Stimmen", + "sounds.type.speech": "Menschliche Laute", "sounds.type.danger": "Gefahrengeräusche", "sounds.type.default": "{0} (Standard)", "sounds.type.mixed": "Gemischt", @@ -148,19 +160,19 @@ "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.modes.note": "Einstellungen wie „Größe“ und „Deckkraft“ werden für jeden Modus separat gespeichert und lassen sich im Tab „Einstellungen“ unter „Detailliert“ ä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.kinds": "Die KI teilt Geräusche in drei Arten ein: Umgebungsgeräusche, menschliche Laute und Gefahrengeräusche. Standardmäßig sind Umgebungsgeräusche weiß, menschliche Laute gelb und Gefahrengeräusche rot. Im Tab „Einstellungen“ können Sie für jede Art festlegen, ob sie angezeigt wird, und unter „Detailliert“ ihre Farbe ändern.", "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.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“ unter „Detailliert“ 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.body": "Klicken Sie im Tab „Einstellungen“ unter „Detailliert“ 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.", @@ -172,15 +184,15 @@ "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.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“ unter „Detailliert“ 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.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 Anzeige der aktuellen Geräuschart nicht ausgeschaltet und die „Deckkraft“ unter „Detailliert“ nicht zu niedrig eingestellt 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.performance_a": "Um sie zu entlasten, verringern Sie im Tab „Einstellungen“ unter „Detailliert“ 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", @@ -285,7 +297,7 @@ "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.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“ unter „Detailliert“ ä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", diff --git a/SoundVisualizer.Core/Localization/Strings/en.json b/SoundVisualizer.Core/Localization/Strings/en.json index 34b6858..9fc8496 100644 --- a/SoundVisualizer.Core/Localization/Strings/en.json +++ b/SoundVisualizer.Core/Localization/Strings/en.json @@ -75,7 +75,7 @@ "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_speech": "Show human sounds", "settings.show_danger": "Show danger sounds", "settings.set_color": "Set color", "settings.set_color_for": "Set color: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "Minimize launcher on start", "settings.auto_minimize.desc": "Automatically minimizes this launcher window when the overlay starts.", + "settings.view.easy": "Simple", + "settings.view.detailed": "Detailed", + + "easy.section.shape": "Shape", + "easy.section.size": "Size", + "easy.section.types": "Sound types", + "easy.size.small": "Small", + "easy.size.medium": "Medium", + "easy.size.large": "Large", + "easy.size.custom": "This size was set in “Detailed”. Select one above to change it.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "All", "sounds.filter.changed": "Changed", "sounds.type.ambient": "Ambient", - "sounds.type.speech": "Speech", + "sounds.type.speech": "Human sounds", "sounds.type.danger": "Danger", "sounds.type.default": "{0} (default)", "sounds.type.mixed": "Mixed", @@ -148,19 +160,19 @@ "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.modes.note": "Settings like size and opacity are saved separately for each mode and can be changed under “Detailed” 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.kinds": "AI sorts what you hear into ambient, human sounds and danger sounds. By default, ambient is white, human sounds are yellow and danger is red. In the Settings tab you can choose whether each type is shown, and change its color under “Detailed”.", "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.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' under “Detailed” 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.body": "Under “Detailed” 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.", @@ -172,15 +184,15 @@ "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.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 under “Detailed” 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.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 showing the current sound type isn't turned off in Settings, and that opacity isn't too low in “Detailed”.", "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.performance_a": "To make it lighter, lower “{0}” under “Detailed” 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", @@ -285,7 +297,7 @@ "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.types.body": "AI sorts sounds into three types and draws each in its own color. You can change the colors under “Detailed” 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", diff --git a/SoundVisualizer.Core/Localization/Strings/es.json b/SoundVisualizer.Core/Localization/Strings/es.json index 119fadf..3f7f30a 100644 --- a/SoundVisualizer.Core/Localization/Strings/es.json +++ b/SoundVisualizer.Core/Localization/Strings/es.json @@ -75,7 +75,7 @@ "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_speech": "Sonidos humanos", "settings.show_danger": "Peligro", "settings.set_color": "Elegir color", "settings.set_color_for": "Elegir color: {0}", @@ -95,6 +95,18 @@ "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.", + "settings.view.easy": "Sencillo", + "settings.view.detailed": "Detallado", + + "easy.section.shape": "Forma", + "easy.section.size": "Tamaño", + "easy.section.types": "Tipos de sonido", + "easy.size.small": "Pequeño", + "easy.size.medium": "Mediano", + "easy.size.large": "Grande", + "easy.size.custom": "Este tamaño se ajustó en “Detallado”. Elige una opción de arriba para cambiarlo.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Todos", "sounds.filter.changed": "Cambiados", "sounds.type.ambient": "Sonido ambiente", - "sounds.type.speech": "Voz", + "sounds.type.speech": "Sonidos humanos", "sounds.type.danger": "Peligro", "sounds.type.default": "{0} (por defecto)", "sounds.type.mixed": "Mixto", @@ -148,19 +160,19 @@ "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.modes.note": "Opciones como Tamaño y Opacidad se guardan por separado para cada modo y puedes cambiarlas en “Detallado”, 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.kinds": "La IA clasifica el audio en sonido ambiente, sonidos humanos y peligro. De forma predeterminada, el sonido ambiente se muestra en blanco, los sonidos humanos en amarillo y el peligro en rojo. En la pestaña Ajustes puedes elegir si cada tipo se muestra o no, y cambiar su color en “Detallado”.", "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.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 “Detallado”, en 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.body": "En “Detallado”, 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.", @@ -172,15 +184,15 @@ "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.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 “Detallado”, 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.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 no esté desactivada la visualización del tipo de sonido que se está reproduciendo y de que la Opacidad no esté demasiado baja en “Detallado”.", "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.performance_a": "Para que consuma menos, baja “{0}” en “Detallado”, 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", @@ -285,7 +297,7 @@ "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.types.body": "La IA clasifica los sonidos en tres tipos y dibuja cada uno con su propio color. Puedes cambiar los colores en “Detallado”, 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", diff --git a/SoundVisualizer.Core/Localization/Strings/fr.json b/SoundVisualizer.Core/Localization/Strings/fr.json index 0f40142..38843d4 100644 --- a/SoundVisualizer.Core/Localization/Strings/fr.json +++ b/SoundVisualizer.Core/Localization/Strings/fr.json @@ -75,7 +75,7 @@ "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_speech": "Afficher les sons humains", "settings.show_danger": "Afficher les dangers", "settings.set_color": "Choisir la couleur", "settings.set_color_for": "Choisir la couleur : {0}", @@ -95,6 +95,18 @@ "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.", + "settings.view.easy": "Simple", + "settings.view.detailed": "Détaillé", + + "easy.section.shape": "Forme", + "easy.section.size": "Taille", + "easy.section.types": "Types de sons", + "easy.size.small": "Petite", + "easy.size.medium": "Moyenne", + "easy.size.large": "Grande", + "easy.size.custom": "Cette taille a été réglée dans la vue « Détaillé ». Choisissez une option ci-dessus pour la modifier.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Tous", "sounds.filter.changed": "Modifiés", "sounds.type.ambient": "Ambiance", - "sounds.type.speech": "Voix", + "sounds.type.speech": "Sons humains", "sounds.type.danger": "Danger", "sounds.type.default": "{0} (par défaut)", "sounds.type.mixed": "Mixte", @@ -148,19 +160,19 @@ "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.modes.note": "Les réglages comme « Taille » et « Opacité » sont enregistrés séparément pour chaque mode et peuvent être modifiés sous « Détaillé », 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.kinds": "L'IA classe les sons entendus en trois types : ambiance, sons humains et danger. Par défaut, l'ambiance s'affiche en blanc, les sons humains en jaune et les dangers en rouge. Dans l'onglet Paramètres, vous pouvez choisir d'afficher ou non chaque type, et changer sa couleur sous « Détaillé ».", "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.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 » sous « Détaillé », dans 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.body": "Sous « Détaillé », 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.", @@ -172,15 +184,15 @@ "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.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é sous « Détaillé », 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.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 l'affichage du type de son entendu n'est pas désactivé et que le réglage « Opacité » n'est pas trop faible sous « Détaillé ».", "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.performance_a": "Pour l'alléger, baissez « {0} » sous « Détaillé », 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", @@ -285,7 +297,7 @@ "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.types.body": "L'IA classe les sons en trois types et dessine chaque type d'une couleur différente. Vous pouvez changer les couleurs sous « Détaillé », 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", diff --git a/SoundVisualizer.Core/Localization/Strings/hi.json b/SoundVisualizer.Core/Localization/Strings/hi.json index 79765fc..49af7b6 100644 --- a/SoundVisualizer.Core/Localization/Strings/hi.json +++ b/SoundVisualizer.Core/Localization/Strings/hi.json @@ -75,7 +75,7 @@ "settings.radius.desc": "वृत्त मोड में बीच वाले घेरे की त्रिज्या बदलता है।", "settings.section.sound_types": "आवाज़ के प्रकार (AI)", "settings.show_ambient": "माहौल की आवाज़ें दिखाएं", - "settings.show_speech": "बातचीत दिखाएं", + "settings.show_speech": "इंसानी आवाज़ें दिखाएं", "settings.show_danger": "खतरे की आवाज़ें दिखाएं", "settings.set_color": "रंग सेट करें", "settings.set_color_for": "रंग सेट करें: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "शुरू करने पर लॉन्चर छोटा करें", "settings.auto_minimize.desc": "ओवरले शुरू होने पर इस लॉन्चर विंडो को अपने आप छोटा कर देता है।", + "settings.view.easy": "आसान सेटिंग", + "settings.view.detailed": "विस्तृत सेटिंग", + + "easy.section.shape": "आकार", + "easy.section.size": "साइज़", + "easy.section.types": "आवाज़ के प्रकार", + "easy.size.small": "छोटा", + "easy.size.medium": "मध्यम", + "easy.size.large": "बड़ा", + "easy.size.custom": "यह साइज़ “विस्तृत सेटिंग” में सेट किया गया है। इसे बदलने के लिए ऊपर से कोई विकल्प चुनें।", + "easy.name_pair": "{0}, {1}", + "sounds.title": "आवाज़ों के प्रकार चुनें", "sounds.desc": "AI जिन आवाज़ों को पहचानता है, वे मिलती-जुलती आवाज़ों के साथ समूहों में रखी गई हैं और अपने डिफ़ॉल्ट प्रकार के हिसाब से दिखती हैं। किसी समूह का प्रकार बदलने पर, उसकी आवाज़ों का रंग तुरंत नए प्रकार के हिसाब से बदल जाता है।", "sounds.sibling_note": "AI अक्सर एक ही आवाज़ को एक साथ कई नामों से पहचानता है। जैसे, सायरन “अलार्म” के रूप में भी पहचाना जाता है, जो किसी दूसरे समूह में है। बदलाव के बाद भी अगर कोई आवाज़ पुराने प्रकार में दिखे, तो उससे जुड़े समूह या आवाज़ें भी बदलें।", @@ -106,7 +118,7 @@ "sounds.filter.all": "सभी", "sounds.filter.changed": "बदली गई", "sounds.type.ambient": "माहौल की आवाज़ें", - "sounds.type.speech": "बातचीत", + "sounds.type.speech": "इंसानी आवाज़ें", "sounds.type.danger": "खतरे की आवाज़ें", "sounds.type.default": "{0} (डिफ़ॉल्ट)", "sounds.type.mixed": "मिला-जुला", @@ -148,19 +160,19 @@ "help.mode.pad": "स्क्रीन के किनारों पर 8 जगह पट्टियां चमकती हैं। इसमें दिशा सबसे साफ़ दिखती है।", "help.mode.circle": "स्क्रीन के बीच में बना घेरा उस ओर फूलता है, जिस ओर से आवाज़ आती है।", "help.mode.outline": "लहर की सिर्फ़ पतली बाहरी रेखा बनाता है, इसलिए गेम की स्क्रीन कम ढकती है।", - "help.modes.note": "“साइज़” और “गहरापन” जैसी सेटिंग हर मोड के लिए अलग से सेव होती हैं। इन्हें “सेटिंग” टैब में बदला जा सकता है।", + "help.modes.note": "“साइज़” और “गहरापन” जैसी सेटिंग हर मोड के लिए अलग से सेव होती हैं। इन्हें “सेटिंग” टैब की “विस्तृत सेटिंग” में बदला जा सकता है।", "help.sound.title": "आवाज़ के प्रकार", - "help.sound.kinds": "AI सुनाई देने वाली आवाज़ों को तीन प्रकारों में बांटता है: माहौल की आवाज़ें, बातचीत और खतरे की आवाज़ें। डिफ़ॉल्ट रूप से माहौल की आवाज़ें सफ़ेद, बातचीत पीले और खतरे की आवाज़ें लाल रंग में दिखती हैं। “सेटिंग” टैब में हर प्रकार का रंग बदला जा सकता है और यह भी चुना जा सकता है कि उसे दिखाना है या नहीं।", + "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.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.body": "“सेटिंग” टैब की “विस्तृत सेटिंग” में किसी शॉर्टकट बटन पर क्लिक करें, फिर कोई एक कुंजी या कुंजियों का कॉम्बिनेशन (जैसे Ctrl + Shift + A) दबाकर शॉर्टकट को अपनी पसंद से बदलें। ऐप बैकग्राउंड में चल रहा हो, तब भी शॉर्टकट कुंजी दबाने पर मोड तुरंत बदल जाता है।", "help.hotkeys.defaults": "डिफ़ॉल्ट रूप से F2 साउंड मोड बदलता है, F3 विज़ुअल मोड बदलता है और F4 ओवरले सेटिंग खोलता है।", "help.size_lock.title": "साइज़ तय रखने वाला मोड", "help.size_lock.body": "साइज़ तय रखने वाला मोड, आवाज़ की तेज़ी के अनुपात में पारदर्शिता बदलकर ग्राफ़िक्स का साइज़ एक जैसा रखता है। यह तब काम आता है, जब आप ग्राफ़िक्स का साइज़ एक जैसा रखते हुए भी देखना चाहते हैं कि वे आवाज़ पर कैसे प्रतिक्रिया देते हैं।", @@ -172,15 +184,15 @@ "help.note.direction_surround": "5.1 या 7.1 चैनल आउटपुट में, स्क्रीन का ऊपरी हिस्सा सामने से आने वाली आवाज़ और निचला हिस्सा पीछे से आने वाली आवाज़ दिखाता है। बाईं और दाईं तरफ़ बाएं और दाएं से आने वाली आवाज़ दिखती है, और स्क्रीन के हर तरफ़ से एक साथ आने वाले ग्राफ़िक्स माहौल की आवाज़ें या बिना दिशा वाली आवाज़ें होते हैं।", "help.note.direction_stereo": "2 चैनल (स्टीरियो) आउटपुट में यह ऐप बाएं और दाएं का फ़र्क पहचान सकता है, लेकिन आगे और पीछे का नहीं। तब स्क्रीन के ऊपर और नीचे दिखने वाले ग्राफ़िक्स, दोनों तरफ़ बराबर आने वाली आवाज़ों और स्थानिक प्रभाव को दिखाते हैं।", "help.note.sources": "Windows के डिफ़ॉल्ट प्लेबैक डिवाइस से चलने वाले सभी ऐप की आवाज़ दिखाई जाती है। कोई ऐप जो आवाज़ किसी दूसरे डिवाइस पर भेजता है, जैसे सिर्फ़ वॉइस चैट के लिए इस्तेमाल होने वाला हेडसेट, वह नहीं दिखती।", - "help.note.volume": "अगर किसी ऐप का वॉल्यूम, ऐप में या 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.no_graphic_a": "देखें कि आवाज़ Windows के डिफ़ॉल्ट प्लेबैक डिवाइस से चल रही है और म्यूट नहीं है, और “होम” टैब में कोई सूचना तो नहीं दिख रही। अगर कोई गेम एक्सक्लूसिव फ़ुल-स्क्रीन मोड में चल रहा है, तो उसका बॉर्डरलेस या विंडो मोड आज़माएं। यह भी देखें कि “सेटिंग” टैब में अभी चल रही आवाज़ के प्रकार को दिखाने की सेटिंग बंद न हो और “विस्तृत सेटिंग” में “गहरापन” बहुत कम न हो।", "help.faq.performance_q": "क्या यह बहुत ज़्यादा CPU या GPU इस्तेमाल करता है?", - "help.faq.performance_a": "इसे हल्का करने के लिए, “सेटिंग” टैब में “{0}” कम करें या “{1}” बंद करें।", + "help.faq.performance_a": "इसे हल्का करने के लिए, “सेटिंग” टैब की “विस्तृत सेटिंग” में “{0}” कम करें या “{1}” बंद करें।", "help.faq.language_q": "ऐप की भाषा कैसे बदलें?", "help.faq.language_a": "इस विंडो में ऊपर दी गई “{0}” सूची से भाषा चुनें। चालू रहने के दौरान आप इसे ओवरले सेटिंग में भी बदल सकते हैं। “{1}” चुनने पर ऐप Windows की डिस्प्ले भाषा में दिखेगा।", "help.report.title": "समस्या की रिपोर्ट करें", @@ -285,7 +297,7 @@ "tutorial.direction.title": "आवाज़ किस तरफ़ से आ रही है, यह दिखाता है", "tutorial.direction.body": "2 चैनल (स्टीरियो) आउटपुट में बाईं तरफ़ से आने वाली आवाज़ स्क्रीन की बाईं तरफ़ ज़्यादा बड़ी दिखती है, और दाईं तरफ़ से आने वाली आवाज़ दाईं तरफ़। 5.1 या 7.1 चैनल आउटपुट में यह ऐप आगे और पीछे का फ़र्क भी पहचानता है: सामने से आने वाली आवाज़ स्क्रीन के ऊपरी हिस्से में और पीछे से आने वाली आवाज़ निचले हिस्से में दिखती है।", "tutorial.types.title": "आवाज़ के हर प्रकार का अपना रंग है", - "tutorial.types.body": "AI आवाज़ों को तीन प्रकारों में बांटता है और हर प्रकार को अलग रंग में बनाता है। रंग “सेटिंग” टैब में बदले जा सकते हैं।", + "tutorial.types.body": "AI आवाज़ों को तीन प्रकारों में बांटता है और हर प्रकार को अलग रंग में बनाता है। रंग “सेटिंग” टैब की “विस्तृत सेटिंग” में बदले जा सकते हैं।", "tutorial.type.ambient_examples": "संगीत, बारिश और बाकी सब", "tutorial.type.speech_examples": "लोगों की आवाज़ें", "tutorial.type.danger_examples": "गोलियों की आवाज़, धमाके, सायरन", diff --git a/SoundVisualizer.Core/Localization/Strings/id.json b/SoundVisualizer.Core/Localization/Strings/id.json index 9359d17..7253fdf 100644 --- a/SoundVisualizer.Core/Localization/Strings/id.json +++ b/SoundVisualizer.Core/Localization/Strings/id.json @@ -75,7 +75,7 @@ "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_speech": "Tampilkan suara manusia", "settings.show_danger": "Tampilkan suara bahaya", "settings.set_color": "Atur warna", "settings.set_color_for": "Atur warna: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "Minimalkan peluncur saat mulai", "settings.auto_minimize.desc": "Otomatis meminimalkan jendela peluncur ini saat overlay dimulai.", + "settings.view.easy": "Sederhana", + "settings.view.detailed": "Detail", + + "easy.section.shape": "Bentuk", + "easy.section.size": "Ukuran", + "easy.section.types": "Jenis suara", + "easy.size.small": "Kecil", + "easy.size.medium": "Sedang", + "easy.size.large": "Besar", + "easy.size.custom": "Ukuran ini diatur di tampilan “Detail”. Pilih salah satu di atas untuk mengubahnya.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Semua", "sounds.filter.changed": "Diubah", "sounds.type.ambient": "Suara latar", - "sounds.type.speech": "Percakapan", + "sounds.type.speech": "Suara manusia", "sounds.type.danger": "Suara bahaya", "sounds.type.default": "{0} (default)", "sounds.type.mixed": "Campuran", @@ -148,19 +160,19 @@ "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.modes.note": "Setelan seperti Ukuran dan Opasitas disimpan terpisah untuk setiap mode dan dapat diubah di tampilan “Detail” pada 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.kinds": "AI mengelompokkan suara yang terdengar menjadi suara latar, suara manusia, dan suara bahaya. Secara default, suara latar berwarna putih, suara manusia kuning, dan suara bahaya merah. Di tab Setelan, Anda dapat memilih apakah setiap jenis ditampilkan, dan mengubah warnanya di tampilan “Detail”.", "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.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 tampilan “Detail” pada 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.body": "Di tampilan “Detail” pada 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.", @@ -172,15 +184,15 @@ "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.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 tampilan “Detail” pada 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.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 opsi tampilan untuk jenis suara yang sedang terdengar tidak dimatikan di tab Setelan, dan Opasitas di tampilan “Detail” tidak terlalu rendah.", "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.performance_a": "Agar lebih ringan, turunkan “{0}” di tampilan “Detail” pada 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", @@ -285,7 +297,7 @@ "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.types.body": "AI mengelompokkan suara menjadi tiga jenis dan menggambar setiap jenis dengan warna yang berbeda. Anda dapat mengubah warnanya di tampilan “Detail” pada tab Setelan.", "tutorial.type.ambient_examples": "Musik, hujan, dan suara lainnya", "tutorial.type.speech_examples": "Suara manusia", "tutorial.type.danger_examples": "Tembakan, ledakan, sirene", diff --git a/SoundVisualizer.Core/Localization/Strings/it.json b/SoundVisualizer.Core/Localization/Strings/it.json index bd45d48..986d4f3 100644 --- a/SoundVisualizer.Core/Localization/Strings/it.json +++ b/SoundVisualizer.Core/Localization/Strings/it.json @@ -75,7 +75,7 @@ "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_speech": "Suoni umani", "settings.show_danger": "Suoni di pericolo", "settings.set_color": "Scegli colore", "settings.set_color_for": "Scegli colore: {0}", @@ -95,6 +95,18 @@ "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.", + "settings.view.easy": "Semplici", + "settings.view.detailed": "Dettagliate", + + "easy.section.shape": "Forma", + "easy.section.size": "Dimensione", + "easy.section.types": "Tipi di suono", + "easy.size.small": "Piccola", + "easy.size.medium": "Media", + "easy.size.large": "Grande", + "easy.size.custom": "Questa dimensione è stata impostata in “Dettagliate”. Scegli un'opzione qui sopra per cambiarla.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Tutti", "sounds.filter.changed": "Modificati", "sounds.type.ambient": "Suoni ambientali", - "sounds.type.speech": "Voci", + "sounds.type.speech": "Suoni umani", "sounds.type.danger": "Suoni di pericolo", "sounds.type.default": "{0} (predefinito)", "sounds.type.mixed": "Misto", @@ -148,19 +160,19 @@ "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.modes.note": "Opzioni come Dimensione e Opacità vengono salvate separatamente per ogni modalità e si possono modificare in “Dettagliate”, 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.kinds": "L'IA classifica l'audio in suoni ambientali, suoni umani e suoni di pericolo. Per impostazione predefinita, i suoni ambientali sono bianchi, i suoni umani gialli e i suoni di pericolo rossi. Nella scheda Impostazioni puoi scegliere se mostrare ogni tipo e, in “Dettagliate”, cambiarne il colore.", "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.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” in “Dettagliate”, 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.body": "In “Dettagliate”, 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.", @@ -172,15 +184,15 @@ "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.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à in “Dettagliate”, 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.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 la visualizzazione del tipo di suono in riproduzione non sia disattivata e, in “Dettagliate”, che l'Opacità non sia troppo bassa.", "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.performance_a": "Per alleggerirla, abbassa “{0}” in “Dettagliate”, 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", @@ -285,7 +297,7 @@ "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.types.body": "L'IA classifica i suoni in tre tipi e disegna ogni tipo con un colore diverso. Puoi cambiare i colori in “Dettagliate”, 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", diff --git a/SoundVisualizer.Core/Localization/Strings/ja.json b/SoundVisualizer.Core/Localization/Strings/ja.json index ac0e2f1..a4cddee 100644 --- a/SoundVisualizer.Core/Localization/Strings/ja.json +++ b/SoundVisualizer.Core/Localization/Strings/ja.json @@ -75,7 +75,7 @@ "settings.radius.desc": "円形モードで、中央のリングの半径を調整します。", "settings.section.sound_types": "音の種類(AI)", "settings.show_ambient": "環境音を表示", - "settings.show_speech": "話し声を表示", + "settings.show_speech": "人の声を表示", "settings.show_danger": "危険音を表示", "settings.set_color": "色を設定", "settings.set_color_for": "色を設定:{0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "開始時にランチャーを最小化", "settings.auto_minimize.desc": "オーバーレイの開始時に、このランチャーウィンドウを自動で最小化します。", + "settings.view.easy": "かんたん設定", + "settings.view.detailed": "詳細設定", + + "easy.section.shape": "形", + "easy.section.size": "サイズ", + "easy.section.types": "音の種類", + "easy.size.small": "小さめ", + "easy.size.medium": "普通", + "easy.size.large": "大きめ", + "easy.size.custom": "このサイズは「詳細設定」で調整されています。上で選択すると変わります。", + "easy.name_pair": "{0}、{1}", + "sounds.title": "音の種類を選ぶ", "sounds.desc": "AI が認識する音を、似た音どうしでグループにまとめ、初期設定の種類ごとに表示します。グループの種類を変えると、そのグループの音の色はすぐに新しい種類に合わせて変わります。", "sounds.sibling_note": "AI は一つの音を複数の名前で同時に聞き取ることがよくあります。たとえばサイレンは「警報音」としても聞き取られますが、警報音は別のグループにあります。変更後も前の種類で表示される場合は、関連するグループや音も変更してください。", @@ -106,7 +118,7 @@ "sounds.filter.all": "すべて", "sounds.filter.changed": "変更した音", "sounds.type.ambient": "環境音", - "sounds.type.speech": "話し声", + "sounds.type.speech": "人の声", "sounds.type.danger": "危険音", "sounds.type.default": "{0}(初期設定)", "sounds.type.mixed": "混在", @@ -148,19 +160,19 @@ "help.mode.pad": "画面の端の8か所にバーが浮かび上がります。方向が最もはっきり分かるモードです。", "help.mode.circle": "画面中央のリングが、音のする方向へふくらみます。", "help.mode.outline": "波の輪郭線だけを細く描くので、ゲーム画面が隠れにくくなります。", - "help.modes.note": "「サイズ」や「濃さ」などの設定はモードごとに保存され、「設定」タブで変更できます。", + "help.modes.note": "「サイズ」や「濃さ」などの設定はモードごとに保存され、「設定」タブの「詳細設定」で変更できます。", "help.sound.title": "音の種類", - "help.sound.kinds": "聞こえてくる音を、AI が環境音・話し声・危険音に分類します。初期設定の色は、環境音が白、話し声が黄色、危険音が赤です。種類ごとの色と表示の有無は「設定」タブで変更できます。", + "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.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.body": "「設定」タブの「詳細設定」でショートカットキーのボタンをクリックし、単独のキーまたはキーの組み合わせ(例:Ctrl + Shift + A)を押すと、ショートカットキーを好きなように変更できます。アプリがバックグラウンドにあるときでも、ショートカットキーを押すとすぐにモードが切り替わります。", "help.hotkeys.defaults": "初期設定では、F2 でサウンドモード、F3 で表示モードを切り替え、F4 でオーバーレイ設定を開きます。", "help.size_lock.title": "サイズ固定モード", "help.size_lock.body": "サイズ固定モードは、音の大きさをサイズではなく濃さで表し、グラフィックのサイズを一定に保つ設定です。グラフィックの大きさを変えずに、音への反応を見たいときに便利です。", @@ -172,15 +184,15 @@ "help.note.direction_surround": "5.1 または 7.1 チャンネルで出力すると、画面の上側は前方からの音、下側は後方からの音を表します。左右はそれぞれ左と右からの音を表し、画面の四方すべてから同時に現れるグラフィックは、環境音や方向のない音です。", "help.note.direction_stereo": "2 チャンネル(ステレオ)で出力すると、左右は区別できますが、前後は区別できません。このとき画面の上下に表示されるグラフィックは、左右から同じくらい聞こえる音と、空間の広がりを表します。", "help.note.sources": "Windows の既定の再生デバイスから出ている、すべてのアプリの音を表示します。アプリがほかのデバイスに送っている音(ボイスチャット専用のヘッドセットなど)は表示されません。", - "help.note.volume": "アプリ内や 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.no_graphic_a": "音が Windows の既定の再生デバイスから出ていて、ミュートになっていないか、「ホーム」タブにお知らせが出ていないかを確認してください。ゲームを排他的フルスクリーンで実行している場合は、ボーダーレスウィンドウやウィンドウモードを試してください。また、「設定」タブで今鳴っている音の種類の表示がオフになっていないか、「詳細設定」で「濃さ」が低すぎないかも確認してください。", "help.faq.performance_q": "CPU や GPU を多く使いますか?", - "help.faq.performance_a": "負荷を軽くするには、「設定」タブで「{0}」を下げるか、「{1}」をオフにしてください。", + "help.faq.performance_a": "負荷を軽くするには、「設定」タブの「詳細設定」で「{0}」を下げるか、「{1}」をオフにしてください。", "help.faq.language_q": "アプリの言語はどうやって変更しますか?", "help.faq.language_a": "このウィンドウの上にある「{0}」の一覧から選びます。実行中はオーバーレイ設定でも変更できます。「{1}」を選ぶと、Windows の表示言語に合わせて表示されます。", "help.report.title": "問題を報告", @@ -285,7 +297,7 @@ "tutorial.direction.title": "音の方向がわかります", "tutorial.direction.body": "2 チャンネル(ステレオ)で出力すると、左からの音は画面の左側が、右からの音は右側が大きく描かれます。5.1 または 7.1 チャンネルで出力すると前後も区別し、前方からの音は画面の上側に、後方からの音は下側に描かれます。", "tutorial.types.title": "音の種類を色で区別します", - "tutorial.types.body": "AI が音を3種類に分類し、それぞれ違う色で描きます。色は「設定」タブで変更できます。", + "tutorial.types.body": "AI が音を3種類に分類し、それぞれ違う色で描きます。色は「設定」タブの「詳細設定」で変更できます。", "tutorial.type.ambient_examples": "音楽・雨音など、その他の音", "tutorial.type.speech_examples": "人の声", "tutorial.type.danger_examples": "銃声・爆発音・サイレン", diff --git a/SoundVisualizer.Core/Localization/Strings/ko.json b/SoundVisualizer.Core/Localization/Strings/ko.json index c4687a1..d8630d2 100644 --- a/SoundVisualizer.Core/Localization/Strings/ko.json +++ b/SoundVisualizer.Core/Localization/Strings/ko.json @@ -75,7 +75,7 @@ "settings.radius.desc": "원형 모드에서 가운데 고리의 반지름을 정합니다.", "settings.section.sound_types": "소리 분류 표시 (AI)", "settings.show_ambient": "환경음 표시", - "settings.show_speech": "대화음 표시", + "settings.show_speech": "사람 소리 표시", "settings.show_danger": "위협음 표시", "settings.set_color": "색상 설정", "settings.set_color_for": "색상 설정: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "실행 시 런처 최소화", "settings.auto_minimize.desc": "오버레이 실행 시 이 런처 창을 자동으로 최소화합니다.", + "settings.view.easy": "쉬운 설정", + "settings.view.detailed": "자세한 설정", + + "easy.section.shape": "모양", + "easy.section.size": "크기", + "easy.section.types": "소리 종류", + "easy.size.small": "작게", + "easy.size.medium": "보통", + "easy.size.large": "크게", + "easy.size.custom": "‘자세한 설정’에서 맞춘 크기입니다. 위에서 선택하면 바뀝니다.", + "easy.name_pair": "{0}, {1}", + "sounds.title": "소리 종류 선택", "sounds.desc": "AI가 알아듣는 소리를 비슷한 것끼리 묶어, 기본 종류별로 보여 줍니다. 묶음의 종류를 바꾸면 그 소리들의 색이 바로 새 종류를 따릅니다.", "sounds.sibling_note": "AI는 한 소리를 여러 이름으로 함께 들을 때가 많습니다. 예를 들어 사이렌은 ‘경보음’으로도 들리는데, 경보음은 다른 묶음에 있습니다. 바꾼 뒤에도 예전 종류로 보이면 관련된 묶음이나 소리도 함께 바꾸세요.", @@ -106,7 +118,7 @@ "sounds.filter.all": "전체", "sounds.filter.changed": "바꾼 소리", "sounds.type.ambient": "환경음", - "sounds.type.speech": "대화음", + "sounds.type.speech": "사람 소리", "sounds.type.danger": "위협음", "sounds.type.default": "{0} (기본)", "sounds.type.mixed": "일부 다름", @@ -148,19 +160,19 @@ "help.mode.pad": "화면 가장자리 8곳에 막대가 떠오릅니다. 방향을 가장 또렷하게 볼 수 있습니다.", "help.mode.circle": "화면 가운데의 고리가 소리 나는 쪽으로 부풀어 오릅니다.", "help.mode.outline": "파도의 윤곽선만 가늘게 그려 게임 화면을 덜 가립니다.", - "help.modes.note": "크기·진하기 같은 설정은 모드마다 따로 저장되고, 설정 탭에서 바꿀 수 있습니다.", + "help.modes.note": "크기·진하기 같은 설정은 모드마다 따로 저장되고, 설정 탭의 ‘자세한 설정’에서 바꿀 수 있습니다.", "help.sound.title": "소리 종류", - "help.sound.kinds": "AI가 들리는 소리를 환경음·대화음·위협음으로 나눕니다. 기본색은 환경음 흰색, 대화음 노란색, 위협음 빨간색이고, 설정 탭에서 종류마다 색과 표시 여부를 바꿀 수 있습니다.", + "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.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.body": "설정 탭의 ‘자세한 설정’에서 핫키 버튼을 클릭한 뒤, 단일 키 또는 복합 키 조합(예: Ctrl + Shift + A)을 눌러 자유롭게 단축키를 변경할 수 있습니다. 백그라운드에서 지정된 단축키를 누르면 실시간으로 모드가 즉시 전환됩니다.", "help.hotkeys.defaults": "기본 단축키는 F2가 사운드 모드 전환, F3가 표현 모드 전환, F4가 오버레이 설정입니다.", "help.size_lock.title": "크기 고정 모드", "help.size_lock.body": "크기 고정 모드는 소리의 크기에 비례하는 투명도를 활용하여 그래픽 크기를 고정시키는 설정입니다. 시각화 그래픽의 크기를 일정하게 유지하면서도 소리에 반응하는 모습을 보고 싶을 때 유용합니다.", @@ -172,15 +184,15 @@ "help.note.direction_surround": "5.1채널이나 7.1채널로 출력하면 화면 위쪽은 앞에서, 아래쪽은 뒤에서 나는 소리를 나타냅니다. 왼쪽과 오른쪽은 각각 왼쪽, 오른쪽 소리이고, 화면 모든 방향에서 한꺼번에 나오는 그래픽은 환경음이나 방향성이 없는 소리입니다.", "help.note.direction_stereo": "2채널(스테레오)로 출력하면 좌우는 구분하지만 앞뒤는 구분하지 못합니다. 이때 화면 위·아래쪽 그래픽은 양쪽에서 비슷하게 들리는 소리와 공간감 효과를 나타냅니다.", "help.note.sources": "Windows 기본 출력 장치로 나오는 모든 앱의 소리를 보여 줍니다. 앱이 다른 장치로 보내는 소리(예: 음성 채팅에만 쓰는 헤드셋)는 보이지 않습니다.", - "help.note.volume": "앱 안이나 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.no_graphic_a": "소리가 Windows 기본 출력 장치로 나오고 있는지, 음소거되어 있지 않은지, 홈에 안내가 떠 있지 않은지 확인해 주세요. 게임을 독점 전체 화면 모드로 하고 있다면 테두리 없는 창 모드나 창 모드로 바꿔 보세요. 설정에서 지금 나는 소리 종류의 표시가 꺼져 있지 않은지, ‘자세한 설정’의 진하기가 너무 낮지 않은지도 확인해 주세요.", "help.faq.performance_q": "CPU나 GPU를 많이 쓰나요?", - "help.faq.performance_a": "더 가볍게 하려면 설정 탭에서 ‘{0}’ 값을 낮추거나 ‘{1}’ 스위치를 끄세요.", + "help.faq.performance_a": "더 가볍게 하려면 설정 탭의 ‘자세한 설정’에서 ‘{0}’ 값을 낮추거나 ‘{1}’ 스위치를 끄세요.", "help.faq.language_q": "앱 언어는 어떻게 바꾸나요?", "help.faq.language_a": "이 창 위쪽의 ‘{0}’ 목록에서 선택합니다. 실행 중에는 오버레이 설정에서도 바꿀 수 있습니다. ‘{1}’ 항목은 Windows 표시 언어를 따릅니다.", "help.report.title": "제보하기", @@ -285,7 +297,7 @@ "tutorial.direction.title": "소리가 나는 방향을 보여 드립니다", "tutorial.direction.body": "2채널(스테레오)로 출력하면 왼쪽에서 나는 소리는 왼쪽이, 오른쪽에서 나는 소리는 오른쪽이 더 크게 그려집니다. 5.1채널이나 7.1채널로 출력하면 앞뒤도 구분해, 앞에서 나는 소리는 화면 위쪽에, 뒤에서 나는 소리는 아래쪽에 그려집니다.", "tutorial.types.title": "소리 종류를 색으로 구분합니다", - "tutorial.types.body": "AI가 소리를 세 종류로 나눠 서로 다른 색으로 그립니다. 색은 설정 탭에서 바꿀 수 있습니다.", + "tutorial.types.body": "AI가 소리를 세 종류로 나눠 서로 다른 색으로 그립니다. 색은 설정 탭의 ‘자세한 설정’에서 바꿀 수 있습니다.", "tutorial.type.ambient_examples": "음악·빗소리 등 그 밖의 소리", "tutorial.type.speech_examples": "사람 목소리", "tutorial.type.danger_examples": "총소리·폭발음·사이렌", diff --git a/SoundVisualizer.Core/Localization/Strings/pl.json b/SoundVisualizer.Core/Localization/Strings/pl.json index a195734..ea9aca0 100644 --- a/SoundVisualizer.Core/Localization/Strings/pl.json +++ b/SoundVisualizer.Core/Localization/Strings/pl.json @@ -75,7 +75,7 @@ "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_speech": "Pokaż dźwięki ludzkie", "settings.show_danger": "Pokaż dźwięki zagrożenia", "settings.set_color": "Ustaw kolor", "settings.set_color_for": "Ustaw kolor: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "Minimalizuj launcher po uruchomieniu", "settings.auto_minimize.desc": "Automatycznie minimalizuje okno launchera, gdy uruchamia się nakładka.", + "settings.view.easy": "Proste", + "settings.view.detailed": "Szczegółowe", + + "easy.section.shape": "Kształt", + "easy.section.size": "Rozmiar", + "easy.section.types": "Rodzaje dźwięków", + "easy.size.small": "Mały", + "easy.size.medium": "Średni", + "easy.size.large": "Duży", + "easy.size.custom": "Ten rozmiar ustawiono w widoku „Szczegółowe”. Wybierz opcję powyżej, aby go zmienić.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Wszystkie", "sounds.filter.changed": "Zmienione", "sounds.type.ambient": "Dźwięki otoczenia", - "sounds.type.speech": "Mowa", + "sounds.type.speech": "Dźwięki ludzkie", "sounds.type.danger": "Dźwięki zagrożenia", "sounds.type.default": "{0} (domyślnie)", "sounds.type.mixed": "Mieszany", @@ -148,19 +160,19 @@ "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.modes.note": "Ustawienia takie jak „Rozmiar” i „Krycie” są zapisywane osobno dla każdego trybu. Możesz je zmienić w widoku „Szczegółowe” 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.kinds": "AI dzieli to, co słychać, na dźwięki otoczenia, dźwięki ludzkie i dźwięki zagrożenia. Domyślnie dźwięki otoczenia są oznaczane na biało, dźwięki ludzkie na żółto, a dźwięki zagrożenia na czerwono. Na karcie „Ustawienia” możesz wybrać, czy każdy rodzaj jest pokazywany, a w widoku „Szczegółowe” – zmienić jego kolor.", "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.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” w widoku „Szczegółowe” 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.body": "W widoku „Szczegółowe” 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.", @@ -172,15 +184,15 @@ "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.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ść” w widoku „Szczegółowe” 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.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” pokazywanie rodzaju dźwięku, który właśnie słychać, nie jest wyłączone, a w widoku „Szczegółowe” wartość „Krycie” nie jest zbyt niska.", "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.performance_a": "Aby ją odciążyć, zmniejsz „{0}” w widoku „Szczegółowe” 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", @@ -285,7 +297,7 @@ "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.types.body": "AI dzieli dźwięki na trzy rodzaje i rysuje każdy w innym kolorze. Kolory możesz zmienić w widoku „Szczegółowe” 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", diff --git a/SoundVisualizer.Core/Localization/Strings/pt-BR.json b/SoundVisualizer.Core/Localization/Strings/pt-BR.json index 09d4043..f9fa802 100644 --- a/SoundVisualizer.Core/Localization/Strings/pt-BR.json +++ b/SoundVisualizer.Core/Localization/Strings/pt-BR.json @@ -75,7 +75,7 @@ "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_speech": "Mostrar sons humanos", "settings.show_danger": "Mostrar sons de perigo", "settings.set_color": "Escolher cor", "settings.set_color_for": "Escolher cor: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "Minimizar o inicializador ao iniciar", "settings.auto_minimize.desc": "Minimiza automaticamente esta janela do inicializador quando a sobreposição é iniciada.", + "settings.view.easy": "Simples", + "settings.view.detailed": "Detalhado", + + "easy.section.shape": "Forma", + "easy.section.size": "Tamanho", + "easy.section.types": "Tipos de som", + "easy.size.small": "Pequeno", + "easy.size.medium": "Médio", + "easy.size.large": "Grande", + "easy.size.custom": "Este tamanho foi ajustado em “Detalhado”. Escolha uma opção acima para mudar.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Todos", "sounds.filter.changed": "Alterados", "sounds.type.ambient": "Som ambiente", - "sounds.type.speech": "Voz", + "sounds.type.speech": "Sons humanos", "sounds.type.danger": "Som de perigo", "sounds.type.default": "{0} (padrão)", "sounds.type.mixed": "Misto", @@ -148,19 +160,19 @@ "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.modes.note": "Ajustes como Tamanho e Opacidade são salvos separadamente para cada modo e podem ser alterados em “Detalhado”, 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.kinds": "A IA separa os sons em três tipos: sons ambientes, sons humanos e sons de perigo. Por padrão, os sons ambientes aparecem em branco, os sons humanos em amarelo e os sons de perigo em vermelho. Na aba Ajustes, você pode escolher se cada tipo é mostrado e, em “Detalhado”, mudar a cor dele.", "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.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” em “Detalhado”, 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.body": "Em “Detalhado”, 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.", @@ -172,15 +184,15 @@ "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.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 em “Detalhado”, 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.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 exibição do tipo de som atual não está desativada e, em “Detalhado”, se a Opacidade não está baixa demais.", "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.performance_a": "Para deixar mais leve, diminua “{0}” em “Detalhado”, 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", @@ -285,7 +297,7 @@ "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.types.body": "A IA separa os sons em três tipos e desenha cada um com uma cor diferente. Você pode mudar as cores em “Detalhado”, 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", diff --git a/SoundVisualizer.Core/Localization/Strings/ru.json b/SoundVisualizer.Core/Localization/Strings/ru.json index e01c9e3..a6766fa 100644 --- a/SoundVisualizer.Core/Localization/Strings/ru.json +++ b/SoundVisualizer.Core/Localization/Strings/ru.json @@ -75,7 +75,7 @@ "settings.radius.desc": "Задаёт радиус центрального кольца в режиме «Круг».", "settings.section.sound_types": "Типы звуков (ИИ)", "settings.show_ambient": "Показывать фоновые звуки", - "settings.show_speech": "Показывать речь", + "settings.show_speech": "Показывать звуки человека", "settings.show_danger": "Показывать опасные звуки", "settings.set_color": "Задать цвет", "settings.set_color_for": "Задать цвет: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "Сворачивать лаунчер при запуске", "settings.auto_minimize.desc": "Автоматически сворачивает окно лаунчера, когда запускается оверлей.", + "settings.view.easy": "Простые", + "settings.view.detailed": "Подробные", + + "easy.section.shape": "Форма", + "easy.section.size": "Размер", + "easy.section.types": "Типы звуков", + "easy.size.small": "Мелкий", + "easy.size.medium": "Средний", + "easy.size.large": "Крупный", + "easy.size.custom": "Размер задан в разделе «Подробные». Выберите вариант выше, чтобы изменить его.", + "easy.name_pair": "{0}, {1}", + "sounds.title": "Выберите типы звуков", "sounds.desc": "Звуки, которые распознаёт ИИ, собраны в группы похожих звуков по типам, к которым они относятся по умолчанию. Если изменить тип группы, цвет её звуков сразу станет таким же, как у нового типа.", "sounds.sibling_note": "ИИ часто распознаёт один и тот же звук сразу под несколькими названиями. Сирена, например, распознаётся ещё и как «Сигнал тревоги», а этот звук в другой группе. Если после изменения звук всё ещё отображается со старым типом, измените и связанные группы или звуки.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Все", "sounds.filter.changed": "Изменённые", "sounds.type.ambient": "Фоновые звуки", - "sounds.type.speech": "Речь", + "sounds.type.speech": "Звуки человека", "sounds.type.danger": "Опасные звуки", "sounds.type.default": "{0} (по умолчанию)", "sounds.type.mixed": "Смешанный", @@ -148,19 +160,19 @@ "help.mode.pad": "В 8 точках по краю экрана загораются полоски. В этом режиме направление видно лучше всего.", "help.mode.circle": "Кольцо в центре экрана вытягивается в сторону звука.", "help.mode.outline": "Рисуется только тонкий контур волны, поэтому графика меньше загораживает игру.", - "help.modes.note": "Такие настройки, как «Размер» и «Плотность», сохраняются отдельно для каждого режима. Изменить их можно на вкладке «Настройки».", + "help.modes.note": "Такие настройки, как «Размер» и «Плотность», сохраняются отдельно для каждого режима. Изменить их можно на вкладке «Настройки» в разделе «Подробные».", "help.sound.title": "Типы звуков", - "help.sound.kinds": "ИИ делит всё, что слышно, на фоновые звуки, речь и опасные звуки. По умолчанию фоновые звуки отображаются белым, речь — жёлтым, а опасные звуки — красным. На вкладке «Настройки» можно изменить цвет каждого типа и выбрать, показывать ли его.", + "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.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.body": "На вкладке «Настройки» в разделе «Подробные» нажмите кнопку горячей клавиши, а затем нажмите одну клавишу или сочетание клавиш (например, Ctrl + Shift + A), чтобы изменить горячую клавишу по своему усмотрению. Горячая клавиша мгновенно переключает режим, даже когда приложение работает в фоне.", "help.hotkeys.defaults": "По умолчанию F2 переключает звуковой режим, F3 — режим отображения, а F4 открывает настройки оверлея.", "help.size_lock.title": "Режим фиксации размера", "help.size_lock.body": "В режиме фиксации размера графика сохраняет постоянный размер, а громкость звука передаётся прозрачностью. Это удобно, если вы хотите, чтобы графика не меняла размер, но при этом было видно, как она реагирует на звук.", @@ -172,15 +184,15 @@ "help.note.direction_surround": "При выводе звука 5.1 или 7.1 верх экрана показывает звук спереди, а низ — звук сзади. Левый и правый края показывают звук слева и справа, а графика, появляющаяся одновременно со всех сторон экрана, — это фоновые или ненаправленные звуки.", "help.note.direction_stereo": "При двухканальном (стерео) выводе приложение различает звуки слева и справа, но не спереди и сзади. Графика вверху и внизу экрана в этом случае обозначает звуки, одинаково слышные с обеих сторон, а также передаёт ощущение пространства.", "help.note.sources": "Показывается звук всех приложений, который идёт через устройство воспроизведения Windows по умолчанию. Звук, который приложение отправляет на другое устройство, например на гарнитуру только для голосового чата, не показывается.", - "help.note.volume": "Если громкость приложения сильно убавлена или выключена — в самом приложении или в микшере громкости 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.no_graphic_a": "Проверьте, что звук идёт через устройство воспроизведения Windows по умолчанию и не выключен, а также нет ли сообщения на вкладке «Главная». Если игра работает в монопольном полноэкранном режиме, попробуйте оконный режим без рамки или оконный режим. Также убедитесь, что на вкладке «Настройки» не выключен показ текущего типа звука и что в разделе «Подробные» значение «Плотность» не слишком низкое.", "help.faq.performance_q": "Приложение сильно нагружает процессор или видеокарту?", - "help.faq.performance_a": "Чтобы снизить нагрузку, уменьшите «{0}» на вкладке «Настройки» или выключите «{1}».", + "help.faq.performance_a": "Чтобы снизить нагрузку, уменьшите «{0}» на вкладке «Настройки» в разделе «Подробные» или выключите «{1}».", "help.faq.language_q": "Как изменить язык приложения?", "help.faq.language_a": "Выберите его в списке «{0}» в верхней части этого окна. Во время работы язык можно сменить и в настройках оверлея. При выборе варианта «{1}» используется язык интерфейса Windows.", "help.report.title": "Сообщить о проблеме", @@ -285,7 +297,7 @@ "tutorial.direction.title": "Показывает, откуда идёт звук", "tutorial.direction.body": "При двухканальном (стерео) выводе звук слева рисуется крупнее у левого края экрана, а звук справа — у правого. При выводе звука 5.1 или 7.1 приложение различает и звуки спереди и сзади: звук спереди рисуется вверху экрана, а звук сзади — внизу.", "tutorial.types.title": "У каждого типа звука свой цвет", - "tutorial.types.body": "ИИ делит звуки на три типа и рисует каждый своим цветом. Цвета можно изменить на вкладке «Настройки».", + "tutorial.types.body": "ИИ делит звуки на три типа и рисует каждый своим цветом. Цвета можно изменить на вкладке «Настройки» в разделе «Подробные».", "tutorial.type.ambient_examples": "Музыка, дождь и всё остальное", "tutorial.type.speech_examples": "Голоса людей", "tutorial.type.danger_examples": "Выстрелы, взрывы, сирены", diff --git a/SoundVisualizer.Core/Localization/Strings/th.json b/SoundVisualizer.Core/Localization/Strings/th.json index 79908cd..91f0a6b 100644 --- a/SoundVisualizer.Core/Localization/Strings/th.json +++ b/SoundVisualizer.Core/Localization/Strings/th.json @@ -75,7 +75,7 @@ "settings.radius.desc": "ปรับรัศมีของวงแหวนตรงกลางในโหมดวงกลม", "settings.section.sound_types": "ประเภทเสียง (AI)", "settings.show_ambient": "แสดงเสียงแวดล้อม", - "settings.show_speech": "แสดงเสียงพูด", + "settings.show_speech": "แสดงเสียงคน", "settings.show_danger": "แสดงเสียงอันตราย", "settings.set_color": "ตั้งค่าสี", "settings.set_color_for": "ตั้งค่าสี: {0}", @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "ย่อตัวเปิดใช้เมื่อเริ่ม", "settings.auto_minimize.desc": "ย่อหน้าต่างตัวเปิดใช้นี้โดยอัตโนมัติเมื่อโอเวอร์เลย์เริ่มทำงาน", + "settings.view.easy": "แบบง่าย", + "settings.view.detailed": "แบบละเอียด", + + "easy.section.shape": "รูปทรง", + "easy.section.size": "ขนาด", + "easy.section.types": "ประเภทเสียง", + "easy.size.small": "เล็ก", + "easy.size.medium": "กลาง", + "easy.size.large": "ใหญ่", + "easy.size.custom": "ขนาดนี้ตั้งไว้ใน “แบบละเอียด” เลือกตัวเลือกด้านบนเพื่อเปลี่ยน", + "easy.name_pair": "{0}, {1}", + "sounds.title": "เลือกประเภทเสียง", "sounds.desc": "เสียงที่ AI รู้จักจะจัดกลุ่มรวมกับเสียงที่คล้ายกัน และแสดงตามประเภทเริ่มต้นของแต่ละเสียง หากเปลี่ยนประเภทของกลุ่ม สีของเสียงในกลุ่มจะเปลี่ยนตามประเภทใหม่ทันที", "sounds.sibling_note": "AI มักได้ยินเสียงเดียวเป็นหลายชื่อพร้อมกัน เช่น ไซเรนยังได้ยินเป็น “สัญญาณเตือนภัย” ด้วย ซึ่งอยู่ในอีกกลุ่มหนึ่ง หากเปลี่ยนแล้วเสียงยังแสดงเป็นประเภทเดิม ให้เปลี่ยนกลุ่มหรือเสียงที่เกี่ยวข้องด้วย", @@ -106,7 +118,7 @@ "sounds.filter.all": "ทั้งหมด", "sounds.filter.changed": "เปลี่ยนแล้ว", "sounds.type.ambient": "เสียงแวดล้อม", - "sounds.type.speech": "เสียงพูด", + "sounds.type.speech": "เสียงคน", "sounds.type.danger": "เสียงอันตราย", "sounds.type.default": "{0} (ค่าเริ่มต้น)", "sounds.type.mixed": "ผสม", @@ -148,19 +160,19 @@ "help.mode.pad": "แถบจะสว่างขึ้นที่ 8 จุดรอบขอบหน้าจอ แสดงทิศทางได้ชัดเจนที่สุด", "help.mode.circle": "วงแหวนตรงกลางหน้าจอจะพองออกไปทางทิศทางที่เสียงมา", "help.mode.outline": "วาดเฉพาะเส้นขอบบางของคลื่น จึงบังหน้าจอเกมน้อยกว่า", - "help.modes.note": "การตั้งค่าต่างๆ เช่น ขนาดและความเข้ม จะบันทึกแยกกันในแต่ละโหมด และเปลี่ยนได้ในแท็บการตั้งค่า", + "help.modes.note": "การตั้งค่าต่างๆ เช่น ขนาดและความเข้ม จะบันทึกแยกกันในแต่ละโหมด และเปลี่ยนได้ที่ “แบบละเอียด” ในแท็บการตั้งค่า", "help.sound.title": "ประเภทเสียง", - "help.sound.kinds": "AI จะแยกเสียงที่ได้ยินออกเป็นเสียงแวดล้อม เสียงพูด และเสียงอันตราย สีเริ่มต้นคือ เสียงแวดล้อมสีขาว เสียงพูดสีเหลือง และเสียงอันตรายสีแดง ในแท็บการตั้งค่า คุณสามารถเปลี่ยนสีของแต่ละประเภทและเลือกได้ว่าจะแสดงหรือไม่", + "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.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.body": "ที่ “แบบละเอียด” ในแท็บการตั้งค่า ให้คลิกปุ่มคีย์ลัด แล้วกดคีย์เดียวหรือคีย์ผสม (เช่น Ctrl + Shift + A) เพื่อเปลี่ยนคีย์ลัดได้ตามต้องการ เมื่อกดคีย์ลัด โหมดจะสลับทันทีแม้แอปจะทำงานอยู่เบื้องหลัง", "help.hotkeys.defaults": "ค่าเริ่มต้นคือ F2 สลับโหมดเสียง F3 สลับโหมดการแสดงผล และ F4 เปิดการตั้งค่าโอเวอร์เลย์", "help.size_lock.title": "โหมดล็อกขนาด", "help.size_lock.body": "โหมดล็อกขนาดจะคงขนาดของกราฟิกไว้ และใช้ความเข้มที่แปรผันตามความดังของเสียงแทน เหมาะเมื่อคุณต้องการให้กราฟิกมีขนาดเท่าเดิม แต่ยังเห็นการตอบสนองต่อเสียง", @@ -172,15 +184,15 @@ "help.note.direction_surround": "เมื่อส่งเสียงออกแบบ 5.1 หรือ 7.1 แชนแนล ด้านบนของหน้าจอแสดงเสียงจากด้านหน้า และด้านล่างแสดงเสียงจากด้านหลัง ด้านซ้ายและขวาแสดงเสียงจากทางซ้ายและทางขวา ส่วนกราฟิกที่ปรากฏจากทุกด้านของหน้าจอพร้อมกันคือเสียงแวดล้อมหรือเสียงที่ไม่มีทิศทาง", "help.note.direction_stereo": "เมื่อส่งเสียงออกแบบ 2 แชนแนล (สเตอริโอ) แอปแยกเสียงซ้ายกับขวาได้ แต่แยกด้านหน้ากับด้านหลังไม่ได้ ในกรณีนี้ กราฟิกที่ด้านบนและด้านล่างของหน้าจอแสดงถึงเสียงที่ได้ยินเท่าๆ กันทั้งสองข้าง รวมถึงมิติเชิงพื้นที่", "help.note.sources": "แอปจะแสดงเสียงของทุกแอปที่เล่นผ่านอุปกรณ์เล่นเสียงเริ่มต้นของ Windows เสียงที่แอปส่งไปยังอุปกรณ์อื่น เช่น หูฟังที่ใช้สำหรับแชทเสียงเท่านั้น จะไม่แสดง", - "help.note.volume": "หากลดระดับเสียงของแอปใดลงมากหรือปิดเสียงไว้ ไม่ว่าจะในแอปนั้นเองหรือในตัวผสมเสียงของ 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.no_graphic_a": "โปรดตรวจสอบว่าเสียงเล่นผ่านอุปกรณ์เล่นเสียงเริ่มต้นของ Windows และไม่ได้ปิดเสียงไว้ รวมถึงดูว่าแท็บหน้าแรกแสดงข้อความแจ้งหรือไม่ หากเกมทำงานในโหมดเต็มหน้าจอแบบเอกสิทธิ์ ให้ลองใช้โหมดหน้าต่างแบบไม่มีขอบหรือโหมดหน้าต่าง นอกจากนี้ โปรดตรวจสอบในแท็บการตั้งค่าด้วยว่าไม่ได้ปิดการแสดงเสียงประเภทที่กำลังดังอยู่ รวมถึงไม่ได้ตั้งความเข้มไว้ต่ำเกินไปใน “แบบละเอียด”", "help.faq.performance_q": "ใช้ CPU หรือ GPU มากไหม", - "help.faq.performance_a": "หากต้องการให้เบาลง ให้ลด “{0}” ในแท็บการตั้งค่า หรือปิด “{1}”", + "help.faq.performance_a": "หากต้องการให้เบาลง ให้ลด “{0}” ที่ “แบบละเอียด” ในแท็บการตั้งค่า หรือปิด “{1}”", "help.faq.language_q": "เปลี่ยนภาษาของแอปได้อย่างไร", "help.faq.language_a": "เลือกจากรายการ “{0}” ที่ด้านบนของหน้าต่างนี้ ขณะทำงานอยู่ คุณยังเปลี่ยนได้ในการตั้งค่าโอเวอร์เลย์ หากเลือก “{1}” แอปจะใช้ภาษาที่แสดงของ Windows", "help.report.title": "รายงานปัญหา", @@ -285,7 +297,7 @@ "tutorial.direction.title": "บอกได้ว่าเสียงมาจากทางไหน", "tutorial.direction.body": "เมื่อส่งเสียงออกแบบ 2 แชนแนล (สเตอริโอ) หากเสียงมาจากทางซ้าย ภาพฝั่งซ้ายของหน้าจอจะใหญ่กว่า หากมาจากทางขวา ภาพฝั่งขวาจะใหญ่กว่า เมื่อส่งเสียงออกแบบ 5.1 หรือ 7.1 แชนแนล แอปจะแยกด้านหน้ากับด้านหลังได้ด้วย โดยเสียงจากด้านหน้าจะแสดงที่ด้านบนของหน้าจอ และเสียงจากด้านหลังจะแสดงที่ด้านล่าง", "tutorial.types.title": "เสียงแต่ละประเภทมีสีของตัวเอง", - "tutorial.types.body": "AI จะแยกเสียงออกเป็น 3 ประเภท และวาดแต่ละประเภทด้วยสีของตัวเอง คุณเปลี่ยนสีได้ในแท็บการตั้งค่า", + "tutorial.types.body": "AI จะแยกเสียงออกเป็น 3 ประเภท และวาดแต่ละประเภทด้วยสีของตัวเอง คุณเปลี่ยนสีได้ที่ “แบบละเอียด” ในแท็บการตั้งค่า", "tutorial.type.ambient_examples": "เพลง เสียงฝน และเสียงอื่นทั้งหมด", "tutorial.type.speech_examples": "เสียงคน", "tutorial.type.danger_examples": "เสียงปืน เสียงระเบิด เสียงไซเรน", diff --git a/SoundVisualizer.Core/Localization/Strings/tr.json b/SoundVisualizer.Core/Localization/Strings/tr.json index eeac03c..d37865d 100644 --- a/SoundVisualizer.Core/Localization/Strings/tr.json +++ b/SoundVisualizer.Core/Localization/Strings/tr.json @@ -75,7 +75,7 @@ "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_speech": "İnsan seslerini göster", "settings.show_danger": "Tehlike seslerini göster", "settings.set_color": "Renk ayarla", "settings.set_color_for": "Renk ayarla: {0}", @@ -95,6 +95,18 @@ "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.", + "settings.view.easy": "Basit", + "settings.view.detailed": "Ayrıntılı", + + "easy.section.shape": "Şekil", + "easy.section.size": "Boyut", + "easy.section.types": "Ses türleri", + "easy.size.small": "Küçük", + "easy.size.medium": "Orta", + "easy.size.large": "Büyük", + "easy.size.custom": "Bu boyut “Ayrıntılı” görünümünde ayarlandı. Değiştirmek için yukarıdan bir seçenek seçin.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "sounds.filter.all": "Tümü", "sounds.filter.changed": "Değiştirilenler", "sounds.type.ambient": "Ortam sesi", - "sounds.type.speech": "Konuşma", + "sounds.type.speech": "İnsan sesleri", "sounds.type.danger": "Tehlike sesi", "sounds.type.default": "{0} (varsayılan)", "sounds.type.mixed": "Karışık", @@ -148,19 +160,19 @@ "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.modes.note": "“Boyut” ve “Opaklık” gibi ayarlar her mod için ayrı kaydedilir ve “Ayarlar” sekmesindeki “Ayrıntılı” görünümünden 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.kinds": "Yapay zeka, duyduğunuz sesleri ortam sesleri, insan sesleri ve tehlike sesleri olarak ayırır. Varsayılan olarak ortam sesleri beyaz, insan sesleri sarı, tehlike sesleri ise kırmızı renkte gösterilir. Her türün gösterilip gösterilmeyeceğini “Ayarlar” sekmesinden seçebilir, rengini ise “Ayrıntılı” görünümünden 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.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 “Ayrıntılı” görünümünde bulunan “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.body": "“Ayarlar” sekmesindeki “Ayrıntılı” görünümünde 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.", @@ -172,15 +184,15 @@ "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.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” sekmesindeki “Ayrıntılı” görünümünde “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.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 o an çalan ses türünün gösteriminin kapalı olmadığından ve “Ayrıntılı” görünümünde “Opaklık” değerinin çok düşük 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.performance_a": "Yükü azaltmak için “Ayarlar” sekmesindeki “Ayrıntılı” görünümünde “{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", @@ -285,7 +297,7 @@ "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.types.body": "Yapay zeka, sesleri üç türe ayırır ve her türü farklı bir renkte çizer. Renkleri “Ayarlar” sekmesindeki “Ayrıntılı” görünümünden 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", diff --git a/SoundVisualizer.Core/Localization/Strings/vi.json b/SoundVisualizer.Core/Localization/Strings/vi.json index d74dd03..d168474 100644 --- a/SoundVisualizer.Core/Localization/Strings/vi.json +++ b/SoundVisualizer.Core/Localization/Strings/vi.json @@ -75,7 +75,7 @@ "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_speech": "Hiện tiếng ngườ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}", @@ -95,6 +95,18 @@ "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.", + "settings.view.easy": "Đơn giản", + "settings.view.detailed": "Chi tiết", + + "easy.section.shape": "Hình dạng", + "easy.section.size": "Kích thước", + "easy.section.types": "Loại âm thanh", + "easy.size.small": "Nhỏ", + "easy.size.medium": "Vừa", + "easy.size.large": "Lớn", + "easy.size.custom": "Kích thước này được đặt trong “Chi tiết”. Chọn một mục ở trên để thay đổi.", + "easy.name_pair": "{0}, {1}", + "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.", @@ -106,7 +118,7 @@ "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.speech": "Tiếng người", "sounds.type.danger": "Âm thanh nguy hiểm", "sounds.type.default": "{0} (mặc định)", "sounds.type.mixed": "Hỗn hợp", @@ -148,19 +160,19 @@ "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.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 ở phần “Chi tiết” 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.kinds": "AI phân loại âm thanh bạn nghe thấy thành âm thanh môi trường, tiếng người và âm thanh nguy hiểm. Theo mặc định, âm thanh môi trường có màu trắng, tiếng người màu vàng và âm thanh nguy hiểm màu đỏ. Bạn có thể chọn có hiện hay không cho từng loại trong thẻ Cài đặt, và đổi màu ở phần “Chi tiế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.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” ở phần “Chi tiết” 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.body": "Ở phần “Chi tiết” 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.", @@ -172,15 +184,15 @@ "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.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 ở phần “Chi tiết” 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.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, trong thẻ Cài đặt, hãy đảm bảo tùy chọn hiện loại âm thanh đang phát vẫn đang bật và Độ đậm ở phần “Chi tiết” không quá thấp.", "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.performance_a": "Để ứng dụng nhẹ hơn, hãy giảm “{0}” ở phần “Chi tiết” 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ố", @@ -285,7 +297,7 @@ "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.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 ở phần “Chi tiết” 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", diff --git a/SoundVisualizer.Core/Localization/Strings/zh-Hans.json b/SoundVisualizer.Core/Localization/Strings/zh-Hans.json index 75761ee..7e20b99 100644 --- a/SoundVisualizer.Core/Localization/Strings/zh-Hans.json +++ b/SoundVisualizer.Core/Localization/Strings/zh-Hans.json @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "启动时最小化启动器", "settings.auto_minimize.desc": "启动覆盖层时,自动最小化此启动器窗口。", + "settings.view.easy": "简易设置", + "settings.view.detailed": "详细设置", + + "easy.section.shape": "形状", + "easy.section.size": "大小", + "easy.section.types": "声音类型", + "easy.size.small": "小", + "easy.size.medium": "中", + "easy.size.large": "大", + "easy.size.custom": "此大小是在“详细设置”中调整的。在上方选择即可更改。", + "easy.name_pair": "{0},{1}", + "sounds.title": "选择声音类型", "sounds.desc": "AI 能识别的声音与相似的声音归为一组,并按各自的默认类型显示。更改一组的类型后,组内声音的颜色会立即随新类型改变。", "sounds.sibling_note": "AI 常常会把同一个声音同时识别为多个名称。例如警笛也会被识别为“警报”,而“警报”在另一组中。如果更改后某个声音仍按原来的类型显示,请一并更改相关的组或声音。", @@ -148,19 +160,19 @@ "help.mode.pad": "屏幕边缘的 8 个位置会亮起光条,方向显示最为清晰。", "help.mode.circle": "屏幕中央的圆环会朝声音传来的方向鼓起。", "help.mode.outline": "只绘制细细的波浪轮廓线,对游戏画面的遮挡更少。", - "help.modes.note": "“大小”“不透明度”等设置会按模式分别保存,可在“设置”标签页中更改。", + "help.modes.note": "“大小”“不透明度”等设置会按模式分别保存,可在“设置”标签页的“详细设置”中更改。", "help.sound.title": "声音类型", - "help.sound.kinds": "AI 会将听到的声音分为环境音、人声和危险音。默认情况下,环境音为白色,人声为黄色,危险音为红色。您可以在“设置”标签页中更改每种类型的颜色以及是否显示。", + "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.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.body": "在“设置”标签页的“详细设置”中点击快捷键按钮,然后按下单个按键或组合键(例如 Ctrl + Shift + A),即可随意更改快捷键。即使应用在后台运行,按下快捷键也会立即切换模式。", "help.hotkeys.defaults": "默认情况下,F2 切换声音模式,F3 切换显示模式,F4 打开覆盖层设置。", "help.size_lock.title": "锁定大小模式", "help.size_lock.body": "锁定大小模式用与声音大小成正比的不透明度来表示声音,从而让图形保持固定大小。如果想在保持图形大小不变的同时看到它对声音的反应,这个模式会很有用。", @@ -172,15 +184,15 @@ "help.note.direction_surround": "以 5.1 或 7.1 声道输出时,屏幕顶部表示来自前方的声音,底部表示来自后方的声音。左右两侧分别表示来自左边和右边的声音,而从屏幕四周同时出现的图形表示环境音或没有方向的声音。", "help.note.direction_stereo": "以 2 声道(立体声)输出时,可以分辨左右,但无法分辨前后。此时屏幕顶部和底部的图形表示左右两侧听起来相近的声音以及空间感。", "help.note.sources": "会显示通过 Windows 默认播放设备播放的所有应用的声音。应用发送到其他设备的声音(例如只用于语音聊天的耳机)不会显示。", - "help.note.volume": "如果在应用内或 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.no_graphic_a": "请确认声音正通过 Windows 默认播放设备播放且没有静音,并查看“首页”是否显示了提示。如果游戏以独占全屏模式运行,请尝试改用无边框窗口或窗口模式。另外,请确认“设置”标签页中当前声音类型的显示没有被关闭,并且“详细设置”中的“不透明度”没有调得太低。", "help.faq.performance_q": "会占用很多 CPU 或 GPU 吗?", - "help.faq.performance_a": "如需减轻负担,请在“设置”标签页中调低“{0}”,或关闭“{1}”。", + "help.faq.performance_a": "如需减轻负担,请在“设置”标签页的“详细设置”中调低“{0}”,或关闭“{1}”。", "help.faq.language_q": "如何更改应用语言?", "help.faq.language_a": "在此窗口顶部的“{0}”列表中选择。运行时也可以在覆盖层设置中更改。选择“{1}”将跟随 Windows 的显示语言。", "help.report.title": "报告问题", @@ -285,7 +297,7 @@ "tutorial.direction.title": "显示声音来自哪个方向", "tutorial.direction.body": "以 2 声道(立体声)输出时,来自左边的声音会在左侧显示得更大,来自右边的声音会在右侧显示得更大。以 5.1 或 7.1 声道输出时,还能分辨前后:来自前方的声音显示在屏幕顶部,来自后方的声音显示在底部。", "tutorial.types.title": "用颜色区分声音类型", - "tutorial.types.body": "AI 会将声音分为三种类型,并用不同的颜色显示。您可以在“设置”标签页中更改颜色。", + "tutorial.types.body": "AI 会将声音分为三种类型,并用不同的颜色显示。您可以在“设置”标签页的“详细设置”中更改颜色。", "tutorial.type.ambient_examples": "音乐、雨声等其他声音", "tutorial.type.speech_examples": "说话声", "tutorial.type.danger_examples": "枪声、爆炸声、警报声", diff --git a/SoundVisualizer.Core/Localization/Strings/zh-Hant.json b/SoundVisualizer.Core/Localization/Strings/zh-Hant.json index 3b8088b..2186e6c 100644 --- a/SoundVisualizer.Core/Localization/Strings/zh-Hant.json +++ b/SoundVisualizer.Core/Localization/Strings/zh-Hant.json @@ -95,6 +95,18 @@ "settings.auto_minimize.title": "開始時將啟動器最小化", "settings.auto_minimize.desc": "覆疊畫面開始執行時,自動將這個啟動器視窗最小化。", + "settings.view.easy": "簡易設定", + "settings.view.detailed": "詳細設定", + + "easy.section.shape": "形狀", + "easy.section.size": "大小", + "easy.section.types": "聲音類型", + "easy.size.small": "小", + "easy.size.medium": "中", + "easy.size.large": "大", + "easy.size.custom": "這個大小是在「詳細設定」中調整的。在上方選擇即可變更。", + "easy.name_pair": "{0},{1}", + "sounds.title": "選擇聲音類型", "sounds.desc": "AI 能辨識的聲音會和相似的聲音歸為同一群組,並依各自的預設類型顯示。變更群組的類型後,其中聲音的顏色會立即跟著新類型改變。", "sounds.sibling_note": "AI 常常會把同一個聲音同時辨識為多個名稱。例如警笛也會被辨識為「警報」,而「警報」在另一個群組。如果變更後某個聲音仍以原本的類型顯示,請一併變更相關的群組或聲音。", @@ -148,19 +160,19 @@ "help.mode.pad": "螢幕邊緣的 8 個位置會亮起光條,最容易看出聲音的方向。", "help.mode.circle": "螢幕中央的圓環會朝聲音來源的方向膨脹。", "help.mode.outline": "只以細線描繪波浪的輪廓,較不會遮住遊戲畫面。", - "help.modes.note": "「大小」、「不透明度」等設定會依模式分別儲存,可以在「設定」分頁中變更。", + "help.modes.note": "「大小」、「不透明度」等設定會依模式分別儲存,可以在「設定」分頁的「詳細設定」中變更。", "help.sound.title": "聲音類型", - "help.sound.kinds": "AI 會將聽到的聲音分為環境音、人聲和危險音。預設情況下,環境音為白色、人聲為黃色、危險音為紅色。你可以在「設定」分頁中變更各類型的顏色,以及是否顯示。", + "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.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.body": "在「設定」分頁的「詳細設定」中按一下快速鍵按鈕,然後按下單一按鍵或組合鍵 (例如 Ctrl + Shift + A),就能隨意變更快速鍵。即使應用程式在背景執行,按下快速鍵也會立即切換模式。", "help.hotkeys.defaults": "預設情況下,F2 切換聲音模式,F3 切換視覺模式,F4 開啟覆疊畫面設定。", "help.size_lock.title": "鎖定大小模式", "help.size_lock.body": "鎖定大小模式會以與音量成正比的不透明度呈現聲音,讓圖形維持固定大小。如果想讓圖形大小保持不變,同時看到它對聲音的反應,這個模式就很實用。", @@ -172,15 +184,15 @@ "help.note.direction_surround": "以 5.1 或 7.1 聲道輸出時,螢幕上方代表來自前方的聲音,下方代表來自後方的聲音。左右兩側分別代表來自左邊和右邊的聲音,而從螢幕四周同時出現的圖形,則是環境音或沒有方向性的聲音。", "help.note.direction_stereo": "以 2 聲道 (立體聲) 輸出時,可以分辨左右,但無法分辨前後。這時螢幕上方和下方的圖形代表兩側聽起來差不多的聲音,以及空間感效果。", "help.note.sources": "會顯示透過 Windows 預設播放裝置播放的所有應用程式的聲音。應用程式傳送到其他裝置的聲音 (例如只用於語音聊天的耳機) 不會顯示。", - "help.note.volume": "如果在應用程式內或 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.no_graphic_a": "請確認聲音正透過 Windows 預設播放裝置播放且沒有靜音,並查看「首頁」是否顯示了提示。如果遊戲以獨佔全螢幕模式執行,請試著改用無邊框視窗或視窗模式。另外,也請確認「設定」分頁中目前這種聲音類型的顯示沒有關閉,以及「詳細設定」中的「不透明度」沒有調得太低。", "help.faq.performance_q": "會占用很多 CPU 或 GPU 嗎?", - "help.faq.performance_a": "如要減輕負擔,請在「設定」分頁中調低「{0}」,或關閉「{1}」。", + "help.faq.performance_a": "如要減輕負擔,請在「設定」分頁的「詳細設定」中調低「{0}」,或關閉「{1}」。", "help.faq.language_q": "如何變更應用程式語言?", "help.faq.language_a": "從此視窗頂端的「{0}」清單中選擇。執行時也可以在覆疊畫面設定中變更。選擇「{1}」會跟隨 Windows 的顯示語言。", "help.report.title": "回報問題", @@ -285,7 +297,7 @@ "tutorial.direction.title": "顯示聲音來自哪個方向", "tutorial.direction.body": "以 2 聲道 (立體聲) 輸出時,來自左邊的聲音會在左側顯示得更大,來自右邊的聲音會在右側顯示得更大。以 5.1 或 7.1 聲道輸出時,還能分辨前後:來自前方的聲音顯示在螢幕上方,來自後方的聲音顯示在下方。", "tutorial.types.title": "用顏色區分聲音類型", - "tutorial.types.body": "AI 會將聲音分為三種類型,並以不同顏色顯示。你可以在「設定」分頁中變更顏色。", + "tutorial.types.body": "AI 會將聲音分為三種類型,並以不同顏色顯示。你可以在「設定」分頁的「詳細設定」中變更顏色。", "tutorial.type.ambient_examples": "音樂、雨聲等其他聲音", "tutorial.type.speech_examples": "說話聲", "tutorial.type.danger_examples": "槍聲、爆炸聲、警報聲", diff --git a/SoundVisualizer.Core/Tutorial/TutorialDemo.cs b/SoundVisualizer.Core/Tutorial/TutorialDemo.cs index c5c6ce5..7030d2c 100644 --- a/SoundVisualizer.Core/Tutorial/TutorialDemo.cs +++ b/SoundVisualizer.Core/Tutorial/TutorialDemo.cs @@ -30,20 +30,48 @@ public sealed class TutorialDemo /// 미리 돌릴 때의 프레임 간격(60fps). private const double PrerollFps = 60; + /// + /// 쉬운 설정의 모양 카드(#408)가 그리는 화면(DIP). 카드의 그림은 튜토리얼 그림보다 훨씬 작아, 기준 화면을 그대로 줄이면 패드의 막대와 + /// 외곽선이 1px 아래로 가늘어진다. 그래서 더 작은 화면(16:9)을 줄인 것으로 보아 띠와 선을 굵게 그린다(안드로이드 모양 카드가 + /// 튜토리얼의 짧은 변 411dp 대신 240dp 로 보는 것과 같은 비율, 1080 × 240 / 411 ≈ 630). 크기 단계의 차이는 그대로다. + /// + public const double PreviewWidth = 1120; + public const double PreviewHeight = 630; + + /// + /// 모양 카드가 한 번 움직이는 시간(초, 안드로이드 PREVIEW_PLAY_SEC). 종류 쪽 대본의 처음부터 환경음·대화음을 지나 첫 위협음이 울린 + /// 직후까지라, 멈춘 그림( 의 위협음)과 이어진다. 저절로 5초 넘게 움직이지 않아야 멈춤 단추 + /// 없이 둘 수 있다(WCAG 2.2.2). + /// + public const double PreviewPlaySec = 4.9; + private readonly float[] _frame = new float[VisualizerEngine.Channels]; - private readonly VisualModeSettings _settings = DemoSettings(); + private readonly VisualModeSettings _settings; /// 그릴 쪽. /// 표현 모드. /// 멈춰 둘 장면까지 미리 돌려 둔다. 끄면 아무것도 흘리지 않은 엔진으로 시작한다(시험이 0초부터 흘릴 때). - public TutorialDemo(TutorialScene scene, int visualMode, bool runToStill = true) + /// 그릴 모드 설정. 없으면 튜토리얼 그림의 설정()이다. 받은 객체를 고치지 않는다. + /// 엔진이 그리는 화면의 너비(DIP). 없으면 기준 화면이다. + /// 엔진이 그리는 화면의 높이(DIP). + public TutorialDemo(TutorialScene scene, int visualMode, bool runToStill = true, VisualModeSettings? settings = null, + double width = ReferenceWidth, double height = ReferenceHeight) { Scene = scene; VisualMode = visualMode; - Engine.SetSurfaceSize(ReferenceWidth, ReferenceHeight); + _settings = settings?.Copy() ?? DemoSettings(); + Width = width; + Height = height; + Engine.SetSurfaceSize(width, height); if (runToStill) RunTo(scene.StillAtSec()); } + /// 엔진이 그리는 화면의 너비(DIP). 그리는 쪽이 이 크기의 도형을 그림 크기로 줄인다. + public double Width { get; } + + /// 엔진이 그리는 화면의 높이(DIP). + public double Height { get; } + public TutorialScene Scene { get; } /// 그리는 표현 모드( 등). 패드·원형의 움직임은 그리는 모드만 계산한다. @@ -70,6 +98,20 @@ public TutorialDemo(TutorialScene scene, int visualMode, bool runToStill = true) Sensitivity = 10, }; + /// + /// 쉬운 설정의 모양 카드가 그릴 설정(안드로이드 previewSettings): 사용자의 그 모드 설정이되 진하기와 반응(민감도)은 튜토리얼 그림만큼은 + /// 올린다. 작은 그림에서 기본값 그대로면 어두운 장면 위의 색이 흐리고 박자가 잘 읽히지 않는다( 와 같은 까닭). + /// 크기는 그대로라 크기 단계를 바꾸면 그림도 바뀐다. 사용자 설정과 섞이지 않게 새로 만든다. + /// + public static VisualModeSettings PreviewSettings(VisualModeSettings user) + { + var demo = DemoSettings(); + var shown = user.Copy(); + shown.VisualOpacity = Math.Max(user.VisualOpacity, demo.VisualOpacity); + shown.Sensitivity = Math.Max(user.Sensitivity, demo.Sensitivity); + return shown; + } + /// 대본 시각 (초)의 프레임 하나를 엔진에 흘린다. 시각은 앞으로만 간다. public void Step(double t) { diff --git a/SoundVisualizer.Tests/Ai/PipelineTestDoubles.cs b/SoundVisualizer.Tests/Ai/PipelineTestDoubles.cs index 668b884..2604d8e 100644 --- a/SoundVisualizer.Tests/Ai/PipelineTestDoubles.cs +++ b/SoundVisualizer.Tests/Ai/PipelineTestDoubles.cs @@ -82,6 +82,49 @@ public void Dispose() } } +/// +/// 말소리 미리보기의 Silero 자리에 끼우는 가짜. 덩어리마다 를 돌려주고, 상태 비우기·닫기 횟수와 닫힌 뒤에 +/// 쓰였는지를 기록한다. 미리보기 스레드 하나만 부르지만, 테스트 스레드가 값을 읽으므로 원자적으로 센다. +/// +internal sealed class FakeSileroModel : ISileroModel +{ + private int _scoreCalls; + private int _resetCalls; + private int _disposeCalls; + private int _usedAfterDispose; + + /// 몇 번째 덩어리인지(1부터)로 확률을 정한다. 기본은 늘 1(말소리). + public Func Probability { get; set; } = static _ => 1f; + + /// 덩어리를 볼 때마다 부른다. 예외를 던지면 미리보기 스레드가 실패한다. + public Action? OnScore { get; set; } + + public int ScoreCalls => Volatile.Read(ref _scoreCalls); + + public int ResetCalls => Volatile.Read(ref _resetCalls); + + public int DisposeCalls => Volatile.Read(ref _disposeCalls); + + public bool UsedAfterDispose => Volatile.Read(ref _usedAfterDispose) != 0; + + public void Reset() + { + if (DisposeCalls != 0) Interlocked.Exchange(ref _usedAfterDispose, 1); + Interlocked.Increment(ref _resetCalls); + } + + public float Score(ReadOnlySpan block) + { + if (DisposeCalls != 0) Interlocked.Exchange(ref _usedAfterDispose, 1); + Assert.Equal(SileroInference.BlockSize, block.Length); + int call = Interlocked.Increment(ref _scoreCalls); + OnScore?.Invoke(call); + return Probability(call); + } + + public void Dispose() => Interlocked.Increment(ref _disposeCalls); +} + /// 파이프라인 테스트에 넣는 소리. internal static class PipelineTestAudio { diff --git a/SoundVisualizer.Tests/Ai/RealtimeAiPipelineSpeechPreviewTests.cs b/SoundVisualizer.Tests/Ai/RealtimeAiPipelineSpeechPreviewTests.cs new file mode 100644 index 0000000..d481d67 --- /dev/null +++ b/SoundVisualizer.Tests/Ai/RealtimeAiPipelineSpeechPreviewTests.cs @@ -0,0 +1,597 @@ +// 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; +using SoundVisualizer.Ai; +using Xunit.Abstractions; + +namespace SoundVisualizer.Tests.Ai; + +/// +/// 파이프라인의 말소리 미리보기(#422). 모바일 계측 테스트 SileroPreviewInstrumentedTest 의 두 경합 시험을 옮기고, 받아들이는 순서· +/// 만료·위협음·사용자 지정·멈춤·닫기·실패와 PC 의 레이트·채널 수를 더했다. +/// +/// 틱은 스케줄러 없이 직접 돌리고, 미리보기 시각(나노초)은 으로 정한다. 미리보기 스레드는 진짜로 따로 돌므로 +/// 소리를 넣은 뒤에는 그 스레드가 링의 완성 덩어리를 다 볼 때까지 기다린다(). +/// +public class RealtimeAiPipelineSpeechPreviewTests(ITestOutputHelper output) +{ + private const int Block = SileroInference.BlockSize; + private const long Ms = 1_000_000L; + private static readonly YamnetClassMap Names = AiTestData.ClassMap; + private static readonly int SpeechIndex = Names.IndexOf("Speech"); + private static readonly int GunshotIndex = Names.IndexOf("Gunshot, gunfire"); + private static readonly TimeSpan WaitLimit = TimeSpan.FromSeconds(10); + private static readonly IReadOnlyDictionary NoOverrides = new Dictionary(); + + [Theory] + [InlineData(16000, 1)] + [InlineData(44100, 2)] + [InlineData(48000, 2)] + [InlineData(88200, 2)] + [InlineData(96000, 6)] + [InlineData(176400, 2)] + [InlineData(192000, 8)] + public void 첫_대화음_후보_뒤의_새_소리를_Silero_가_확인하면_화면에만_대화음이_한_틱_먼저_보인다(int rate, int channels) + { + using var h = new Harness(rate, channels); + var regular = h.FirstSpeechCandidate(); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); // 아직 확인 전 + + long fresh = h.Frames; + h.FeedFreshBlocks(2, fresh); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); // 두 덩어리로는 모자란다 + h.FeedFreshBlocks(3, fresh); + + var preview = h.Pipeline.LastDisplayClassification(); + Assert.NotNull(preview); + Assert.True(preview.SpeechPreview); + Assert.Equal(SoundType.Speech, preview.Coarse); + Assert.Equal("Speech", preview.Display); + Assert.InRange(preview.Confidence, AiPostProcessor.DefaultBaseThreshold, 1f); + Assert.Same(regular.Top5, preview.Top5); + Assert.Equal(regular.MappingSignature, preview.MappingSignature); + // 정규 결과는 그대로다(진동·알림이 생기면 이쪽을 읽는다). + Assert.Same(regular, h.Pipeline.LastClassification()); + Assert.False(regular.SpeechPreview); + + // 다음 정규 틱이 대화음을 확정하면 그 결과가 미리보기를 대신한다. + Assert.Equal(AiTickOutcome.Completed, h.Pipeline.RunTickOnce(250)); + var confirmed = h.Pipeline.LastClassification(); + Assert.NotNull(confirmed); + Assert.Equal(SoundType.Speech, confirmed.Coarse); + Assert.False(confirmed.SpeechPreview); + Assert.Same(confirmed, h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 미리보기는_정규_스냅샷부터_275ms_에_사라지고_정규_결과로_돌아간다() + { + using var h = new Harness(16000, 1); + var regular = h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + + h.Nanos.Set(RealtimeAiPipeline.SpeechPreviewExpiryNs - 1); + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + h.Nanos.Set(RealtimeAiPipeline.SpeechPreviewExpiryNs); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + h.Nanos.Set(0); // 시계를 되돌려도 지운 미리보기는 돌아오지 않는다 + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 받아들임_마감_250ms_가_지난_뒤에_모인_증거로는_미리보기를_만들지_않는다() + { + using var h = new Harness(16000, 1); + var regular = h.FirstSpeechCandidate(); + h.Nanos.Set(RealtimeAiPipeline.SpeechPreviewAcceptNs); + h.FeedFreshBlocks(3, h.Frames); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 다음_정규_추론이_시작되면_새로_받지_않지만_이미_보인_미리보기는_만료까지_둔다() + { + // 다음 틱이 결과 없이 끝나는(실패) 경우로 본다. 결과가 나오면 그 결과가 미리보기를 대신한다. + using (var h = new Harness(16000, 1)) + { + var regular = h.FirstSpeechCandidate(); + long fresh = h.Frames; + h.FeedFreshBlocks(2, fresh); + h.Yamnet.OnInfer = _ => throw new InvalidOperationException("tick failed"); + Assert.Equal(AiTickOutcome.Failed, h.Pipeline.RunTickOnce(250)); + h.FeedFreshBlocks(3, fresh); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + } + using (var h = new Harness(16000, 1)) + { + h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + h.Yamnet.OnInfer = _ => throw new InvalidOperationException("tick failed"); + Assert.Equal(AiTickOutcome.Failed, h.Pipeline.RunTickOnce(250)); + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + } + } + + [Fact] + public void 정규_위협음이_오면_미리보기_대신_위협음이_보이고_위협음_뒤의_대화음은_후보를_거치지_않는다() + { + using var h = new Harness(16000, 1); + h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + + h.Yamnet.TopClass = GunshotIndex; + Assert.Equal(AiTickOutcome.Completed, h.Pipeline.RunTickOnce(250)); + var danger = h.Pipeline.LastClassification(); + Assert.Equal(SoundType.Danger, danger?.Coarse); + Assert.Same(danger, h.Pipeline.LastDisplayClassification()); + h.FeedFreshBlocks(3, h.Frames); + Assert.Same(danger, h.Pipeline.LastDisplayClassification()); + + // 위협음에서 확신 있는 대화음으로는 한 프레임에 확정되므로(후처리의 위협음 빠져나가기) 후보도 미리보기도 없다. + h.Yamnet.TopClass = SpeechIndex; + h.Nanos.Set(300 * Ms); + Assert.Equal(AiTickOutcome.Completed, h.Pipeline.RunTickOnce(500)); + var speech = h.Pipeline.LastClassification(); + Assert.Equal(SoundType.Speech, speech?.Coarse); + h.FeedFreshBlocks(3, h.Frames); + Assert.Same(speech, h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 사용자_지정이_하나라도_있으면_후보를_만들지_않는다() + { + // 기본과 같은 값으로 지정해도 사용자 지정이 있는 것이다(모바일의 mappingOverrides().isEmpty()). + IReadOnlyDictionary overrides = new Dictionary { ["Speech"] = "speech" }; + using var h = new Harness(16000, 1, () => overrides); + var regular = h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + + Assert.True(h.Silero.ScoreCalls > 0, "the worker still runs"); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 후보를_건_뒤에_사용자_지정이_생기면_받아들여도_미리보기를_만들지_않는다() + { + IReadOnlyDictionary overrides = NoOverrides; + using var h = new Harness(16000, 1, () => overrides); + var regular = h.FirstSpeechCandidate(); + overrides = new Dictionary { ["Speech"] = "speech" }; + h.FeedFreshBlocks(3, h.Frames); + Assert.Null(h.Pipeline.LastDisplayClassification()); // 매핑이 바뀐 결과는 화면에서도 바로 숨는다 + + overrides = NoOverrides; + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 미리보기가_보이는_중에_사용자_지정이_생기면_바로_사라진다() + { + IReadOnlyDictionary overrides = NoOverrides; + using var h = new Harness(16000, 1, () => overrides); + h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + + overrides = new Dictionary { ["Speech"] = "speech" }; + Assert.Null(h.Pipeline.LastDisplayClassification()); + // 다음 틱이 새 매핑을 반영하며 미리보기도 지운다. 지정을 되돌려도 지난 미리보기는 돌아오지 않는다. + Assert.Equal(AiTickOutcome.Completed, h.Pipeline.RunTickOnce(250)); + overrides = NoOverrides; + Assert.Null(h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 멈추면_미리보기가_사라지고_다시_시작하면_이전_캡처의_후보로_받아들이지_않는다() + { + using var h = new Harness(16000, 1); + h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + + h.Pipeline.Stop(); + Assert.Null(h.Pipeline.LastDisplayClassification()); + int resets = h.Silero.ResetCalls; + h.Pipeline.Start(); + Assert.Null(h.Pipeline.LastDisplayClassification()); + + // 새 캡처의 첫 후보는 다시 처음부터 확인한다. 미리보기 스레드는 세대가 바뀐 것을 보고 모델 상태를 비운다. + var regular = h.FirstSpeechCandidate(); + Assert.True(h.Silero.ResetCalls > resets); + Assert.Same(regular, h.Pipeline.LastDisplayClassification()); + h.FeedFreshBlocks(3, h.Frames); + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + } + + [Fact] + public void 확률이_0점9_아래인_덩어리가_끼면_받아들이지_않는다() + { + // 새 덩어리 가운데 두 번째만 0.89 다. 이어진 세 개가 모이지 않는다. + var silero = new FakeSileroModel(); + using var h = new Harness(16000, 1, silero: silero); + h.FirstSpeechCandidate(); + int before = silero.ScoreCalls; + long fresh = h.Frames; + // 다음 덩어리는 정규 추론이 본 소리와 걸친 덩어리 하나, 그 뒤가 새 덩어리다. + silero.Probability = call => call == before + 3 ? .89f : 1f; + h.FeedFreshBlocks(3, fresh); + Assert.False(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + h.FeedFreshBlocks(5, fresh); // 세 번째부터 다섯 번째 새 덩어리가 이어지면 받아들인다 + Assert.True(h.Pipeline.LastDisplayClassification()?.SpeechPreview); + } + + [Fact] + public void 닫으면_미리보기_스레드가_끝나고_Silero_를_닫는다() + { + var h = new Harness(44100, 2); + h.FirstSpeechCandidate(); + h.FeedFreshBlocks(3, h.Frames); + var worker = h.Worker; + + h.Pipeline.Dispose(); + + Assert.False(worker.IsAlive); + Assert.Equal(1, h.Silero.DisposeCalls); + Assert.False(h.Silero.UsedAfterDispose); + Assert.Null(h.Pipeline.LastDisplayClassification()); + // 닫은 뒤의 캡처와 깨우기는 아무것도 하지 않는다(캡처 스레드가 닫기와 엇갈려도 터지지 않는다). + h.Pipeline.Ingest(PipelineTestAudio.Sine(1024, 2)); + worker.Signal(); + worker.RequestClose(); + Assert.Equal(1, h.Silero.DisposeCalls); + } + + [Fact] + public void 모델을_만드는_중에_닫아도_1초_안에_돌아오고_스레드가_끝나며_모델을_닫는다() + { + using var entered = new ManualResetEventSlim(); + using var release = new ManualResetEventSlim(); + var silero = new FakeSileroModel(); + var pipeline = new RealtimeAiPipeline(16000, 1, new FakeYamnetModel(), Names, clock: new ManualClock().Now, runScheduler: false, + speechModel: () => + { + entered.Set(); + release.Wait(WaitLimit); + return silero; + }); + Assert.True(entered.Wait(WaitLimit)); + var worker = pipeline.SpeechWorkerForTest!; + + long started = Stopwatch.GetTimestamp(); + pipeline.Dispose(); + double elapsedMs = Stopwatch.GetElapsedTime(started).TotalMilliseconds; + output.WriteLine($"Dispose while Silero is loading: {elapsedMs:F0} ms"); + Assert.InRange(elapsedMs, 0, RealtimeAiPipeline.CloseWaitMs + 2000); + Assert.True(worker.IsAlive); + + release.Set(); + Assert.True(worker.Join(5000)); + Assert.Equal(1, silero.DisposeCalls); + Assert.Equal(0, silero.ScoreCalls); + } + + [Fact] + public void Silero_를_만들지_못하면_미리보기만_꺼지고_정규_분류는_돈다() + { + var model = new FakeYamnetModel { TopClass = SpeechIndex }; + var clock = new ManualClock(); + using var pipeline = new RealtimeAiPipeline(16000, 1, model, Names, clock: clock.Now, runScheduler: false, + speechModel: () => throw new FileNotFoundException("silero_vad.onnx")); + var worker = pipeline.SpeechWorkerForTest!; + Assert.True(worker.Join(5000)); + Assert.True(worker.Failed); + + pipeline.Start(); + pipeline.Ingest(PipelineTestAudio.Sine(CaptureAudioMath.RequiredMono16kSamples)); // 끝난 스레드를 깨워도 아무 일도 없다 + Assert.Equal(AiTickOutcome.Completed, pipeline.RunTickOnce(0)); + Assert.Equal(AiTickOutcome.Completed, pipeline.RunTickOnce(250)); + Assert.Equal(SoundType.Speech, pipeline.LastClassification()?.Coarse); + Assert.Same(pipeline.LastClassification(), pipeline.LastDisplayClassification()); + } + + [Fact] + public void Silero_가_돌다_실패하면_미리보기만_꺼지고_정규_분류는_돈다() + { + var silero = new FakeSileroModel { OnScore = _ => throw new InvalidDataException("bad probability") }; + using var h = new Harness(16000, 1, silero: silero); + h.Feed(PipelineTestAudio.WindowFrames(16000)); + SpinUntil(() => h.Worker.Failed); + Assert.True(h.Worker.Join(5000)); + Assert.Equal(1, silero.DisposeCalls); + + Assert.Equal(AiTickOutcome.Completed, h.Pipeline.RunTickOnce(0)); + h.Feed(Block * 4); + Assert.Equal(AiTickOutcome.Completed, h.Pipeline.RunTickOnce(250)); + Assert.Equal(SoundType.Speech, h.Pipeline.LastClassification()?.Coarse); + Assert.Same(h.Pipeline.LastClassification(), h.Pipeline.LastDisplayClassification()); + } + + [Fact] + public void 미리보기_스레드가_밀리면_밀린_소리를_버리고_상태를_비운_뒤_최근_쪽부터_본다() + { + using var hold = new ManualResetEventSlim(); + using var entered = new ManualResetEventSlim(); + var silero = new FakeSileroModel(); + using var h = new Harness(16000, 1, silero: silero); + h.Feed(PipelineTestAudio.WindowFrames(16000)); + h.WaitCaughtUp(); + long resets = h.Worker.Resets; + int before = silero.ScoreCalls; + + // 덩어리 하나 안에 붙잡아 둔 채 1초를 넣는다. 풀면 다섯 덩어리(160ms)보다 많이 밀렸으므로 건너뛴다. + h.Worker.BeforeScoreForTest = () => + { + entered.Set(); + hold.Wait(WaitLimit); + }; + h.Feed(Block); + Assert.True(entered.Wait(WaitLimit)); + h.Feed(16000); + h.Worker.BeforeScoreForTest = null; + hold.Set(); + h.WaitCaughtUp(); + + // 붙잡힌 덩어리를 포함해 한 번 깰 때 본 다섯 개, 건너뛴 뒤의 한두 개만 본다. 건너뛰지 않았다면 서른 개 넘게 본다. + Assert.Equal(resets + 1, h.Worker.Resets); + int seen = silero.ScoreCalls - before; + Assert.InRange(seen, 6, 8); + } + + [Fact] // stalledOptionalOwnerCannotBlockRegularDangerOrClose + public void 덩어리_안에서_멈춘_미리보기_스레드는_정규_위협음과_닫기를_막지_못한다() + { + using var entered = new ManualResetEventSlim(); + using var release = new ManualResetEventSlim(); + // 이 경합 시험만을 위해 모든 이름을 위협음으로 지정한다. 실제 경보 인식의 근거가 아니다(모바일과 같다). + IReadOnlyDictionary overrides = Names.ToDictionary(name => name, _ => "danger"); + var clock = new ManualClock(); + var pipeline = new RealtimeAiPipeline(16000, 1, YamnetInference.Create(AiTestData.ModelDirectory), Names, () => overrides, + clock: clock.Now, runScheduler: false, speechModel: () => SileroInference.Create(AiTestData.ModelDirectory)); + try + { + pipeline.SpeechWorkerForTest!.BeforeScoreForTest = () => + { + entered.Set(); + release.Wait(WaitLimit); + }; + pipeline.Start(); + var mono = new float[16000]; + for (int i = 0; i < mono.Length; i++) mono[i] = (float)Math.Sin(i * (2 * Math.PI * 440 / 16000)) * .4f; + pipeline.Ingest(mono); + Assert.True(entered.Wait(TimeSpan.FromSeconds(5)), "Silero did not start"); + + AiTickOutcome? outcome = null; + var tick = new Thread(() => outcome = pipeline.RunTickOnce(0)) { IsBackground = true }; + tick.Start(); + Assert.True(tick.Join(TimeSpan.FromSeconds(5)), "a stalled Silero blocked the regular tick"); + Assert.Equal(AiTickOutcome.Completed, outcome); + Assert.Equal(SoundType.Danger, pipeline.LastClassification()?.Coarse); + Assert.Equal(SoundType.Danger, pipeline.LastDisplayClassification()?.Coarse); + + var close = new Thread(pipeline.Dispose) { IsBackground = true }; + long started = Stopwatch.GetTimestamp(); + close.Start(); + Assert.True(close.Join(TimeSpan.FromMilliseconds(RealtimeAiPipeline.CloseWaitMs + 4000)), "a stalled Silero blocked Dispose"); + output.WriteLine($"Dispose with Silero stalled inside a block: {Stopwatch.GetElapsedTime(started).TotalMilliseconds:F0} ms"); + } + finally + { + release.Set(); + pipeline.Dispose(); + } + Assert.True(pipeline.SpeechWorkerForTest!.Join(5000), "the Silero thread did not end after release"); + } + + [Fact] // enabledWorkerDoesNotChangeRegularOnnxOrMappingInvalidation + public void 미리보기를_켜도_정규_ONNX_결과는_그대로이고_멈추거나_다시_시작하면_사라진다() + { + float[] mono = PipelineTestAudio.Sine(16000); + var clock = new ManualClock(); + using var baseline = new RealtimeAiPipeline(16000, 1, YamnetInference.Create(AiTestData.ModelDirectory), Names, + clock: clock.Now, runScheduler: false); + using var candidate = new RealtimeAiPipeline(16000, 1, YamnetInference.Create(AiTestData.ModelDirectory), Names, + clock: clock.Now, runScheduler: false, speechModel: () => SileroInference.Create(AiTestData.ModelDirectory)); + baseline.Start(); + candidate.Start(); + baseline.Ingest(mono); + candidate.Ingest(mono); + for (int i = 0; i < 2; i++) + { + Assert.Equal(AiTickOutcome.Completed, baseline.RunTickOnce(i * 250)); + Assert.Equal(AiTickOutcome.Completed, candidate.RunTickOnce(i * 250)); + Assert.Equal(baseline.ProbabilitiesForTest.ToArray(), candidate.ProbabilitiesForTest.ToArray()); + var a = baseline.LastClassification(); + var b = candidate.LastClassification(); + Assert.NotNull(a); + Assert.NotNull(b); + Assert.Equal((a.Coarse, a.Display, a.Confidence, a.MeetsThreshold), (b.Coarse, b.Display, b.Confidence, b.MeetsThreshold)); + Assert.False(b.SpeechPreview); + } + candidate.Stop(); + Assert.False(candidate.LastDisplayClassification()?.SpeechPreview ?? false); + candidate.Start(); + Assert.Null(candidate.LastDisplayClassification()); + Assert.Null(baseline.SpeechWorkerForTest); + Assert.NotNull(candidate.SpeechWorkerForTest); + Assert.False(candidate.SpeechWorkerForTest.Failed); + } + + [Fact] + public void 앱이_만드는_파이프라인은_기본으로_미리보기를_켜고_끌_수도_있다() + { + Assert.True(RealtimeAiPipeline.SpeechPreviewEnabled); + using (var on = RealtimeAiPipeline.Create(48000, 2, AiTestData.ModelDirectory, Names)) + { + Assert.NotNull(on.SpeechWorkerForTest); + } + using (var off = RealtimeAiPipeline.Create(48000, 2, AiTestData.ModelDirectory, Names, speechPreview: false)) + { + Assert.Null(off.SpeechWorkerForTest); + } + } + + [Fact] + public void 실제_말소리를_실시간처럼_넣으면_정규_결과는_그대로이고_미리보기는_정규_대화음보다_먼저_온다() + { + // 1초 무음 → 튜토리얼 말소리(2.2초) → 0.5초 무음을 44.1kHz 스테레오 10ms 버퍼로 넣고, 소리 시각 250ms 마다 틱을 돌린다. + // 시계는 넣은 소리의 시각이다. 미리보기를 끈 파이프라인과 정규 결과를 틱마다 맞춰 본다. + const int rate = 44100; + float[] talk = SileroInferenceTests.Load44100Mono("tutorial_talk.wav"); + float[] mono = new float[rate + talk.Length + rate / 2]; + talk.CopyTo(mono, rate); + + var ms = new ManualClock(); + var nanos = new ManualClock(); + using var baseline = new RealtimeAiPipeline(rate, 2, YamnetInference.Create(AiTestData.ModelDirectory), Names, + clock: ms.Now, runScheduler: false); + using var candidate = new RealtimeAiPipeline(rate, 2, YamnetInference.Create(AiTestData.ModelDirectory), Names, + clock: ms.Now, runScheduler: false, speechModel: () => SileroInference.Create(AiTestData.ModelDirectory), nanoClock: nanos.Now); + baseline.Start(); + candidate.Start(); + var worker = candidate.SpeechWorkerForTest!; + + const int chunkFrames = 441; + var stereo = new float[chunkFrames * 2]; + long nextTickMs = 1000; + long? firstPreviewMs = null, firstRegularSpeechMs = null; + for (int offset = 0; offset + chunkFrames <= mono.Length; offset += chunkFrames) + { + long endMs = (offset + chunkFrames) * 1000L / rate; + ms.Set(endMs); + nanos.Set((offset + chunkFrames) * 1_000_000_000L / rate); + for (int i = 0; i < chunkFrames; i++) stereo[2 * i] = stereo[2 * i + 1] = mono[offset + i]; + baseline.Ingest(stereo); + candidate.Ingest(stereo); + WaitCaughtUp(worker, candidate.AudioBufferForTest, rate); + + if (endMs >= nextTickMs) + { + var a = baseline.RunTickOnce(nextTickMs); + var b = candidate.RunTickOnce(nextTickMs); + Assert.Equal(a, b); + var regularA = baseline.LastClassification(); + var regularB = candidate.LastClassification(); + Assert.Equal((regularA?.Coarse, regularA?.Display, regularA?.Confidence), (regularB?.Coarse, regularB?.Display, regularB?.Confidence)); + Assert.False(regularB?.SpeechPreview ?? false); + if (regularB?.Coarse == SoundType.Speech) firstRegularSpeechMs ??= endMs; + output.WriteLine($"tick {nextTickMs,5} ms: {b} {regularB?.Coarse}/{regularB?.Display}/{regularB?.Confidence:F2} thr={regularB?.MeetsThreshold}"); + nextTickMs += RealtimeAiPipeline.PredictIntervalMs; + } + + var shown = candidate.LastDisplayClassification(); + if (shown?.SpeechPreview == true) + { + Assert.Equal(SoundType.Ambient, candidate.LastClassification()?.Coarse); + if (firstPreviewMs is null) output.WriteLine($"preview at {endMs} ms: {shown.Display}/{shown.Confidence:F2}"); + firstPreviewMs ??= endMs; + } + } + + output.WriteLine($"first preview {firstPreviewMs} ms, first regular speech {firstRegularSpeechMs} ms, Silero blocks {worker.Calls}"); + Assert.False(worker.Failed); + Assert.NotNull(firstRegularSpeechMs); + Assert.NotNull(firstPreviewMs); + Assert.True(firstPreviewMs < firstRegularSpeechMs, $"preview {firstPreviewMs} ms is not earlier than regular {firstRegularSpeechMs} ms"); + } + + private static void SpinUntil(Func condition) + { + Assert.True(SpinWait.SpinUntil(condition, WaitLimit), "condition not reached"); + } + + /// 미리보기 스레드가 지금 링에 있는 완성 덩어리를 모두 볼 때까지 기다린다. + private static void WaitCaughtUp(SileroStreamWorker worker, AiAudioBuffer buffer, int rate) + { + long expected = CaughtUpPosition(buffer.Cursor().Frames, rate); + var watch = Stopwatch.StartNew(); + while (worker.NextOutputSample < expected) + { + Assert.False(worker.Failed, "Silero worker failed"); + Assert.True(watch.Elapsed < WaitLimit, $"worker stuck at {worker.NextOutputSample}, expected {expected}"); + // 한 번 깰 때 다섯 덩어리까지만 보므로, 남은 것이 있으면 다음 캡처 버퍼처럼 다시 깨운다. + worker.Signal(); + Thread.Sleep(1); + } + } + + /// 캡처 프레임 개로 다 볼 수 있는 덩어리들의 바로 뒤(16kHz 샘플). + private static long CaughtUpPosition(long frames, int rate) + { + long next = 0; + while (CaptureAudioMath.StreamSourceRange(next, Block, rate).End <= frames) next += Block; + return next; + } + + /// 가짜 YAMNet(늘 Speech)과 가짜 Silero(늘 1)로 만든 파이프라인. 시작해 둔다. + private sealed class Harness : IDisposable + { + public Harness(int rate, int channels, Func>? overrides = null, FakeSileroModel? silero = null) + { + Rate = rate; + Channels = channels; + Silero = silero ?? new FakeSileroModel(); + Pipeline = new RealtimeAiPipeline(rate, channels, Yamnet, Names, overrides, clock: Clock.Now, runScheduler: false, + speechModel: () => Silero, nanoClock: Nanos.Now); + Pipeline.Start(); + } + + public int Rate { get; } + + public int Channels { get; } + + public ManualClock Clock { get; } = new(); + + public ManualClock Nanos { get; } = new(); + + public FakeYamnetModel Yamnet { get; } = new() { TopClass = SpeechIndex }; + + public FakeSileroModel Silero { get; } + + public RealtimeAiPipeline Pipeline { get; } + + public SileroStreamWorker Worker => Pipeline.SpeechWorkerForTest!; + + public long Frames => Pipeline.AudioBufferForTest.Cursor().Frames; + + /// + /// 한 창을 넣고 첫 틱을 돌린다. 가짜 YAMNet 이 대화음을 내므로 확정은 아직 환경음이고 대화음이 첫 후보다. + /// 그 정규 결과를 돌려준다. 틱을 돌리기 전에 미리보기 스레드가 창을 다 보게 해, 후보 뒤의 덩어리만 남게 한다. + /// + public AiClassificationResult FirstSpeechCandidate() + { + Feed(PipelineTestAudio.WindowFrames(Rate)); + WaitCaughtUp(); + Assert.Equal(AiTickOutcome.Completed, Pipeline.RunTickOnce(Clock.Now())); + var regular = Pipeline.LastClassification(); + Assert.NotNull(regular); + Assert.Equal(SoundType.Ambient, regular.Coarse); + Assert.True(regular.MeetsThreshold); + Assert.False(regular.SpeechPreview); + return regular; + } + + /// 사인 프레임을 10ms 남짓 버퍼로 나눠 넣는다. + public void Feed(long frames) + { + if (frames <= 0) return; + float[] pcm = PipelineTestAudio.Sine((int)frames, Channels); + PipelineTestAudio.IngestInChunks(Pipeline, pcm, chunk: 480 * Channels); + } + + /// 프레임 뒤에서 시작하는 새 덩어리 개가 다 들어오게 넣고 따라잡기를 기다린다. + public void FeedFreshBlocks(int count, long freshAfter) + { + long first = 0; + while (first * Rate / CaptureAudioMath.TargetSampleRate < freshAfter) first += Block; + long needed = CaptureAudioMath.StreamSourceRange(first + (count - 1L) * Block, Block, Rate).End; + Feed(needed - Frames); + WaitCaughtUp(); + } + + public void WaitCaughtUp() => RealtimeAiPipelineSpeechPreviewTests.WaitCaughtUp(Worker, Pipeline.AudioBufferForTest, Rate); + + public void Dispose() => Pipeline.Dispose(); + } +} diff --git a/SoundVisualizer.Tests/Ai/SileroInferenceTests.cs b/SoundVisualizer.Tests/Ai/SileroInferenceTests.cs new file mode 100644 index 0000000..cdb0b43 --- /dev/null +++ b/SoundVisualizer.Tests/Ai/SileroInferenceTests.cs @@ -0,0 +1,168 @@ +// 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.Buffers.Binary; +using System.Diagnostics; +using System.Text; +using SoundVisualizer.Ai; +using Xunit.Abstractions; + +namespace SoundVisualizer.Tests.Ai; + +/// +/// 앱과 함께 배포하는 실제 Silero VAD. 모바일 계측 테스트 SileroPreviewInstrumentedTest 의 모델 확인을 옮기고(PC 의 ONNX Runtime 은 +/// 단위 테스트에서 돈다), 실제 말소리(튜토리얼의 말소리, 퍼블릭 도메인)와 무음에서의 확률, 고정값과 깨진 파일을 더했다. +/// +public class SileroInferenceTests(ITestOutputHelper output) +{ + private const int Block = SileroInference.BlockSize; + + [Fact] + public void 배포하는_모델_파일은_Android_와_같은_크기와_SHA256이다() + { + // 두 앱이 같은 모델을 쓴다는 약속이다. 모델을 바꾸면 SileroInference.kt 의 SHA256 과 함께 여기도 고친다. + Assert.Equal( + ("silero_vad.onnx", 2_327_524L, "1a153a22f4509e292a94e67d6f9b85e8deb25b4988682b7e174c65279d8788e3"), + (SileroModelFile.Onnx.Name, SileroModelFile.Onnx.ByteCount, SileroModelFile.Onnx.Sha256)); + string path = Path.Combine(AiTestData.ModelDirectory, SileroModelFile.Onnx.Name); + Assert.Equal(SileroModelFile.Onnx.ByteCount, new FileInfo(path).Length); + Assert.Equal(SileroModelFile.Onnx.Sha256, ModelFileIntegrity.Sha256Hex(path)); + } + + [Fact] // pinnedRealModelLoadsAndRecurrentStateResets + public void 고정한_실제_모델을_불러오고_상태를_비우면_처음과_같은_값을_낸다() + { + using var model = SileroInference.Create(AiTestData.ModelDirectory); + var chunk = new float[Block]; + for (int i = 0; i < chunk.Length; i++) chunk[i] = (float)Math.Sin(i * .07) * .4f; + + float first = model.Score(chunk); + Assert.InRange(first, 0f, 1f); + for (int i = 0; i < 4; i++) model.Score(chunk); + model.Reset(); + Assert.Equal(first, model.Score(chunk)); + } + + [Fact] + public void 실제_말소리에서는_받아들일_만큼_이어서_높고_무음에서는_낮다() + { + // 튜토리얼의 말소리("Anna Pete, hi! Hi, we are here!", 44.1kHz 모노)를 미리보기 스레드와 같은 스트림 리샘플로 16kHz 덩어리로 바꾼다. + float[] speech = Load44100Mono("tutorial_talk.wav"); + float[] speech16k = StreamTo16k(speech, 44100); + using var model = SileroInference.Create(AiTestData.ModelDirectory); + + var probabilities = new List(); + long started = Stopwatch.GetTimestamp(); + for (int start = 0; start + Block <= speech16k.Length; start += Block) + { + probabilities.Add(model.Score(speech16k.AsSpan(start, Block))); + } + double perBlockMs = Stopwatch.GetElapsedTime(started).TotalMilliseconds / probabilities.Count; + output.WriteLine($"speech blocks={probabilities.Count} max={probabilities.Max():F3} per block {perBlockMs:F3} ms"); + output.WriteLine(string.Join(" ", probabilities.Select(p => p.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)))); + + int longest = LongestRun(probabilities, SileroPreviewGate.SpeechProbability); + Assert.True(longest >= SileroPreviewGate.RequiredBlocks, $"longest run of p >= 0.9 is {longest} blocks"); + + model.Reset(); + var silence = new float[Block]; + var quiet = Enumerable.Range(0, 30).Select(_ => model.Score(silence)).ToArray(); + output.WriteLine($"silence max={quiet.Max():F5}"); + Assert.All(quiet, p => Assert.True(p < 0.1f, $"silence p={p}")); + } + + [Fact] + public void 한_바이트만_바뀌었거나_잘렸거나_없는_모델로는_세션을_만들지_않는다() + { + string dir = Directory.CreateTempSubdirectory("sv-silero-").FullName; + try + { + string path = Path.Combine(dir, SileroModelFile.Onnx.Name); + Assert.Equal(path, Assert.Throws(() => SileroInference.Create(dir)).FileName); + + byte[] bytes = File.ReadAllBytes(Path.Combine(AiTestData.ModelDirectory, SileroModelFile.Onnx.Name)); + bytes[bytes.Length / 2] ^= 0x01; + File.WriteAllBytes(path, bytes); + var flipped = Assert.Throws(() => SileroInference.Create(dir)); + Assert.Contains("SHA-256", flipped.Message); + + File.WriteAllBytes(path, bytes[..^1]); + var truncated = Assert.Throws(() => SileroInference.Create(dir)); + Assert.Contains("2327523 bytes", truncated.Message); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Fact] + public void 덩어리_길이가_다르거나_닫은_뒤에는_부르지_않는다() + { + var model = SileroInference.Create(AiTestData.ModelDirectory); + Assert.Throws(() => model.Score(new float[Block - 1])); + model.Dispose(); + model.Dispose(); // 여러 번 닫아도 된다 + Assert.Throws(() => model.Score(new float[Block])); + } + + /// 연달아 이상인 가장 긴 덩어리 수. + internal static int LongestRun(IEnumerable probabilities, float threshold) + { + int longest = 0, run = 0; + foreach (float p in probabilities) + { + run = p >= threshold ? run + 1 : 0; + longest = Math.Max(longest, run); + } + return longest; + } + + /// 미리보기 스레드처럼 덩어리마다 범위를 꺼내 16kHz 로 바꾼다. 끝의 모자란 덩어리는 버린다. + internal static float[] StreamTo16k(float[] source, int rate) + { + var result = new List(); + var block = new float[Block]; + var capture = new float[CaptureAudioMath.StreamSourceCapacity(Block, rate)]; + for (long start = 0; ; start += Block) + { + var (first, end) = CaptureAudioMath.StreamSourceRange(start, Block, rate); + if (end > source.Length) break; + int size = (int)(end - first); + for (int i = 0; i < size; i++) capture[i] = first + i < 0 ? 0f : source[first + i]; + CaptureAudioMath.ResampleStreamBlock(capture.AsSpan(0, size), first, start, rate, block); + result.AddRange(block); + } + return result.ToArray(); + } + + /// 테스트 출력 폴더 Sounds 의 16비트 PCM 모노 WAV(44.1kHz)를 -1~1 로 읽는다. + internal static float[] Load44100Mono(string fileName) + { + byte[] wav = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Sounds", fileName)); + Assert.Equal("RIFF", Encoding.ASCII.GetString(wav, 0, 4)); + Assert.Equal("WAVE", Encoding.ASCII.GetString(wav, 8, 4)); + int offset = 12; + float[]? samples = null; + while (offset + 8 <= wav.Length) + { + string id = Encoding.ASCII.GetString(wav, offset, 4); + int size = BinaryPrimitives.ReadInt32LittleEndian(wav.AsSpan(offset + 4)); + var body = wav.AsSpan(offset + 8, size); + if (id == "fmt ") + { + Assert.Equal(1, BinaryPrimitives.ReadInt16LittleEndian(body)); // PCM + Assert.Equal(1, BinaryPrimitives.ReadInt16LittleEndian(body[2..])); // 모노 + Assert.Equal(44100, BinaryPrimitives.ReadInt32LittleEndian(body[4..])); + Assert.Equal(16, BinaryPrimitives.ReadInt16LittleEndian(body[14..])); // 16비트 + } + else if (id == "data") + { + samples = new float[size / 2]; + for (int i = 0; i < samples.Length; i++) samples[i] = BinaryPrimitives.ReadInt16LittleEndian(body[(i * 2)..]) / 32768f; + } + offset += 8 + size + (size & 1); + } + return samples ?? throw new InvalidDataException("no data chunk in " + fileName); + } +} diff --git a/SoundVisualizer.Tests/Ai/SileroPreviewGateTests.cs b/SoundVisualizer.Tests/Ai/SileroPreviewGateTests.cs new file mode 100644 index 0000000..4d3f2b2 --- /dev/null +++ b/SoundVisualizer.Tests/Ai/SileroPreviewGateTests.cs @@ -0,0 +1,90 @@ +// 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.Tests.Ai; + +/// 말소리 미리보기를 받아들이는 규칙. 모바일 SileroPreviewGateTest 다섯 개를 그대로 옮겼다(#422). +public class SileroPreviewGateTests +{ + /// 프레임 100 뒤의 소리만 보고, 1000 전에만 받아들이며, 1300 에 사라지는 세대 7 의 후보. + private static SileroPreviewGate Gate() + { + var gate = new SileroPreviewGate(); + gate.Regular(new SileroPreviewGate.Candidate("parent", 100, 1000, 1300, 7)); + return gate; + } + + [Fact] // needsThreeContiguousFreshBlocks + public void 새로_들어온_이어진_덩어리_세_개가_필요하다() + { + var gate = Gate(); + Assert.Null(gate.Block(7, 0, 100, 1f, 100)); // 정규 추론이 이미 본 소리 + Assert.Null(gate.Block(7, 100, 200, 1f, 200)); + Assert.Null(gate.Block(7, 200, 300, 1f, 300)); + Assert.NotNull(gate.Block(7, 300, 400, 1f, 400)); + Assert.Null(gate.Block(7, 400, 500, 1f, 500)); // 한 후보에 한 번만 + } + + [Fact] // lowNonfiniteOrMissingBlockResetsEvidence + public void 낮거나_유한하지_않거나_빠진_덩어리는_증거를_처음부터_다시_센다() + { + foreach (float p in new[] { .89f, float.NaN, float.PositiveInfinity }) + { + var gate = Gate(); + gate.Block(7, 100, 200, 1f, 200); + gate.Block(7, 200, 300, p, 300); + Assert.Null(gate.Block(7, 300, 400, 1f, 400)); + } + var skipped = Gate(); + skipped.Block(7, 100, 200, 1f, 200); + skipped.Block(7, 200, 300, 1f, 300); + Assert.Null(skipped.Block(7, 400, 500, 1f, 500)); // 300~400 이 빠졌다 + } + + [Fact] // deadlineAndGenerationRejectLateWork + public void 마감이_지났거나_세대가_다른_덩어리는_받아들이지_않는다() + { + var gate = Gate(); + gate.Block(7, 100, 200, 1f, 200); + gate.Block(7, 200, 300, 1f, 300); + Assert.Null(gate.Block(6, 300, 400, 1f, 400)); + Assert.Null(gate.Block(7, 300, 400, 1f, 1000)); + Assert.Null(gate.View(1001)); + } + + [Fact] // nextRegularStartCannotAcceptMoreButDoesNotPrematurelyRetract + public void 다음_정규_추론이_시작되면_새로_받지_않지만_받아들인_것을_일찍_거두지도_않는다() + { + var gate = Gate(); + for (int i = 1; i <= 3; i++) gate.Block(7, i * 100L, (i + 1) * 100L, 1f, i * 100L); + gate.RegularStarted(); + Assert.Equal("parent", gate.View(1100)?.Parent); + Assert.Null(gate.View(1300)); + gate.Regular(null); // 새 정규 결과, 위협음, 멈춤, 초기화 + Assert.Null(gate.View(1100)); + } + + [Fact] // newParentDoesNotInheritOldEvidence + public void 새_후보는_이전_후보의_증거를_물려받지_않는다() + { + var gate = Gate(); + gate.Block(7, 100, 200, 1f, 200); + gate.Block(7, 200, 300, 1f, 300); + gate.Regular(new SileroPreviewGate.Candidate("new", 300, 1000, 1300, 7)); + Assert.Null(gate.Block(7, 300, 400, 1f, 400)); + } + + [Fact] + public void 받아들인_것은_후보의_부모와_만료와_마감을_그대로_담는다() + { + var gate = Gate(); + gate.Block(7, 100, 200, .9f, 200); // 0.9 는 넘는 것으로 본다 + gate.Block(7, 200, 300, .95f, 300); + var accepted = gate.Block(7, 300, 400, 1f, 400); + + Assert.Equal(new SileroPreviewGate.Accepted("parent", 1300, 400, 1000), accepted); + Assert.Same(accepted, gate.View(1299)); + } +} diff --git a/SoundVisualizer.Tests/Ai/SileroStreamInputTests.cs b/SoundVisualizer.Tests/Ai/SileroStreamInputTests.cs new file mode 100644 index 0000000..e478586 --- /dev/null +++ b/SoundVisualizer.Tests/Ai/SileroStreamInputTests.cs @@ -0,0 +1,159 @@ +// 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.Tests.Ai; + +/// +/// 말소리 미리보기의 입력. 모바일 SileroStreamInputTest 를 옮기고(#422), PC 레이트(88.2~192kHz, 늘린 FIR)와 +/// 덩어리 범위·작업 버퍼 크기를 더했다. +/// +public class SileroStreamInputTests +{ + private const int Block = SileroInference.BlockSize; + + [Theory] // sequentialBlocksMatchRegularFirExactlyAtAllSupportedRates + [InlineData(16000)] + [InlineData(44100)] + [InlineData(48000)] + [InlineData(88200)] + [InlineData(96000)] + [InlineData(176400)] + [InlineData(192000)] + public void 차례로_만든_덩어리는_정규_리샘플러와_비트까지_같다(int rate) + { + float[] source = new float[rate / 2]; + for (int i = 0; i < source.Length; i++) source[i] = (float)Math.Sin(i * .07) * .3f; + var expected = new float[Block * 5]; + CaptureAudioMath.ResampleMonoFloatTo16kCustom(source, rate, expected); + + var actual = new float[Block * 5]; + var capture = new float[CaptureAudioMath.StreamSourceCapacity(Block, rate)]; + for (int start = 0; start < actual.Length; start += Block) + { + var (first, end) = CaptureAudioMath.StreamSourceRange(start, Block, rate); + int size = (int)(end - first); + Assert.InRange(size, 1, capture.Length); + for (int i = 0; i < size; i++) capture[i] = first + i < 0 ? 0f : source[first + i]; + CaptureAudioMath.ResampleStreamBlock(capture.AsSpan(0, size), first, start, rate, actual.AsSpan(start, Block)); + } + + Assert.Equal(expected, actual); // 허용 오차 없이 + } + + [Theory] + [InlineData(16000, 0, 512)] + [InlineData(44100, -47, 1457)] // 0..511 의 중심은 0..1408, 왼쪽 47·오른쪽 48 (모바일과 같은 수) + [InlineData(48000, -47, 1582)] + [InlineData(96000, -95, 3163)] // 탭 192: 왼쪽 95·오른쪽 96 + [InlineData(192000, -191, 6325)] // 탭 384: 왼쪽 191·오른쪽 192 + public void 첫_덩어리의_원본_범위는_레이트의_탭_수를_따른다(int rate, long first, long end) + { + Assert.Equal((first, end), CaptureAudioMath.StreamSourceRange(0, Block, rate)); + } + + [Theory] + [InlineData(16000)] + [InlineData(44100)] + [InlineData(48000)] + [InlineData(88200)] + [InlineData(96000)] + [InlineData(176400)] + [InlineData(192000)] + public void 작업_버퍼는_어느_덩어리의_범위도_담는다(int rate) + { + int capacity = CaptureAudioMath.StreamSourceCapacity(Block, rate); + long largest = 0; + // 위상이 한 바퀴 도는 동안(16kHz 출력 16,000샘플 = 1초)의 모든 덩어리 시작을 본다. + for (long start = 0; start < 16000 * 2; start += Block) + { + var (first, end) = CaptureAudioMath.StreamSourceRange(start, Block, rate); + largest = Math.Max(largest, end - first); + } + Assert.InRange(largest, 1, capacity); + Assert.True(capacity - largest <= 1, $"capacity {capacity} is much larger than needed {largest}"); + } + + [Theory] + [InlineData(16000, 15600, 14848)] // 30 덩어리가 다 들어왔다. 마지막의 바로 앞(29번째) 덩어리부터 + [InlineData(16000, 1000, 0)] + [InlineData(16000, 0, 0)] + [InlineData(44100, 48, 0)] + [InlineData(48000, 48_000, 15360)] // 1초, 오른쪽 48 을 빼면 15,984샘플 → 31 덩어리, 그 앞(30번째)부터 + public void 밀린_것을_버리고_다시_시작할_곳은_마지막_완성_덩어리의_앞이다(int rate, long frames, long expected) + { + Assert.Equal(expected, CaptureAudioMath.NewestStreamBlockStart(frames, Block, rate)); + } + + [Fact] // ringRangesCannotReadFutureOrPreviousSession + public void 링_범위로는_미래의_소리나_이전_캡처의_소리를_읽지_못한다() + { + var ring = new AiAudioBuffer(48000, 1); + ring.IngestMono(Enumerable.Range(0, 100).Select(i => (float)i).ToArray()); + var cursor = ring.Cursor(); + Assert.Equal(100, cursor.Frames); + var dst = new float[10]; + + Assert.True(ring.CopyRange(cursor.Generation, -3, dst)); + Assert.Equal(new float[] { 0, 0, 0, 0, 1, 2, 3, 4, 5, 6 }, dst); + Assert.False(ring.CopyRange(cursor.Generation, 95, dst)); + + ring.Reset(); + ring.IngestMono(Enumerable.Repeat(999f, 100).ToArray()); + Assert.False(ring.CopyRange(cursor.Generation, 0, dst)); + Assert.NotEqual(cursor.Generation, ring.Cursor().Generation); + Assert.Equal(100, ring.Cursor().Frames); + } + + [Fact] // overwrittenPcmCannotBeReplayedAsFresh + public void 덮어쓴_소리는_새_소리로_다시_읽지_못한다() + { + var ring = new AiAudioBuffer(48000, 1); + ring.IngestMono(Enumerable.Range(0, 100_000).Select(i => (float)i).ToArray()); + var cursor = ring.Cursor(); + var dst = new float[2]; + + Assert.False(ring.CopyRange(cursor.Generation, 0, dst)); + Assert.True(ring.CopyRange(cursor.Generation, 99_998, dst)); + Assert.Equal(new float[] { 99_998, 99_999 }, dst); + } + + [Fact] + public void 링을_한_바퀴_넘게_돌아도_범위는_이어진_순서로_읽힌다() + { + // 192kHz 링(524,288칸)을 여러 번 감아 끝과 처음에 걸친 범위를 읽는다. 2초(384,000) 안쪽만 남아 있다. + var ring = new AiAudioBuffer(192000, 1); + var chunk = new float[50_000]; + long written = 0; + for (int round = 0; round < 25; round++) + { + for (int i = 0; i < chunk.Length; i++) chunk[i] = (written + i) % 1_000_003; + ring.IngestMono(chunk); + written += chunk.Length; + } + var cursor = ring.Cursor(); + Assert.Equal(written, cursor.Frames); + long wrapStart = written - (written % ring.RingCapacity) - 100; // 링 끝을 가로지르는 곳 + var dst = new float[300]; + + Assert.True(ring.CopyRange(cursor.Generation, wrapStart, dst)); + for (int i = 0; i < dst.Length; i++) Assert.Equal((wrapStart + i) % 1_000_003, dst[i]); + Assert.False(ring.CopyRange(cursor.Generation, written - 384_001, new float[1])); + Assert.True(ring.CopyRange(cursor.Generation, written - 384_000, new float[1])); + } + + [Fact] + public void 여러_채널은_정규_경로와_같게_섞은_소리를_읽는다() + { + // 5.1 의 앞 가운데만 소리가 있으면 ITU 계수(0.707)로 섞인 모노가 링에 남는다. 미리보기도 같은 모노를 읽는다. + var ring = new AiAudioBuffer(48000, 6); + var pcm = new float[6 * 4]; + for (int frame = 0; frame < 4; frame++) pcm[frame * 6 + 2] = 1f; + ring.IngestInterleaved(pcm); + var dst = new float[4]; + + Assert.True(ring.CopyRange(ring.Cursor().Generation, 0, dst)); + Assert.All(dst, value => Assert.Equal(0.707f, value, 1e-6f)); + } +} diff --git a/SoundVisualizer.Tests/Ai/YamnetLabelListTests.cs b/SoundVisualizer.Tests/Ai/YamnetLabelListTests.cs index 0e48d22..1d6577b 100644 --- a/SoundVisualizer.Tests/Ai/YamnetLabelListTests.cs +++ b/SoundVisualizer.Tests/Ai/YamnetLabelListTests.cs @@ -7,7 +7,7 @@ namespace SoundVisualizer.Tests.Ai; /// /// C# 라벨 목록과 규칙이 모바일과 같은 이름을 같은 종류로 묶는지. 모바일 저장소를 읽지 않도록, 모바일 규칙을 실제 521개 이름에 -/// 적용한 결과(위협음 37, 대화음 33, 환경음 451 — 모바일 tools/ai_reference/mapper.py 결과와도 같다)를 여기에 그대로 적어 둔다. +/// 적용한 결과(위협음 37, 사람 소리 36, 환경음 448 — 모바일 #421 뒤 tools/ai_reference/mapper.py 결과와도 같다)를 여기에 그대로 적어 둔다. /// 규칙을 바꾸면 이 목록도 모바일과 함께 바꾼다. /// public class YamnetLabelListTests @@ -32,19 +32,20 @@ public class YamnetLabelListTests "Children shouting", "Screaming", "Whispering", "Laughter", "Baby laughter", "Giggle", "Snicker", "Belly laugh", "Chuckle, chortle", "Crying, sobbing", "Baby cry, infant cry", "Whimper", "Wail, moan", "Singing", "Choir", - "Child singing", "Synthetic singing", "Rapping", "Cheering", "Chatter", - "Crowd", "Hubbub, speech noise, speech babble", "Children playing", + "Child singing", "Synthetic singing", "Rapping", "Breathing", "Cough", + "Sneeze", "Cheering", "Chatter", "Crowd", "Hubbub, speech noise, speech babble", + "Children playing", }; private static List NamesMappedTo(SoundType type) => AiTestData.ClassMap.Where(name => YamnetThreeClassMapper.MapDisplayNameToCoarse(name) == type).ToList(); [Fact] - public void 실제_클래스는_위협음_37_대화음_33_환경음_451로_나뉜다() + public void 실제_클래스는_위협음_37_사람_소리_36_환경음_448로_나뉜다() { Assert.Equal(37, NamesMappedTo(SoundType.Danger).Count); - Assert.Equal(33, NamesMappedTo(SoundType.Speech).Count); - Assert.Equal(451, NamesMappedTo(SoundType.Ambient).Count); + Assert.Equal(36, NamesMappedTo(SoundType.Speech).Count); + Assert.Equal(448, NamesMappedTo(SoundType.Ambient).Count); } [Fact] @@ -54,7 +55,7 @@ private static List NamesMappedTo(SoundType type) => } [Fact] - public void 대화음으로_묶이는_이름은_모바일과_같다() + public void 사람_소리로_묶이는_이름은_모바일과_같다() { Assert.Equal(ExpectedSpeech, NamesMappedTo(SoundType.Speech)); } @@ -66,7 +67,7 @@ private static List NamesMappedTo(SoundType type) => var lowered = AiTestData.ClassMap.Select(name => name.ToLowerInvariant()).ToHashSet(StringComparer.Ordinal); Assert.Equal(9, YamnetThreeClassMapper.AttentionLabels.Count); - Assert.Equal(11, YamnetThreeClassMapper.HumanVocalizationLabels.Count); + Assert.Equal(14, YamnetThreeClassMapper.HumanVocalizationLabels.Count); Assert.Equal(8, YamnetSafetyCueDecision.ApprovedAttentionCueLabels.Count); Assert.All( YamnetThreeClassMapper.AttentionLabels diff --git a/SoundVisualizer.Tests/Ai/YamnetThreeClassMapperTests.cs b/SoundVisualizer.Tests/Ai/YamnetThreeClassMapperTests.cs index 890308d..d625424 100644 --- a/SoundVisualizer.Tests/Ai/YamnetThreeClassMapperTests.cs +++ b/SoundVisualizer.Tests/Ai/YamnetThreeClassMapperTests.cs @@ -106,15 +106,16 @@ public class YamnetThreeClassMapperTests } [Fact] // approvedHumanVocalizationsAreSpeechUsingExactLabels - public void 사람이_내는_소리는_정확히_같은_이름만_대화음() + public void 사람이_내는_소리는_정확히_같은_이름만_사람_소리() { Assert.All(new[] { "Bellow", "Whoop", "Yell", "Cheering", "Giggle", "Snicker", "Belly laugh", "Chuckle, chortle", "Baby cry, infant cry", "Whimper", "Wail, moan", + "Sneeze", "Cough", "Breathing", }, name => Assert.Equal(SoundType.Speech, YamnetThreeClassMapper.MapDisplayNameToCoarse(name))); - Assert.All(new[] { "Whimper (dog)", "Bellow-like sound", "Cheering music" }, name => + Assert.All(new[] { "Whimper (dog)", "Bellow-like sound", "Cheering music", "Breathing machine", "Cough-like sound" }, name => Assert.Equal(SoundType.Ambient, YamnetThreeClassMapper.MapDisplayNameToCoarse(name))); } diff --git a/SoundVisualizer.Tests/AppSettingsTests.cs b/SoundVisualizer.Tests/AppSettingsTests.cs index a707eb4..f8ba5d1 100644 --- a/SoundVisualizer.Tests/AppSettingsTests.cs +++ b/SoundVisualizer.Tests/AppSettingsTests.cs @@ -36,12 +36,13 @@ public void Dispose() if (File.Exists(_path)) File.Delete(_path); } - // 전체 초기화는 소리 탭의 두 값과 튜토리얼을 본 기록을 건드리지 않으므로 테스트마다 따로 비운다. + // 전체 초기화는 소리 탭의 두 값과 튜토리얼을 본 기록, 설정 탭의 화면(쉬운·자세한 설정)을 건드리지 않으므로 테스트마다 따로 비운다. private static void ClearSoundTab() { AppSettings.SoundTypes = new Dictionary(); AppSettings.ClassifyAdvanced = false; AppSettings.TutorialSeen = false; + AppSettings.EasySettings = AppSettings.DefaultEasySettings; } [Fact] @@ -475,4 +476,128 @@ private static Dictionary Types(params (string Name, string Labe Assert.True(AppSettings.TutorialSeen); } + + // ===== 설정 탭을 쉬운 설정으로 열지(모바일 EasySettingsSettingTest, #408) ===== + // 새로 설치한 사람과 모드별 상세 값을 바꾼 적이 없는 사람은 쉬운 설정으로 연다. 바꾼 적이 있는 사람은 손에 익은 자세한 설정으로 연다. + + [Fact] + public void 새로_설치하면_쉬운_설정이다() + { + Assert.True(AppSettings.DefaultEasySettings, "기본값을 끄려면 이 테스트부터 고쳐야 한다"); + AppSettings.EasySettings = false; // 앞에서 들고 있던 값이 남지 않는지도 본다 + + AppSettings.Load(); + + Assert.False(File.Exists(_path)); + Assert.True(AppSettings.EasySettings); + } + + [Theory] + [InlineData("WaveMode", "PositionSpeed", "77")] + [InlineData("PadMode", "Intensity", "77")] + [InlineData("CircleMode", "CircleRadius", "77")] + [InlineData("OutlineMode", "GlowIntensity", "77")] + [InlineData("PadMode", "IsGlowMode", "true")] + [InlineData("WaveMode", "IntensityAsOpacity", "true")] + public void 모드별_상세_값을_하나라도_바꾼_사람은_자세한_설정으로_연다(string mode, string property, string value) + { + File.WriteAllText(_path, "{ \"" + mode + "\": { \"" + property + "\": " + value + " } }"); + + AppSettings.Load(); + + Assert.False(AppSettings.EasySettings); + } + + [Fact] + public void 처음_값과_같은_모드_설정이나_쉬운_설정에도_보이는_값만_바꾼_사람은_쉬운_설정이다() + { + // 표현 모드·색·표시처럼 쉬운 설정에도 보이는 값과 모드 카드 밖의 값(최대 프레임, 단축키)은 따지지 않는다. + File.WriteAllText(_path, """ + { "VisualMode": 3, "ColorDanger": "#FF00FF00", "ShowSpeech": false, "TargetFps": 60, "VisualModeKeyBind": [ 66 ], + "WaveMode": { "Intensity": 50, "Sensitivity": 3.75 }, "CircleMode": { "CircleRadius": 40 } } + """); + + AppSettings.Load(); + + Assert.True(AppSettings.EasySettings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void 저장한_화면이_있으면_모드_설정과_상관없이_그_화면이다(bool easy) + { + string mode = easy ? """{ "PositionSpeed": 90 }""" : "{ }"; + File.WriteAllText(_path, "{ \"EasySettings\": " + (easy ? "true" : "false") + ", \"WaveMode\": " + mode + " }"); + + AppSettings.Load(); + + Assert.Equal(easy, AppSettings.EasySettings); + } + + [Fact] + public void 깨진_설정_파일은_처음_값으로_시작하므로_쉬운_설정이다() + { + File.WriteAllText(_path, "{ not json"); + + AppSettings.Load(); + + Assert.True(AppSettings.EasySettings); + } + + [Fact] + public void 고른_화면은_저장한_뒤_다시_읽힌다() + { + AppSettings.EasySettings = false; + AppSettings.SaveNow(); + AppSettings.EasySettings = true; + + AppSettings.Load(); + + Assert.False(AppSettings.EasySettings); + using var doc = JsonDocument.Parse(File.ReadAllText(_path)); + Assert.False(doc.RootElement.GetProperty("EasySettings").GetBoolean()); + } + + [Fact] + public void 쉬운_설정에서_크기를_바꿔도_다음에_열_때_쉬운_설정이다() + { + // 저장한 적이 없어 읽을 때 정한 값은 다음 저장에 함께 남는다. 남지 않으면 바꾼 크기 때문에 다음에는 자세한 설정으로 열린다(모바일과 같은 함정). + AppSettings.Load(); + Assert.True(AppSettings.EasySettings); + + EasySize.Large.ApplyTo(AppSettings.CurrentModeSettings); + AppSettings.SaveNow(); + AppSettings.Load(); + + Assert.True(AppSettings.EasySettings); + Assert.Equal(EasySize.Large, EasySizes.Of(AppSettings.CurrentModeSettings)); + } + + [Fact] + public void 이_기능_전의_파일에서_정한_자세한_설정도_다음_저장에_남는다() + { + File.WriteAllText(_path, """{ "PadMode": { "Intensity": 77 } }"""); + AppSettings.Load(); + Assert.False(AppSettings.EasySettings); + + AppSettings.SaveNow(); + + using var doc = JsonDocument.Parse(File.ReadAllText(_path)); + Assert.False(doc.RootElement.GetProperty("EasySettings").GetBoolean()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void 전체_초기화는_고른_화면을_건드리지_않는다(bool easy) + { + // 화면 설정이다(소리 탭의 고급 모드와 같다). 자세한 설정 맨 아래의 초기화를 누르자마자 쉬운 설정으로 바뀌면 헷갈린다. + AppSettings.EasySettings = easy; + AppSettings.PadMode.Intensity = 90; + + AppSettings.ResetToDefaults(); + + Assert.Equal(easy, AppSettings.EasySettings); + } } diff --git a/SoundVisualizer.Tests/Diagnostics/AiDebugTextTests.cs b/SoundVisualizer.Tests/Diagnostics/AiDebugTextTests.cs index fd26a79..c156119 100644 --- a/SoundVisualizer.Tests/Diagnostics/AiDebugTextTests.cs +++ b/SoundVisualizer.Tests/Diagnostics/AiDebugTextTests.cs @@ -189,6 +189,21 @@ private static AiDebugLines Format(AiClassificationResult? result, long nowMs = Assert.Empty(text.Where(c => !(c is >= ' ' and <= '~' || c == '…')).Distinct()); } + [Fact] + public void 말소리_미리보기는_종류_뒤에_PREVIEW_를_붙여_정규_확정과_구분한다() + { + // 모바일 AiDebugText 의 " ~PREVIEW"(#422). 점의 색과 이름·확신도는 미리보기의 대화음 값이다. + var regular = Result(SoundType.Ambient, "Music", 0.6f); + var preview = Format(regular.AsSpeechPreview("Speech", 0.83f, timestampMs: 1_100L)); + + Assert.Equal("AMBIENT", Format(regular).Coarse); + Assert.Equal("SPEECH ~PREVIEW", preview.Coarse); + Assert.Equal(SoundType.Speech, preview.ColorType); + Assert.Equal("Speech", preview.Label); + Assert.Equal("0.83", preview.Confidence); + Assert.Equal("thr Y age 0.1s", preview.Detail); // 나이는 미리보기를 만든 때부터 + } + [Fact] public void 같은_입력이면_같은_줄로_본다() { diff --git a/SoundVisualizer.Tests/Help/LicenseNoticesTests.cs b/SoundVisualizer.Tests/Help/LicenseNoticesTests.cs index fa71584..3d13326 100644 --- a/SoundVisualizer.Tests/Help/LicenseNoticesTests.cs +++ b/SoundVisualizer.Tests/Help/LicenseNoticesTests.cs @@ -81,6 +81,45 @@ public static TheoryData Codes() phrase => Assert.Contains(phrase, notice, StringComparison.Ordinal)); } + [Fact] + public void 앱에_넣은_Silero_VAD_는_고정한_해시와_안드로이드와_같은_MIT_전문을_적는다() + { + // 안드로이드 NOTICE 의 "8. Silero VAD" 와 같은 글이어야 한다(#422). 줄을 바꾸지 않고 그대로 옮겼다. + string mit = string.Join("\n", + "MIT License", + "Copyright (c) 2020-present Silero Team", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE."); + string thirdParty = Lf(LicenseNotices.Read(LicenseNotices.Documents[0])); + + Assert.Contains(mit, thirdParty, StringComparison.Ordinal); + Assert.All( + new[] + { + "SoundVisualizer/AIModel/silero_vad.onnx", + "v6.2.3, commit 5cd7945676eb32225748052e2e6a0580e4686a08", + "https://github.com/snakers4/silero-vad", + SoundVisualizer.Ai.SileroModelFile.Onnx.Sha256, + }, + phrase => Assert.Contains(phrase, thirdParty, StringComparison.Ordinal)); + } + [Fact] public void 온톨로지의_라이선스를_데이터셋의_CC_BY_4_0_으로_적지_않는다() { diff --git a/SoundVisualizer.Tests/Help/ReportLinkTests.cs b/SoundVisualizer.Tests/Help/ReportLinkTests.cs index 63b5796..66f639f 100644 --- a/SoundVisualizer.Tests/Help/ReportLinkTests.cs +++ b/SoundVisualizer.Tests/Help/ReportLinkTests.cs @@ -108,7 +108,7 @@ public class ReportLinkTests [Fact] public void 제보는_PC_판_저장소와_안드로이드와_같은_메일로_간다() { - Assert.Equal("https://github.com/amophi/SoundVisualizer/issues/new", ReportLink.IssuesUrl); + Assert.Equal("https://github.com/SOVIS-Team/SoundVisualizer-Windows/issues/new", ReportLink.IssuesUrl); Assert.Equal("nawwhod@gmail.com", ReportLink.ReportEmail); } diff --git a/SoundVisualizer.Tests/Localization/EasySettingsStringsTests.cs b/SoundVisualizer.Tests/Localization/EasySettingsStringsTests.cs new file mode 100644 index 0000000..1679e21 --- /dev/null +++ b/SoundVisualizer.Tests/Localization/EasySettingsStringsTests.cs @@ -0,0 +1,113 @@ +// 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.Localization; + +namespace SoundVisualizer.Tests.Localization; + +/// +/// 쉬운 설정(안드로이드 EasySettingsStringsTest, #408)의 문구. 쉬운 설정은 글을 덜 읽게 하려고 만든 화면이라, 한 언어라도 빠지면 그 언어 +/// 사용자만 쉬운 화면에서 영어 낱말을 읽게 된다. 키가 모든 표에 같은지는 StringTableFilesTests 도 보지만, 여기서는 쉬운 설정의 문구가 +/// 실제로 들어왔는지와 문구끼리의 약속(단추 글자 그대로 인용, 화면 읽어주기 이름)을 본다. +/// +public class EasySettingsStringsTests +{ + /// 맨 위 두 칸(settings.view.*)과 쉬운 화면(easy.*)의 키. + private static readonly string[] EasyKeys = StringTables.Get(UiLanguages.DefaultCode)!.Entries.Keys + .Where(key => key.StartsWith("settings.view.", StringComparison.Ordinal) || key.StartsWith("easy.", StringComparison.Ordinal)) + .Order(StringComparer.Ordinal) + .ToArray(); + + private static readonly string[] Tabs = { "launcher.tab.home", "launcher.tab.settings", "launcher.tab.sounds", "launcher.tab.help", "launcher.tab.more_info" }; + + public static TheoryData Codes() + { + var data = new TheoryData(); + foreach (var code in StringTables.AvailableCodes) data.Add(code); + return data; + } + + private static string Get(string code, string key) + { + Assert.True(StringTables.Get(code)!.TryGet(key, out var text), $"{code}: '{key}' 가 없습니다."); + return text; + } + + [Fact] + public void 쉬운_설정_문구는_18개_언어_모두에_있다() + { + Assert.True(EasyKeys.Length >= 10, "쉬운 설정 문구를 찾지 못했습니다: " + string.Join(", ", EasyKeys)); + Assert.Equal(18, StringTables.AvailableCodes.Count); + var missing = StringTables.AvailableCodes + .SelectMany(code => EasyKeys.Where(key => !StringTables.Get(code)!.TryGet(key, out _)).Select(key => $"{code}: {key}")) + .ToArray(); + Assert.True(missing.Length == 0, "쉬운 설정 문구는 모든 언어에 넣습니다: " + string.Join(", ", missing)); + } + + [Theory] + [MemberData(nameof(Codes))] + public void 두_칸의_이름은_서로_다르고_탭_이름과도_다르다(string code) + { + string easy = Get(code, "settings.view.easy"); + string detailed = Get(code, "settings.view.detailed"); + Assert.NotEqual(easy, detailed); + foreach (var tab in Tabs) + { + Assert.NotEqual(Get(code, tab), easy); + Assert.NotEqual(Get(code, tab), detailed); + } + } + + [Theory] + [MemberData(nameof(Codes))] + public void 크기_세_단계의_낱말은_서로_다르다(string code) + { + var words = new[] { "easy.size.small", "easy.size.medium", "easy.size.large" }.Select(key => Get(code, key)).ToArray(); + Assert.Equal(3, words.Distinct(StringComparer.Ordinal).Count()); + } + + [Theory] + [MemberData(nameof(Codes))] + public void 크기_안내는_자세한_설정을_단추_글자_그대로_인용한다(string code) + { + // 인용한 이름이 단추 글자와 다르면 사용자가 그 단추를 찾지 못한다. + Assert.Contains(Get(code, "settings.view.detailed"), Get(code, "easy.size.custom"), StringComparison.Ordinal); + } + + [Fact] + public void 한국어_문구는_고르다_대신_선택을_쓴다() + { + // 화면의 동사는 '선택'이다(고르다·골라·고른 같은 꼴을 쓰지 않는다). + var wrong = EasyKeys.Where(key => Get("ko", key) is var text && (text.Contains("고르", StringComparison.Ordinal) + || text.Contains("골라", StringComparison.Ordinal) || text.Contains("고른", StringComparison.Ordinal) || text.Contains("골랐", StringComparison.Ordinal))) + .ToArray(); + Assert.True(wrong.Length == 0, "고르다 대신 선택을 씁니다: " + string.Join(", ", wrong)); + Assert.Contains("선택", Get("ko", "easy.size.custom"), StringComparison.Ordinal); + } + + [Theory] + [MemberData(nameof(Codes))] + public void 소리_종류_줄의_읽는_이름에는_보이는_종류_이름이_들어_있다(string code) + { + // 보이는 글자를 말해 누르는 음성 조작이 이 줄을 찾을 수 있어야 한다(WCAG 2.5.3, 안드로이드 #434). + // 종류 이름은 소리 탭의 것(sounds.type.*)을 그대로 쓰므로 그 이름이 바뀌어도 함께 따라간다. + string pair = Get(code, "easy.name_pair"); + foreach (var type in new[] { "danger", "speech", "ambient" }) + { + string shown = Get(code, "sounds.type." + type); + string name = EasyNames.TypeRow(shown, Get(code, "settings.show_" + type), pair); + Assert.Contains(shown, name, StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + public void 읽는_이름은_들어_있으면_자세한_설정의_스위치_이름_그대로이고_없으면_앞에_붙인다() + { + Assert.Equal(Get("ko", "settings.show_danger"), + EasyNames.TypeRow(Get("ko", "sounds.type.danger"), Get("ko", "settings.show_danger"), Get("ko", "easy.name_pair"))); + Assert.Equal("Som de perigo, Mostrar sons de perigo", EasyNames.TypeRow("Som de perigo", "Mostrar sons de perigo", "{0}, {1}")); + Assert.Equal("Voz", EasyNames.TypeRow("voz", "Voz", "{0}, {1}")); + // 쉼표는 언어마다 다르다. + Assert.Equal("a، b", EasyNames.TypeRow("a", "b", Get("ar", "easy.name_pair"))); + } +} diff --git a/SoundVisualizer.Tests/SoundVisualizer.Tests.csproj b/SoundVisualizer.Tests/SoundVisualizer.Tests.csproj index e39d4d4..8c96401 100644 --- a/SoundVisualizer.Tests/SoundVisualizer.Tests.csproj +++ b/SoundVisualizer.Tests/SoundVisualizer.Tests.csproj @@ -28,6 +28,12 @@ + + + + + + diff --git a/SoundVisualizer.Tests/Sounds/SoundGroupsTests.cs b/SoundVisualizer.Tests/Sounds/SoundGroupsTests.cs index e03359a..2bfb070 100644 --- a/SoundVisualizer.Tests/Sounds/SoundGroupsTests.cs +++ b/SoundVisualizer.Tests/Sounds/SoundGroupsTests.cs @@ -11,7 +11,7 @@ namespace SoundVisualizer.Tests.Sounds; /// 모바일 SoundGroupsTest 를 옮겼다. /// /// 지금 매핑으로는 어떤 매핑에서도 지켜야 할 규칙만 보고, 나머지는 기본 종류를 정해 둔 가짜 매핑을 써서 매핑이 바뀌어도 흔들리지 않게 -/// 한다. 지금 매핑의 구역 구성(위협음 11, 대화음 6, 환경음 20)은 한 테스트에만 적어 둔다. +/// 한다. 지금 매핑의 구역 구성(위협음 11, 사람 소리 7, 환경음 20 카드)은 한 테스트에만 적어 둔다. /// public class SoundGroupsTests { @@ -85,14 +85,23 @@ private static void AssertCardRules(IReadOnlyList sections, IReadO } [Fact] - public void 지금_매핑으로는_위협음_11_대화음_6_환경음_20_묶음이다() + public void 지금_매핑으로는_위협음_11_사람_소리_7_환경음_20_카드다() { - // 지금 매핑에서는 묶음마다 기본 종류가 하나라 카드 하나가 곧 묶음 하나다. 매핑을 모바일에서 옮겨 바꾸면 이 수도 함께 본다 - // (YamnetLabelListTests 와 같다). 카드 나누기 자체는 매핑과 상관없이 위의 규칙을 지킨다. + // 매핑을 모바일에서 옮겨 바꾸면 이 수도 함께 본다(YamnetLabelListTests 와 같다). 카드 나누기 자체는 매핑과 상관없이 위의 규칙을 지킨다. + // 모바일 #421 로 숨소리·기침·재채기가 사람 소리가 되어, 몸 소리 묶음만 사람 소리 카드와 환경음 카드로 나뉜다. + // 나머지 36개 묶음은 기본 종류가 하나라 카드 하나가 곧 묶음 하나다. var sections = SoundGroups.Sections(SoundTestData.Entries()); Assert.Equal(new[] { SoundType.Danger, SoundType.Speech, SoundType.Ambient }, sections.Select(section => section.Type)); - Assert.Equal(new[] { 11, 6, 20 }, sections.Select(section => section.Cards.Count)); - Assert.Equal(SoundGroupInfo.All, sections.SelectMany(section => section.Cards).Select(card => card.Group!.Value)); + Assert.Equal(new[] { 11, 7, 20 }, sections.Select(section => section.Cards.Count)); + + var groups = sections.SelectMany(section => section.Cards).Select(card => card.Group!.Value).ToList(); + Assert.Equal(SoundGroupInfo.All.Order(), groups.Distinct().Order()); + Assert.Equal(2, groups.Count(group => group == SoundGroup.Body)); + Assert.All(groups.Where(group => group != SoundGroup.Body).GroupBy(group => group), same => Assert.Single(same)); + + var bodySpeech = Card(sections, SoundGroup.Body, SoundType.Speech); + Assert.Equal(new[] { "Cough", "Breathing", "Sneeze" }, Names(bodySpeech.Members)); // 묶음 표 순서 + Assert.DoesNotContain(Card(sections, SoundGroup.Body).Members, entry => entry.DefaultType == SoundType.Speech); } [Fact] diff --git a/SoundVisualizer.Tests/Tutorial/ModePreviewTests.cs b/SoundVisualizer.Tests/Tutorial/ModePreviewTests.cs new file mode 100644 index 0000000..7c3b759 --- /dev/null +++ b/SoundVisualizer.Tests/Tutorial/ModePreviewTests.cs @@ -0,0 +1,100 @@ +// 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.Rendering; +using SoundVisualizer.Tutorial; + +namespace SoundVisualizer.Tests.Tutorial; + +/// +/// 쉬운 설정의 모양 카드(안드로이드 ModePreviewPhone, #408)가 실제 엔진으로 그리는 그림. 카드는 사용자의 그 모드 설정으로 그려 +/// 크기 단계를 바꾸면 그림도 바뀌어야 하고, 멈춘 그림이 비면 애니메이션을 꺼 둔 사람에게는 빈 카드가 된다. +/// +public class ModePreviewTests +{ + private static readonly int[] Modes = { VisualizerEngine.ModeWave, VisualizerEngine.ModePad, VisualizerEngine.ModeCircle, VisualizerEngine.ModeOutline }; + + private static TutorialDemo Still(int mode, VisualModeSettings user) => + new(TutorialScene.Types, mode, runToStill: true, TutorialDemo.PreviewSettings(user), TutorialDemo.PreviewWidth, TutorialDemo.PreviewHeight); + + private static VisualModeSettings At(EasySize step) + { + var settings = new VisualModeSettings(); + step.ApplyTo(settings); + return settings; + } + + [Fact] + public void 카드는_진하기와_반응만_튜토리얼만큼_올리고_크기는_사용자_것이다() + { + var user = new VisualModeSettings { Intensity = 75, VisualOpacity = 20, Sensitivity = 1, PositionSpeed = 60 }; + var demo = TutorialDemo.DemoSettings(); + + var shown = TutorialDemo.PreviewSettings(user); + + Assert.Equal(75, shown.Intensity); + Assert.Equal(60, shown.PositionSpeed); + Assert.Equal(demo.VisualOpacity, shown.VisualOpacity); + Assert.Equal(demo.Sensitivity, shown.Sensitivity); + // 사용자 설정과 섞이지 않는다. 오버레이가 프레임마다 읽는 객체다. + Assert.Equal(20, user.VisualOpacity); + Assert.Equal(1, user.Sensitivity); + + // 사용자가 더 진하게 해 두었으면 그대로다. + Assert.Equal(95, TutorialDemo.PreviewSettings(new VisualModeSettings { VisualOpacity = 95 }).VisualOpacity); + } + + [Fact] + public void 한_번_움직이는_시간은_5초를_넘지_않고_멈춘_그림과_같은_위협음에서_멈춘다() + { + // 저절로 5초 넘게 움직이지 않아야 멈춤 단추 없이 둘 수 있다(WCAG 2.2.2). + Assert.InRange(TutorialDemo.PreviewPlaySec, 4.0, 5.0); + Assert.Equal(SoundType.Danger, TutorialScript.Label(TutorialScene.Types, TutorialDemo.PreviewPlaySec)); + Assert.Equal(SoundType.Danger, TutorialScript.Label(TutorialScene.Types, TutorialScene.Types.StillAtSec())); + + // 처음부터 움직이면 환경음 → 대화음 → 위협음 세 종류의 색이 모두 나온다. + var seen = new HashSet(); + for (double t = 0; t <= TutorialDemo.PreviewPlaySec; t += 1 / 30.0) seen.Add(TutorialScript.Label(TutorialScene.Types, t)); + Assert.Equal(new[] { SoundType.Ambient, SoundType.Speech, SoundType.Danger }, seen.Order()); + } + + [Fact] + public void 멈춘_그림은_모든_모드와_단계에서_비지_않는다() + { + foreach (int mode in Modes) + { + foreach (var step in EasySizes.All) + { + var still = Still(mode, At(step)); + Assert.True(still.Engine.Visible, $"모드 {mode}, {step} 의 멈춘 그림이 비었다"); + Assert.Equal(SoundType.Danger, still.Label); + } + } + } + + [Fact] + public void 크기_단계를_올리면_그림도_커진다() + { + foreach (int mode in Modes) + { + double[] baseDepths = EasySizes.All.Select(step => Still(mode, At(step)).Engine.BaseDepth).ToArray(); + Assert.True(baseDepths[0] < baseDepths[1] && baseDepths[1] < baseDepths[2], $"모드 {mode}: {string.Join(", ", baseDepths)}"); + } + } + + [Fact] + public void 카드는_작은_화면으로_그리고_넘긴_설정을_고치지_않는다() + { + var user = At(EasySize.Small); + var demo = new TutorialDemo(TutorialScene.Types, VisualizerEngine.ModeWave, runToStill: true, user, TutorialDemo.PreviewWidth, TutorialDemo.PreviewHeight); + + Assert.Equal((TutorialDemo.PreviewWidth, TutorialDemo.PreviewHeight), (demo.Width, demo.Height)); + Assert.Equal(16.0 / 9.0, TutorialDemo.PreviewWidth / TutorialDemo.PreviewHeight, 3); + Assert.True(user.HasSameValues(At(EasySize.Small))); + + // 튜토리얼은 그대로 기준 화면과 튜토리얼 설정으로 그린다. + var tutorial = new TutorialDemo(TutorialScene.Types, VisualizerEngine.ModeWave); + Assert.Equal((TutorialDemo.ReferenceWidth, TutorialDemo.ReferenceHeight), (tutorial.Width, tutorial.Height)); + } +} diff --git a/SoundVisualizer.Tests/Ui/EasySettingsSourceTests.cs b/SoundVisualizer.Tests/Ui/EasySettingsSourceTests.cs new file mode 100644 index 0000000..6403faa --- /dev/null +++ b/SoundVisualizer.Tests/Ui/EasySettingsSourceTests.cs @@ -0,0 +1,117 @@ +// 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.Text.RegularExpressions; +using System.Xml.Linq; + +namespace SoundVisualizer.Tests.Ui; + +/// +/// 쉬운 설정 화면(Ui/EasySettingsView)과 런처의 두 칸·'자세한 설정' 이동을 소스로 확인한다(안드로이드 EasySettingsSourceTest). +/// 런처는 WPF 창이라 단위 시험에서 띄우지 않는다. 규칙 자체(크기 단계, 기본 화면)는 EasySizesTests·AppSettingsTests 가 본다. +/// +public class EasySettingsSourceTests +{ + private static readonly XNamespace Xaml = "http://schemas.microsoft.com/winfx/2006/xaml"; + + private static string Source(params string[] parts) => File.ReadAllText(Path.Combine(new[] { RepositoryPaths.Root, "SoundVisualizer" }.Concat(parts).ToArray())); + + private static XDocument ViewXaml => XDocument.Parse(Source("Ui", "EasySettingsView.xaml")); + + private static XElement Named(XDocument document, string name) => + document.Descendants().Single(e => (string?)e.Attribute(Xaml + "Name") == name); + + [Fact] + public void 크기_칸의_차례는_단계의_차례와_같다() + { + // 화면은 고른 번호로 단계를 찾는다(EasySizes.All[번호]). + var tags = Named(ViewXaml, "SizeChips").Elements().Select(e => (string?)e.Attribute("Tag")).ToArray(); + Assert.Equal(EasySizes.All.Select(step => "{x:Static core:EasySize." + step + "}"), tags); + } + + [Fact] + public void 모양_카드의_차례는_표현_모드_번호와_같다() + { + var tags = Named(ViewXaml, "ModeCards").Elements().Select(e => (string?)e.Attribute("Tag")).ToArray(); + Assert.Equal(new[] { "ModeWave", "ModePad", "ModeCircle", "ModeOutline" }.Select(name => "{x:Static r:VisualizerEngine." + name + "}"), tags); + } + + [Fact] + public void 자세한_설정_칸은_크기_선택지_묶음_밖의_단추다() + { + // 선택지 묶음(ListBox)은 화살표 키로 옮기기만 해도 칸이 골라진다. 같은 묶음에 두면 크기를 화살표로 훑다가 자세한 설정으로 넘어간다. + var view = ViewXaml; + var button = Named(view, "BtnSizeDetailed"); + Assert.Equal("DetailedChoiceButton", button.Name.LocalName); + Assert.DoesNotContain(button, Named(view, "SizeChips").Descendants()); + // 이름은 맨 위 두 칸의 '자세한 설정'과 같은 글자다. + Assert.Equal("{l:Loc settings.view.detailed}", (string?)button.Attribute("Content")); + + // 선택이 비는 까닭을 알리는 한 줄(#425)은 칸들과 단추 사이에 있고, 나타날 때 화면 읽어주기가 읽는다. + var parent = button.Parent!.Elements().ToList(); + var note = Named(view, "TxtSizeCustom"); + Assert.True(parent.IndexOf(Named(view, "SizeChips")) < parent.IndexOf(note) && parent.IndexOf(note) < parent.IndexOf(button)); + Assert.Equal("Polite", (string?)note.Attribute("AutomationProperties.LiveSetting")); + } + + [Fact] + public void 소리_종류는_소리_탭의_종류_이름을_그대로_쓰고_위협음부터_둔다() + { + // 종류 이름을 새로 만들지 않는다. 소리 탭의 이름이 바뀌면(대화음 → 사람 소리) 쉬운 설정도 함께 바뀐다. + string xaml = Source("Ui", "EasySettingsView.xaml"); + var contents = new[] { "ChkDanger", "ChkSpeech", "ChkAmbient" }.Select(name => (string?)Named(ViewXaml, name).Attribute("Content")).ToArray(); + Assert.Equal(new[] { "{l:Loc sounds.type.danger}", "{l:Loc sounds.type.speech}", "{l:Loc sounds.type.ambient}" }, contents); + Assert.DoesNotMatch(new Regex(@"\{l:Loc easy\.type"), xaml); + + // 화면 읽어주기의 이름은 자세한 설정의 스위치 이름으로 짓는다. + string code = Source("Ui", "EasySettingsView.xaml.cs"); + foreach (var type in new[] { "danger", "speech", "ambient" }) + { + Assert.Contains($"EasyNames.TypeRow(Loc.Get(\"sounds.type.{type}\"), Loc.Get(\"settings.show_{type}\"), pair)", code); + } + } + + [Fact] + public void 모양_카드는_Windows_애니메이션_효과가_켜져_있고_보일_때만_움직인다() + { + string preview = Source("Ui", "ModePreview.cs"); + string play = preview[preview.IndexOf("public void Play()", StringComparison.Ordinal)..]; + play = play[..play.IndexOf("public void Stop()", StringComparison.Ordinal)]; + Assert.Contains("!SystemParameters.ClientAreaAnimation || !IsVisible", play); + // 정해 둔 시간만큼 움직이면 스스로 멈춘다. + Assert.Contains("if (t >= TutorialDemo.PreviewPlaySec) Stop();", preview); + // 그림은 실제 방향이라 오른쪽에서 왼쪽으로 쓰는 언어에서도 뒤집지 않는다. + Assert.Contains("FlowDirection = FlowDirection.LeftToRight;", preview); + } + + [Fact] + public void 체크_표시는_오른쪽에서_왼쪽으로_쓰는_언어에서도_뒤집지_않는다() + { + // 뒤집으면 아래로 꺾인 화살표처럼 보였다(아랍어 화면에서 확인). + var style = ViewXaml.Descendants().Single(e => e.Name.LocalName == "Style" && (string?)e.Attribute(Xaml + "Key") == "CheckMark"); + Assert.Contains(style.Elements(), setter => (string?)setter.Attribute("Property") == "FlowDirection" && (string?)setter.Attribute("Value") == "LeftToRight"); + } + + [Fact] + public void 런처는_저장된_화면으로_열고_자세한_설정_칸은_그_칸에_초점을_둔다() + { + string launcher = Source("LauncherWindow.xaml.cs"); + Assert.Contains("ShowSettingsView();", launcher); + Assert.Contains("EasyView?.Reload();", launcher); + Assert.Contains("EasyView.ApplyCodeTexts();", launcher); + + string easy = Source("LauncherWindow.EasySettings.cs"); + Assert.Contains("EasySizes.DetailControl(controls.Settings())", easy); + Assert.Contains("card.IsExpanded = true;", easy); + Assert.Contains("target.Focus();", easy); + Assert.Contains("AppSettings.EasySettings = easy;", easy); + + // 두 칸과 쉬운 설정 화면은 설정 탭 맨 위에 있고, 자세한 설정(초기화 버튼 포함)은 그 밑의 한 묶음이다. + var window = XDocument.Parse(Source("LauncherWindow.xaml")); + var detailed = Named(window, "PanelDetailed"); + var order = detailed.Parent!.Elements().ToList(); + Assert.True(order.IndexOf(Named(window, "ChipsSettingsView").Parent!) < order.IndexOf(Named(window, "EasyView"))); + Assert.True(order.IndexOf(Named(window, "EasyView")) < order.IndexOf(detailed)); + Assert.Contains(Named(window, "BtnReset"), detailed.Descendants()); + } +} diff --git a/SoundVisualizer.Tests/Ui/EasySizesTests.cs b/SoundVisualizer.Tests/Ui/EasySizesTests.cs new file mode 100644 index 0000000..a35d04b --- /dev/null +++ b/SoundVisualizer.Tests/Ui/EasySizesTests.cs @@ -0,0 +1,132 @@ +// 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.Tests.Ui; + +/// +/// 쉬운 설정의 크기 세 단계(안드로이드 EasySize, #408)와, 자세한 설정에서 맞춘 크기면 선택을 비우는 규칙(#425), '자세한 설정' 칸이 +/// 보여 줄 칸(#429). 화면은 이 규칙대로 칸을 고르기만 하므로 여기서 지킨다. +/// +public class EasySizesTests +{ + [Fact] + public void 단계_값은_안드로이드와_같고_보통은_처음_값이다() + { + var defaults = new VisualModeSettings(); + Assert.Equal(new[] { EasySize.Small, EasySize.Medium, EasySize.Large }, EasySizes.All); + Assert.Equal((30.0, 40.0), (EasySize.Small.Intensity(), EasySize.Small.Opacity())); + Assert.Equal((defaults.Intensity, defaults.VisualOpacity), (EasySize.Medium.Intensity(), EasySize.Medium.Opacity())); + Assert.Equal((75.0, 70.0), (EasySize.Large.Intensity(), EasySize.Large.Opacity())); + + // 새로 설치한 사람에게는 보통이 선택되어 보인다. + Assert.Equal(EasySize.Medium, EasySizes.Of(defaults)); + } + + [Fact] + public void 단계를_쓰면_크기와_진하기만_바꾸고_크기_고정을_끈다() + { + foreach (var step in EasySizes.All) + { + var settings = new VisualModeSettings + { + IntensityAsOpacity = true, + PositionSpeed = 77, + Sensitivity = 9, + IsGlowMode = true, + GlowIntensity = 40, + CircleRadius = 90, + OpacityFixedSize = 55, + OpacityFixedMaxOpacity = 60, + }; + + step.ApplyTo(settings); + + Assert.Equal(step, EasySizes.Of(settings)); + Assert.Equal(step.Intensity(), settings.Intensity); + Assert.Equal(step.Opacity(), settings.VisualOpacity); + // 크기 고정을 켜 둔 채면 누른 크기가 보이지 않는다. + Assert.False(settings.IntensityAsOpacity); + // 자세한 설정에만 있는 값은 그대로 남아 계속 적용된다. + Assert.Equal((77.0, 9.0, true, 40.0, 90.0, 55.0, 60.0), + (settings.PositionSpeed, settings.Sensitivity, settings.IsGlowMode, settings.GlowIntensity, settings.CircleRadius, + settings.OpacityFixedSize, settings.OpacityFixedMaxOpacity)); + } + } + + [Theory] + [InlineData(63, 50, false)] // 크기가 어느 단계와도 다르다 + [InlineData(50, 90, false)] // 크기는 보통인데 진하기가 다르다 + [InlineData(30, 70, false)] // 작게의 크기에 크게의 진하기 + [InlineData(50, 50, true)] // 보통인데 크기 고정을 켰다 + public void 자세한_설정에서_단계와_다르게_맞춘_크기는_어느_단계도_아니다(double intensity, double opacity, bool sizeLock) + { + // 가장 가까운 단계로 보이면 실제 크기와 다르고, 그 칸을 누르면 오히려 크기가 바뀐다(안드로이드 #424). + var settings = new VisualModeSettings { Intensity = intensity, VisualOpacity = opacity, IntensityAsOpacity = sizeLock }; + + Assert.Null(EasySizes.Of(settings)); + } + + [Theory] + [InlineData(49.6, 50.4, EasySize.Medium)] + [InlineData(75.49, 69.51, EasySize.Large)] + [InlineData(29.5001, 40, EasySize.Small)] + public void 자세한_설정에서_같은_정수로_보이는_값은_그_단계다(double intensity, double opacity, EasySize expected) + { + // 슬라이더를 끌면 소수 값이 되지만 화면에는 정수로 보인다. 자세한 설정에서 50 으로 보이는데 쉬운 설정이 "자세한 설정에서 맞춘 + // 크기"라고 하면 엇갈린다. + Assert.Equal(expected, EasySizes.Of(new VisualModeSettings { Intensity = intensity, VisualOpacity = opacity })); + } + + [Theory] + [InlineData(49.4, 50)] + [InlineData(50, 50.6)] + public void 다른_정수로_보이는_값은_단계가_아니다(double intensity, double opacity) + { + Assert.Null(EasySizes.Of(new VisualModeSettings { Intensity = intensity, VisualOpacity = opacity })); + } + + [Theory] + [InlineData(63, 50, false, EasySizeControl.Size)] + [InlineData(50, 90, false, EasySizeControl.Opacity)] + [InlineData(30, 70, false, EasySizeControl.Opacity)] + [InlineData(63, 90, false, EasySizeControl.Size)] + [InlineData(50, 50, true, EasySizeControl.SizeLock)] + [InlineData(63, 90, true, EasySizeControl.SizeLock)] + [InlineData(75, 70, false, EasySizeControl.Size)] + public void 자세한_설정_칸은_단계에_맞지_않게_만든_첫_칸으로_간다(double intensity, double opacity, bool sizeLock, EasySizeControl expected) + { + // 모드 카드 안의 차례(크기 고정 → 크기 → 진하기)로 고른다. 단계에 맞으면 크기다(#427). + var settings = new VisualModeSettings { Intensity = intensity, VisualOpacity = opacity, IntensityAsOpacity = sizeLock }; + + Assert.Equal(expected, EasySizes.DetailControl(settings)); + } + + [Fact] + public void 처음_값인지는_모든_속성을_본다() + { + // 모드 설정에 값을 더해도 쉬운 설정의 기본 화면 판단(AppSettings.EasySettings)이 그 값을 빠뜨리지 않는다. + Assert.True(new VisualModeSettings().HasDefaultValues()); + foreach (var property in typeof(VisualModeSettings).GetProperties()) + { + var settings = new VisualModeSettings(); + object changed = property.PropertyType == typeof(bool) ? !(bool)property.GetValue(settings)! : (double)property.GetValue(settings)! + 1; + property.SetValue(settings, changed); + + Assert.False(settings.HasDefaultValues(), property.Name); + Assert.False(settings.HasSameValues(new VisualModeSettings()), property.Name); + } + } + + [Fact] + public void 복사본은_원본과_따로_움직인다() + { + var original = new VisualModeSettings { Intensity = 70, IsGlowMode = true }; + + var copy = original.Copy(); + copy.Intensity = 20; + + Assert.True(original.Copy().HasSameValues(original)); + Assert.Equal(70, original.Intensity); + Assert.True(copy.IsGlowMode); + } +} diff --git a/SoundVisualizer/AIModel/silero_vad.onnx b/SoundVisualizer/AIModel/silero_vad.onnx new file mode 100644 index 0000000..80c5592 Binary files /dev/null and b/SoundVisualizer/AIModel/silero_vad.onnx differ diff --git a/SoundVisualizer/LauncherWindow.EasySettings.cs b/SoundVisualizer/LauncherWindow.EasySettings.cs new file mode 100644 index 0000000..826f70a --- /dev/null +++ b/SoundVisualizer/LauncherWindow.EasySettings.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; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Threading; + +namespace SoundVisualizer +{ + // 설정 탭의 두 화면(안드로이드 SettingsTab, #408): 맨 위의 두 칸으로 쉬운 설정(Ui/EasySettingsView)과 자세한 설정(나머지 설정 카드)을 오간다. + // 쉬운 설정의 '자세한 설정' 칸을 누르면 자세한 설정으로 넘어와 그 설정이 있는 칸까지 옮겨 가고 초점을 둔다(#427). + public partial class LauncherWindow + { + /// 두 칸의 차례. 고른 번호가 곧 화면이다. + private const int EasyViewIndex = 0; + private const int DetailedViewIndex = 1; + + /// 저장된 화면(AppSettings.EasySettings)을 두 칸과 설정 탭에 보인다. 처음 열 때와 화면을 바꿀 때 부른다. + private void ShowSettingsView() + { + bool easy = AppSettings.EasySettings; + bool wasInitializing = _isInitializing; + _isInitializing = true; // 칸을 맞추는 동안 다시 저장하지 않게 + ChipsSettingsView.SelectedIndex = easy ? EasyViewIndex : DetailedViewIndex; + _isInitializing = wasInitializing; + + EasyView.Visibility = easy ? Visibility.Visible : Visibility.Collapsed; + PanelDetailed.Visibility = easy ? Visibility.Collapsed : Visibility.Visible; + } + + private void ChipsSettingsView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_isInitializing || ChipsSettingsView.SelectedIndex < 0) return; + SetSettingsView(ChipsSettingsView.SelectedIndex == EasyViewIndex); + } + + /// 화면을 바꾸고 저장한다. 화면만 바뀌고 다른 설정은 건드리지 않는다. + private void SetSettingsView(bool easy) + { + if (AppSettings.EasySettings != easy) + { + AppSettings.EasySettings = easy; + AppSettings.Save(); + } + ShowSettingsView(); + } + + /// 쉬운 설정에서 값을 바꿨다(이미 AppSettings 에 쓰고 저장을 예약했다). 자세한 설정의 칸과 소리 탭, 홈 안내를 맞춘다. + private void EasyView_SettingsChanged(object? sender, EventArgs e) + { + bool wasInitializing = _isInitializing; + _isInitializing = true; // 칸을 맞추며 생기는 바뀜 알림이 다시 저장하지 않게 + LoadSettingsToUI(); + _isInitializing = wasInitializing; + } + + /// + /// 쉬운 설정의 크기 줄에서 '자세한 설정' 칸을 눌렀다(안드로이드 #427). 자세한 설정으로 넘어가 지금 모드의 카드를 펼쳐(접어 두었어도) 설정 화면 + /// 맨 위로 올리고, 단계에 맞지 않게 만든 칸(크기 고정 스위치, 크기, 진하기 가운데 하나, EasySizes.DetailControl)에 초점을 둔다. + /// 안드로이드는 카드까지 옮기기만 하지만, PC 는 키보드로 누른 사람이 바로 그 칸을 조절할 수 있게 초점까지 옮긴다. + /// + private void EasyView_DetailedRequested(object? sender, EventArgs e) + { + // 지금 모드의 설정(AppSettings.CurrentModeSettings)과 같은 차례로 고른다. 번호가 범위를 벗어나면 외곽선의 설정을 쓴다. + int mode = AppSettings.VisualMode is >= 0 and < 3 ? AppSettings.VisualMode : 3; + var controls = _modes[mode]; + var card = ModeCard(mode); + + SetSettingsView(easy: false); + card.IsExpanded = true; + + FrameworkElement target = EasySizes.DetailControl(controls.Settings()) switch + { + EasySizeControl.SizeLock => controls.SizeLock, + EasySizeControl.Opacity => controls.Opacity, + _ => controls.Size, + }; + // 자세한 설정이 배치된 뒤에 옮긴다. 그 전에는 카드의 자리를 모른다. + Dispatcher.InvokeAsync(() => ScrollToCardAndFocus(card, target), DispatcherPriority.Loaded); + } + + private Expander ModeCard(int mode) => mode switch + { + 0 => ExpanderWave, + 1 => ExpanderPad, + 2 => ExpanderCircle, + _ => ExpanderOutline, + }; + + /// 카드의 맨 위가 설정 화면 맨 위에 오게 옮기고 에 초점을 둔다. 카드 안의 칸이라 함께 보인다. + private void ScrollToCardAndFocus(FrameworkElement card, FrameworkElement target) + { + if (SettingsScroller.Content is not Visual content || !card.IsVisible) return; + double top = card.TransformToAncestor(content).Transform(new Point(0, 0)).Y; + SettingsScroller.ScrollToVerticalOffset(top); + // 옮긴 자리를 먼저 정한다. 초점을 두면 그 칸이 보이게 다시 옮기는데, 옮기기 전 자리로 재면 카드가 맨 위에서 밀려난다. + SettingsScroller.UpdateLayout(); + target.Focus(); + } + } +} diff --git a/SoundVisualizer/LauncherWindow.xaml b/SoundVisualizer/LauncherWindow.xaml index ac80110..08cfe8c 100644 --- a/SoundVisualizer/LauncherWindow.xaml +++ b/SoundVisualizer/LauncherWindow.xaml @@ -86,205 +86,221 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + - - - + + + - - - - - - - - - -