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") altvoice := flag.Bool("a", false, "Use alternate (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 altvoice flag prefix := "sam" if *altvoice { prefix = "altsam" } // 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 }