| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- // Package wav reads the header of a WAV file: format, channels, sample rate,
- // bit depth and frame count. It does not decode audio.
- package wav
- import (
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "os"
- "time"
- )
- // Format codes from the fmt chunk.
- const (
- FormatPCM = 1
- FormatFloat = 3
- FormatExtensible = 0xFFFE
- )
- // Limits for a sample mpckit will assign (build plan 4.3 and section 7).
- const (
- MaxChannels = 2
- MaxDuration = 20 * time.Minute
- )
- // Info describes a WAV file.
- type Info struct {
- Format int // FormatPCM or FormatFloat (the sub-format of an extensible header)
- Extensible bool
- Channels int
- SampleRate int
- Bits int
- BlockAlign int
- Frames int64
- DataOffset int64 // byte offset of the first sample
- DataSize int64 // bytes of sample data actually present
- }
- // Duration returns the length of the audio.
- func (i Info) Duration() time.Duration {
- if i.SampleRate == 0 {
- return 0
- }
- return time.Duration(i.Frames) * time.Second / time.Duration(i.SampleRate)
- }
- // ReadFile reads the header of the WAV file at path.
- func ReadFile(path string) (Info, error) {
- f, err := os.Open(path)
- if err != nil {
- return Info{}, err
- }
- defer f.Close()
- st, err := f.Stat()
- if err != nil {
- return Info{}, err
- }
- return Read(f, st.Size())
- }
- // Read reads a WAV header from r, which holds size bytes.
- func Read(r io.ReaderAt, size int64) (Info, error) {
- var hdr [12]byte
- if _, err := r.ReadAt(hdr[:], 0); err != nil {
- return Info{}, errors.New("not a WAV file: too short")
- }
- if string(hdr[0:4]) != "RIFF" || string(hdr[8:12]) != "WAVE" {
- if string(hdr[0:4]) == "RF64" {
- return Info{}, errors.New("RF64 WAV files are not supported")
- }
- return Info{}, errors.New("not a WAV file")
- }
- var info Info
- var haveFmt, haveData bool
- pos := int64(12)
- for pos+8 <= size && !(haveFmt && haveData) {
- var ch [8]byte
- if _, err := r.ReadAt(ch[:], pos); err != nil {
- return Info{}, fmt.Errorf("reading chunk header: %w", err)
- }
- id := string(ch[0:4])
- n := int64(binary.LittleEndian.Uint32(ch[4:8]))
- body := pos + 8
- switch id {
- case "fmt ":
- if n < 16 || body+n > size {
- return Info{}, errors.New("fmt chunk is too short")
- }
- buf := make([]byte, n)
- if _, err := r.ReadAt(buf, body); err != nil {
- return Info{}, fmt.Errorf("reading fmt chunk: %w", err)
- }
- if err := info.parseFmt(buf); err != nil {
- return Info{}, err
- }
- haveFmt = true
- case "data":
- info.DataOffset = body
- // Some writers leave the size at 0 or 0xFFFFFFFF; count what is there.
- info.DataSize = min(n, size-body)
- if n == 0 || n == 0xFFFFFFFF {
- info.DataSize = size - body
- }
- haveData = true
- }
- pos = body + n + n&1
- }
- if !haveFmt {
- return Info{}, errors.New("WAV file has no fmt chunk")
- }
- if !haveData {
- return Info{}, errors.New("WAV file has no data chunk")
- }
- info.Frames = info.DataSize / int64(info.BlockAlign)
- return info, nil
- }
- func (i *Info) parseFmt(b []byte) error {
- le := binary.LittleEndian
- i.Format = int(le.Uint16(b[0:2]))
- i.Channels = int(le.Uint16(b[2:4]))
- i.SampleRate = int(le.Uint32(b[4:8]))
- i.BlockAlign = int(le.Uint16(b[12:14]))
- i.Bits = int(le.Uint16(b[14:16]))
- if i.Format == FormatExtensible {
- // cbSize(2) validBits(2) channelMask(4) subFormat GUID(16): the
- // first two bytes of the GUID are the real format code.
- if len(b) < 40 {
- return errors.New("extensible fmt chunk is too short")
- }
- i.Extensible = true
- i.Format = int(le.Uint16(b[24:26]))
- }
- if i.Channels == 0 || i.SampleRate == 0 || i.BlockAlign == 0 {
- return errors.New("WAV header has zero channels, rate or block size")
- }
- if i.BlockAlign != i.Channels*((i.Bits+7)/8) {
- return fmt.Errorf("WAV block size %d does not match %d channels of %d bits", i.BlockAlign, i.Channels, i.Bits)
- }
- return nil
- }
- // Validate reports why the device or mpckit cannot use the file, or nil.
- // Accepted: uncompressed integer PCM (8, 16, 24, 32-bit) or 32-bit float,
- // mono or stereo, at most 20 minutes.
- func (i Info) Validate() error {
- switch i.Format {
- case FormatPCM:
- switch i.Bits {
- case 8, 16, 24, 32:
- default:
- return fmt.Errorf("%d-bit PCM is not supported (use 8, 16, 24 or 32-bit)", i.Bits)
- }
- case FormatFloat:
- if i.Bits != 32 {
- return fmt.Errorf("%d-bit float is not supported (use 32-bit float)", i.Bits)
- }
- default:
- return fmt.Errorf("compressed WAV (format code %#x) is not supported; use uncompressed PCM or float", i.Format)
- }
- if i.Channels > MaxChannels {
- return fmt.Errorf("%d channels: only mono and stereo are supported", i.Channels)
- }
- if i.Frames == 0 {
- return errors.New("WAV file holds no audio")
- }
- if i.Duration() > MaxDuration {
- return fmt.Errorf("sample is %s long; the limit is 20 minutes", i.Duration().Round(time.Second))
- }
- return nil
- }
|