2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3。

2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3。

答案2023-03-19:

本文将介绍如何使用Go语言和FFmpeg库实现PCM音频文件编码为MP3格式。我们将使用moonfdd/ffmpeg-go库,并在Windows 10 64位操作系统下完成本次实验。

代码参考了FFmpeg —— 15.示例程序(九):音频编码器(PCM编码为MP3)19:pcm编码为mp3

看完整代码,这个肯定能运行通过。

1.准备工作

安装moonfdd/ffmpeg-go库,运行命令:go get -u github.com/moonfdd/ffmpeg-go

2.实现步骤

2.1.设置FFmpeg库路径

首先需要设置FFmpeg库的路径,在本例中是”./lib”目录下。通过moonfdd/ffmpeg-go库提供的函数SetXXXXPath()可以分别设置各个库的路径:

os.Setenv("Path", os.Getenv("Path")+";./lib")
ffcommon.SetAvutilPath("./lib/avutil-56.dll")
ffcommon.SetAvcodecPath("./lib/avcodec-58.dll")
ffcommon.SetAvdevicePath("./lib/avdevice-58.dll")
ffcommon.SetAvfilterPath("./lib/avfilter-56.dll")
ffcommon.SetAvformatPath("./lib/avformat-58.dll")
ffcommon.SetAvpostprocPath("./lib/postproc-55.dll")
ffcommon.SetAvswresamplePath("./lib/swresample-3.dll")
ffcommon.SetAvswscalePath("./lib/swscale-5.dll")

2.2.准备输入PCM文件

本例中输入的PCM文件是16位采样精度、立体声(2个声道)、44100Hz采样率,文件名为”s16le.pcm”,存放在”./out”目录下。如果该文件不存在,则从一个视频文件中提取音频数据并转换格式生成该PCM文件:

inFileName := "./out/s16le.pcm"
_, err = os.Stat(inFileName)
if err != nil {
    if os.IsNotExist(err) {
        fmt.Println("create pcm file")
        exec.Command("./lib/ffmpeg", "-i", "./resources/big_buck_bunny.mp4", "-f", "s16le", "-ar", "44100", "-ac", "2", "-acodec", "pcm_s16le", "-vn", inFileName, "-y").CombinedOutput()
    }
}

2.3.打开输出MP3文件

本例中输出的MP3文件名为”out19.mp3”,存放在”./out”目录下。首先需要调用libavformat.AvformatAllocOutputContext2()函数分配AVFormatContext结构体,并调用libavformat.AvioOpen()函数打开输出文件:

var pFormatCtx *libavformat.AVFormatContext
libavformat.AvformatAllocOutputContext2(&pFormatCtx, nil, "", outFileName)
if libavformat.AvioOpen(&pFormatCtx.Pb, outFileName, libavformat.AVIO_FLAG_READ_WRITE) < 0 {
    fmt.Printf("Cannot open output file.\n")
    return
}

2.4.添加输出流

将要输出的音频流添加到输出文件中。首先需要调用libavformat.AvformatNewStream()函数创建一个新的流对象,并将该流对象的Codec属性设置为要输出的音频编解码器属性:

stream := pFormatCtx.AvformatNewStream(nil)
if stream == nil {
    fmt.Printf("Cannot create a new stream to output file.\n")
    return
}
pCodecCtx = stream.Codec
pCodecCtx.CodecType = libavutil.AVMEDIA_TYPE_AUDIO
pCodecCtx.CodecId = pFormatCtx.Oformat.AudioCodec
pCodecCtx.SampleFmt = libavutil.AV_SAMPLE_FMT_FLTP
pCodecCtx.SampleRate = 44100
pCodecCtx.ChannelLayout = libavutil.AV_CH_LAYOUT_STEREO
pCodecCtx.BitRate = 128000
pCodecCtx.Channels = libavutil.AvGetChannelLayoutNbChannels(pCodecCtx.ChannelLayout)

2.5.查找并打开编码器

根据指定的编码器ID查找对应的编码器对象,调用libavcodec.AvcodecFindEncoder()函数返回对应的AVCodec对象。然后,调用libavcodec.AvcodecOpen2()函数打开编码器并初始化编码器上下文:

pCodec := libavcodec.AvcodecFindEncoder(pCodecCtx.CodecId)
if pCodec == nil {
    fmt.Printf("Cannot find encoder.\n")
    return
}
if pCodec.AvcodecOpen2(pCodecCtx, nil) < 0 {
    fmt.Printf("Cannot open encoder.\n")
    return
}

2.6.写入文件头

调用libavformat.AvformatWriteHeader()函数写入输出文件的文件头信息:

if libavformat.AvformatWriteHeader(pFormatCtx, nil) < 0 {
    fmt.Printf("Error occurred while writing header.\n")
    return
}

