audio.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package audio
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "io"
  7. "os/exec"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "github.com/labring/aiproxy/core/common/config"
  12. log "github.com/sirupsen/logrus"
  13. )
  14. var (
  15. ErrAudioDurationNAN = errors.New("audio duration is N/A")
  16. re = regexp.MustCompile(`time=(\d+:\d+:\d+\.\d+)`)
  17. )
  18. func GetAudioDuration(ctx context.Context, audio io.Reader) (float64, error) {
  19. if !config.FfmpegEnabled {
  20. return 0, nil
  21. }
  22. ffprobeCmd := exec.CommandContext(
  23. ctx,
  24. "ffprobe",
  25. "-v", "error",
  26. "-select_streams", "a:0",
  27. "-show_entries", "stream=duration",
  28. "-of", "default=noprint_wrappers=1:nokey=1",
  29. "-i", "-",
  30. )
  31. ffprobeCmd.Stdin = audio
  32. output, err := ffprobeCmd.Output()
  33. if err != nil {
  34. return 0, err
  35. }
  36. str := strings.TrimSpace(string(output))
  37. if str == "" || str == "N/A" {
  38. seeker, ok := audio.(io.Seeker)
  39. if !ok {
  40. return 0, ErrAudioDurationNAN
  41. }
  42. _, err := seeker.Seek(0, io.SeekStart)
  43. if err != nil {
  44. return 0, ErrAudioDurationNAN
  45. }
  46. return getAudioDurationFallback(ctx, audio)
  47. }
  48. duration, err := strconv.ParseFloat(str, 64)
  49. if err != nil {
  50. return 0, err
  51. }
  52. return duration, nil
  53. }
  54. func getAudioDurationFallback(ctx context.Context, audio io.Reader) (float64, error) {
  55. if !config.FfmpegEnabled {
  56. return 0, nil
  57. }
  58. ffmpegCmd := exec.CommandContext(
  59. ctx,
  60. "ffmpeg",
  61. "-i", "-",
  62. "-f", "null", "-",
  63. )
  64. ffmpegCmd.Stdin = audio
  65. var stderr bytes.Buffer
  66. ffmpegCmd.Stderr = &stderr
  67. err := ffmpegCmd.Run()
  68. if err != nil {
  69. return 0, err
  70. }
  71. log.Debugf("ffmpeg -i - -f null -\n%s", stderr.Bytes())
  72. // Parse the time from ffmpeg output
  73. // Example: size=N/A time=00:00:05.52 bitrate=N/A speed= 785x
  74. return parseTimeFromFfmpegOutput(stderr.String())
  75. }
  76. func GetAudioDurationFromFilePath(ctx context.Context, filePath string) (float64, error) {
  77. if !config.FfmpegEnabled {
  78. return 0, nil
  79. }
  80. ffprobeCmd := exec.CommandContext(
  81. ctx,
  82. "ffprobe",
  83. "-v", "error",
  84. "-select_streams", "a:0",
  85. "-show_entries", "format=duration",
  86. "-of", "default=noprint_wrappers=1:nokey=1",
  87. "-i", filePath,
  88. )
  89. output, err := ffprobeCmd.Output()
  90. if err != nil {
  91. return 0, err
  92. }
  93. str := strings.TrimSpace(string(output))
  94. if str == "" || str == "N/A" {
  95. return getAudioDurationFromFilePathFallback(ctx, filePath)
  96. }
  97. duration, err := strconv.ParseFloat(str, 64)
  98. if err != nil {
  99. return 0, err
  100. }
  101. return duration, nil
  102. }
  103. func getAudioDurationFromFilePathFallback(ctx context.Context, filePath string) (float64, error) {
  104. if !config.FfmpegEnabled {
  105. return 0, nil
  106. }
  107. ffmpegCmd := exec.CommandContext(
  108. ctx,
  109. "ffmpeg",
  110. "-i", filePath,
  111. "-f", "null", "-",
  112. )
  113. var stderr bytes.Buffer
  114. ffmpegCmd.Stderr = &stderr
  115. err := ffmpegCmd.Run()
  116. if err != nil {
  117. return 0, err
  118. }
  119. log.Debugf("ffmpeg -i %s -f null -\n%s", filePath, stderr.Bytes())
  120. // Parse the time from ffmpeg output
  121. return parseTimeFromFfmpegOutput(stderr.String())
  122. }
  123. // parseTimeFromFfmpegOutput extracts and converts time from ffmpeg output to seconds
  124. func parseTimeFromFfmpegOutput(output string) (float64, error) {
  125. // Find all matches of time pattern
  126. matches := re.FindAllStringSubmatch(output, -1)
  127. if len(matches) == 0 {
  128. return 0, ErrAudioDurationNAN
  129. }
  130. // Get the last time match (as per the instruction)
  131. match := matches[len(matches)-1]
  132. if len(match) < 2 {
  133. return 0, ErrAudioDurationNAN
  134. }
  135. // Convert time format HH:MM:SS.MS to seconds
  136. timeStr := match[1]
  137. parts := strings.Split(timeStr, ":")
  138. if len(parts) != 3 {
  139. return 0, errors.New("invalid time format")
  140. }
  141. hours, err := strconv.ParseFloat(parts[0], 64)
  142. if err != nil {
  143. return 0, err
  144. }
  145. minutes, err := strconv.ParseFloat(parts[1], 64)
  146. if err != nil {
  147. return 0, err
  148. }
  149. seconds, err := strconv.ParseFloat(parts[2], 64)
  150. if err != nil {
  151. return 0, err
  152. }
  153. duration := hours*3600 + minutes*60 + seconds
  154. return duration, nil
  155. }