// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package sound import ( "bytes" "encoding/binary" "math" "strconv" "strings" ) const progressCuePrefix = "generated-progress-tone:" func ProgressCueName(percentage float64) string { return progressCuePrefix + strconv.FormatFloat(percentage, 'g', -1, 64) } func progress_percentage(cue string) (float64, bool) { if !strings.HasPrefix(cue, progressCuePrefix) { return 0, false } percentage, err := strconv.ParseFloat(strings.TrimPrefix(cue, progressCuePrefix), 64) return percentage, err == nil && !math.IsNaN(percentage) && !math.IsInf(percentage, 0) && percentage >= 0 && percentage <= 100 } // Match NVDA's default ProgressBar event_valueChange and generateBeep: // 110 * 2^(percentage/25) Hz, 40 ms padded to the next integer cycle, // 44100 Hz stereo PCM, and a sine clipped after multiplying by two. // NVDA passes frequency through a C float and defaults each channel to 50%. // Reference: nvaccess/nvda a4a89720ccbf3973a0e1e50a76dd8bff38db3d15, // source/NVDAObjects/behaviors.py and nvdaHelper/local/beeps.cpp. func progress_tone_wav(percentage float64) []byte { const sampleRate = 44100 frequency := float64(float32(110 * math.Pow(2, percentage/25))) // sampleRate/hz is evaluated as a float in NVDA's C++ generator. cycleLength := float64(float32(sampleRate) / float32(frequency)) samplesPerCycle := int(cycleLength) sampleCount := int(0.040 / (1.0 / sampleRate)) sampleCount += samplesPerCycle - sampleCount%samplesPerCycle dataSize := sampleCount * 4 buffer := bytes.NewBuffer(make([]byte, 0, 44+dataSize)) buffer.WriteString("RIFF") _ = binary.Write(buffer, binary.LittleEndian, uint32(36+dataSize)) buffer.WriteString("WAVEfmt ") _ = binary.Write(buffer, binary.LittleEndian, uint32(16)) _ = binary.Write(buffer, binary.LittleEndian, uint16(1)) _ = binary.Write(buffer, binary.LittleEndian, uint16(2)) _ = binary.Write(buffer, binary.LittleEndian, uint32(sampleRate)) _ = binary.Write(buffer, binary.LittleEndian, uint32(sampleRate*4)) _ = binary.Write(buffer, binary.LittleEndian, uint16(4)) _ = binary.Write(buffer, binary.LittleEndian, uint16(16)) buffer.WriteString("data") _ = binary.Write(buffer, binary.LittleEndian, uint32(dataSize)) sinFrequency := 2 * math.Pi / cycleLength for sample := 0; sample < sampleCount; sample++ { value := int16(min(1.0, max(-1.0, math.Sin(float64(sample%sampleRate)*sinFrequency)*2)) * 7000) _ = binary.Write(buffer, binary.LittleEndian, value) _ = binary.Write(buffer, binary.LittleEndian, value) } return buffer.Bytes() }