2.7.编码音频数据

循环读取输入PCM文件中的音频数据,将其填充到AVFrame对象中,并调用libavcodec.AvcodecSendFrame()函数发送该帧音频数据给编码器。然后循环调用libavcodec.AvcodecReceivePacket()函数接收编码器编码后的数据包,并调用libavformat.AvInterleavedWriteFrame()函数将该数据包写入输出文件中:

for {
    ret := inF.Read(buf)
    if ret == 0 {
        break
    }
    inBufSize := len(buf)

    // fill data to AVFrame structure
    pFrame := libavutil.AvFrameAlloc()
    defer libavutil.AvFrameFree(pFrame)

    pFrame.SetNbSamples(int32(inBufSize) / (2 * 2))
    pFrame.SetFormat(pCodecCtx.SampleFmt)
    pFrame.SetSampleRate(pCodecCtx.SampleRate)
    pFrame.SetChannelLayout(pCodecCtx.ChannelLayout)

    for i := 0; i < int(pFrame.NbSamples()); i++ {
        for t := 0; t < int(pFrame.Channels()); t++ {
            idx := (i*int(pFrame.Channels()) + t) * 2
            val := float32(int16(binary.LittleEndian.Uint16(buf[idx:idx+2]))) / (1 << 15)
            *(*[]float32)(unsafe.Pointer(&pFrame.ExtendedData))[t][i] = val
        }
    }

    // encode audio frame
    if pCodecCtx.AvcodecSendFrame(pFrame) < 0 {
        fmt.Printf("Error while sending a frame to the encoder.\n")
        return
    }
    for {
        pkt := libavcodec.AvPacketAlloc()
        isEof := false
        defer libavcodec.AvPacketFree(pkt)

        ret := pCodecCtx.AvcodecReceivePacket(pkt)
        if ret < 0 {
            fmt.Printf("Error while receiving a packet from the encoder.\n")
            return
        }
        if ret == 0 {
            break
        }
        pkt.SetStreamIndex(stream.Index())
        pkt.SetPts(libavutil.AvRescaleQ(samplesCount, libavutil.AVR{Num: 1, Den: 44100}, stream.TimeBase()))
        samplesCount += int64(pFrame.NbSamples())

        // write encoded data to output file
        if libavformat.AvInterleavedWriteFrame(pFormatCtx, pkt) != 0 {
            fmt.Printf("Error while writing a packet to the container.\n")
            return
        }
        if isEof {
            break
        }
    }
}

2.8.写入文件尾部

最后,调用libavformat.AvWriteTrailer()函数写入输出文件的尾部信息,完成整个音频编码过程:

if libavformat.AvWriteTrailer(pFormatCtx) < 0 {
    fmt.Printf("Error occurred while writing trailer.\n")
    return
}

3.完整代码

package main

import (
    "fmt"
    "os"
    "os/exec"
    "unsafe"

    "github.com/moonfdd/ffmpeg-go/ffcommon"
    "github.com/moonfdd/ffmpeg-go/libavcodec"
    "github.com/moonfdd/ffmpeg-go/libavformat"
    "github.com/moonfdd/ffmpeg-go/libavutil"
    "github.com/moonfdd/ffmpeg-go/libswresample"
)

