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

5.结论

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

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

  1. [转]FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  2. [总结]FFMPEG视音频编解码零基础学习方法--转

    ffmpeg编解码学习   目录(?)[-] ffmpeg程序的使用ffmpegexeffplayexeffprobeexe 1 ffmpegexe 2 ffplayexe 3 ffprobeexe ...

  3. FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  4. FFMPEG视音频编解码零基础学习方法-b

    感谢大神分享,虽然现在还看不懂,留着大家一起看啦 PS:有不少人不清楚“FFmpeg”应该怎么读.它读作“ef ef em peg” 0. 背景知识 本章主要介绍一下FFMPEG都用在了哪里(在这里仅 ...

  5. [总结]FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  6. 【转】[总结]FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  7. FFMPEG视音频编解码零基础学习方法 【荐】

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频 编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在 ...

  8. [转载] FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  9. 转[总结]FFMPEG视音频编解码零基础学习方法 .

    http://blog.csdn.net/leixiaohua1020/article/details/15811977 在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视 ...

  10. [总结]FFMPEG视音频编解码零基础学习方法【转】

    本文转载自:http://blog.csdn.net/leixiaohua1020/article/details/15811977 在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFM ...

随机推荐

  1. 测试环境docker化实践

    测试环境对于任何一个软件公司来讲,都是核心基础组件之一.测试环境伴随着发展也从单一的几套环境发展成现在的任意的docker动态环境+docker稳定环境环境体系.期间环境系统不断的演进,去适应集群扩张 ...

  2. ChatGPT的那些事 -1- 背景资料

    ChatGPT的那些事 -1- 背景资料 多处搬运,学无止境 目     录 1  关键词 1 1.1.  AIGC(百度百科) 1 1.2.  AlphaGo(百度百科) 1 1.3.  ChatG ...

  3. margin:auto实现盒子水平垂直居中

    margin:auto为什么不垂直居中 margin:auto是具有强烈计算意味的关键字,用来计算元素对应方向上应该获得的剩余空间大小. 行内元素margin:auto; 不能水平居中在一行的中央位置 ...

  4. rename基本操作

    电脑是Macbook, 用Homebrew先安装rename. 如果没安装Homebrew 直接复制到terminal中回车, 时间稍长. ruby -e "$(curl -fsSL htt ...

  5. DVWA-XSS (DOM) DOM型跨站脚本攻击

    XSS(Cross Site Scripting),跨站脚本攻击,能使攻击者在页面嵌入一些脚本代码,用户再访问,被诱导点击时,执行恶意脚本,常见为javascript,也有Flash.VBscript ...

  6. nginx+lua+openresty+kafka相关问题汇总

    这里使用的是kafka插件是doujiang大佬的https://github.com/doujiang24/lua-resty-kafka,版本为v0.2.0. 应用场景 在nginx转发中,记录非 ...

  7. 跳出思维的“盒子”,聊聊 RTE 应用创新大赛的那些作品

    由声网Agora 与环信联合主办的"RTE 2021 编程挑战赛"已圆满落幕.在今年的大赛中,来自行业中不同领域的专家参与了决赛评选,包括 Linux 中国技术社区技术负责人 白宦 ...

  8. NOIP2022游记

    NOIP2022游记 今年是第二次考NOIP了,去年第一次考的时候没学过什么东西,混了个省二.今年以高中生的身份考,不仅仅是要省一,还得拿个不错的名次,任务不小. 考试当天早上校园里的雾很大,不知道会 ...

  9. ks.cfg 怎么读取光盘 (cdrom) 上的文件并执行对应的脚本

    ks.cfg 文件怎么实现读取光盘 (CDROM) 上的内容并执行自定义脚本我们知道 linux 系统安装过程中,要想实现自动化安装,一般都是利用 Kickstart 这个工具实现,最重要的就是其配置 ...

  10. Kali中python问题

    Kali中python问题 1.查看python有哪些版本 update-alternatives --display python 2.如果没有,可以去/usr/bin查看kali自带哪些版本 查看 ...