You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
numberstation/numberstation.go

204 lines
5.4 KiB

package main
import (
"bytes"
"embed"
"fmt"
"strings"
"time"
"flag"
"bufio"
"os"
"github.com/chzyer/readline"
"github.com/gopxl/beep"
"github.com/gopxl/beep/speaker"
"github.com/gopxl/beep/wav"
)
//go:embed sound/*
var soundFS embed.FS
const (
pauseBetweenCharacters = 400 * time.Millisecond
longPause = 1400 * time.Millisecond
)
func main() {
// Define and parse the flags
repeat := flag.Bool("r", false, "Repeat each section separated by space, '-', '_', or a new line")
low := flag.Bool("l", false, "Use lower pitched SAM sound bank")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s [options] <<< \"String\"\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s [options] < file.txt\n", os.Args[0])
fmt.Fprintf(os.Stderr, " echo \"string\" | %s [options]\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nDescription:\n")
fmt.Fprintf(os.Stderr, " A basic numbers station program that will read out loud the text of a provided string, one character at a time. It will\n")
fmt.Fprintf(os.Stderr, " pause on line breaks, spaces, -, and _, and will repeat each section delimited by one of these characters if -r is used.\n")
fmt.Fprintf(os.Stderr, " It also allows for an interactive mode if nothing is provided on stdin.\n")
fmt.Fprintf(os.Stderr, "\nOptions:\n")
flag.PrintDefaults() // Print the default flag help information
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " %s <<< \"22101 12102 11210 20302 22072 122\"\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -r < file.txt\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s\n", os.Args[0])
}
flag.Parse()
// Select the correct file prefix based on the low flag
prefix := "sam"
if *low {
prefix = "losam"
}
// Create a map to store sound data for each character
soundMap := make(map[rune][]byte)
// Load all sound files into the map
for _, c := range "abcdefghijklmnopqrstuvwxyz0123456789" {
filename := fmt.Sprintf("sound/%s%c.wav", prefix, c)
data, err := soundFS.ReadFile(filename)
if err == nil {
soundMap[c] = data
} else {
fmt.Printf("Failed to load sound for '%c': %v\n", c, err)
}
}
var input string
if *repeat {
fmt.Println("Repeat mode is ON")
}
// Check if input is being redirected from a file
if isInputFromPipe() {
// Read from stdin (file input)
scanner := bufio.NewScanner(os.Stdin)
var inputBuilder strings.Builder
for scanner.Scan() {
line := scanner.Text()
inputBuilder.WriteString(line + "\n")
}
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading input: %s\n", err)
return
}
input = inputBuilder.String()
// Display the input, filtered to uppercase, before playback
fmt.Println("Input:\n")
fmt.Println(strings.ToUpper(input))
} else {
// Use readline for interactive input
rl, err := readline.NewEx(&readline.Config{
Prompt: "> ", // Use a simple prompt for each line input
InterruptPrompt: "^C",
EOFPrompt: "exit",
FuncFilterInputRune: uppercaseFilter, // Filter to show uppercase
})
if err != nil {
fmt.Printf("Error initializing readline: %s\n", err)
return
}
defer rl.Close()
// Inform the user about how to enter input
fmt.Println("Enter text to be read (A-Z and 0-9 only). Press Enter on a blank line to process:")
var inputBuilder strings.Builder
for {
line, err := rl.Readline()
if err != nil {
if err.Error() == "EOF" {
fmt.Println("Exiting.")
return
}
fmt.Printf("Error reading input: %s\n", err)
return
}
// Check if the user entered a blank line to finish input
if strings.TrimSpace(line) == "" {
break
}
// Append the line to the input builder
inputBuilder.WriteString(line + "\n")
}
input = inputBuilder.String()
}
// Process the input
input = strings.ToLower(strings.TrimSpace(input))
sections := strings.FieldsFunc(input, func(r rune) bool {
return r == ' ' || r == '-' || r == '_' || r == '\n' || r == '\r'
})
fmt.Print("Beginning playback... ")
for _, section := range sections {
numRepeats := 1
if *repeat {
numRepeats = 2
}
// Read and optionally repeat each section
for i := 0; i < numRepeats; i++ {
for _, c := range section {
if soundData, ok := soundMap[c]; ok {
playSound(soundData)
} else {
fmt.Printf("Invalid character (skipping): %c\n", c)
}
time.Sleep(pauseBetweenCharacters)
}
// Pause between repeats if repeating
if *repeat && i == 0 {
time.Sleep(longPause)
}
}
// Long pause between sections
time.Sleep(longPause)
}
fmt.Println("Playback finished. Exiting.")
}
// Function to check if input is from a pipe or file
func isInputFromPipe() bool {
fileInfo, _ := os.Stdin.Stat()
return (fileInfo.Mode() & os.ModeCharDevice) == 0
}
// Function to play sound
func playSound(soundData []byte) {
streamer, format, err := wav.Decode(bytes.NewReader(soundData))
if err != nil {
fmt.Printf("Could not decode sound data: %s\n", err)
return
}
defer streamer.Close()
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
done := make(chan bool)
speaker.Play(beep.Seq(streamer, beep.Callback(func() {
done <- true
})))
<-done
}
// Filter function to convert all typed input to uppercase for display
func uppercaseFilter(r rune) (rune, bool) {
if r >= 'a' && r <= 'z' {
return r - ('a' - 'A'), true // Convert to uppercase
}
return r, true // Keep other characters unchanged
}