nativeaudio

audio playback for Go
Log | Files | Refs | README | LICENSE

commit 93b6bd5db16d6b83c262b16505b9626d65444c37
parent 660c4fdaaf824a7313ea60956580fa02d69f7b11
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Thu, 28 Oct 2021 13:17:13 +0800

nativeaudio: [ffmpeg] probe to get format details

This should fix platforms that rely on ffmpeg by outputting the correct
format details namely sample rate and channels. Bit depth is known
ahead of time because we specify s16le which is 16bit/2byte per sample.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
Maudio_ffmpeg.go | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 83 insertions(+), 9 deletions(-)

diff --git a/audio_ffmpeg.go b/audio_ffmpeg.go @@ -8,9 +8,12 @@ package nativeaudio import ( "bytes" + "encoding/json" "fmt" "os" "os/exec" + "strconv" + "strings" ) // FFmpegPlay an audio file with ffplay. @@ -40,6 +43,10 @@ func FFmpegPlay(path string) error { // s16le is the PCM format specifier, the final dash means "pipe to // stdout". func FFmpegLoad(path string) ([]byte, Format, error) { + f, err := probe(path) + if err != nil { + return nil, f, fmt.Errorf("probing file for metadata: %w", err) + } buffer := bytes.NewBuffer(nil) cmd := exec.Command( "ffmpeg", @@ -51,7 +58,7 @@ func FFmpegLoad(path string) ([]byte, Format, error) { if err := cmd.Run(); err != nil { return nil, Format{}, fmt.Errorf("ffmpeg: %w", err) } - return buffer.Bytes(), Format{}, nil + return buffer.Bytes(), f, nil } // FFmpegDecode raw PCM with ffmpeg. @@ -68,16 +75,83 @@ func FFmpegDecode(by []byte) ([]byte, Format, error) { return nil, Format{}, fmt.Errorf("creating tmp file: %w", err) } defer os.Remove("tmp") - buffer := bytes.NewBuffer(nil) + return FFmpegLoad("tmp") +} + +// probe queries the format information for a given audio file by parsing +// ffprobe results. +// +// ffprobe -i <path> -v quiet -print_format json -show_format -show_streams +// +func probe(path string) (Format, error) { + var ( + stderr = bytes.NewBuffer(nil) + stdout = bytes.NewBuffer(nil) + ) cmd := exec.Command( - "ffmpeg", - "-i", "tmp", - "-f", "s16le", - "-", + "ffprobe", + "-i", path, + "-v", "quiet", + "-print_format", "json", + "-show_format", + "-show_streams", ) - cmd.Stdout = buffer + cmd.Stdout = stdout + cmd.Stderr = stderr if err := cmd.Run(); err != nil { - return nil, Format{}, fmt.Errorf("ffmpeg: %w", err) + return Format{}, fmt.Errorf("%q: %w %s", strings.Join(cmd.Args, " "), err, stderr.String()) + } + var f md + if err := json.Unmarshal(stdout.Bytes(), &f); err != nil { + return Format{}, fmt.Errorf("unmarshalling json: %w", err) + } + s := f.FirstAudioStream() + if s == nil { + return Format{}, fmt.Errorf("file has no audio streams") + } + return s.Format() +} + +// md partially describes the structured metadata output from ffprobe. +type md struct { + // Streams contains all streams, we will filter for "first audio stream". + Streams []stream `json:"streams"` +} + +// stream description. +type stream struct { + // CodecType is the "major" type: {audio,video}. + CodecType string `json:"codec_type"` + // SampleRate in Hz. + SampleRate string `json:"sample_rate"` + // Channel count. + Channels int `json:"channels"` +} + +// FirstAudioStream returns the first audio stream described, if any. +func (f md) FirstAudioStream() *stream { + for ii, s := range f.Streams { + if s.CodecType == "audio" { + return &f.Streams[ii] + } + } + return nil +} + +// Format returns the unified Format struct from stream info. +func (s stream) Format() (Format, error) { + if s.Channels < 1 || s.Channels > 2 { + return Format{}, fmt.Errorf("can only handle {1,2} channels got %d", s.Channels) + } + sr, err := strconv.Atoi(s.SampleRate) + if err != nil { + return Format{}, fmt.Errorf("invalid sample rate: must be number got %q", s.SampleRate) } - return buffer.Bytes(), Format{}, nil + return Format{ + SampleRate: sr, + Channels: s.Channels, + // We are going to tell ffmpeg to output s16le, though there + // might be a better place to make this assumption. + BitDepth: 2, + }, nil }