File: ./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: ./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: ./info.txt
   1 playwave
   2 
   3 Plays wave-audio data from standard input.

     File: ./logo.ico   <BINARY>

     File: ./logo.png   <BINARY>

     File: ./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: ./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: ./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"

     File: ./winapp_amd64.syso   <BINARY>