docs: document packages, exported API and non-obvious logic
This commit is contained in:
+51
-3
@@ -1,3 +1,9 @@
|
||||
// Package audio converts downloaded audio between formats.
|
||||
//
|
||||
// Deezer does not serve wav, so a wav download is really a flac download
|
||||
// followed by FLACToWAV. The conversion is lossless in both directions: flac
|
||||
// decodes to exactly the PCM samples it was encoded from, so nothing is lost
|
||||
// by going through it.
|
||||
package audio
|
||||
|
||||
import (
|
||||
@@ -16,12 +22,37 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
headerSize = 44
|
||||
formatPCM = 1
|
||||
// headerSize is the canonical PCM wav header: a 12 byte RIFF/WAVE header,
|
||||
// a 24 byte fmt chunk, and an 8 byte data chunk header.
|
||||
headerSize = 44
|
||||
formatPCM = 1
|
||||
|
||||
// ctxCheckInterval is how often, in flac frames, cancellation is polled.
|
||||
// A flac frame is a few thousand samples, so checking every frame would
|
||||
// add a select to the innermost decode loop for no practical gain in
|
||||
// responsiveness.
|
||||
ctxCheckInterval = 64
|
||||
maxDataSize = math.MaxUint32 - (headerSize - 8)
|
||||
|
||||
// maxDataSize is the largest audio payload that still fits. RIFF stores
|
||||
// its sizes as uint32, and the RIFF size field covers the header after
|
||||
// its own first 8 bytes as well as the data, so the audio itself has to
|
||||
// stay that much below the limit. This works out to roughly 6 hours of
|
||||
// CD quality stereo, which no single track will reach, but silently
|
||||
// producing a file with a wrapped size field would be worse than an
|
||||
// error.
|
||||
maxDataSize = math.MaxUint32 - (headerSize - 8)
|
||||
)
|
||||
|
||||
// FLACToWAV decodes the flac at srcPath and writes it as a PCM wav to
|
||||
// dstPath.
|
||||
//
|
||||
// The size is checked twice, once from the flac header before doing any work
|
||||
// and once against the bytes actually written, because NSamples is zero in
|
||||
// flac streams that were encoded without a known length.
|
||||
//
|
||||
// Output goes to a temporary file in the destination directory and is renamed
|
||||
// into place at the end, so a cancelled or failed conversion never leaves a
|
||||
// half decoded file where a playable one is expected.
|
||||
func FLACToWAV(ctx context.Context, srcPath, dstPath string) error {
|
||||
stream, err := flac.Open(srcPath)
|
||||
if err != nil {
|
||||
@@ -54,6 +85,9 @@ func FLACToWAV(ctx context.Context, srcPath, dstPath string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// The header goes down with a zero data size and is patched afterwards:
|
||||
// the real length is only known once every frame has been decoded, and
|
||||
// buffering the whole stream in memory to find out first is not worth it.
|
||||
w := bufio.NewWriter(file)
|
||||
if err := writeHeader(w, info.SampleRate, info.NChannels, info.BitsPerSample, 0); err != nil {
|
||||
return err
|
||||
@@ -66,6 +100,8 @@ func FLACToWAV(ctx context.Context, srcPath, dstPath string) error {
|
||||
if dataSize > maxDataSize {
|
||||
return fmt.Errorf("audio data of %d bytes exceeds the wav format limit", dataSize)
|
||||
}
|
||||
// RIFF chunks must end on an even offset. Only reachable with 8 or 24 bit
|
||||
// mono, where a sample is an odd number of bytes.
|
||||
if dataSize%2 != 0 {
|
||||
if err := w.WriteByte(0); err != nil {
|
||||
return err
|
||||
@@ -162,6 +198,12 @@ func writeSamples(ctx context.Context, w io.Writer, stream *flac.Stream, nChanne
|
||||
return dataSize, nil
|
||||
}
|
||||
|
||||
// putSample encodes one sample little endian into buf.
|
||||
//
|
||||
// 8 bit wav is the odd one out: it stores unsigned samples biased by 128,
|
||||
// while every wider depth is signed two's complement. Writing an 8 bit sample
|
||||
// signed produces audio that sounds like loud static, so the bias is not
|
||||
// optional.
|
||||
func putSample(buf []byte, sample int32, bytesPerSample int) {
|
||||
if bytesPerSample == 1 {
|
||||
buf[0] = byte(sample + 128)
|
||||
@@ -174,6 +216,12 @@ func putSample(buf []byte, sample int32, bytesPerSample int) {
|
||||
}
|
||||
}
|
||||
|
||||
// patchSizes rewrites the two length fields once the real data size is known:
|
||||
// the RIFF size at offset 4 and the data chunk size just before the samples
|
||||
// begin.
|
||||
//
|
||||
// The pad byte counts towards the RIFF size but not towards the data chunk
|
||||
// size, which is why only the first of the two includes it.
|
||||
func patchSizes(file *os.File, dataSize int64) error {
|
||||
buf := make([]byte, 4)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user