From f5b6aca5170d1f7c50c6d6c645023070c93135ae Mon Sep 17 00:00:00 2001 From: amophi Date: Wed, 11 Nov 2026 13:28:39 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=9E=AC=EC=B1=84=EA=B8=B0=C2=B7?= =?UTF-8?q?=EA=B8=B0=EC=B9=A8=C2=B7=EC=88=A8=EC=86=8C=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EB=9E=8C=20=EC=86=8C=EB=A6=AC=EB=A1=9C=20=EB=B3=B4?= =?UTF-8?q?=EA=B3=A0=20'=EB=8C=80=ED=99=94=EC=9D=8C'=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20'=EC=82=AC=EB=9E=8C=20=EC=86=8C=EB=A6=AC'=EB=A1=9C?= =?UTF-8?q?=20=EB=B0=94=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android #421/#426/#447 과 같게 맞춘다. 사람 소리 이름은 18개 언어 중 16개를 Android 번역으로 바꾸고, 중국어 두 곳(人声/人聲)은 이미 같은 뜻이라 그대로 둔다. 분류 탭의 몸 소리 묶음은 사람 소리 카드(기침·숨소리·재채기)와 환경음 카드로 나뉜다. --- README.ko.md | 10 ++++----- README.md | 10 ++++----- SoundVisualizer.Core/Ai/SoundType.cs | 2 +- .../Ai/YamnetThreeClassMapper.cs | 13 +++++++----- .../Localization/Strings/ar.json | 6 +++--- .../Localization/Strings/de.json | 6 +++--- .../Localization/Strings/en.json | 6 +++--- .../Localization/Strings/es.json | 6 +++--- .../Localization/Strings/fr.json | 6 +++--- .../Localization/Strings/hi.json | 6 +++--- .../Localization/Strings/id.json | 6 +++--- .../Localization/Strings/it.json | 6 +++--- .../Localization/Strings/ja.json | 6 +++--- .../Localization/Strings/ko.json | 6 +++--- .../Localization/Strings/pl.json | 6 +++--- .../Localization/Strings/pt-BR.json | 6 +++--- .../Localization/Strings/ru.json | 6 +++--- .../Localization/Strings/th.json | 6 +++--- .../Localization/Strings/tr.json | 6 +++--- .../Localization/Strings/vi.json | 6 +++--- .../Ai/YamnetLabelListTests.cs | 17 ++++++++------- .../Ai/YamnetThreeClassMapperTests.cs | 5 +++-- .../Sounds/SoundGroupsTests.cs | 21 +++++++++++++------ 23 files changed, 94 insertions(+), 80 deletions(-) diff --git a/README.ko.md b/README.ko.md index a8988d9..48460dc 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,7 +23,7 @@
> **SoundVisualizer**는 컴퓨터의 시스템 오디오를 실시간으로 캡처하여 방향·크기·의미를 그래픽 오버레이로 변환하는 데스크톱 애플리케이션입니다. -단순한 파형 시각화를 넘어, **YAMNet**과 **ONNX Runtime** 기반의 AI 사운드 분류기를 내장하여 현재 재생 중인 소리의 종류(배경음, 대화음, 위협음 등)를 실시간으로 인지하고 피드백을 제공합니다. +단순한 파형 시각화를 넘어, **YAMNet**과 **ONNX Runtime** 기반의 AI 사운드 분류기를 내장하여 현재 재생 중인 소리의 종류(환경음, 사람 소리, 위협음)를 실시간으로 인지하고 피드백을 제공합니다. > 📱 안드로이드 버전도 있습니다: [SoundVisualizer for Android](https://github.com/bibibic76/SoundVisualizer) @@ -71,14 +71,14 @@ - 스테레오 출력에서는 좌우에 함께 있는 소리를 화면 위쪽에, 좌우 차이만 옆쪽에 그립니다. 그래서 가운데에서 나는 소리가 양쪽에서 나는 소리처럼 보이지 않습니다. ### 4. AI 실시간 사운드 분류 (ONNX & YAMNet) -- **3-Class 분류 체계**: 모든 소리를 `Ambient(환경음)`, `Speech(대화음)`, `Danger(위협음)` 3가지로 분석하여 카테고리별로 지정된 UI 색상과 투명도로 동적 시각 피드백을 제공합니다. 색상과 투명도는 커스터마이징이 가능합니다. +- **3-Class 분류 체계**: 모든 소리를 `Ambient(환경음)`, `Human sounds(사람 소리)`, `Danger(위협음)` 3가지로 분석하여 카테고리별로 지정된 UI 색상과 투명도로 동적 시각 피드백을 제공합니다. 색상과 투명도는 커스터마이징이 가능합니다. - **위협 단서**: 사이렌, 경보, 폭발음, 폭죽 같은 경고음은 모델의 상위 후보 다섯 개 안에(5% 이상) 들기만 해도 `위협음`으로 봅니다. 음악이나 게임 소리에 묻혀도 놓치지 않기 위해서입니다. 총소리 이름은 오경보를 막기 위해 이렇게 끌어올리지 않습니다. -- **흔들리지 않는 색**: `위협음`은 조건을 넘는 첫 결과에 바로 바뀌고, `대화음`과 `환경음`은 두 번 연달아 같아야 바뀌어 색이 깜빡이지 않습니다. 조용한 동안에는 AI 가 쉬고, 소리가 나면 바로 다시 듣습니다. +- **흔들리지 않는 색**: `위협음`은 조건을 넘는 첫 결과에 바로 바뀌고, `사람 소리`와 `환경음`은 두 번 연달아 같아야 바뀌어 색이 깜빡이지 않습니다. 조용한 동안에는 AI 가 쉬고, 소리가 나면 바로 다시 듣습니다. - **모든 채널을 듣습니다**: 5.1/7.1 환경에서는 앞쪽뿐 아니라 뒤쪽·옆쪽 채널의 소리도 AI 가 듣습니다. - **늦게 온 위협음**: `위협음` 판정이 소리보다 조금 늦게 와도, 그 소리를 나던 때의 크기로 그립니다. 앞서 숨겨 둔 종류였던 소리도 그렇습니다. ### 5. 소리마다 종류 정하기 (분류 탭) -- AI 가 알아듣는 소리 521개를 안드로이드 버전과 같은 묶음으로 보여 주고, 묶음마다(고급 모드에서는 소리마다) `환경음`, `대화음`, `위협음` 가운데 무엇으로 볼지 선택합니다. +- AI 가 알아듣는 소리 521개를 안드로이드 버전과 같은 묶음으로 보여 주고, 묶음마다(고급 모드에서는 소리마다) `환경음`, `사람 소리`, `위협음` 가운데 무엇으로 볼지 선택합니다. - 이름으로 찾고, 종류나 바꾼 소리로 거르고, 한 번에 모두 되돌릴 수 있습니다. 바꾼 것은 오버레이의 AI 에 바로 반영됩니다. ### 6. 도움말, 안내, 트레이 아이콘 diff --git a/README.md b/README.md index c6f08de..3a19458 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,7 +23,7 @@
> **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). @@ -71,14 +71,14 @@ 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. - **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 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/Localization/Strings/ar.json b/SoundVisualizer.Core/Localization/Strings/ar.json index 7ed2d1e..2ff8e3c 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}", @@ -106,7 +106,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": "مختلط", @@ -150,7 +150,7 @@ "help.mode.outline": "يرسم خطًا رفيعًا يمثّل حدود الموجة فقط، لذا يغطي مساحة أقل من شاشة اللعبة.", "help.modes.note": "تُحفظ إعدادات مثل «الحجم» و«العتامة» بشكل منفصل لكل وضع، ويمكنك تغييرها من علامة تبويب «الإعدادات».", "help.sound.title": "أنواع الأصوات", - "help.sound.kinds": "يصنّف الذكاء الاصطناعي ما تسمعه إلى أصوات محيطة وكلام وأصوات خطر. الألوان التلقائية هي: الأبيض للأصوات المحيطة، والأصفر للكلام، والأحمر لأصوات الخطر. يمكنك تغيير لون كل نوع واختيار إظهاره أو إخفائه من علامة تبويب «الإعدادات».", + "help.sound.kinds": "يصنّف الذكاء الاصطناعي ما تسمعه إلى أصوات محيطة وأصوات بشرية وأصوات خطر. الألوان التلقائية هي: الأبيض للأصوات المحيطة، والأصفر للأصوات البشرية، والأحمر لأصوات الخطر. يمكنك تغيير لون كل نوع واختيار إظهاره أو إخفائه من علامة تبويب «الإعدادات».", "help.sound.groups": "في علامة تبويب «الأصوات» تُجمع الأصوات المتشابهة في مجموعات، وتختار أنت النوع الذي تُحسب عليه كل مجموعة. فإذا غيّرت مجموعة «جرس الباب» مثلًا إلى أصوات الخطر، فحين يتعرّف الذكاء الاصطناعي على جرس الباب أو طرق الباب، يأخذ الصوت لون أصوات الخطر. لكن الصوت الذي تضيفه بنفسك إلى أصوات الخطر يشارك فقط في تصويت الذكاء الاصطناعي على النوع، لذلك لا يعني هذا أن كل رنين لجرس الباب سيظهر على أنه من أصوات الخطر.", "help.sound.advanced": "لتغيير الأصوات بشكل فردي، فعّل الوضع المتقدّم.", "help.sound.danger": "عند رصد صوت خطر (مثل طلق ناري)، يتغير اللون فورًا وليس تدريجيًا.", diff --git a/SoundVisualizer.Core/Localization/Strings/de.json b/SoundVisualizer.Core/Localization/Strings/de.json index 5a0ed4a..9d6210b 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Zeichnet nur eine dünne Kontur der Welle und verdeckt so weniger von Ihrem Spiel.", "help.modes.note": "Einstellungen wie „Größe“ und „Deckkraft“ werden für jeden Modus separat gespeichert und lassen sich im Tab „Einstellungen“ ändern.", "help.sound.title": "Geräuscharten", - "help.sound.kinds": "Die KI teilt Geräusche in drei Arten ein: Umgebungsgeräusche, Stimmen und Gefahrengeräusche. Standardmäßig sind Umgebungsgeräusche weiß, Stimmen gelb und Gefahrengeräusche rot. Im Tab „Einstellungen“ können Sie für jede Art die Farbe ändern und festlegen, ob sie angezeigt wird.", + "help.sound.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 die Farbe ändern und festlegen, ob sie angezeigt wird.", "help.sound.groups": "Im Tab „Geräusche“ sind ähnliche Geräusche in Gruppen zusammengefasst, und Sie legen für jede Gruppe fest, zu welcher Art sie zählt. Ändern Sie zum Beispiel die Gruppe „Türklingel“ in Gefahrengeräusche, dann erhält ein Geräusch, das die KI als Türklingel oder Klopfen erkennt, die Farbe für Gefahrengeräusche. Ein Geräusch, das Sie neu zu den Gefahrengeräuschen zählen, fließt allerdings nur in die Abstimmung der KI über die Art ein. Das heißt also nicht, dass jede Türklingel als Gefahrengeräusch angezeigt wird.", "help.sound.advanced": "Um Geräusche einzeln zu ändern, schalten Sie „Erweiterter Modus“ ein.", "help.sound.danger": "Wird ein Gefahrengeräusch (z. B. ein Schuss) erkannt, wechselt die Farbe sofort statt allmählich.", diff --git a/SoundVisualizer.Core/Localization/Strings/en.json b/SoundVisualizer.Core/Localization/Strings/en.json index 34b6858..9cea3ae 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Draws only a thin outline of the wave, so it covers less of your game.", "help.modes.note": "Settings like size and opacity are saved separately for each mode and can be changed in the Settings tab.", "help.sound.title": "Sound types", - "help.sound.kinds": "AI sorts what you hear into ambient, speech and danger sounds. By default, ambient is white, speech is yellow and danger is red. You can change the color of each type and whether it's shown in the Settings tab.", + "help.sound.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. You can change the color of each type and whether it's shown in the Settings tab.", "help.sound.groups": "In the Sounds tab, similar sounds are grouped, and you pick which type each group counts as. For example, change the “Doorbell” group to Danger, and when the AI hears a doorbell or a knock it gets the danger color. A sound you newly set to Danger only takes part in the AI's vote on the type, though, so this doesn't mean every doorbell will be shown as danger.", "help.sound.advanced": "Turn on Advanced mode to change sounds one by one.", "help.sound.danger": "When a danger sound (like a gunshot) is detected, the color changes instantly instead of fading.", diff --git a/SoundVisualizer.Core/Localization/Strings/es.json b/SoundVisualizer.Core/Localization/Strings/es.json index 119fadf..273e9c2 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Dibuja solo un contorno fino de la onda, así que tapa menos la pantalla del juego.", "help.modes.note": "Opciones como Tamaño y Opacidad se guardan por separado para cada modo y puedes cambiarlas en la pestaña Ajustes.", "help.sound.title": "Tipos de sonido", - "help.sound.kinds": "La IA clasifica el audio en sonido ambiente, voz y peligro. De forma predeterminada, el sonido ambiente se muestra en blanco, la voz en amarillo y el peligro en rojo. En la pestaña Ajustes puedes cambiar el color de cada tipo y elegir si se muestra o no.", + "help.sound.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 cambiar el color de cada tipo y elegir si se muestra o no.", "help.sound.groups": "En la pestaña Sonidos, los sonidos parecidos están agrupados y eliges de qué tipo es cada grupo. Por ejemplo, si cambias el grupo “Timbre de la puerta” a Peligro, cuando la IA reconozca un timbre de la puerta o golpes en la puerta, el sonido tendrá el color de peligro. Eso sí, un sonido que tú cambias a Peligro solo participa en la votación de la IA sobre el tipo, así que no significa que cada timbre de la puerta se vaya a mostrar como peligro.", "help.sound.advanced": "Activa el modo avanzado para cambiar los sonidos uno a uno.", "help.sound.danger": "Cuando se detecta un sonido de peligro (como un disparo), el color cambia al instante en lugar de hacerlo poco a poco.", diff --git a/SoundVisualizer.Core/Localization/Strings/fr.json b/SoundVisualizer.Core/Localization/Strings/fr.json index 0f40142..751f233 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Dessine uniquement un fin contour de la vague, pour moins masquer votre jeu.", "help.modes.note": "Les réglages comme « Taille » et « Opacité » sont enregistrés séparément pour chaque mode et peuvent être modifiés dans l'onglet Paramètres.", "help.sound.title": "Types de sons", - "help.sound.kinds": "L'IA classe les sons entendus en trois types : ambiance, voix et danger. Par défaut, l'ambiance s'affiche en blanc, les voix en jaune et les dangers en rouge. Dans l'onglet Paramètres, vous pouvez changer la couleur de chaque type et choisir de l'afficher ou non.", + "help.sound.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 changer la couleur de chaque type et choisir de l'afficher ou non.", "help.sound.groups": "Dans l'onglet Sons, les sons proches sont regroupés et vous choisissez à quel type correspond chaque groupe. Par exemple, si vous passez le groupe « Sonnette » en Danger, quand l'IA reconnaît une sonnette ou des coups à la porte, le son prend la couleur des sons de danger. Toutefois, un son que vous passez vous-même en Danger ne participe qu'au vote de l'IA sur le type, donc chaque sonnette ne sera pas forcément affichée comme un danger.", "help.sound.advanced": "Activez le mode avancé pour modifier les sons un par un.", "help.sound.danger": "Lorsqu'un son de danger (comme un coup de feu) est détecté, la couleur change instantanément, sans transition progressive.", diff --git a/SoundVisualizer.Core/Localization/Strings/hi.json b/SoundVisualizer.Core/Localization/Strings/hi.json index 79765fc..41f22d8 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}", @@ -106,7 +106,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": "मिला-जुला", @@ -150,7 +150,7 @@ "help.mode.outline": "लहर की सिर्फ़ पतली बाहरी रेखा बनाता है, इसलिए गेम की स्क्रीन कम ढकती है।", "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": "खतरे की आवाज़ (जैसे गोली चलने की आवाज़) पहचानी जाने पर रंग धीरे-धीरे बदलने के बजाय तुरंत बदल जाता है।", diff --git a/SoundVisualizer.Core/Localization/Strings/id.json b/SoundVisualizer.Core/Localization/Strings/id.json index 9359d17..0fb1199 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Hanya menggambar garis tipis di sepanjang kontur ombak, sehingga tidak banyak menutupi layar game Anda.", "help.modes.note": "Setelan seperti Ukuran dan Opasitas disimpan terpisah untuk setiap mode dan dapat diubah di tab Setelan.", "help.sound.title": "Jenis suara", - "help.sound.kinds": "AI mengelompokkan suara yang terdengar menjadi suara latar, percakapan, dan suara bahaya. Secara default, suara latar berwarna putih, percakapan kuning, dan suara bahaya merah. Di tab Setelan, Anda dapat mengubah warna setiap jenis dan memilih apakah jenis tersebut ditampilkan.", + "help.sound.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 mengubah warna setiap jenis dan memilih apakah jenis tersebut ditampilkan.", "help.sound.groups": "Di tab Suara, suara yang mirip dikelompokkan, dan Anda memilih jenis untuk setiap grup. Misalnya, jika grup “Bel pintu” diubah menjadi suara bahaya, saat AI mengenali bel pintu atau ketukan pintu, suara itu akan mendapat warna suara bahaya. Namun, suara yang Anda ubah sendiri menjadi suara bahaya hanya ikut dihitung dalam voting AI untuk menentukan jenisnya, jadi bukan berarti setiap bel pintu akan ditampilkan sebagai suara bahaya.", "help.sound.advanced": "Aktifkan mode lanjutan untuk mengubah suara satu per satu.", "help.sound.danger": "Saat suara bahaya (seperti suara tembakan) terdeteksi, warna langsung berubah tanpa memudar perlahan.", diff --git a/SoundVisualizer.Core/Localization/Strings/it.json b/SoundVisualizer.Core/Localization/Strings/it.json index bd45d48..243956a 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Disegna solo un sottile contorno dell'onda, così copre meno il gioco.", "help.modes.note": "Opzioni come Dimensione e Opacità vengono salvate separatamente per ogni modalità e si possono modificare nella scheda Impostazioni.", "help.sound.title": "Tipi di suono", - "help.sound.kinds": "L'IA classifica l'audio in suoni ambientali, voci e suoni di pericolo. Per impostazione predefinita, i suoni ambientali sono bianchi, le voci gialle e i suoni di pericolo rossi. Nella scheda Impostazioni puoi cambiare il colore di ogni tipo e scegliere se mostrarlo.", + "help.sound.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 cambiare il colore di ogni tipo e scegliere se mostrarlo.", "help.sound.groups": "Nella scheda Suoni i suoni simili sono raggruppati e scegli il tipo di ogni gruppo. Per esempio, se sposti il gruppo “Campanello della porta” tra i suoni di pericolo, quando l'IA riconosce un campanello della porta o il bussare, il suono prende il colore dei suoni di pericolo. Un suono che sposti tu tra i suoni di pericolo, però, partecipa solo al voto dell'IA sul tipo, quindi non significa che ogni campanello della porta verrà mostrato come suono di pericolo.", "help.sound.advanced": "Per cambiare i suoni uno per uno, attiva la modalità avanzata.", "help.sound.danger": "Quando viene rilevato un suono di pericolo (come uno sparo), il colore cambia all'istante invece di sfumare gradualmente.", diff --git a/SoundVisualizer.Core/Localization/Strings/ja.json b/SoundVisualizer.Core/Localization/Strings/ja.json index ac0e2f1..045ebdb 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}", @@ -106,7 +106,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": "混在", @@ -150,7 +150,7 @@ "help.mode.outline": "波の輪郭線だけを細く描くので、ゲーム画面が隠れにくくなります。", "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": "危険音(銃声など)を検出すると、色が徐々にではなく、すぐに切り替わります。", diff --git a/SoundVisualizer.Core/Localization/Strings/ko.json b/SoundVisualizer.Core/Localization/Strings/ko.json index c4687a1..3c7df08 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}", @@ -106,7 +106,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": "일부 다름", @@ -150,7 +150,7 @@ "help.mode.outline": "파도의 윤곽선만 가늘게 그려 게임 화면을 덜 가립니다.", "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": "위협음(총소리 등)이 감지되면 색이 서서히 바뀌지 않고 즉시 바뀝니다.", diff --git a/SoundVisualizer.Core/Localization/Strings/pl.json b/SoundVisualizer.Core/Localization/Strings/pl.json index a195734..a8ab211 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Rysuje tylko cienki kontur fali, więc mniej zasłania grę.", "help.modes.note": "Ustawienia takie jak „Rozmiar” i „Krycie” są zapisywane osobno dla każdego trybu. Możesz je zmienić na karcie „Ustawienia”.", "help.sound.title": "Rodzaje dźwięków", - "help.sound.kinds": "AI dzieli to, co słychać, na dźwięki otoczenia, mowę i dźwięki zagrożenia. Domyślnie dźwięki otoczenia są oznaczane na biało, mowa na żółto, a dźwięki zagrożenia na czerwono. Na karcie „Ustawienia” możesz zmienić kolor każdego rodzaju i to, czy jest pokazywany.", + "help.sound.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 zmienić kolor każdego rodzaju i to, czy jest pokazywany.", "help.sound.groups": "Na karcie „Dźwięki” podobne dźwięki są zebrane w grupy, a Ty wybierasz, do jakiego rodzaju należy każda grupa. Jeśli na przykład zmienisz grupę „Dzwonek do drzwi” na dźwięki zagrożenia, to gdy AI rozpozna dzwonek do drzwi albo pukanie, dźwięk dostanie kolor dźwięków zagrożenia. Dźwięk, który ustawisz jako dźwięk zagrożenia, bierze jednak udział tylko w głosowaniu AI nad rodzajem, więc nie oznacza to, że każdy dzwonek do drzwi zostanie pokazany jako dźwięk zagrożenia.", "help.sound.advanced": "Aby zmieniać dźwięki pojedynczo, włącz tryb zaawansowany.", "help.sound.danger": "Gdy zostanie wykryty dźwięk zagrożenia (np. strzał), kolor zmienia się natychmiast, a nie stopniowo.", diff --git a/SoundVisualizer.Core/Localization/Strings/pt-BR.json b/SoundVisualizer.Core/Localization/Strings/pt-BR.json index 09d4043..4389cac 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Desenha só um contorno fino da onda, então cobre menos a tela do jogo.", "help.modes.note": "Ajustes como Tamanho e Opacidade são salvos separadamente para cada modo e podem ser alterados na aba Ajustes.", "help.sound.title": "Tipos de som", - "help.sound.kinds": "A IA separa os sons em três tipos: sons ambientes, vozes e sons de perigo. Por padrão, os sons ambientes aparecem em branco, as vozes em amarelo e os sons de perigo em vermelho. Na aba Ajustes, você pode mudar a cor de cada tipo e escolher se ele é mostrado.", + "help.sound.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 mudar a cor de cada tipo e escolher se ele é mostrado.", "help.sound.groups": "Na aba Sons, os sons parecidos ficam agrupados e você escolhe o tipo de cada grupo. Por exemplo, se mudar o grupo “Campainha” para som de perigo, quando a IA reconhecer uma campainha ou uma batida na porta, o som passa a ter a cor dos sons de perigo. Mas um som que você mesmo muda para som de perigo só participa da votação da IA sobre o tipo, então isso não quer dizer que toda campainha vai aparecer como som de perigo.", "help.sound.advanced": "Ative o modo avançado para mudar os sons um a um.", "help.sound.danger": "Quando um som de perigo (como um tiro) é detectado, a cor muda na hora, sem transição gradual.", diff --git a/SoundVisualizer.Core/Localization/Strings/ru.json b/SoundVisualizer.Core/Localization/Strings/ru.json index e01c9e3..4cb2c1a 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}", @@ -106,7 +106,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": "Смешанный", @@ -150,7 +150,7 @@ "help.mode.outline": "Рисуется только тонкий контур волны, поэтому графика меньше загораживает игру.", "help.modes.note": "Такие настройки, как «Размер» и «Плотность», сохраняются отдельно для каждого режима. Изменить их можно на вкладке «Настройки».", "help.sound.title": "Типы звуков", - "help.sound.kinds": "ИИ делит всё, что слышно, на фоновые звуки, речь и опасные звуки. По умолчанию фоновые звуки отображаются белым, речь — жёлтым, а опасные звуки — красным. На вкладке «Настройки» можно изменить цвет каждого типа и выбрать, показывать ли его.", + "help.sound.kinds": "ИИ делит всё, что слышно, на фоновые звуки, звуки человека и опасные звуки. По умолчанию фоновые звуки отображаются белым, звуки человека — жёлтым, а опасные звуки — красным. На вкладке «Настройки» можно изменить цвет каждого типа и выбрать, показывать ли его.", "help.sound.groups": "На вкладке «Звуки» похожие звуки собраны в группы, и вы выбираете, к какому типу относится каждая группа. Например, если перевести группу «Дверной звонок» в опасные звуки, то когда ИИ распознаёт дверной звонок или стук в дверь, звук получит цвет опасных звуков. Однако звук, который вы сами перевели в опасные звуки, лишь участвует в голосовании ИИ о типе, поэтому это не значит, что каждый дверной звонок будет показан как опасный звук.", "help.sound.advanced": "Чтобы менять звуки по одному, включите расширенный режим.", "help.sound.danger": "Когда обнаружен опасный звук (например, выстрел), цвет меняется сразу, а не плавно.", diff --git a/SoundVisualizer.Core/Localization/Strings/th.json b/SoundVisualizer.Core/Localization/Strings/th.json index 79908cd..782d123 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}", @@ -106,7 +106,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": "ผสม", @@ -150,7 +150,7 @@ "help.mode.outline": "วาดเฉพาะเส้นขอบบางของคลื่น จึงบังหน้าจอเกมน้อยกว่า", "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": "เมื่อตรวจพบเสียงอันตราย (เช่น เสียงปืน) สีจะเปลี่ยนทันทีแทนที่จะค่อยๆ เปลี่ยน", diff --git a/SoundVisualizer.Core/Localization/Strings/tr.json b/SoundVisualizer.Core/Localization/Strings/tr.json index eeac03c..0fcbcdf 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Dalganın yalnızca ince dış çizgisini çizer; böylece oyun ekranınızı daha az örter.", "help.modes.note": "“Boyut” ve “Opaklık” gibi ayarlar her mod için ayrı kaydedilir ve “Ayarlar” sekmesinden değiştirilebilir.", "help.sound.title": "Ses türleri", - "help.sound.kinds": "Yapay zeka, duyduğunuz sesleri ortam sesleri, konuşma ve tehlike sesleri olarak ayırır. Varsayılan olarak ortam sesleri beyaz, konuşma sarı, tehlike sesleri ise kırmızı renkte gösterilir. Her türün rengini ve gösterilip gösterilmeyeceğini “Ayarlar” sekmesinden değiştirebilirsiniz.", + "help.sound.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 rengini ve gösterilip gösterilmeyeceğini “Ayarlar” sekmesinden değiştirebilirsiniz.", "help.sound.groups": "“Sesler” sekmesinde benzer sesler gruplanır ve her grubun hangi türden sayılacağını siz seçersiniz. Örneğin “Kapı zili” grubunu tehlike sesi yaparsanız yapay zeka bir kapı zili veya kapıya vurma sesi tanıdığında bu ses tehlike sesi rengini alır. Ancak yeni tehlike sesi yaptığınız bir ses, yalnızca yapay zekanın türü belirlemek için yaptığı oylamaya katılır; bu yüzden her kapı zilinin tehlike sesi olarak gösterileceği anlamına gelmez.", "help.sound.advanced": "Her sesi ayrı olarak değiştirmek için gelişmiş modu açın.", "help.sound.danger": "Bir tehlike sesi (ör. silah sesi) algılandığında renk yavaşça geçiş yapmak yerine anında değişir.", diff --git a/SoundVisualizer.Core/Localization/Strings/vi.json b/SoundVisualizer.Core/Localization/Strings/vi.json index d74dd03..46911a0 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}", @@ -106,7 +106,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", @@ -150,7 +150,7 @@ "help.mode.outline": "Chỉ vẽ đường viền mảnh của sóng, nên ít che màn hình trò chơi hơn.", "help.modes.note": "Các cài đặt như Kích thước và Độ đậm được lưu riêng cho từng chế độ và có thể thay đổi trong thẻ Cài đặt.", "help.sound.title": "Loại âm thanh", - "help.sound.kinds": "AI phân loại âm thanh bạn nghe thấy thành âm thanh môi trường, giọng nói và âm thanh nguy hiểm. Theo mặc định, âm thanh môi trường có màu trắng, giọng nói màu vàng và âm thanh nguy hiểm màu đỏ. Bạn có thể đổi màu và chọn có hiện hay không cho từng loại trong thẻ Cài đặt.", + "help.sound.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ể đổi màu và chọn có hiện hay không cho từng loại trong thẻ Cài đặt.", "help.sound.groups": "Trong thẻ Phân loại, các âm thanh tương tự được gom thành nhóm và bạn chọn mỗi nhóm được tính là loại nào. Ví dụ, nếu đổi nhóm “Chuông cửa” thành âm thanh nguy hiểm, khi AI nhận ra chuông cửa hoặc gõ cửa, âm thanh đó sẽ có màu của âm thanh nguy hiểm. Tuy nhiên, âm thanh do bạn tự đổi thành âm thanh nguy hiểm chỉ tham gia vào việc bỏ phiếu chọn loại của AI, nên không có nghĩa là chuông cửa lúc nào cũng sẽ hiện là âm thanh nguy hiểm.", "help.sound.advanced": "Để đổi riêng từng âm thanh, hãy bật chế độ nâng cao.", "help.sound.danger": "Khi phát hiện âm thanh nguy hiểm (như tiếng súng), màu sẽ đổi ngay lập tức thay vì chuyển dần.", 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 @@ public void 위협음으로_묶이는_이름은_모바일과_같다() } [Fact] - public void 대화음으로_묶이는_이름은_모바일과_같다() + public void 사람_소리로_묶이는_이름은_모바일과_같다() { Assert.Equal(ExpectedSpeech, NamesMappedTo(SoundType.Speech)); } @@ -66,7 +67,7 @@ public void 정확한_이름_목록은_개수가_같고_모두_실제_클래스_ 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 void 전기톱과_천둥은_정확히_같은_이름만_위협음() } [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/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 @@ public void 지금_매핑으로_나눠도_카드마다_기본_종류가_하나 } [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] From 0fe593c730e8fc820ddd00bf45d3ada8eca08028 Mon Sep 17 00:00:00 2001 From: amophi Date: Wed, 11 Nov 2026 13:48:57 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=89=AC=EC=9A=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=ED=81=AC=EA=B8=B0=20=EC=84=B8=20=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=EC=99=80=20=EC=B2=98=EC=9D=8C=20=EC=97=B4=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=84=20=EC=A0=95=ED=95=98=EB=8A=94=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 안드로이드 쉬운 설정(#408, #425, #429)의 규칙을 Core 에 옮긴다. 화면은 다음 커밋에서 더한다. - 크기 세 단계(EasySizes): 작게 30/40, 보통 = 모드 설정의 처음 값(50/50), 크게 75/70. 안드로이드와 같은 값이다. PC 엔진도 크기·진하기를 같은 식으로 쓰고 네 모드 모두 같은 두 값을 쓰므로 모드마다 달리 두지 않는다. 단계를 쓰면 크기 고정을 끄고 나머지 값은 그대로 둔다. - 자세한 설정에서 단계와 다르게 맞춘 크기는 어느 단계도 아니다(#425). 화면에 같은 정수로 보이는 값(±0.5)은 같은 단계로 본다. - '자세한 설정' 칸이 보여 줄 칸(#429): 크기 고정 → 크기 → 진하기 가운데 단계에 맞지 않게 만든 첫 칸. - AppSettings.EasySettings: 새로 설치하면 쉬운 설정, 항목이 없는 예전 설정 파일은 네 모드가 모두 처음 값일 때만 쉬운 설정이다 (모바일 loadEasySettings). 정한 값은 다음 저장에 함께 남는다. 화면 설정이라 전체 초기화는 건드리지 않는다. - 모양 카드 그림: TutorialDemo 가 그릴 설정과 작은 화면(1120×630)을 받는다. PreviewSettings 는 진하기와 반응만 튜토리얼만큼 올리고, 한 번 움직이는 시간은 4.9초다(WCAG 2.2.2). --- SoundVisualizer.Core/AppSettings.cs | 45 ++++++ SoundVisualizer.Core/EasySettings.cs | 138 ++++++++++++++++++ SoundVisualizer.Core/Tutorial/TutorialDemo.cs | 48 +++++- SoundVisualizer.Tests/AppSettingsTests.cs | 127 +++++++++++++++- .../Tutorial/ModePreviewTests.cs | 100 +++++++++++++ SoundVisualizer.Tests/Ui/EasySizesTests.cs | 132 +++++++++++++++++ 6 files changed, 586 insertions(+), 4 deletions(-) create mode 100644 SoundVisualizer.Core/EasySettings.cs create mode 100644 SoundVisualizer.Tests/Tutorial/ModePreviewTests.cs create mode 100644 SoundVisualizer.Tests/Ui/EasySizesTests.cs 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/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/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/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 @@ public void 전체_초기화는_튜토리얼을_본_기록을_건드리지_않 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/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/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); + } +} From 0ebc7b26017e24faa71979bc2800b9d8e965ac3b Mon Sep 17 00:00:00 2001 From: amophi Date: Wed, 11 Nov 2026 13:49:09 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=ED=83=AD?= =?UTF-8?q?=EC=97=90=20=EA=B7=B8=EB=A6=BC=EC=9C=BC=EB=A1=9C=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=ED=95=98=EB=8A=94=20=EC=89=AC=EC=9A=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 안드로이드 #408(#417)과 #425·#429·#436 을 PC 런처에 옮긴다. 설정 탭 맨 위의 두 칸(쉬운 설정 | 자세한 설정)으로 그림으로 선택하는 화면과 지금까지의 모든 설정을 오간다. 새로 설치하면 쉬운 설정으로 연다. - 모양: 실제 오버레이 엔진으로 그린 카드 넷(ModePreview). 고른 카드만 4.9초 움직인다. Windows 의 애니메이션 효과를 꺼 두었으면 멈춘 그림이다. 그림은 실제 방향이라 아랍어에서도 뒤집지 않는다. - 크기: 지금 모드의 세 단계. 자세한 설정에서 맞춘 크기면 선택을 비우고 한 줄로 알린다(#425, 나타날 때 화면 읽어주기가 읽는다). 밑의 '자세한 설정' 칸은 자세한 설정으로 넘어가 그 모드 카드를 맨 위로 올리고 그 칸(크기 고정·크기·진하기)에 초점을 둔다(#429). 이 칸은 화살표로 크기를 훑다 넘어가지 않게 선택지 묶음 밖의 단추로 두었다. - 소리 종류: 위협음 → 대화음 → 환경음 줄에서 화면 표시를 켜고 끈다. 이름은 소리 탭의 종류 이름을 그대로 쓰고, 화면 읽어주기에는 자세한 설정의 스위치 이름으로 읽힌다. 보이는 종류 이름이 없는 언어는 앞에 붙인다(#436). - 안드로이드의 진동 줄, 빠른 설정 타일, 외부 사운드 모드 문구는 PC 에 없어 뺐다. - 새 문구 10개를 18개 언어로 넣었다(안드로이드 easy_* 번역). --- .../Localization/Strings/ar.json | 12 + .../Localization/Strings/de.json | 12 + .../Localization/Strings/en.json | 12 + .../Localization/Strings/es.json | 12 + .../Localization/Strings/fr.json | 12 + .../Localization/Strings/hi.json | 12 + .../Localization/Strings/id.json | 12 + .../Localization/Strings/it.json | 12 + .../Localization/Strings/ja.json | 12 + .../Localization/Strings/ko.json | 12 + .../Localization/Strings/pl.json | 12 + .../Localization/Strings/pt-BR.json | 12 + .../Localization/Strings/ru.json | 12 + .../Localization/Strings/th.json | 12 + .../Localization/Strings/tr.json | 12 + .../Localization/Strings/vi.json | 12 + .../Localization/Strings/zh-Hans.json | 12 + .../Localization/Strings/zh-Hant.json | 12 + .../Localization/EasySettingsStringsTests.cs | 113 ++++++ .../Ui/EasySettingsSourceTests.cs | 117 ++++++ .../LauncherWindow.EasySettings.cs | 103 +++++ SoundVisualizer/LauncherWindow.xaml | 376 +++++++++--------- SoundVisualizer/LauncherWindow.xaml.cs | 8 +- SoundVisualizer/Themes/Theme.xaml | 19 + .../Tutorial/TutorialDemoRenderer.cs | 18 +- SoundVisualizer/Ui/ChoiceChipList.cs | 21 +- SoundVisualizer/Ui/DetailedChoiceButton.cs | 29 ++ SoundVisualizer/Ui/EasyIcons.cs | 333 ++++++++++++++++ SoundVisualizer/Ui/EasySettingsView.xaml | 327 +++++++++++++++ SoundVisualizer/Ui/EasySettingsView.xaml.cs | 279 +++++++++++++ SoundVisualizer/Ui/ModePreview.cs | 252 ++++++++++++ 31 files changed, 2025 insertions(+), 186 deletions(-) create mode 100644 SoundVisualizer.Tests/Localization/EasySettingsStringsTests.cs create mode 100644 SoundVisualizer.Tests/Ui/EasySettingsSourceTests.cs create mode 100644 SoundVisualizer/LauncherWindow.EasySettings.cs create mode 100644 SoundVisualizer/Ui/DetailedChoiceButton.cs create mode 100644 SoundVisualizer/Ui/EasyIcons.cs create mode 100644 SoundVisualizer/Ui/EasySettingsView.xaml create mode 100644 SoundVisualizer/Ui/EasySettingsView.xaml.cs create mode 100644 SoundVisualizer/Ui/ModePreview.cs diff --git a/SoundVisualizer.Core/Localization/Strings/ar.json b/SoundVisualizer.Core/Localization/Strings/ar.json index 2ff8e3c..a1f844e 100644 --- a/SoundVisualizer.Core/Localization/Strings/ar.json +++ b/SoundVisualizer.Core/Localization/Strings/ar.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": "تُجمع الأصوات التي يتعرّف عليها الذكاء الاصطناعي مع الأصوات المشابهة لها في مجموعات، وتُعرض حسب النوع الذي تُصنَّف ضمنه تلقائيًا. إذا غيّرت نوع مجموعة، فستأخذ أصواتها فورًا لون النوع الجديد.", "sounds.sibling_note": "كثيرًا ما يسمع الذكاء الاصطناعي الصوت الواحد بعدة أسماء معًا. على سبيل المثال، تُسمع «صفارة إنذار» أيضًا على أنها «إنذار»، وهو في مجموعة أخرى. إذا ظل صوت يظهر بنوعه القديم بعد التغيير، فغيّر أيضًا المجموعات أو الأصوات المرتبطة به.", diff --git a/SoundVisualizer.Core/Localization/Strings/de.json b/SoundVisualizer.Core/Localization/Strings/de.json index 9d6210b..e43d0ce 100644 --- a/SoundVisualizer.Core/Localization/Strings/de.json +++ b/SoundVisualizer.Core/Localization/Strings/de.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/en.json b/SoundVisualizer.Core/Localization/Strings/en.json index 9cea3ae..454bced 100644 --- a/SoundVisualizer.Core/Localization/Strings/en.json +++ b/SoundVisualizer.Core/Localization/Strings/en.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/es.json b/SoundVisualizer.Core/Localization/Strings/es.json index 273e9c2..6eacad0 100644 --- a/SoundVisualizer.Core/Localization/Strings/es.json +++ b/SoundVisualizer.Core/Localization/Strings/es.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/fr.json b/SoundVisualizer.Core/Localization/Strings/fr.json index 751f233..aa60aaa 100644 --- a/SoundVisualizer.Core/Localization/Strings/fr.json +++ b/SoundVisualizer.Core/Localization/Strings/fr.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/hi.json b/SoundVisualizer.Core/Localization/Strings/hi.json index 41f22d8..52fcaf9 100644 --- a/SoundVisualizer.Core/Localization/Strings/hi.json +++ b/SoundVisualizer.Core/Localization/Strings/hi.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 अक्सर एक ही आवाज़ को एक साथ कई नामों से पहचानता है। जैसे, सायरन “अलार्म” के रूप में भी पहचाना जाता है, जो किसी दूसरे समूह में है। बदलाव के बाद भी अगर कोई आवाज़ पुराने प्रकार में दिखे, तो उससे जुड़े समूह या आवाज़ें भी बदलें।", diff --git a/SoundVisualizer.Core/Localization/Strings/id.json b/SoundVisualizer.Core/Localization/Strings/id.json index 0fb1199..76f0e67 100644 --- a/SoundVisualizer.Core/Localization/Strings/id.json +++ b/SoundVisualizer.Core/Localization/Strings/id.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/it.json b/SoundVisualizer.Core/Localization/Strings/it.json index 243956a..7deece3 100644 --- a/SoundVisualizer.Core/Localization/Strings/it.json +++ b/SoundVisualizer.Core/Localization/Strings/it.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/ja.json b/SoundVisualizer.Core/Localization/Strings/ja.json index 045ebdb..ce9938e 100644 --- a/SoundVisualizer.Core/Localization/Strings/ja.json +++ b/SoundVisualizer.Core/Localization/Strings/ja.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 は一つの音を複数の名前で同時に聞き取ることがよくあります。たとえばサイレンは「警報音」としても聞き取られますが、警報音は別のグループにあります。変更後も前の種類で表示される場合は、関連するグループや音も変更してください。", diff --git a/SoundVisualizer.Core/Localization/Strings/ko.json b/SoundVisualizer.Core/Localization/Strings/ko.json index 3c7df08..a0d2e19 100644 --- a/SoundVisualizer.Core/Localization/Strings/ko.json +++ b/SoundVisualizer.Core/Localization/Strings/ko.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는 한 소리를 여러 이름으로 함께 들을 때가 많습니다. 예를 들어 사이렌은 ‘경보음’으로도 들리는데, 경보음은 다른 묶음에 있습니다. 바꾼 뒤에도 예전 종류로 보이면 관련된 묶음이나 소리도 함께 바꾸세요.", diff --git a/SoundVisualizer.Core/Localization/Strings/pl.json b/SoundVisualizer.Core/Localization/Strings/pl.json index a8ab211..284f1a9 100644 --- a/SoundVisualizer.Core/Localization/Strings/pl.json +++ b/SoundVisualizer.Core/Localization/Strings/pl.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/pt-BR.json b/SoundVisualizer.Core/Localization/Strings/pt-BR.json index 4389cac..ed83071 100644 --- a/SoundVisualizer.Core/Localization/Strings/pt-BR.json +++ b/SoundVisualizer.Core/Localization/Strings/pt-BR.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/ru.json b/SoundVisualizer.Core/Localization/Strings/ru.json index 4cb2c1a..8038973 100644 --- a/SoundVisualizer.Core/Localization/Strings/ru.json +++ b/SoundVisualizer.Core/Localization/Strings/ru.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": "Звуки, которые распознаёт ИИ, собраны в группы похожих звуков по типам, к которым они относятся по умолчанию. Если изменить тип группы, цвет её звуков сразу станет таким же, как у нового типа.", "sounds.sibling_note": "ИИ часто распознаёт один и тот же звук сразу под несколькими названиями. Сирена, например, распознаётся ещё и как «Сигнал тревоги», а этот звук в другой группе. Если после изменения звук всё ещё отображается со старым типом, измените и связанные группы или звуки.", diff --git a/SoundVisualizer.Core/Localization/Strings/th.json b/SoundVisualizer.Core/Localization/Strings/th.json index 782d123..98a8f5e 100644 --- a/SoundVisualizer.Core/Localization/Strings/th.json +++ b/SoundVisualizer.Core/Localization/Strings/th.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 มักได้ยินเสียงเดียวเป็นหลายชื่อพร้อมกัน เช่น ไซเรนยังได้ยินเป็น “สัญญาณเตือนภัย” ด้วย ซึ่งอยู่ในอีกกลุ่มหนึ่ง หากเปลี่ยนแล้วเสียงยังแสดงเป็นประเภทเดิม ให้เปลี่ยนกลุ่มหรือเสียงที่เกี่ยวข้องด้วย", diff --git a/SoundVisualizer.Core/Localization/Strings/tr.json b/SoundVisualizer.Core/Localization/Strings/tr.json index 0fcbcdf..10398a1 100644 --- a/SoundVisualizer.Core/Localization/Strings/tr.json +++ b/SoundVisualizer.Core/Localization/Strings/tr.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/vi.json b/SoundVisualizer.Core/Localization/Strings/vi.json index 46911a0..e44ecda 100644 --- a/SoundVisualizer.Core/Localization/Strings/vi.json +++ b/SoundVisualizer.Core/Localization/Strings/vi.json @@ -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.", diff --git a/SoundVisualizer.Core/Localization/Strings/zh-Hans.json b/SoundVisualizer.Core/Localization/Strings/zh-Hans.json index 75761ee..8717a9a 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 常常会把同一个声音同时识别为多个名称。例如警笛也会被识别为“警报”,而“警报”在另一组中。如果更改后某个声音仍按原来的类型显示,请一并更改相关的组或声音。", diff --git a/SoundVisualizer.Core/Localization/Strings/zh-Hant.json b/SoundVisualizer.Core/Localization/Strings/zh-Hant.json index 3b8088b..9b5b05c 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 常常會把同一個聲音同時辨識為多個名稱。例如警笛也會被辨識為「警報」,而「警報」在另一個群組。如果變更後某個聲音仍以原本的類型顯示,請一併變更相關的群組或聲音。", 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/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/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 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + - - - + + + - - - - - - - - - -