File: playwave/go.mod 1 module playwave 2 3 go 1.18 4 5 require github.com/hajimehoshi/oto/v2 v2.4.0 6 7 require ( 8 github.com/ebitengine/purego v0.3.0 // indirect 9 golang.org/x/sys v0.3.0 // indirect 10 ) File: playwave/go.sum 1 github.com/ebitengine/purego v0.3.0 h1:BDv9pD98k6AuGNQf3IF41dDppGBOe0F4AofvhFtBXF4= 2 github.com/ebitengine/purego v0.3.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 3 github.com/hajimehoshi/oto/v2 v2.4.0 h1:2A8QvGJZ7nXwcfIIthaqWdzDn9Ul/er6oASiKcsfiLg= 4 github.com/hajimehoshi/oto/v2 v2.4.0/go.mod h1:74bRBgfJaEDpP3NyVyHIYBJE4DgzJ2IP5l/st5qcJog= 5 golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 6 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= File: playwave/info.txt 1 playwave 2 3 Plays wave-audio data from standard input. File: playwave/logo.ico <BINARY> File: playwave/logo.png <BINARY> File: playwave/main.go 1 package main 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "fmt" 7 "io" 8 "os" 9 "time" 10 11 _ "embed" 12 13 "github.com/hajimehoshi/oto/v2" 14 ) 15 16 //go:embed info.txt 17 var info string 18 19 func main() { 20 // handle help option 21 if len(os.Args) == 2 { 22 switch os.Args[1] { 23 case "-h", "-help", "--help": 24 fmt.Fprintln(os.Stderr, info) 25 return 26 } 27 } 28 29 // no files given, so play from standard input 30 if len(os.Args) == 1 { 31 r, err := adaptReader(os.Stdin) 32 if err != nil { 33 fmt.Fprintln(os.Stderr, err.Error()) 34 os.Exit(1) 35 } 36 if err := playReader(r); err != nil { 37 fmt.Fprintln(os.Stderr, err.Error()) 38 os.Exit(1) 39 } 40 return 41 } 42 43 // check if all sound files exist and are supported: this helps prevent 44 // bad surprises when trying long-lasting playbacks with multiple files 45 errors := 0 46 for _, arg := range os.Args[1:] { 47 if err := checkFile(arg); err != nil { 48 fmt.Fprintln(os.Stderr, err.Error()) 49 errors++ 50 } 51 } 52 53 // quit, so user can retry with existing files next time 54 if errors > 0 { 55 os.Exit(1) 56 } 57 58 // play all files given 59 for _, arg := range os.Args[1:] { 60 fmt.Fprintf(os.Stdout, "playing\t%s\n", arg) 61 if err := runFile(arg); err != nil { 62 fmt.Fprintln(os.Stderr, err.Error()) 63 errors++ 64 } 65 } 66 } 67 68 // checkFile ensures the filename given works 69 func checkFile(fname string) error { 70 // ensure file exists 71 f, err := os.Open(fname) 72 if err != nil { 73 return err 74 } 75 defer f.Close() 76 77 r, err := adaptReader(f) 78 if err != nil { 79 return err 80 } 81 82 // ensure file is a supported wave sound 83 _, err = readWaveInfo(r) 84 return err 85 } 86 87 // adaptReader auto-detects/decodes base64 input, or keeps a reader as given 88 func adaptReader(r io.Reader) (io.Reader, error) { 89 var start [24]byte 90 n, err := r.Read(start[:]) 91 if err != nil { 92 return r, err 93 } 94 95 base64Prefix := []byte("data:audio/x-wav;base64,") 96 if bytes.HasPrefix(start[:n], base64Prefix) { 97 return base64.NewDecoder(base64.StdEncoding, r), nil 98 } 99 r = io.MultiReader(bytes.NewReader(start[:n]), r) 100 return r, nil 101 } 102 103 // playReader does what is says 104 func playReader(r io.Reader) error { 105 // f, err := os.Create("playwave.prof") 106 // if err != nil { 107 // return err 108 // } 109 // defer f.Close() 110 // pprof.StartCPUProfile(f) 111 // defer pprof.StopCPUProfile() 112 113 ctx, info, err := preparePlayback(r) 114 if err != nil { 115 return err 116 } 117 118 // a few rare wave files have image metadata right after the 119 // sound payload: such data may sound awful when mistakenly 120 // played, so avoid the chance of that ever happening 121 lim := io.LimitReader(r, int64(info.DataLength)) 122 123 pl := ctx.NewPlayer(lim) 124 defer pl.Close() 125 pl.Play() 126 return wait(pl) 127 } 128 129 // runFile allows running a file, even multiple times; in that case, 130 // the aim is to provide gapless sound loops by reusing an existing 131 // hook to the sound-system and merely seeking an already-open file 132 func runFile(fname string) error { 133 f, err := os.Open(fname) 134 if err != nil { 135 return err 136 } 137 defer f.Close() 138 return playReader(f) 139 } 140 141 // preparePlayback connects to the sound system, waiting until it's ready to 142 // play; the handle returned allows reuse, such as when a sound file needs 143 // playing more than once back-to-back 144 func preparePlayback(r io.Reader) (*oto.Context, waveInfo, error) { 145 info, err := readWaveInfo(r) 146 if err != nil { 147 return nil, info, err 148 } 149 150 ch := int(info.Channels) 151 sr := int(info.SampleRate) 152 byteDepth := int(info.BitsPerSample / 8) 153 154 ctx, ready, err := oto.NewContext(sr, ch, byteDepth) 155 if err != nil { 156 return ctx, info, err 157 } 158 // wait until sound destination is ready; the channel is auto-closed 159 <-ready 160 161 return ctx, info, nil 162 } 163 164 // wait hangs until sound data are over 165 func wait(pl oto.Player) error { 166 for pl.IsPlaying() { 167 // avoid hogging a core with a busy-loop 168 time.Sleep(15 * time.Millisecond) 169 } 170 return pl.Err() 171 } File: playwave/wav.go 1 package main 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "errors" 7 "io" 8 "math" 9 ) 10 11 // http://www.topherlee.com/software/pcm-tut-wavformat.html 12 13 var ( 14 errNotPCM = errors.New("RIFF/wave data aren't PCM-encoded") 15 errInvalidWave = errors.New("invalid RIFF/wave data") 16 errNotEnoughData = errors.New("unexpected end of data") 17 ) 18 19 // waveInfo is a summary of PCM-wave settings used to start playback 20 type waveInfo struct { 21 // Channels is the sound-channel count, usually 1 (mono) or 2 (stereo) 22 Channels uint16 23 24 // SampleRate is usually either 44100 or 48000 25 SampleRate uint32 26 27 // BytesPerSecond is what is says 28 BytesPerSecond uint32 29 30 // BitsPerSample should always be a multiple of 8 31 BitsPerSample uint16 32 33 // DataLength is the byte-count for the actual sound data 34 DataLength uint32 35 } 36 37 // Duration returns the (declared) sound-data duration in seconds. 38 func (info waveInfo) Duration(filesize uint32) (seconds float64) { 39 n := math.Max(float64(filesize-info.DataLength), float64(info.DataLength)) 40 seconds = n / float64(info.BytesPerSecond) 41 return seconds 42 } 43 44 // riffHeader is the very beginning of a valid wav file 45 type riffHeader struct { 46 Format [4]byte // "RIFF" 47 Size uint32 48 Type [4]byte // "WAVE" 49 Chunk [4]byte // "fmt " 50 } 51 52 // isValid checks if the header, which is the very beginning of valid wav data, 53 // has all the right format identifiers 54 func (h riffHeader) isValid() bool { 55 if !bytes.Equal(h.Format[:], []byte{'R', 'I', 'F', 'F'}) { 56 return false 57 } 58 if !bytes.Equal(h.Type[:], []byte{'W', 'A', 'V', 'E'}) { 59 return false 60 } 61 if !bytes.Equal(h.Chunk[:], []byte{'f', 'm', 't', ' '}) { 62 return false 63 } 64 return true 65 } 66 67 // SoundFormatPCM is the only wav data stream this app supports, and is an 68 // uncompressed digitized sequence of pulses 69 const SoundFormatPCM = 1 70 71 // riffWave32Info has the bytes right after a wav-file header 72 type riffWave32Info struct { 73 // FormatLength is how many bytes the format description takes 74 FormatLength uint32 75 76 // Format is only valid as 1, which means pulse code modulation (PCM) 77 Format uint16 78 79 // Channels is the sound-channel count, usually 1 (mono) or 2 (stereo) 80 Channels uint16 81 82 // SampleRate is usually either 44100 or 48000 83 SampleRate uint32 84 85 // BytesPerSecond is what it says 86 BytesPerSecond uint32 87 88 // HardToName isn't used in this app: maybe it's a padding count 89 HardToName uint16 90 91 // BitsPerSample should always be a multiple of 8 92 BitsPerSample uint16 93 } 94 95 // riffChunkHeader has the metadata at the start of each RIFF-format chunk 96 type riffChunkHeader struct { 97 // Data is the ASCII tag for the chunk type 98 Data [4]byte 99 100 // DataLength is the byte-count for the chunk which follows 101 DataLength uint32 102 } 103 104 // isOnData checks if the chunk header ends with the beginning of the sound 105 // data, meaning the reader which read it is ready to use for the playback 106 func (info riffChunkHeader) isOnData() bool { 107 return bytes.Equal(info.Data[:], []byte{'d', 'a', 't', 'a'}) 108 } 109 110 // readWaveInfo is the constructor for the riffWave32Info type 111 func readWaveInfo(r io.Reader) (waveInfo, error) { 112 info, datalen, err := rawReadWaveInfo(r) 113 res := waveInfo{ 114 Channels: info.Channels, 115 SampleRate: info.SampleRate, 116 BytesPerSecond: info.BytesPerSecond, 117 BitsPerSample: info.BitsPerSample, 118 DataLength: datalen, 119 } 120 121 if err == io.EOF { 122 return res, errNotEnoughData 123 } 124 return res, err 125 } 126 127 // rawReadWaveInfo is what func readInfo uses internally; avoid calling it, and 128 // use func readWaveInfo directly instead 129 func rawReadWaveInfo(r io.Reader) (riffWave32Info, uint32, error) { 130 var h riffHeader 131 err := binary.Read(r, binary.LittleEndian, &h) 132 if err != nil { 133 return riffWave32Info{}, 0, err 134 } 135 if !h.isValid() { 136 return riffWave32Info{}, 0, errInvalidWave 137 } 138 139 var info riffWave32Info 140 err = binary.Read(r, binary.LittleEndian, &info) 141 if err != nil { 142 return info, 0, err 143 } 144 if info.Format != SoundFormatPCM { 145 return info, 0, errNotPCM 146 } 147 148 var chunkhdr riffChunkHeader 149 err = binary.Read(r, binary.LittleEndian, &chunkhdr) 150 if err != nil { 151 return info, 0, err 152 } 153 154 err = seekData(r, &chunkhdr) 155 if err != nil { 156 return info, 0, err 157 } 158 159 if !chunkhdr.isOnData() || info.BitsPerSample%8 != 0 { 160 return info, chunkhdr.DataLength, errInvalidWave 161 } 162 return info, chunkhdr.DataLength, nil 163 } 164 165 // seekData ensures the reader is positioned right after the sound-data chunk 166 // has begun, if that's possible; the riff-info given to may be updated to 167 // have the right/valid info 168 func seekData(r io.Reader, info *riffChunkHeader) error { 169 for !info.isOnData() { 170 err := skipBytes(r, int64(info.DataLength)) 171 if err != nil { 172 return err 173 } 174 175 var data [4]byte 176 var datalen uint32 177 err = binary.Read(r, binary.LittleEndian, &data) 178 if err != nil { 179 return err 180 } 181 err = binary.Read(r, binary.LittleEndian, &datalen) 182 if err != nil { 183 return err 184 } 185 186 info.Data = data 187 info.DataLength = datalen 188 } 189 190 return nil 191 } 192 193 // skipBytes does as it says, unless fewer bytes are skipped than asked for, 194 // due to a short stream 195 func skipBytes(r io.Reader, n int64) error { 196 skip := n 197 var dummy [64]byte 198 199 for skip > int64(len(dummy)) { 200 n, err := r.Read(dummy[:]) 201 if n < len(dummy) || err != nil { 202 return err 203 } 204 skip -= int64(len(dummy)) 205 } 206 207 if skip > 0 { 208 _, err := r.Read(dummy[:skip]) 209 return err 210 } 211 return nil 212 } File: playwave/winapp_amd64.syso <BINARY> File: playwave/winapp.rc 1 // https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource 2 // windres -o winapp_amd64.syso winapp.rc 3 4 IDI_ICON1 ICON "logo.ico"