2023-03-17:使用Go语言和FFmpeg库实现音频重采样解码,并将其保存为PCM格式的文件。

2023-03-17:使用Go语言和FFmpeg库实现音频重采样解码,并将其保存为PCM格式的文件。

答案2023-03-17:

在音视频处理领域,常常需要对音频进行重采样和解码,以便于后续的处理和分析。本文将介绍如何使用Go语言及FFmpeg库实现音频重采样解码为PCM数据的过程。

1.前置知识和背景介绍

在介绍音频重采样解码之前,我们需要了解几个基本概念:

音频采样率:指音频信号每秒钟采样的次数,通常用赫兹(Hz)表示。常见的采样率有44100Hz、48000Hz等。

音频编码格式:指把声音转成数字信号后所采用的编码格式,常见的编码格式有MP3、AAC、WAV等。

音频重采样:指改变音频采样率的过程,也可以理解为对音频做插值运算,使得原来采样率与目标采样率不一致的音频能够适配到目标采样率上。

音频解码:指把已经编码压缩的音频文件解码成原始的音频数据流的过程。

2.实现步骤

实现音频重采样解码为PCM数据的具体步骤如下:

2.1.导入所需的FFmpeg库和Go语言包

首先,我们需要导入一些必要的FFmpeg库和Go语言包,以便后续代码中能够正常调用相关接口和方法。代码示例如下:

import (
    "fmt"
    "os"
    "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"
)

2.2.打开输入音频文件

需要打开输入音频文件,并检查是否打开成功。若无法打开,则应该返回错误信息。

fmtCtx := libavformat.AvformatAllocContext()
if libavformat.AvformatOpenInput(&fmtCtx, inFileName, nil, nil) < 0 {
    fmt.Printf("Cannot open input file.\n")
    return
}

其中,inFileName是输入音频文件名。

2.3.获取音频流信息

获取音频流信息,包括音频流的相关参数(采样率、声道数、采样格式等),并检查是否获取成功。若无法获取成功,则应该返回错误信息。

if fmtCtx.AvformatFindStreamInfo(nil) < 0 {
    fmt.Printf("Cannot find stream info in input file.\n")
    return
}

aStreamIndex := -1
for i := uint32(0); i < fmtCtx.NbStreams; i++ {
    if fmtCtx.GetStream(i).Codecpar.CodecType == libavutil.AVMEDIA_TYPE_AUDIO {
        aStreamIndex = int(i)
        break
    }
}

if aStreamIndex == -1 {
    fmt.Printf("Cannot find audio stream.\n")
    return
}

aCodecPara := fmtCtx.GetStream(uint32(aStreamIndex)).Codecpar

其中,aCodecPara是音频流的参数。

2.4.查找音频解码器并打开音频解码器

根据音频流的参数,查找对应的音频解码器,并打开解码器。在打开解码器时,需要将音频流的参数设置为解码器的参数。

codec := libavcodec.AvcodecFindDecoder(aCodecPara.CodecId)
if codec == nil {
    fmt.Printf("Cannot find any codec for audio.\n")
    return
}

codecCtx = codec.AvcodecAllocContext3()

if codecCtx.AvcodecParametersToContext(aCodecPara) < 0 {
    fmt.Printf("Cannot alloc codec context.\n")
    return
}

if codecCtx.AvcodecOpen2(codec, nil) < 0 {
    fmt.Printf("Cannot open audio codec.\n")
    return
}

其中,codecCtx是解码器的上下文。

2.5.计算重采样参数

计算重采样后的采样率、声道数和采样格式等参数。

out_channel_layout := codecCtx.ChannelLayout
out_sample_fmt := libavutil.AV_SAMPLE_FMT_S16
out_sample_rate := int32(44100)
out_channels := libavutil.AvGetChannelLayoutNbChannels(out_channel_layout)

2.6.初始化重采样上下文

根据输入和输出参数,初始化重采样上下文

swrCtx := libswresample.SwrAllocSetOpts(
    nil,
    out_channel_layout, out_sample_fmt, out_sample_rate,
    codecCtx.ChannelLayout, codecCtx.SampleFmt(), int(codecCtx.SampleRate()), 0, nil,
)

if swrCtx == nil {
    fmt.Printf("Cannot allocate resampler context.\n")
    return
}

if swrCtx.SwrInit() < 0 {
    fmt.Printf("Cannot initialize resampling context.\n")
    return
}

其中,swrCtx是重采样上下文。

2.7.分配AVPacket和AVFrame

