nativeaudio

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

formats_test.go (3916B)


      1 package test
      2 
      3 import (
      4 	"os"
      5 	"os/exec"
      6 	"path/filepath"
      7 	"testing"
      8 
      9 	"git.sr.ht/~jackmordaunt/nativeaudio"
     10 )
     11 
     12 // formatProbe is one row of the support matrix: a way to produce a file
     13 // with ffmpeg, and whether the package promises to decode it.
     14 type formatProbe struct {
     15 	// name labels the row in the matrix.
     16 	name string
     17 	// file is the name to write, whose extension picks the container.
     18 	file string
     19 	// args are the ffmpeg codec arguments.
     20 	args []string
     21 	// guaranteed marks formats the readme claims support for. Those are
     22 	// assertions; everything else is reported and left to the matrix.
     23 	guaranteed bool
     24 }
     25 
     26 var formatProbes = []formatProbe{
     27 	{"AAC-LC in MP4", "out.m4a", []string{"-c:a", "aac", "-b:a", "128k"}, true},
     28 	{"AAC-LC in ADTS", "out.aac", []string{"-c:a", "aac", "-b:a", "128k", "-f", "adts"}, true},
     29 	{"MP3", "out.mp3", []string{"-c:a", "libmp3lame"}, true},
     30 	{"WAV 16-bit", "out.wav", []string{"-c:a", "pcm_s16le"}, true},
     31 	{"WAV 24-bit", "out24.wav", []string{"-c:a", "pcm_s24le"}, true},
     32 	{"FLAC", "out.flac", []string{"-c:a", "flac"}, false},
     33 	{"ALAC in MP4", "alac.m4a", []string{"-c:a", "alac"}, false},
     34 	{"Opus in Ogg", "out.opus", []string{"-c:a", "libopus"}, false},
     35 	{"Vorbis in Ogg", "out.ogg", []string{"-c:a", "libvorbis"}, false},
     36 	{"WMA v2", "out.wma", []string{"-c:a", "wmav2"}, false},
     37 }
     38 
     39 // generate writes one second of tone in the probe's format and returns
     40 // its path, or skips when ffmpeg cannot produce it.
     41 func (p formatProbe) generate(t *testing.T, dir string) string {
     42 	t.Helper()
     43 	path := filepath.Join(dir, p.file)
     44 	args := []string{
     45 		"-hide_banner", "-loglevel", "error", "-y",
     46 		"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=44100",
     47 		"-t", "1", "-ac", "2",
     48 	}
     49 	args = append(args, p.args...)
     50 	args = append(args, path)
     51 	if out, err := exec.Command("ffmpeg", args...).CombinedOutput(); err != nil {
     52 		t.Skipf("ffmpeg cannot produce %s: %v: %s", p.name, err, out)
     53 	}
     54 	return path
     55 }
     56 
     57 // TestFormatSupport decodes one file per format and reports what the
     58 // platform's decoder made of it.
     59 //
     60 // The published support matrix comes from running this on each platform,
     61 // which is the only way to keep it honest: the operating system decoders
     62 // handle far more than AAC, and which extras they handle differs. Rows
     63 // marked guaranteed are assertions; the rest are reported so the matrix
     64 // can be filled in from a CI run rather than from vendor documentation.
     65 func TestFormatSupport(t *testing.T) {
     66 	requireTools(t, "ffmpeg")
     67 	dir := t.TempDir()
     68 
     69 	for _, probe := range formatProbes {
     70 		t.Run(probe.name, func(t *testing.T) {
     71 			path := probe.generate(t, dir)
     72 
     73 			in, err := os.ReadFile(path)
     74 			if err != nil {
     75 				t.Fatalf("reading generated file: %v", err)
     76 			}
     77 
     78 			d := newDecoder(t)
     79 			pcm, format, err := d.Decode(in)
     80 			if err != nil {
     81 				if probe.guaranteed {
     82 					t.Errorf("MATRIX %-16s UNSUPPORTED (but claimed): %v", probe.name, err)
     83 				} else {
     84 					t.Logf("MATRIX %-16s unsupported: %v", probe.name, err)
     85 				}
     86 				return
     87 			}
     88 
     89 			t.Logf("MATRIX %-16s native supported: %d bytes, %+v", probe.name, len(pcm), format)
     90 
     91 			// The ffmpeg fallback is the backend on platforms with no
     92 			// native decoder, so probe it wherever ffmpeg exists rather
     93 			// than only where it is in use.
     94 			if fpcm, fformat, ferr := nativeaudio.FFmpegDecode(in); ferr != nil {
     95 				t.Logf("MATRIX %-16s ffmpeg unsupported: %v", probe.name, ferr)
     96 			} else {
     97 				t.Logf("MATRIX %-16s ffmpeg supported: %d bytes, %+v", probe.name, len(fpcm), fformat)
     98 			}
     99 
    100 			// Whatever the input, the output contract is the same.
    101 			if format.BytesPerSample != 2 {
    102 				t.Errorf("%s decoded to %d bytes per sample, want 2", probe.name, format.BytesPerSample)
    103 			}
    104 			if format.SampleRate < 1 || format.Channels < 1 {
    105 				t.Errorf("%s reported an implausible format: %+v", probe.name, format)
    106 			}
    107 			if len(pcm) == 0 {
    108 				t.Errorf("%s decoded to no audio", probe.name)
    109 			}
    110 		})
    111 	}
    112 }