func main() {

    // https://blog.csdn.net/guoyunfei123/article/details/105643255
    // 时长没误差
    os.Setenv("Path", os.Getenv("Path")+";./lib")
    ffcommon.SetAvutilPath("./lib/avutil-56.dll")
    ffcommon.SetAvcodecPath("./lib/avcodec-58.dll")
    ffcommon.SetAvdevicePath("./lib/avdevice-58.dll")
    ffcommon.SetAvfilterPath("./lib/avfilter-56.dll")
    ffcommon.SetAvformatPath("./lib/avformat-58.dll")
    ffcommon.SetAvpostprocPath("./lib/postproc-55.dll")
    ffcommon.SetAvswresamplePath("./lib/swresample-3.dll")
    ffcommon.SetAvswscalePath("./lib/swscale-5.dll")

    genDir := "./out"
    _, err := os.Stat(genDir)
    if err != nil {
        if os.IsNotExist(err) {
            os.Mkdir(genDir, 0777) //  Everyone can read write and execute
        }
    }

    //./lib/ffmpeg -i .\resources\big_buck_bunny.mp4 -f s16le -ar 44100 -ac 2 -acodec pcm_s16le -vn ./out/s16le.pcm
    // ./lib/ffmpeg -y -f s16le -ac 2 -ar 44100 -acodec pcm_s16le -vn -i ./out/s16le.pcm ./out/s16le.mp3
    inFileName := "./out/s16le.pcm"
    // inFileName := "./out/test16.pcm"
    outFileName := "./out/out19.mp3"

    //是否存在pcm文件
    _, err = os.Stat(inFileName)
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("create pcm file")
            exec.Command("./lib/ffmpeg", "-i", "./resources/big_buck_bunny.mp4", "-f", "s16le", "-ar", "44100", "-ac", "2", "-acodec", "pcm_s16le", "-vn", inFileName, "-y").CombinedOutput()
        }
    }

    var pFormatCtx *libavformat.AVFormatContext
    var pCodecCtx *libavcodec.AVCodecContext
    var pCodec *libavcodec.AVCodec
    var pkt libavcodec.AVPacket
    var pFrame *libavutil.AVFrame

    //libavdevice.AvdeviceRegisterAll()

    for {

        libavformat.AvformatAllocOutputContext2(&pFormatCtx, nil, "", outFileName)

        if libavformat.AvioOpen(&pFormatCtx.Pb, outFileName, libavformat.AVIO_FLAG_READ_WRITE) < 0 {
            fmt.Printf("Cannot open output file.\n")
            return
        }

        stream := pFormatCtx.AvformatNewStream(nil)
        if stream == nil {
            fmt.Printf("Cannot create a new stream to output file.\n")
            return
        }

        //设置参数
        pCodecCtx = stream.Codec
        pCodecCtx.CodecType = libavutil.AVMEDIA_TYPE_AUDIO
        pCodecCtx.CodecId = pFormatCtx.Oformat.AudioCodec
        pCodecCtx.SampleFmt = libavutil.AV_SAMPLE_FMT_FLTP
        pCodecCtx.SampleRate = 44100
        pCodecCtx.ChannelLayout = libavutil.AV_CH_LAYOUT_STEREO
        pCodecCtx.BitRate = 128000
        pCodecCtx.Channels = libavutil.AvGetChannelLayoutNbChannels(pCodecCtx.ChannelLayout)

        //查找编码器
        pCodec = libavcodec.AvcodecFindEncoder(pCodecCtx.CodecId)
        if pCodec == nil {
            fmt.Printf("Cannot find audio encoder.\n")
            return
        }

        //打开编码器
        if pCodecCtx.AvcodecOpen2(pCodec, nil) < 0 {
            fmt.Printf("Cannot open encoder.\n")
            return
        }

        //fmtCtx.AvDumpFormat(0, outFileName, 1)
        pFrame = libavutil.AvFrameAlloc()
        if pFrame == nil {
            fmt.Printf("can't alloc frame\n")
            return
        }

        //===========
        pFrame.NbSamples = pCodecCtx.FrameSize
        pFrame.Format = int32(pCodecCtx.SampleFmt)
        pFrame.Channels = 2

        // PCM重采样
        var swr_ctx *libswresample.SwrContext = libswresample.SwrAlloc()
        swr_ctx.SwrAllocSetOpts(libavutil.AvGetDefaultChannelLayout(pCodecCtx.Channels),
            pCodecCtx.SampleFmt,
            pCodecCtx.SampleRate,
            libavutil.AvGetDefaultChannelLayout(pFrame.Channels),
            libavutil.AV_SAMPLE_FMT_S16, // PCM源文件的采样格式
            44100,
            0, uintptr(0))
        swr_ctx.SwrInit()

        /* 分配空间 */
        // uint8_t **convert_data = (uint8_t**)calloc(codecCtx->channels,sizeof(*convert_data));
        convert_data := (**byte)(unsafe.Pointer(libavutil.AvCalloc(uint64(pCodecCtx.Channels), 8)))
        libavutil.AvSamplesAlloc(convert_data, nil, pCodecCtx.Channels, pCodecCtx.FrameSize,
            pCodecCtx.SampleFmt, 0)

        size := libavutil.AvSamplesGetBufferSize(nil, pCodecCtx.Channels,
            pCodecCtx.FrameSize, pCodecCtx.SampleFmt, 1)
        frameBuf := libavutil.AvMalloc(uint64(size))
        libavcodec.AvcodecFillAudioFrame(pFrame, pCodecCtx.Channels, pCodecCtx.SampleFmt,
            (*byte)(unsafe.Pointer(frameBuf)), size, 1)

        //写帧头
        pFormatCtx.AvformatWriteHeader(nil)

        inFile, err := os.Open(inFileName)
        if err != nil {
            fmt.Printf("annot open input file.\n")
            return
        }

        pkt.AvInitPacket()
        pkt.Data = nil
        pkt.Size = 0

        for i := 0; ; i++ {
            //输入一帧数据的长度
            length := pFrame.NbSamples * libavutil.AvGetBytesPerSample(libavutil.AV_SAMPLE_FMT_S16) * pFrame.Channels
            //读PCM:特意注意读取的长度,否则可能出现转码之后声音变快或者变慢
            buf := make([]byte, length)
            n, err := inFile.Read(buf)
            if err != nil {
                fmt.Println("read end")
                break
            }
            if n <= 0 {
                break
            }

            for j := 0; j < n; j++ {
                *(*byte)(unsafe.Pointer(frameBuf + uintptr(j))) = buf[j]
            }

            swr_ctx.SwrConvert(convert_data, pCodecCtx.FrameSize,
                (**byte)(unsafe.Pointer(&pFrame.Data)),
                pFrame.NbSamples)

            //输出一帧数据的长度
            length = pCodecCtx.FrameSize * libavutil.AvGetBytesPerSample(pCodecCtx.SampleFmt)
            //双通道赋值(输出的AAC为双通道)
            // memcpy(frame->data[0],convert_data[0],length);
            // memcpy(frame->data[1],convert_data[1],length);
            c := *(*[2]uintptr)(unsafe.Pointer(convert_data))
            fd0 := uintptr(unsafe.Pointer(pFrame.Data[0]))
            cd0 := uintptr(unsafe.Pointer(c[0]))
            fd1 := uintptr(unsafe.Pointer(pFrame.Data[1]))
            cd1 := uintptr(unsafe.Pointer(c[1]))
            for j := int32(0); j < length; j++ {
                *(*byte)(unsafe.Pointer(fd0)) = *(*byte)(unsafe.Pointer(cd0))
                *(*byte)(unsafe.Pointer(fd1)) = *(*byte)(unsafe.Pointer(cd1))
                fd0++
                cd0++
                fd1++
                cd1++
            }

            pFrame.Pts = int64(i * 100)
            if pCodecCtx.AvcodecSendFrame(pFrame) < 0 {
                fmt.Printf("can't send frame for encoding\n")
                break
            }
            if pCodecCtx.AvcodecReceivePacket(&pkt) >= 0 {
                pkt.StreamIndex = uint32(stream.Index)
                fmt.Printf("write %4d frame, size = %d, length = %d\n", i, size, length)
                pFormatCtx.AvWriteFrame(&pkt)

            }
            pkt.AvPacketUnref()
        }

        // flush encoder
        if flush_encoder(pFormatCtx, 0) < 0 {
            fmt.Printf("flushing encoder failed\n")
            return
        }

        // write trailer
        pFormatCtx.AvWriteTrailer()

        inFile.Close()
        stream.Codec.AvcodecClose()
        libavutil.AvFree(uintptr(unsafe.Pointer(pFrame)))
        libavutil.AvFree(frameBuf)
        pFormatCtx.Pb.AvioClose()
        pFormatCtx.AvformatFreeContext()
        break
    }

    // codecCtx.AvcodecClose()
    // libavutil.AvFree(uintptr(unsafe.Pointer(frame)))
    // fmtCtx.Pb.AvioClose()
    // fmtCtx.AvformatFreeContext()
    fmt.Println("-----------------------------------------")
    // ./lib/ffplay -ar 44100 -ac 2 -f s16le -i ./out/test.pcm
    //_, err = exec.Command("./lib/ffplay.exe", "-ar", "44100", "-ac", "2", "-f", "s16le", "-i", "./out/test16.pcm").Output()
    _, err = exec.Command("./lib/ffplay.exe", outFileName).Output()
    if err != nil {
        fmt.Println("play err = ", err)
    }
}
func flush_encoder(fmt_ctx *libavformat.AVFormatContext, stream_index int) int32 {
    ret := int32(0)
    var got_frame int32
    var enc_pkt libavcodec.AVPacket
    if fmt_ctx.GetStream(uint32(stream_index)).Codec.Codec.Capabilities&libavcodec.AV_CODEC_CAP_DELAY == 0 {
        return 0
    }
    for {
        enc_pkt.Data = nil
        enc_pkt.Size = 0
        enc_pkt.AvInitPacket()
        ret = fmt_ctx.GetStream(uint32(stream_index)).Codec.AvcodecEncodeAudio2(&enc_pkt,
            nil, &got_frame)
        //av_frame_free(NULL)
        if ret < 0 {
            break
        }
        if got_frame == 0 {
            ret = 0
            break
        }
        fmt.Printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.Size)
        /* mux encoded frame */
        ret = fmt_ctx.AvWriteFrame(&enc_pkt)
        if ret < 0 {
            break
        }
    }

    return ret
}

4.运行结果

执行命令:

go run ./examples/a19.audio_encode_pcm2mp3/main.go

2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3。

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信公众号:福大大架构师每日一题。最新面试题,涉及golang,rust,mysql,redis,云原生,算法,分布式,网络,操作系统。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
469
粉丝
21
喜欢
37
收藏
22
排名:457
访问:1.9 万
私信
所有博文
社区赞助商