Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 5 additions & 8 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
22 changes: 0 additions & 22 deletions assets/sounds/LICENSE-Kenney-CC0.txt

This file was deleted.

21 changes: 0 additions & 21 deletions assets/sounds/LICENSE-Kenney-UI-Audio-CC0.txt

This file was deleted.

Binary file removed assets/sounds/back.wav
Binary file not shown.
Binary file removed assets/sounds/confirm.wav
Binary file not shown.
Binary file removed assets/sounds/move.wav
Binary file not shown.
Binary file removed assets/sounds/select.wav
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/GUIDE.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
91 changes: 53 additions & 38 deletions src/ControlFS.Core/Audio/SoundBank.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,72 @@
using System.Reflection;

namespace ControlFS.Core.Audio;

/// <summary>
/// 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; <c>UiSoundSynth</c>), 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.
/// </summary>
public static class SoundBank
{
public const int SampleRate = 44100;
private const int HeaderBytes = 44;

private static readonly Dictionary<SoundCue, byte[]> Masters = [];
/// <summary>Uma nota: vai de <c>From</c> a <c>To</c> Hz durante <c>Ms</c>.</summary>
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)],
};

/// <summary>O WAV do som (PCM 16 bits, mono, 44,1 kHz) no volume de 1 a 100.</summary>
/// <summary>
/// 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.
/// </summary>
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<float>();
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();
}
}
4 changes: 0 additions & 4 deletions src/ControlFS.Core/ControlFS.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,4 @@
<ItemGroup>
<InternalsVisibleTo Include="ControlFS.UnitTests" />
</ItemGroup>
<ItemGroup>
<!-- Sons do controle (#287): quatro WAV curtos (Kenney, CC0), embutidos para tocar da memória. -->
<EmbeddedResource Include="..\..\assets\sounds\*.wav" LogicalName="sounds/%(Filename)%(Extension)" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace ControlFS.Infrastructure.Windows.Shell;

/// <summary>
/// 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.
/// </summary>
public sealed partial class WindowsSoundPlayer : ISoundPlayer, IDisposable
Expand Down
14 changes: 7 additions & 7 deletions tests/ControlFS.UnitTests/Core/ControllerSoundsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SoundCue>().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<SoundCue>())
{
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)
Expand Down
65 changes: 0 additions & 65 deletions tools/make-sounds.py

This file was deleted.

Loading