Files

65 lines
2.6 KiB
Go

// 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()
}