分别分配AVPacket和AVFrame,用于从输入音频流中读取数据、向解码器传递数据和从解码器接收数据等操作。

pkt := libavcodec.AvPacketAlloc()
defer pkt.AvPacketFree()

frame := libavutil.AvFrameAlloc()
defer frame.AvFrameFree()

2.8.从输入音频流中读取数据,并将其送入解码器进行解码

循环从输入音频流中读取数据,并将数据送入解码器进行解码。若读取到的数据为空,则跳出循环。

for {
    ret := fmtCtx.AvReadFrame(pkt)
    if ret < 0 {
        break
    }

    if pkt.StreamIndex() != int32(aStreamIndex) {
        continue
    }

    ret = codecCtx.AvCodecSendPacket(pkt)
    if ret < 0 {
        fmt.Printf("Error sending a packet for decoding.\n")
        break
    }

    for {
        ret = codecCtx.AvCodecReceiveFrame(frame)
        if ret < 0 {
            break
        }

        // 进行重采样
        data := make([]*uint8, 0)
        for i := 0; i < int(frame.NbSamples()); i++ {
            data = append(data, (*uint8)(frame.ExtendedData(0)[i]))
        }
        out_nb_samples := swrCtx.SwrGetDelay(int64(codecCtx.SampleRate())) + int(frame.NbSamples())
        out_samples_per_channel := out_nb_samples * out_channels
        out_buffer := libavutil.AvMalloc(uintptr(out_samples_per_channel) * uintptr(libavutil.AvGetBytesPerSample(out_sample_fmt)))
        defer libavutil.AvFree(out_buffer)
        out_data := (**uint8)(unsafe.Pointer(&out_buffer))
        swrCtx.SwrConvert(out_data, out_nb_samples, data, int(frame.NbSamples()))
    }

    pkt.AvPacketUnref()
}

注意,在解码时需要根据每个AVPacket的stream index判断是否是目标音频流。

2.9.编写PCM数据到文件中

将重采样后的PCM数据写入输出文件中。

outFile, err := os.Create(outFileName)
if err != nil {
    fmt.Printf("Can not create output file.\n")
    return
}
defer outFile.Close()

samples_size := libavutil.AvGetBytesPerSample(out_sample_fmt)
for i := 0; i < out_samples_per_channel; i++ {
    for j := 0; j < out_channels; j++ {
        sample_value := *(*int16)(unsafe.Pointer(uintptr(out_buffer) + uintptr(i*out_channels+j)*uintptr(samples_size)))
        binary.Write(outFile, binary.LittleEndian, &sample_value)
    }
}

其中,outFileName是输出音频文件名。

3.go语言完整代码

// https://feater.top/ffmpeg/ffmpeg-audio-resample-decode-mp3-to-pcm-with-cpu
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"
)

const MAX_AUDIO_FRAME_SIZE = 192000

