在“豆子工具”众多的功能里,音频转换(m4a 转 mp3) 是我使用频率最高、也最具有“个人救赎”色彩的一个。

起因:被软件更新“背刺”后的郁闷

这个功能的由来非常接地气:有一段时间,我需要频繁地将苹果手机录音产生的 m4a 格式文件转换成 mp3,因为当时某个必须使用的业务软件只认 mp3

那时候我找遍了各种转换工具。最后发现某款主流音乐软件自带的转换功能挺好用。然而,好景不长,在一次软件自动更新后,这个功能竟然被砍掉了。我去搜老版本安装包,却发现根本找不到安全的下载路径。

那种“被绑架”的无奈感,相信每个工具控都深有体会。

进阶:大名鼎鼎的 ffmpeg

郁闷之后,我转向了技术人的终极方案——ffmpeg

命令行虽然硬核,但确实强大到无以复加。一条简单的指令就能解决所有问题:
ffmpeg -i input.m4a output.mp3

用了很长一段时间的命令行后,新的问题又来了,一次在外面,突然用户需要转换音频,我总不能随时随地都带着电脑吧?于是,我动了把 ffmpeg 搬进“豆子工具”的心思。

实现:极简架构下的“随时随地”

在小程序里实现这个功能,原理其实并不复杂,核心在于后端调度

  1. 前端上传:小程序端选择 m4a 文件,上传至服务器。
  2. 后端处理:后端使用 Go 语言接收文件,通过 exec 模块调用服务器系统环境中的 ffmpeg 进程进行转换。
  3. 实时试听:为了保证体验,我在小程序里集成了一个音频播放器。转换前可以听一下是否选对了文件,转换后也可以即时试听确认效果。
  4. 即用即删:转换生成的文件在用户下载后会立即从服务器删除,既保护了隐私,也完全不占用宝贵的服务器存储空间。

下面是核心代码片段的注解,这是小程序端:

<page-meta root-font-size="system" />
<view class="page">
  <view class="weui-form">
    <view class="weui-form__text-area">
      <h2 class="weui-form__title">音频格式转MP3</h2>
      <view class="weui-form__desc">因微信限制,保存文件即为分享文件,可通过微信文件传输助手下载文件。目前仅支持以下格式(M4A、WAV、AMR)文件转换为MP3文件。</view>
    </view>

    <view class="weui-form__control-area">
      <view class="weui-cells__group weui-cells__group_form">
        <view class="weui-cells  weui-cells_radio">
          <view class="weui-cell weui-cell_active weui-cell_vcode weui-cell_wrap">
            <view class="weui-cell__hd"><label class="weui-label">文件</label></view>
            <view class="weui-cell__bd">
              <input class="weui-cell__control weui-cell__control_flex weui-input" type="text" placeholder="请选取5M内的音频" placeholder-class="weui-input__placeholder" value="{{filename}}" />
              <view aria-role="button" class="weui-cell__control weui-btn weui-btn_default weui-vcode-btn" bindtap="chooseMedia">选择</view>
            </view>
          </view>

          <view class="weui-cell">
            <view wx:if="{{audioExist}}">
              <audio name="{{filename}}" src="{{filepath}}" id="myAudio" controls></audio>
            </view>
            <view wx:else><text>选择音频文件后可以在这里试听</text></view>
          </view>
          <view class="weui-cell">
            <view wx:if="{{makeAudioExist}}">
              <audio name="{{makeAudioName}}" src="{{makeAudioPath}}" id="myAudio" controls></audio>
            </view>
            <view wx:else><text>转换完成的文件会展示在这里</text></view>
          </view>
        </view>
      </view>
    </view>
    <view class="weui-form__opr-area">
      <view class="button-sp-area">
        <view class="weui-btn  weui-btn_primary weui-wa-hotarea" aria-role="button" bindtap="audioConvert">开始转换</view>
        <view class="weui-btn  weui-btn_warn weui-wa-hotarea" aria-role="button" bindtap="saveFile">保存文件</view>
      </view>
    </view>
  </view>
</view>

上面是界面骨架,现在说下它的灵魂。首先通过小程序的上传文件API,wx.uploadfile将音频文件上传到服务器,然后使用Go调用ffmpeg进行格式转换,转换成功后,下载文件到小程序本地,然后删除服务器端的文件。

// AudioConvert 音频格式转换
func (srv *Service) AudioConvert(src string, filetype string) (string, error) {
	srcAudio := global.UploadCachePath + "/" + src
	srcType := utils.GetFileExt(srcAudio)
	convertFileName := utils.GetFileNameByFileType(src, filetype)
	destAudio := global.UploadCachePath + "/" + convertFileName

	// 使用命令行实现
	switch filetype {
	// ffmpeg -i input.m4a -c:a libmp3lame -q:a 2 output.mp3
	// ffmpeg -i audio1.amr -ar 22050 audio1.mp3
	// ffmpeg -i input.wav -codec:a libmp3lame -qscale:a 2 output.mp3
	case ".mp3":
		hasSupport := strings.Index(".wav,.m4a,.amr", srcType)
		if hasSupport != -1 {
			switch srcType {
			case ".wav", ".m4a":
				cmdStr := fmt.Sprintf("ffmpeg -hide_banner -i %s -c:a libmp3lame -aq 2 %s", srcAudio, destAudio)
				cmd := exec.Command("sh", "-c", cmdStr)
				stdoutStderr, err := cmd.CombinedOutput()
				if err != nil {
					return "", err
				}
				logger.Infof("AudioConvert:%s\n", stdoutStderr)
			case ".amr":
				cmdStr := fmt.Sprintf("ffmpeg -hide_banner -i %s -ar 22050 %s", srcAudio, destAudio)
				cmd := exec.Command("sh", "-c", cmdStr)
				stdoutStderr, err := cmd.Output()
				if err != nil {
					return "", err
				}
				logger.Infof("AudioConvert:%s\n", stdoutStderr)
			}

		} else {
			return "", ErrTypeNotSupport
		}
	case ".m4a":
		return "", ErrTypeNotSupport
	case ".amr":
		return "", ErrTypeNotSupport
	case ".wav":
		return "", ErrTypeNotSupport
	default:
		return "", ErrTypeNotSupport
	}
	return convertFileName, nil
}

这段代码是核心,调用ffmpeg命令进行转换。需要注意的是需要安装ffmpeg命令,还有权限问题,运行服务的用户要拥有执行ffmpeg命令的权限。

感受:工具的本质是“自由”

音频格式转换

自从这个功能上线后,我就彻底告别了 ffmpeg 命令行。

最爽的一点是随时随地:无论是在地铁上还是在户外,掏出手机,几秒钟就能完成格式转换。这种把强大工具揣在兜里的感觉,就是开发者最大的浪漫。

如果你也经常被各种音频格式限制折磨,或者对 Go 调用 ffmpeg 的具体代码实现感兴趣,欢迎和我交流。