Last active
August 10, 2025 06:19
-
-
Save wjkoh/5f8748a589507dea28875f1301b24ffb to your computer and use it in GitHub Desktop.
Go + FFmpeg: How to Segment (Chunk) Audio File
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type File struct { | |
| Name string | |
| Start time.Duration | |
| End time.Duration | |
| ContentType string | |
| } | |
| func SegmentAudio(ctx context.Context, filename string, maxDuration time.Duration, dir string, pattern string) ([]*File, error) { | |
| if dir == "" { | |
| dir = os.TempDir() | |
| } | |
| if pattern == "" { | |
| pattern = "%d" | |
| } | |
| if strings.Contains(fmt.Sprintf(pattern, 0), "%!") { | |
| return nil, fmt.Errorf("invalid pattern: %v", pattern) | |
| } | |
| formatName, err := FormatName(ctx, filename) | |
| if err != nil { | |
| return nil, err | |
| } | |
| segmentTime := strconv.FormatFloat(maxDuration.Seconds(), 'f', -1, 64) | |
| f, err := os.CreateTemp("", "") | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer os.Remove(f.Name()) | |
| defer f.Close() | |
| ctx, cancel := context.WithCancel(ctx) | |
| defer cancel() | |
| cmd := exec.CommandContext( | |
| ctx, | |
| "ffmpeg", | |
| "-f", formatName, | |
| "-i", filename, | |
| "-codec", "copy", | |
| "-map", "0", | |
| "-f", "segment", | |
| "-segment_list_type", "csv", | |
| "-segment_list", f.Name(), | |
| "-segment_time", segmentTime, | |
| "-segment_format", formatName, | |
| filepath.Join(dir, pattern), | |
| ) | |
| stderr, err := cmd.StderrPipe() | |
| if err != nil { | |
| return nil, err | |
| } | |
| if err := cmd.Start(); err != nil { | |
| return nil, err | |
| } | |
| slurp, _ := io.ReadAll(stderr) | |
| slog.Debug("ffmpeg", slog.String("stderr", string(slurp))) | |
| if err := cmd.Wait(); err != nil { | |
| return nil, err | |
| } | |
| var files []*File | |
| csvReader := csv.NewReader(f) | |
| for { | |
| record, err := csvReader.Read() | |
| if err == io.EOF { | |
| break | |
| } | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(record) != 3 { | |
| return nil, fmt.Errorf("invalid segment list: %v", record) | |
| } | |
| fname := filepath.Join(dir, record[0]) | |
| f, err := os.Open(fname) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer f.Close() | |
| contentType, err := detectContentType(f) | |
| if err != nil { | |
| return nil, err | |
| } | |
| start, err := time.ParseDuration(record[1] + "s") | |
| if err != nil { | |
| return nil, err | |
| } | |
| end, err := time.ParseDuration(record[2] + "s") | |
| if err != nil { | |
| return nil, err | |
| } | |
| files = append(files, &File{Name: fname, Start: start, End: end, ContentType: contentType}) | |
| } | |
| return files, nil | |
| } | |
| func FormatName(ctx context.Context, filename string) (string, error) { | |
| ctx, cancel := context.WithCancel(ctx) | |
| defer cancel() | |
| cmd := exec.CommandContext(ctx, "ffprobe", "-i", filename, "-show_format", "-output_format", "json") | |
| stdout, err := cmd.StdoutPipe() | |
| if err != nil { | |
| return "", nil | |
| } | |
| if err := cmd.Start(); err != nil { | |
| return "", nil | |
| } | |
| var output struct { | |
| Format struct { | |
| FormatName string `json:"format_name"` | |
| Duration string `json:"duration"` | |
| } `json:"format"` | |
| } | |
| if err := json.NewDecoder(stdout).Decode(&output); err != nil { | |
| return "", nil | |
| } | |
| if err := cmd.Wait(); err != nil { | |
| return "", nil | |
| } | |
| return output.Format.FormatName, nil | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment