198 lines
5.0 KiB
Go
198 lines
5.0 KiB
Go
package ffmpeg
|
||
|
||
import (
|
||
"fmt"
|
||
"github.com/tcolgate/mp3"
|
||
"github.com/youpy/go-wav"
|
||
"io"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// 音频转码配置
|
||
type TranscodeConfig struct {
|
||
InputPath string // 输入文件路径(MP3/WAV)
|
||
OutputPath string // 输出文件路径(G711A)
|
||
SampleRate int // 采样率(默认 8000 Hz,G711A 标准采样率)
|
||
Channels int // 声道数(默认 1,单声道)
|
||
}
|
||
|
||
// 默认转码配置
|
||
func defaultTranscodeConfig(input, output string) TranscodeConfig {
|
||
return TranscodeConfig{
|
||
InputPath: input,
|
||
OutputPath: output,
|
||
SampleRate: 8000, // G711A 标准采样率
|
||
Channels: 1, // 单声道(电话/语音常用)
|
||
}
|
||
}
|
||
|
||
// 校验输入文件类型(仅允许 MP3/WAV)
|
||
func validateInputFile(inputPath string) error {
|
||
// 检查文件是否存在
|
||
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
|
||
return fmt.Errorf("输入文件不存在: %s", inputPath)
|
||
}
|
||
|
||
// 校验文件扩展名
|
||
ext := strings.ToLower(filepath.Ext(inputPath))
|
||
if ext != ".mp3" && ext != ".wav" {
|
||
return fmt.Errorf("仅支持 MP3/WAV 格式,当前文件扩展名: %s", ext)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// MP3/WAV 转 G711A(PCMA)
|
||
func TranscodeToG711A(config TranscodeConfig) error {
|
||
// 1. 校验输入文件
|
||
if err := validateInputFile(config.InputPath); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 2. 补全默认配置
|
||
if config.SampleRate <= 0 {
|
||
config.SampleRate = 8000
|
||
}
|
||
if config.Channels <= 0 {
|
||
config.Channels = 1
|
||
}
|
||
|
||
// 3. 确保输出目录存在
|
||
outputDir := filepath.Dir(config.OutputPath)
|
||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||
return fmt.Errorf("创建输出目录失败: %v", err)
|
||
}
|
||
|
||
// 4. 构建 FFmpeg 命令
|
||
// 核心参数说明:
|
||
// -i: 输入文件
|
||
// -ar: 采样率
|
||
// -ac: 声道数
|
||
// -f: 输出格式(alaw 即 G711A)
|
||
// -y: 覆盖已存在的输出文件
|
||
cmdArgs := []string{
|
||
"-i", config.InputPath,
|
||
"-ar", fmt.Sprintf("%d", config.SampleRate),
|
||
"-ac", fmt.Sprintf("%d", config.Channels),
|
||
"-f", "alaw", // 指定输出格式为 G711A(PCMA)
|
||
"-y", // 覆盖输出文件(无需确认)
|
||
config.OutputPath,
|
||
}
|
||
dir, _ := os.Getwd() // Windows 路径用反斜杠,或双正斜杠
|
||
ffmpegPath := ""
|
||
switch runtime.GOOS {
|
||
case "linux":
|
||
ffmpegPath = filepath.Join(dir, "ffmpeg")
|
||
case "windows":
|
||
ffmpegPath = filepath.Join(dir, "ffmpeg.exe")
|
||
}
|
||
|
||
// 执行 FFmpeg 命令
|
||
cmd := exec.Command(ffmpegPath, cmdArgs...)
|
||
// 捕获 FFmpeg 输出(便于调试)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
return fmt.Errorf("转码失败: %v, FFmpeg 输出: %s", err, string(output))
|
||
}
|
||
|
||
// 校验输出文件是否生成
|
||
if _, err := os.Stat(config.OutputPath); os.IsNotExist(err) {
|
||
return fmt.Errorf("转码后输出文件未生成: %s", config.OutputPath)
|
||
}
|
||
|
||
fmt.Printf("转码成功!输入: %s → 输出: %s\n", config.InputPath, config.OutputPath)
|
||
return nil
|
||
}
|
||
func TranscodeToG711AFile(inputFile, outputFile string) error {
|
||
// 也可自定义配置(比如调整采样率)
|
||
/*
|
||
customConfig := TranscodeConfig{
|
||
InputPath: "./uploads/test.wav",
|
||
OutputPath: "./uploads/test_custom.g711a",
|
||
SampleRate: 16000, // 自定义采样率
|
||
Channels: 1,
|
||
}
|
||
err = TranscodeToG711A(customConfig)
|
||
*/
|
||
// 使用默认配置转码
|
||
err := TranscodeToG711A(defaultTranscodeConfig(inputFile, outputFile))
|
||
if err != nil {
|
||
fmt.Printf("转码失败: %v\n", err)
|
||
return fmt.Errorf("转码失败: %v\n", err)
|
||
}
|
||
return nil
|
||
|
||
}
|
||
|
||
// GetMP3Duration 获取MP3文件时长
|
||
func GetMP3Duration(filePath string) (time.Duration, error) {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer file.Close()
|
||
|
||
decoder := mp3.NewDecoder(file)
|
||
var frame mp3.Frame
|
||
var totalDuration float64
|
||
skipped := 0
|
||
|
||
for {
|
||
if err := decoder.Decode(&frame, &skipped); err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
return 0, err
|
||
}
|
||
totalDuration += frame.Duration().Seconds()
|
||
}
|
||
|
||
duration := time.Duration(totalDuration * float64(time.Second))
|
||
return duration, nil
|
||
}
|
||
|
||
// GetWAVDurationOptimized 优化的WAV文件时长获取方法
|
||
func GetWAVDurationOptimized(filePath string) (time.Duration, error) {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer file.Close()
|
||
|
||
reader := wav.NewReader(file)
|
||
|
||
// 使用库提供的Duration方法
|
||
duration, err := reader.Duration()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
return duration, nil
|
||
}
|
||
|
||
// GetAudioDuration 获取音频文件时长(支持MP3和WAV)
|
||
func GetAudioDuration(filePath string) (time.Duration, string, error) {
|
||
// 根据文件扩展名判断文件类型
|
||
if len(filePath) > 4 {
|
||
ext := filePath[len(filePath)-4:]
|
||
switch ext {
|
||
case ".mp3":
|
||
duration, err := GetMP3Duration(filePath)
|
||
return duration, "MP3", err
|
||
case ".wav":
|
||
duration, err := GetWAVDurationOptimized(filePath)
|
||
return duration, "WAV", err
|
||
default:
|
||
return 0, "", fmt.Errorf("不支持的音频格式: %s", ext)
|
||
}
|
||
}
|
||
|
||
// 如果无法从扩展名判断,可以尝试根据文件头部信息判断
|
||
return 0, "", fmt.Errorf("无法识别的音频格式")
|
||
}
|