func main() {
    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
        }
    }

    inVFileName := "./out/test.mp3"
    outFileName := "./out/test16.pcm"

    // ./lib/ffmpeg -i ./resources/big_buck_bunny.mp4 -acodec libmp3lame -vn ./out/test.mp3
    //是否存在mp3文件
    _, err = os.Stat(inVFileName)
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("create mp3 file")
            exec.Command("./lib/ffmpeg", "-i", "./resources/big_buck_bunny.mp4", "-acodec", "libmp3lame", "-vn", inVFileName, "-y").CombinedOutput()
        }
    }

    os.Remove(outFileName)
    f, err := os.OpenFile(outFileName, os.O_CREATE|os.O_RDWR, 0777)
    if err != nil {
        fmt.Println("open file failed,err:", err)
        return
    }

    fmtCtx := libavformat.AvformatAllocContext()
    var codecCtx *libavcodec.AVCodecContext
    pkt := libavcodec.AvPacketAlloc()
    frame := libavutil.AvFrameAlloc()

    aStreamIndex := -1

    for {
        if libavformat.AvformatOpenInput(&fmtCtx, inVFileName, nil, nil) < 0 {
            fmt.Printf("Cannot open input file.\n")
            break
        }

        if fmtCtx.AvformatFindStreamInfo(nil) < 0 {
            fmt.Printf("Cannot find stream info in input file.\n")
            break
        }

        fmtCtx.AvDumpFormat(0, inVFileName, 0)

        //查找视频流在文件中的位置
        for i := uint32(0); i < fmtCtx.NbStreams; i++ {
            if fmtCtx.GetStream(i).Codecpar.CodecType == libavutil.AVMEDIA_TYPE_AUDIO {
                aStreamIndex = int(i)
                break
            }
        }

        if aStreamIndex == -1 {
            fmt.Printf("Cannot find audio stream.\n")
            return
        }

        aCodecPara := fmtCtx.GetStream(uint32(aStreamIndex)).Codecpar
        codec := libavcodec.AvcodecFindDecoder(aCodecPara.CodecId)
        if codec == nil {
            fmt.Printf("Cannot find any codec for audio.\n")
            return
        }

        codecCtx = codec.AvcodecAllocContext3()

        if codecCtx.AvcodecParametersToContext(aCodecPara) < 0 {
            fmt.Printf("Cannot alloc codec context.\n")
            return
        }

        codecCtx.PktTimebase = fmtCtx.GetStream(uint32(aStreamIndex)).TimeBase

        if codecCtx.AvcodecOpen2(codec, nil) < 0 {
            fmt.Printf("Cannot open audio codec.\n")
            return
        }

        //设置转码参数
        out_channel_layout := codecCtx.ChannelLayout
        out_sample_fmt := libavutil.AV_SAMPLE_FMT_S16
        out_sample_rate := int32(44100) //codecCtx.SampleRate
        out_channels := libavutil.AvGetChannelLayoutNbChannels(out_channel_layout)

        audio_out_buffer := libavutil.AvMalloc(MAX_AUDIO_FRAME_SIZE * 2)

        var swr_ctx *libswresample.SwrContext
        swr_ctx = swr_ctx.SwrAllocSetOpts(int64(out_channel_layout),
            libavutil.AVSampleFormat(out_sample_fmt),
            out_sample_rate,
            int64(codecCtx.ChannelLayout),
            codecCtx.SampleFmt,
            codecCtx.SampleRate,
            0, uintptr(0))
        swr_ctx.SwrInit()

        for (fmtCtx.AvReadFrame(pkt)) >= 0 {
            if pkt.StreamIndex == uint32(aStreamIndex) {
                if codecCtx.AvcodecSendPacket(pkt) >= 0 {
                    for codecCtx.AvcodecReceiveFrame(frame) >= 0 {
                        /*
                           Planar(平面),其数据格式排列方式为 (特别记住,该处是以点nb_samples采样点来交错,不是以字节交错):
                           LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每个LLLLLLRRRRRR为一个音频帧)
                           而不带P的数据格式(即交错排列)排列方式为:
                           LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每个LR为一个音频样本)
                        */
                        if libavutil.AvSampleFmtIsPlanar(codecCtx.SampleFmt) != 0 {
                            len0 := swr_ctx.SwrConvert((**byte)(unsafe.Pointer(&audio_out_buffer)),
                                MAX_AUDIO_FRAME_SIZE*2,
                                (**byte)(unsafe.Pointer(&frame.Data)),
                                frame.NbSamples)
                            if len0 < 0 {
                                continue
                            }
                            dst_bufsize := libavutil.AvSamplesGetBufferSize(nil,
                                out_channels,
                                len0,
                                libavutil.AVSampleFormat(out_sample_fmt),
                                1)

                            bytes := []byte{}
                            ptr := audio_out_buffer
                            for i := int32(0); i < dst_bufsize; i++ {
                                bytes = append(bytes, *(*byte)(unsafe.Pointer(ptr)))
                                ptr++
                            }
                            f.Write(bytes)
                        }
                    }
                }
            }
            pkt.AvPacketUnref()
        }

        break
    }

    libavutil.AvFrameFree(&frame)
    libavcodec.AvPacketFree(&pkt)
    codecCtx.AvcodecClose()
    libavcodec.AvcodecFreeContext(&codecCtx)
    fmtCtx.AvformatFreeContext()
    f.Close()
    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()
    if err != nil {
        fmt.Println("play err = ", err)
    }
}

4.测试和演示结果

go run ./examples/a16.audio_decode_swr_mp32pcm/main.go
1679058448703.jpg

5.结论

通过调用Go语言和FFmpeg库提供的接口和方法,我们可以轻松实现音频重采样解码,并将其保存为PCM格式的文件。这对于音视频应用开发和研究等领域非常有帮助。在实际工作中,我们可以根据具体需求和场景,进一步优化和扩展相关功能。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,578评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,701评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,691评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,974评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,694评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,026评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,015评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,193评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,719评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,442评论 3 360
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,668评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,151评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,846评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,255评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,592评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,394评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,635评论 2 380

推荐阅读更多精彩内容