mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
184 lines
4.9 KiB
Go
184 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const twoPi = 2 * math.Pi
|
|
|
|
func main() {
|
|
var (
|
|
device = flag.String("device", "", "ALSA device (empty = default)")
|
|
rate = flag.Int("rate", 48000, "Sample rate in Hz")
|
|
channels = flag.Int("channels", 1, "Number of audio channels")
|
|
duration = flag.Duration("duration", 2*time.Second, "Total horn duration")
|
|
freqsRaw = flag.String("freqs", "440,550,660", "Comma-separated frequencies in Hz")
|
|
volume = flag.Float64("volume", 0.25, "Output volume 0.0-1.0")
|
|
attack = flag.Duration("attack", 20*time.Millisecond, "Attack time")
|
|
release = flag.Duration("release", 60*time.Millisecond, "Release time")
|
|
)
|
|
flag.Parse()
|
|
|
|
if *rate <= 0 {
|
|
log.Fatalf("rate must be > 0 (got %d)", *rate)
|
|
}
|
|
if *channels <= 0 {
|
|
log.Fatalf("channels must be > 0 (got %d)", *channels)
|
|
}
|
|
if *duration <= 0 {
|
|
log.Fatalf("duration must be > 0 (got %s)", *duration)
|
|
}
|
|
if *volume <= 0 || *volume > 1.0 {
|
|
log.Fatalf("volume must be within (0,1] (got %.3f)", *volume)
|
|
}
|
|
if *attack < 0 || *release < 0 {
|
|
log.Fatalf("attack/release must be >= 0")
|
|
}
|
|
|
|
freqs, err := parseFreqs(*freqsRaw)
|
|
if err != nil {
|
|
log.Fatalf("parse freqs: %v", err)
|
|
}
|
|
if len(freqs) == 0 {
|
|
log.Fatal("no frequencies provided")
|
|
}
|
|
|
|
if *attack+*release > *duration {
|
|
log.Fatalf("attack+release must be <= duration (%s + %s > %s)", *attack, *release, *duration)
|
|
}
|
|
|
|
args := []string{"-q", "-f", "S16_LE", "-c", fmt.Sprintf("%d", *channels), "-r", fmt.Sprintf("%d", *rate), "-t", "raw"}
|
|
if *device != "" {
|
|
args = append(args, "-D", *device)
|
|
}
|
|
cmd := exec.Command("aplay", args...)
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
log.Fatalf("aplay stdin: %v", err)
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
log.Fatalf("start aplay: %v", err)
|
|
}
|
|
|
|
writer := bufio.NewWriterSize(stdin, 32*1024)
|
|
if err := synthChord(writer, freqs, *rate, *channels, *duration, *volume, *attack, *release); err != nil {
|
|
_ = stdin.Close()
|
|
_ = cmd.Wait()
|
|
log.Fatalf("synth: %v", err)
|
|
}
|
|
if err := writer.Flush(); err != nil {
|
|
_ = stdin.Close()
|
|
_ = cmd.Wait()
|
|
log.Fatalf("flush: %v", err)
|
|
}
|
|
if err := stdin.Close(); err != nil {
|
|
_ = cmd.Wait()
|
|
log.Fatalf("close stdin: %v", err)
|
|
}
|
|
if err := cmd.Wait(); err != nil {
|
|
log.Fatalf("aplay failed: %v", err)
|
|
}
|
|
log.Print("Horn verification complete")
|
|
}
|
|
|
|
func parseFreqs(raw string) ([]float64, error) {
|
|
trimmed := strings.TrimSpace(raw)
|
|
if trimmed == "" {
|
|
return nil, nil
|
|
}
|
|
parts := strings.Split(trimmed, ",")
|
|
freqs := make([]float64, 0, len(parts))
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
value, err := strconv.ParseFloat(part, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid freq %q", part)
|
|
}
|
|
if value <= 0 {
|
|
return nil, fmt.Errorf("freq must be > 0 (got %.3f)", value)
|
|
}
|
|
freqs = append(freqs, value)
|
|
}
|
|
return freqs, nil
|
|
}
|
|
|
|
func synthChord(writer *bufio.Writer, freqs []float64, rate, channels int, duration time.Duration, volume float64, attack, release time.Duration) error {
|
|
totalFrames := int(float64(rate) * duration.Seconds())
|
|
if totalFrames <= 0 {
|
|
return fmt.Errorf("duration too short")
|
|
}
|
|
|
|
phase := make([]float64, len(freqs))
|
|
increment := make([]float64, len(freqs))
|
|
for i, f := range freqs {
|
|
increment[i] = twoPi * f / float64(rate)
|
|
}
|
|
|
|
attackFrames := int(float64(rate) * attack.Seconds())
|
|
releaseFrames := int(float64(rate) * release.Seconds())
|
|
steadyFrames := totalFrames - attackFrames - releaseFrames
|
|
|
|
framesPerChunk := 512
|
|
buf := make([]byte, framesPerChunk*channels*2)
|
|
sampleIndex := 0
|
|
scale := volume / float64(len(freqs))
|
|
|
|
for framesLeft := totalFrames; framesLeft > 0; {
|
|
framesNow := framesPerChunk
|
|
if framesLeft < framesNow {
|
|
framesNow = framesLeft
|
|
}
|
|
for i := 0; i < framesNow; i++ {
|
|
env := envelope(sampleIndex, attackFrames, steadyFrames, releaseFrames)
|
|
sample := 0.0
|
|
for j := range freqs {
|
|
sample += math.Sin(phase[j])
|
|
phase[j] += increment[j]
|
|
if phase[j] > twoPi {
|
|
phase[j] -= twoPi
|
|
}
|
|
}
|
|
sample *= scale * env
|
|
if sample > 1.0 {
|
|
sample = 1.0
|
|
} else if sample < -1.0 {
|
|
sample = -1.0
|
|
}
|
|
intSample := int16(sample * math.MaxInt16)
|
|
offset := i * channels * 2
|
|
for ch := 0; ch < channels; ch++ {
|
|
binary.LittleEndian.PutUint16(buf[offset+ch*2:], uint16(intSample))
|
|
}
|
|
sampleIndex++
|
|
}
|
|
if _, err := writer.Write(buf[:framesNow*channels*2]); err != nil {
|
|
return err
|
|
}
|
|
framesLeft -= framesNow
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func envelope(sampleIndex, attackFrames, steadyFrames, releaseFrames int) float64 {
|
|
if attackFrames > 0 && sampleIndex < attackFrames {
|
|
return float64(sampleIndex) / float64(attackFrames)
|
|
}
|
|
if releaseFrames > 0 && sampleIndex >= attackFrames+steadyFrames {
|
|
relIndex := sampleIndex - (attackFrames + steadyFrames)
|
|
return float64(releaseFrames-relIndex) / float64(releaseFrames)
|
|
}
|
|
return 1.0
|
|
}
|