diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 03d03d8b..1224d286 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -3,6 +3,8 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the release workflow uses the section matching the tag and fails if either is missing. ## [Unreleased] +### Changes +- **Controller sounds: the Console Mode ones** (#287): the sounds are now the same soft console blips as Console Mode (another team app, AGPL-3.0, the same licence): a soft sine with a quiet octave, fast attack and decay, no audio files (built in code). Move, confirm (rising) and back are Console Mode's; mark is a fourth blip in the same style. **Médio** (medium) is Console Mode's original level, **alto** (high) twice that and **baixo** (low) half. The Kenney CC0 files from 0.16 are gone. ## [0.16.0-alpha.1] ### Changes diff --git a/CHANGELOG.md b/CHANGELOG.md index 66952003..7ca46b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` **nos dois arquivos**. O workflow de release usa a seção da tag e falha se faltar alguma. ## [Unreleased] +### Mudanças +- **Sons do controle: os do Console Mode** (#287): os sons passam a ser os mesmos toques macios de console do Console Mode (outro app da equipe, AGPL-3.0, a mesma licença): uma senoide suave com uma oitava baixinha, ataque rápido e queda, sem arquivo de áudio (montados em código). Mover, confirmar (sobe) e voltar são os do Console Mode; marcar é um quarto toque no mesmo estilo. O volume **médio** é o nível original do Console Mode, **alto** o dobro e **baixo** a metade. Os arquivos CC0 de Kenney da 0.16 saíram. ## [0.16.0-alpha.1] ### Mudanças diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index d6b28e8e..f88fe01d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -101,13 +101,10 @@ AGPL-3.0-only deste código e não podem ser reutilizadas sem permissão dos don Sons do controle ------------------------------------------------------------------------------ -Os quatro toques de mover, marcar, confirmar e voltar (assets/sounds/*.wav, embutidos no aplicativo) vêm de dois pacotes de -Kenney (https://kenney.nl), ambos Creative Commons Zero (CC0 1.0, domínio público): uso pessoal, educacional e comercial e -redistribuição livres, sem exigir crédito (o crédito a Kenney é voluntário e está aqui). Do pacote "UI Audio": rollover5 (mover). -Do pacote "Interface Sounds": glass_002 (marcar), maximize_009 (confirmar) e back_002 (voltar). Foram aparados, suavizados com -um filtro passa-baixa, com fades e mesma energia por tools/make-sounds.py. As licenças originais estão em -assets/sounds/LICENSE-Kenney-CC0.txt e LICENSE-Kenney-UI-Audio-CC0.txt. Por serem CC0, não limitam a AGPL-3.0-only do restante -do projeto. A interface do PS5 é apenas referência de sensação (toques curtos, redondos e suaves, confirmar subindo e voltar -descendo); nenhum som da Sony foi usado ou imitado. +Os toques de mover, confirmar e voltar (src/ControlFS.Core/Audio/SoundBank.cs) são os mesmos do Console Mode +(https://github.com/lippdev/consolemode, src/ConsoleMode/Services/UiSoundSynth.cs), outro aplicativo da equipe, sob a +GNU Affero General Public License v3.0, a mesma licença deste projeto: senoide com uma oitava baixinha, ataque rápido e queda, +montados em código (nenhum arquivo de áudio é incluído). O toque de marcar é um quarto som no mesmo estilo, escrito aqui. +Nenhum som de terceiros, de biblioteca ou da Sony é usado: a interface do PS5 é apenas referência de sensação. O Windows e seus serviços não fazem parte deste código aberto. diff --git a/assets/sounds/LICENSE-Kenney-CC0.txt b/assets/sounds/LICENSE-Kenney-CC0.txt deleted file mode 100644 index 1fc5acf8..00000000 --- a/assets/sounds/LICENSE-Kenney-CC0.txt +++ /dev/null @@ -1,22 +0,0 @@ - - - Interface Sounds (1.0) - - Created/distributed by Kenney (www.kenney.nl) - Creation date: 11-02-2020 - - ------------------------------ - - License: (Creative Commons Zero, CC0) - http://creativecommons.org/publicdomain/zero/1.0/ - - This content is free to use in personal, educational and commercial projects. - Support us by crediting Kenney or www.kenney.nl (this is not mandatory) - - ------------------------------ - - Donate: http://support.kenney.nl - Patreon: http://patreon.com/kenney/ - - Follow on Twitter for updates: - http://twitter.com/KenneyNL \ No newline at end of file diff --git a/assets/sounds/LICENSE-Kenney-UI-Audio-CC0.txt b/assets/sounds/LICENSE-Kenney-UI-Audio-CC0.txt deleted file mode 100644 index 4ff46a65..00000000 --- a/assets/sounds/LICENSE-Kenney-UI-Audio-CC0.txt +++ /dev/null @@ -1,21 +0,0 @@ - - - UI SFX Set - - by Kenney Vleugels (Kenney.nl) - - ------------------------------ - - License (Creative Commons Zero, CC0) - http://creativecommons.org/publicdomain/zero/1.0/ - - You may use these assets in personal and commercial projects. - Credit (Kenney or www.kenney.nl) would be nice but is not mandatory. - - ------------------------------ - - Donate: http://support.kenney.nl - Request: http://request.kenney.nl - - Follow on Twitter for updates: - @KenneyNL \ No newline at end of file diff --git a/assets/sounds/back.wav b/assets/sounds/back.wav deleted file mode 100644 index 1722ea1b..00000000 Binary files a/assets/sounds/back.wav and /dev/null differ diff --git a/assets/sounds/confirm.wav b/assets/sounds/confirm.wav deleted file mode 100644 index 48ff6a94..00000000 Binary files a/assets/sounds/confirm.wav and /dev/null differ diff --git a/assets/sounds/move.wav b/assets/sounds/move.wav deleted file mode 100644 index 9a094e81..00000000 Binary files a/assets/sounds/move.wav and /dev/null differ diff --git a/assets/sounds/select.wav b/assets/sounds/select.wav deleted file mode 100644 index a024612a..00000000 Binary files a/assets/sounds/select.wav and /dev/null differ diff --git a/docs/GUIDE.md b/docs/GUIDE.md index f1158e38..0a4bd263 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -308,7 +308,7 @@ The `controlfs://` scheme is registered in Windows by the installer (and re-regi ## Controller sounds -Menu → Configurações → **Sons do controle** plays short, soft cues for what you do with the controller: a tick when focus moves, a rising note for confirm/open, a falling one for back and a small blip when you mark an item. They are **on by default at medium volume**; choose **desligados** (off), **baixo**, **médio**, **alto** or **máximo** (your choice is kept across restarts and updates). They play only for controller actions (never the keyboard), don't repeat faster than about 12 times a second when you hold the D-pad, use the default Windows audio output and never change a button or how navigation works. The four sounds are short CC0 cues from Kenney's "Interface Sounds" pack, embedded in the app (licence in THIRD_PARTY_NOTICES.md). +Menu → Configurações → **Sons do controle** plays short, soft cues for what you do with the controller: a tick when focus moves, a rising note for confirm/open, a falling one for back and a small blip when you mark an item. They are **on by default at medium volume**; choose **desligados** (off), **baixo**, **médio**, **alto** or **máximo** (your choice is kept across restarts and updates). They play only for controller actions (never the keyboard), don't repeat faster than about 12 times a second when you hold the D-pad, use the default Windows audio output and never change a button or how navigation works. The sounds are the same soft console-style blips as Console Mode, generated in code (credit in THIRD_PARTY_NOTICES.md). ## Screen readers diff --git a/docs/GUIDE.pt-BR.md b/docs/GUIDE.pt-BR.md index 782ad93d..844033cb 100644 --- a/docs/GUIDE.pt-BR.md +++ b/docs/GUIDE.pt-BR.md @@ -308,7 +308,7 @@ O esquema `controlfs://` é registrado no Windows pelo instalador (e de novo ao ## Sons do controle -Menu → Configurações → **Sons do controle** toca sons curtos e suaves para o que você faz com o controle: um tique ao mover o foco, uma nota que sobe ao confirmar/abrir, uma que desce ao voltar e um toque pequeno ao marcar um item. Vêm **ligados em volume médio**; escolha **desligados**, **baixo**, **médio**, **alto** ou **máximo** (a escolha fica salva ao reabrir e ao atualizar). Tocam só nas ações do controle (nunca do teclado), não repetem mais que umas 12 vezes por segundo ao segurar o direcional, usam a saída de áudio padrão do Windows e nunca mudam um botão nem a navegação. Os quatro sons são toques curtos CC0 do pacote "Interface Sounds" de Kenney, embutidos no app (licença em THIRD_PARTY_NOTICES.md). +Menu → Configurações → **Sons do controle** toca sons curtos e suaves para o que você faz com o controle: um tique ao mover o foco, uma nota que sobe ao confirmar/abrir, uma que desce ao voltar e um toque pequeno ao marcar um item. Vêm **ligados em volume médio**; escolha **desligados**, **baixo**, **médio**, **alto** ou **máximo** (a escolha fica salva ao reabrir e ao atualizar). Tocam só nas ações do controle (nunca do teclado), não repetem mais que umas 12 vezes por segundo ao segurar o direcional, usam a saída de áudio padrão do Windows e nunca mudam um botão nem a navegação. Os sons são os mesmos toques macios de console do Console Mode, gerados em código (crédito em THIRD_PARTY_NOTICES.md). ## Leitores de tela diff --git a/src/ControlFS.Core/Audio/SoundBank.cs b/src/ControlFS.Core/Audio/SoundBank.cs index 831033a4..9406d9c8 100644 --- a/src/ControlFS.Core/Audio/SoundBank.cs +++ b/src/ControlFS.Core/Audio/SoundBank.cs @@ -1,57 +1,72 @@ -using System.Reflection; - namespace ControlFS.Core.Audio; /// -/// Os sons do controle (#276, #287): quatro toques curtos do pacote "Interface Sounds" de Kenney (licença CC0, domínio público, -/// redistribuição livre; origem e licença em assets/sounds e THIRD_PARTY_NOTICES.md), já aparados, sem estalo e com a mesma energia -/// (tools/make-sounds.py). Ficam embutidos no assembly; aqui só se aplica o volume do usuário, sempre para baixo a partir do nível -/// mestre (curva perceptiva: 25% soa bem mais baixo que 100%). A interface do PS5 é só referência de sensação: nada da Sony foi usado. +/// Os sons do controle (#276, #287): os mesmos toques do Console Mode (https://github.com/lippdev/consolemode, AGPL-3.0, da mesma +/// equipe; UiSoundSynth), montados em código, então o app não leva nenhum arquivo de áudio: toques curtos e macios (uma +/// senoide com uma oitava baixinha, ataque rápido e queda) em volume baixo, na linha de um menu de console. Mover, confirmar (sobe) +/// e voltar (desce) são os do Console Mode; marcar é um quarto toque no mesmo estilo. A interface do PS5 é só referência de +/// sensação. Saem como WAV PCM de 16 bits, mono, 44,1 kHz, prontos para tocar da memória. /// public static class SoundBank { public const int SampleRate = 44100; - private const int HeaderBytes = 44; - private static readonly Dictionary Masters = []; + /// Uma nota: vai de From a To Hz durante Ms. + private readonly record struct Note(double From, double To, int Ms, double Gain); - private static string FileFor(SoundCue cue) => cue switch + private static Note[] Notes(SoundCue cue) => cue switch { - SoundCue.Move => "move", - SoundCue.Select => "select", - SoundCue.Confirm => "confirm", - _ => "back", + SoundCue.Move => [new(1150, 1000, 45, 0.30)], + SoundCue.Select => [new(880, 940, 55, 0.30)], + SoundCue.Confirm => [new(740, 740, 55, 0.32), new(1110, 1110, 95, 0.34)], + _ => [new(820, 820, 50, 0.30), new(560, 540, 85, 0.30)], }; - /// O WAV do som (PCM 16 bits, mono, 44,1 kHz) no volume de 1 a 100. + /// + /// O WAV do som no volume de 1 a 100. 50 ("médio") é o nível original do Console Mode; 100 é o dobro (nunca satura) e 25 a + /// metade, em curva linear. + /// public static byte[] Render(SoundCue cue, int volume) { - var master = Master(cue); - var wav = (byte[])master.Clone(); - var gain = Math.Pow(Math.Clamp(volume, 1, 100) / 100.0, 1.5); - for (var i = HeaderBytes; i + 1 < wav.Length; i += 2) + var scale = Math.Clamp(volume, 1, 100) / 50.0; + var samples = new List(); + double phase = 0; + foreach (var note in Notes(cue)) { - var sample = (short)Math.Round(BitConverter.ToInt16(master, i) * gain); - wav[i] = (byte)(sample & 0xFF); - wav[i + 1] = (byte)((sample >> 8) & 0xFF); + var count = SampleRate * note.Ms / 1000; + var attack = SampleRate * 3 / 1000; + var release = SampleRate * 6 / 1000; + for (var i = 0; i < count; i++) + { + var t = (double)i / count; + var frequency = note.From + ((note.To - note.From) * t); + phase += 2 * Math.PI * frequency / SampleRate; + var tone = Math.Sin(phase) + (0.25 * Math.Sin(2 * phase)); + var envelope = Math.Exp(-3.2 * t) + * Math.Min(1.0, (double)i / attack) + * Math.Min(1.0, (double)(count - i) / release); + samples.Add((float)(tone / 1.25 * note.Gain * envelope * scale)); + } } - return wav; - } - private static byte[] Master(SoundCue cue) - { - lock (Masters) - { - if (Masters.TryGetValue(cue, out var cached)) return cached; - var name = $"sounds/{FileFor(cue)}.wav"; - using var stream = typeof(SoundBank).Assembly.GetManifestResourceStream(name) - ?? throw new InvalidOperationException($"Som embutido ausente: {name}"); - using var memory = new MemoryStream(); - stream.CopyTo(memory); - var bytes = memory.ToArray(); - if (bytes.Length <= HeaderBytes || bytes[36] != 'd' || bytes[37] != 'a' || bytes[38] != 't' || bytes[39] != 'a') - throw new InvalidOperationException($"Som embutido inválido (esperado WAV PCM de 44 bytes de cabeçalho): {name}"); - return Masters[cue] = bytes; - } + var dataLength = samples.Count * 2; + using var stream = new MemoryStream(44 + dataLength); + using var writer = new BinaryWriter(stream); + writer.Write("RIFF"u8); + writer.Write(36 + dataLength); + writer.Write("WAVEfmt "u8); + writer.Write(16); // cabeçalho PCM + writer.Write((short)1); // PCM + writer.Write((short)1); // mono + writer.Write(SampleRate); + writer.Write(SampleRate * 2); // bytes por segundo + writer.Write((short)2); // alinhamento do bloco + writer.Write((short)16); // bits por amostra + writer.Write("data"u8); + writer.Write(dataLength); + foreach (var sample in samples) + writer.Write((short)Math.Round(Math.Clamp(sample, -1f, 1f) * short.MaxValue)); + writer.Flush(); + return stream.ToArray(); } } diff --git a/src/ControlFS.Core/ControlFS.Core.csproj b/src/ControlFS.Core/ControlFS.Core.csproj index 6e7ba01c..7cfc9188 100644 --- a/src/ControlFS.Core/ControlFS.Core.csproj +++ b/src/ControlFS.Core/ControlFS.Core.csproj @@ -7,8 +7,4 @@ - - - - diff --git a/src/ControlFS.Infrastructure.Windows/Shell/WindowsSoundPlayer.cs b/src/ControlFS.Infrastructure.Windows/Shell/WindowsSoundPlayer.cs index 1840aa68..b716f961 100644 --- a/src/ControlFS.Infrastructure.Windows/Shell/WindowsSoundPlayer.cs +++ b/src/ControlFS.Infrastructure.Windows/Shell/WindowsSoundPlayer.cs @@ -6,7 +6,7 @@ namespace ControlFS.Infrastructure.Windows.Shell; /// /// Toca os sons do controle (#276) pelo dispositivo de áudio padrão do Windows (winmm PlaySound, da memória e assíncrono: -/// volta na hora e um som novo substitui o anterior). Cada som (WAV embutido) é preparado uma vez por volume e fica na memória +/// volta na hora e um som novo substitui o anterior). Cada som (montado em código) é preparado uma vez por volume e fica na memória /// nativa enquanto o app roda. Sem dispositivo de som, PlaySound só devolve falso: nada quebra. /// public sealed partial class WindowsSoundPlayer : ISoundPlayer, IDisposable diff --git a/tests/ControlFS.UnitTests/Core/ControllerSoundsTests.cs b/tests/ControlFS.UnitTests/Core/ControllerSoundsTests.cs index da78fd85..b2265ef9 100644 --- a/tests/ControlFS.UnitTests/Core/ControllerSoundsTests.cs +++ b/tests/ControlFS.UnitTests/Core/ControllerSoundsTests.cs @@ -88,23 +88,23 @@ public void Synthesized_cues_are_short_valid_wav_files_quieter_at_lower_volume_a Assert.True(Peak(wav) > 500, $"{cue}: sem sinal"); } Assert.True(Peak(SoundBank.Render(SoundCue.Confirm, 100)) > Peak(SoundBank.Render(SoundCue.Confirm, 25)) * 3); - Assert.InRange(Peak(SoundBank.Render(SoundCue.Confirm, 100)), 1, (int)(short.MaxValue * 0.6)); // discreto mesmo no máximo + Assert.InRange(Peak(SoundBank.Render(SoundCue.Confirm, 100)), 1, (int)(short.MaxValue * 0.8)); // discreto mesmo no máximo Assert.Equal(4, bytes.Values.Select(Convert.ToBase64String).Distinct().Count()); } [Fact] - public void Every_cue_has_the_same_loudness_and_starts_and_ends_in_silence() + public void Every_cue_starts_and_ends_in_silence_stays_soft_and_the_volume_setting_scales_it() { - var levels = Enum.GetValues().ToDictionary(c => c, c => Rms(SoundBank.Render(c, 60))); - Assert.InRange(levels.Values.Max() / levels.Values.Min(), 1.0, 1.25); // dentro de ~2 dB: nenhum som "grita" foreach (var cue in Enum.GetValues()) { var wav = SoundBank.Render(cue, 100); var peak = Peak(wav); - Assert.True(Math.Abs((int)BitConverter.ToInt16(wav, 44)) < peak * 0.02, $"{cue}: começa fora do silêncio"); - Assert.True(Math.Abs((int)BitConverter.ToInt16(wav, wav.Length - 2)) < peak * 0.02, $"{cue}: termina fora do silêncio"); + Assert.True(Math.Abs((int)BitConverter.ToInt16(wav, 44)) < peak * 0.05, $"{cue}: começa fora do silêncio"); + Assert.True(Math.Abs((int)BitConverter.ToInt16(wav, wav.Length - 2)) < peak * 0.05, $"{cue}: termina fora do silêncio"); + Assert.True(Rms(wav) < 0.25, $"{cue}: alto demais"); } - Assert.True(Rms(SoundBank.Render(SoundCue.Move, 100)) > Rms(SoundBank.Render(SoundCue.Move, 25)) * 4); // 25% soa bem mais baixo + Assert.InRange(Rms(SoundBank.Render(SoundCue.Move, 100)) / Rms(SoundBank.Render(SoundCue.Move, 50)), 1.95, 2.05); // médio = o nível do Console Mode; máximo = o dobro + Assert.True(Rms(SoundBank.Render(SoundCue.Move, 100)) > Rms(SoundBank.Render(SoundCue.Move, 25)) * 3); } private static double Rms(byte[] wav) diff --git a/tools/make-sounds.py b/tools/make-sounds.py deleted file mode 100644 index 449af865..00000000 --- a/tools/make-sounds.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -""" -Gera assets/sounds/*.wav (os sons do controle, #276/#287) a partir de dois pacotes de Kenney em CC0: "Interface Sounds" e "UI Audio". - -Só é preciso rodar de novo para trocar um som; o app usa os WAV já prontos. Requer `pip install soundfile numpy`. -Uso: python tools/make-sounds.py