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