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
1 change: 1 addition & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
### Fixes
- **Xbox controllers no longer ask to be configured** (#284): when an Xbox controller also showed up as a "joystick without a profile" (a second instance, a receiver or a generic driver), the app said "Controle sem perfil… hold any button" and opened the mapping wizard even though the controller already worked. Xbox controllers (Microsoft vendor, or "Xbox"/"XInput" in the name) now never trigger the notice or the wizard; real generic joysticks can still be configured.
### Changes
- **Controller sounds on by default and nicer** (#286, #287): a fresh install (or no saved choice) now has the sounds on at medium volume; anyone who picked "desligados" (off) stays silent, and the choice is kept across restarts and updates. The four cues were redone: glassy timbre (a fundamental with soft harmonics), rounded attack, gentle decay, all at the same loudness and without clicks; "baixo" (low) is now really low. They are still synthesized in the app (no third-party files; origin and license in THIRD_PARTY_NOTICES.md).
- **Git status left Configurações** (#284): the option, the "Status do Git: ligado/desligado" notice, the branch in the header and the "Git: modificado" marks are gone from the interface.
### What's new
- **Checks and updates by itself on open**: ControlFS now looks for a new version **every time it opens** (before, at most once a day) and, in the installed build, if a verified version exists (signed manifest, SHA-256 and size) and you haven't started using the app and nothing is running, it **downloads, installs and reopens already updated**, without asking. If you already started using it, the usual notice stays (Install and restart / Later). Turn it off in Menu → Configurações → Atualizações → **Atualizar sozinho ao abrir**. The portable build only notifies. It never runs in CI or with `--no-auto-update`.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
### Correções
- **Controle Xbox não pede mais para ser configurado** (#284): quando um controle Xbox aparecia também como "joystick sem perfil" (segunda instância, receptor ou driver genérico), o app avisava "Controle sem perfil… segure qualquer botão" e abria o assistente de configuração, mesmo com o controle já funcionando. Agora controles Xbox (fabricante Microsoft, ou "Xbox"/"XInput" no nome) nunca disparam o aviso nem o assistente; joysticks genéricos de verdade continuam podendo ser configurados.
### Mudanças
- **Sons do controle ligados por padrão e mais bonitos** (#286, #287): numa instalação nova (ou sem escolha salva) os sons já vêm ligados em volume médio; quem escolheu "desligados" continua sem som, e a escolha fica salva ao reabrir e ao atualizar. Os quatro sons foram refeitos: timbre de vidro (fundamental com harmônicos suaves), ataque arredondado, queda suave, todos com o mesmo volume e sem estalos; o volume "baixo" agora é realmente baixo. Continuam sintetizados no app (sem arquivos de terceiros; a origem e a licença estão em THIRD_PARTY_NOTICES.md).
- **Status do Git saiu das Configurações** (#284): a opção, o aviso "Status do Git: ligado/desligado", o ramo no cabeçalho e as marcas "Git: modificado" deixaram de existir na interface.
### Novidades
- **Verifica e se atualiza sozinho ao abrir**: o ControlFS agora procura versão nova **a cada vez que abre** (antes, no máximo uma vez por dia) e, na versão instalada, se há uma versão verificada (manifesto assinado, SHA-256 e tamanho) e você ainda não começou a usar o app e nada está em andamento, **baixa, instala e reabre já atualizado**, sem perguntar. Se você já começou a usar, fica o aviso de sempre (Instalar e reiniciar / Depois). Desligue em Menu → Configurações → Atualizações → **Atualizar sozinho ao abrir**. A versão portátil só avisa. Nunca roda em CI nem com `--no-auto-update`.
Expand Down
8 changes: 8 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,12 @@ https://github.com/lippdev/consolemode) são os logos de outros aplicativos da e
mantenedor só para a tela "Mais da equipe". São marcas e artes dos respectivos projetos: NÃO estão cobertas pela
AGPL-3.0-only deste código e não podem ser reutilizadas sem permissão dos donos.

Sons do controle
------------------------------------------------------------------------------

Os toques de mover, confirmar, voltar e marcar (src/ControlFS.Core/Audio/ToneSynth.cs) são sintetizados em código neste
projeto: ondas senoidais com dois harmônicos e um envelope próprio. Nenhum arquivo de áudio, biblioteca de sons ou
amostra de terceiros é incluído, então não há licença de áudio externa: os sons valem sob a AGPL-3.0-only do projeto. A
interface do PS5 é apenas referência de sensação (sons curtos, suaves e limpos); nada da Sony foi copiado ou imitado nota por nota.

O Windows e seus serviços não fazem parte deste código aberto.
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. Choose **desligados** (off, the default), **baixo**, **médio**, **alto** or **máximo**. 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 generated inside the app (no audio files).
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 generated inside the app (no audio files).

## 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. Escolha **desligados** (o padrão), **baixo**, **médio**, **alto** ou **máximo**. 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 gerados dentro do app (sem arquivos de áudio).
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 gerados dentro do app (sem arquivos de áudio).

## Leitores de tela

Expand Down
5 changes: 5 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -861,3 +861,8 @@ Só a CI (WinRAR 7.23 pelo winget, arquivo criado pelo Rar.exe real e extraído
- [ ] Instalado numa versão antiga, com internet: abrir o app sem tocar em nada. Ele baixa a versão nova, fecha e reabre sozinho já atualizada (Menu → Sobre mostra a nova).
- [ ] Abrir e mexer no controle antes de terminar o download: aparece "Atualização pronta" (Instalar e reiniciar / Depois), sem reiniciar sozinho.
- [ ] Menu → Configurações → Atualizações → "Atualizar sozinho ao abrir: não": só o aviso. Com uma cópia em andamento nunca reinicia.

## Sons do controle: qualidade e padrão (#286, #287) — não validado em hardware
- [ ] Instalação nova (ou apagar `settings.json`): os sons já tocam ao navegar com o controle, sem ligar nada. Em Configurações → Sons do controle aparece "médio".
- [ ] Escolher "desligados", fechar e reabrir (e atualizar): continua mudo. Escolher "baixo": continua baixo.
- [ ] Ouvir os quatro sons (mover, confirmar, voltar, marcar) em fones e em alto-falante: limpos, sem estalo, do mesmo volume, distintos entre si; segurar o direcional não vira chiado; "baixo" é realmente baixo.
2 changes: 1 addition & 1 deletion src/ControlFS.App/Views/MainWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ internal MainWindow(string? dataDirectory)
if (dataDirectory is null) _soundPlayer = new WindowsSoundPlayer();
var clock = System.Diagnostics.Stopwatch.StartNew();
_input = new InputHost(_app, DispatcherQueue, _soundPlayer is null ? null
: new Core.Audio.ControllerSounds(_soundPlayer, () => _app.Settings.ControllerSoundVolume, () => clock.Elapsed));
: new Core.Audio.ControllerSounds(_soundPlayer, () => _app.Settings.EffectiveControllerSoundVolume, () => clock.Elapsed));
if (dataDirectory is null)
{
_background = new BackgroundMode(_app, DispatcherQueue, _drives);
Expand Down
2 changes: 1 addition & 1 deletion src/ControlFS.Application/AppController.Browser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ private List<MenuItem> SettingsItems()
ChoiceRow("Fluidez", Settings.SyncInputToDisplay, SmoothnessChoices, on => UpdateSettings(s => s with { SyncInputToDisplay = on }), ActionIcon.Settings, section: controls, placement: tile,
shortLabel: "Fluidez", context: settingsContext,
detail: "Máxima lê o controle ~125 vezes por segundo (o bastante para telas de 120 Hz). Economia gasta menos bateria em portáteis."),
ChoiceRow("Sons do controle", Settings.ControllerSoundVolume, SoundChoices, volume => UpdateSettings(s => s with { ControllerSoundVolume = volume }), ActionIcon.Audio, section: controls,
ChoiceRow("Sons do controle", NearestSoundChoice(Settings.EffectiveControllerSoundVolume), SoundChoices, volume => UpdateSettings(s => s with { ControllerSoundLevel = volume }), ActionIcon.Audio, section: controls,
placement: tile, shortLabel: "Sons", context: settingsContext,
detail: "Toques curtos ao mover o foco, confirmar, voltar e marcar, só com o controle (nunca com o teclado). Respeita a saída de áudio do Windows."),
new($"Leve em segundo plano: {OnOff(Settings.LightInBackground)}",
Expand Down
3 changes: 3 additions & 0 deletions src/ControlFS.Application/AppController.Choices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ private void ApplyChoice<T>(T value, Action<T> onPick)
new(100, "máximo", "O volume mais alto dos toques (ainda curtos e suaves)."),
];

/// <summary>A alternativa da lista mais próxima do volume salvo (o seletor marca uma delas).</summary>
private static int NearestSoundChoice(int volume) => SoundChoices.Select(c => c.Value).MinBy(v => Math.Abs(v - volume));

private void SetView(Core.Contracts.ViewMode view)
{
if (Settings.View == view) return;
Expand Down
69 changes: 46 additions & 23 deletions src/ControlFS.Core/Audio/ToneSynth.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,67 @@
namespace ControlFS.Core.Audio;

/// <summary>
/// Os sons do controle, sintetizados aqui (#276): tons senoidais curtos com ataque e queda suaves, sem nenhum arquivo de
/// terceiros (os sons da interface do PS5 são só uma referência de sensação, não foram copiados). Saem como WAV PCM de
/// 16 bits, mono, 44,1 kHz, prontos para tocar da memória.
/// Os sons do controle, sintetizados aqui (#276, #287): toques curtos de timbre "vidro" (fundamental com dois harmônicos
/// suaves), ataque arredondado e queda exponencial, sem nenhum arquivo de terceiros. A sensação discreta e limpa da interface
/// do PS5 é só referência de estilo; nada foi copiado. Saem como WAV PCM de 16 bits, mono, 44,1 kHz, prontos para tocar da memória.
/// Todos têm a mesma energia (RMS), então nenhum som "grita" mais que outro, e começam e terminam em silêncio (sem estalo).
/// </summary>
public static class ToneSynth
{
public const int SampleRate = 44100;

/// <summary>Cada som: notas (frequência em Hz, duração em ms), sem pausa entre elas.</summary>
private static (double Hz, double Ms)[] Notes(SoundCue cue) => cue switch
/// <summary>RMS de cada som no volume 100 (~ -21 dBFS): presente, mas nunca alto.</summary>
private const double TargetRms = 0.09;

/// <summary>Pico máximo (60% do fundo de escala) mesmo no volume 100: margem para o som nunca saturar.</summary>
private const double PeakCap = 0.6;

/// <summary>Uma nota: frequência, começo, duração total e constante de queda (tempo para cair a ~37%).</summary>
private readonly record struct Note(double Hz, double StartMs, double Ms, double DecayMs);

// Escala pentatônica maior (sem dissonância por mais que se repita): E5 659, G#5 831, B5 988, D6 1175.
private static Note[] Notes(SoundCue cue) => cue switch
{
SoundCue.Move => [(1320, 28)],
SoundCue.Select => [(880, 55)],
SoundCue.Confirm => [(660, 55), (990, 85)], // sobe
SoundCue.Back => [(660, 55), (440, 85)], // desce
_ => [(880, 40)],
SoundCue.Move => [new(988, 0, 45, 11)],
SoundCue.Select => [new(1319, 0, 70, 16)],
SoundCue.Confirm => [new(659, 0, 80, 22), new(988, 55, 130, 34)], // sobe
SoundCue.Back => [new(988, 0, 80, 22), new(659, 55, 130, 34)], // desce
_ => [new(988, 0, 45, 11)],
};

/// <summary>WAV do som no volume de 1 a 100 (o pico fica em até ~45% do máximo: discreto mesmo no volume 100).</summary>
/// <summary>WAV do som no volume de 1 a 100 (curva perceptiva: o volume baixo é mesmo baixo).</summary>
public static byte[] Render(SoundCue cue, int volume)
{
var gain = 0.45 * Math.Clamp(volume, 1, 100) / 100.0;
var samples = new List<short>();
foreach (var (hz, ms) in Notes(cue))
var notes = Notes(cue);
var length = (int)(SampleRate * notes.Max(n => n.StartMs + n.Ms) / 1000.0) + 1;
var mix = new double[length];
foreach (var note in notes)
{
var count = (int)(SampleRate * ms / 1000.0);
var fade = Math.Min(count / 2, (int)(SampleRate * 0.006)); // 6 ms de entrada e de saída: sem estalo
for (var i = 0; i < count; i++)
var start = (int)(SampleRate * note.StartMs / 1000.0);
var count = (int)(SampleRate * note.Ms / 1000.0);
var attack = Math.Max(1, (int)(SampleRate * 0.004)); // 4 ms: sem estalo
var release = Math.Max(1, (int)(SampleRate * 0.012)); // 12 ms finais até o silêncio exato
for (var i = 0; i < count && start + i < length; i++)
{
var envelope = i < fade ? (double)i / fade : i >= count - fade ? (double)(count - 1 - i) / fade : 1.0;
// Queda suave ao longo do som: o fim é mais fraco que o começo, como um toque.
var decay = 1.0 - (0.6 * i / count);
samples.Add((short)Math.Round(Math.Sin(2 * Math.PI * hz * i / SampleRate) * envelope * decay * gain * short.MaxValue));
var t = (double)i / SampleRate;
var phase = 2 * Math.PI * note.Hz * t;
var tone = Math.Sin(phase) + (0.22 * Math.Sin(2 * phase)) + (0.06 * Math.Sin(3 * phase));
var envelope = Math.Exp(-(i * 1000.0 / SampleRate) / note.DecayMs);
if (i < attack) envelope *= 0.5 - (0.5 * Math.Cos(Math.PI * i / attack));
if (i >= count - release) envelope *= 0.5 + (0.5 * Math.Cos(Math.PI * (i - (count - release)) / release));
mix[start + i] += tone * envelope;
}
}
// Mesma energia para todos os sons; o volume escolhido escala com curva 1,5 (25% soa bem mais baixo que 100%).
var rms = Math.Sqrt(mix.Sum(v => v * v) / mix.Length);
var level = Math.Pow(Math.Clamp(volume, 1, 100) / 100.0, 1.5);
var gain = rms > 0 ? TargetRms * level / rms : 0;
var peak = mix.Max(Math.Abs);
if (peak * gain > PeakCap) gain = PeakCap / peak;

using var memory = new MemoryStream();
using var writer = new BinaryWriter(memory);
var dataBytes = samples.Count * 2;
var dataBytes = length * 2;
writer.Write("RIFF"u8);
writer.Write(36 + dataBytes);
writer.Write("WAVEfmt "u8);
Expand All @@ -51,7 +74,7 @@ public static byte[] Render(SoundCue cue, int volume)
writer.Write((short)16);
writer.Write("data"u8);
writer.Write(dataBytes);
foreach (var sample in samples) writer.Write(sample);
foreach (var value in mix) writer.Write((short)Math.Round(Math.Clamp(value * gain, -1, 1) * short.MaxValue));
writer.Flush();
return memory.ToArray();
}
Expand Down
19 changes: 17 additions & 2 deletions src/ControlFS.Core/Contracts/ISettingsStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,27 @@ public sealed record AppSettings
/// </summary>
public bool GyroKeyboard { get; init; }

/// <summary>Volume padrão dos sons do controle (#286): ligados de fábrica, em volume médio.</summary>
public const int DefaultControllerSoundVolume = 50;

/// <summary>
/// Sons do controle (#276): volume de 0 a 100. 0 = desligado (padrão: o ControlFS não faz barulho sem você pedir). Os sons
/// só tocam nas ações vindas do controle, nunca do teclado.
/// Sons do controle (#276, #286): volume de 0 a 100 escolhido pelo usuário; 0 = desligados. Null = nunca escolheu: vale o
/// padrão (ligado, <see cref="DefaultControllerSoundVolume"/>). Os sons só tocam nas ações vindas do controle.
/// </summary>
public int? ControllerSoundLevel { get; init; }

/// <summary>
/// Só para ler preferências da 0.13 (campo antigo, padrão 0): um valor acima de 0 foi escolha do usuário e vale; 0 foi o
/// padrão de então (indistinguível de "desligei"), então vale o padrão novo. Quem desliga agora grava
/// <see cref="ControllerSoundLevel"/> = 0 e nunca mais é ligado de novo.
/// </summary>
public int ControllerSoundVolume { get; init; }

/// <summary>O volume que vale agora (0 = desligados).</summary>
[System.Text.Json.Serialization.JsonIgnore]
public int EffectiveControllerSoundVolume =>
Math.Clamp(ControllerSoundLevel ?? (ControllerSoundVolume > 0 ? ControllerSoundVolume : DefaultControllerSoundVolume), 0, 100);

/// <summary>Status do Git (#75): ramo e marcas de modificado/novo em pastas de repositórios. Desligado por padrão.</summary>
public bool ShowGitStatus { get; init; }

Expand Down
Loading
Loading