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("无法识别的音频格式") }