1
0
mirror of https://github.com/aNNiMON/ffmpegbot synced 2024-09-19 22:54:20 +03:00

Add audio spectrum output

This commit is contained in:
aNNiMON 2024-09-15 19:53:04 +03:00
parent 5ca02ba7d4
commit bebc9ce911
8 changed files with 78 additions and 34 deletions

View File

@ -5,15 +5,19 @@ import com.annimon.ffmpegbot.file.FilePath;
import com.annimon.ffmpegbot.session.FileType; import com.annimon.ffmpegbot.session.FileType;
import com.annimon.ffmpegbot.session.FileTypes; import com.annimon.ffmpegbot.session.FileTypes;
import com.annimon.ffmpegbot.session.MediaSession; import com.annimon.ffmpegbot.session.MediaSession;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FFmpegCommandBuilder implements Visitor<MediaSession> { public class FFmpegCommandBuilder implements Visitor<MediaSession> {
private boolean discardAudio; private boolean discardAudio;
private String recipe;
private final List<String> audioCommands; private final List<String> audioCommands;
private final List<String> videoCommands; private final List<String> videoCommands;
private final List<String> audioFilters; private final List<String> audioFilters;
@ -26,6 +30,7 @@ public class FFmpegCommandBuilder implements Visitor<MediaSession> {
audioFilters = new ArrayList<>(); audioFilters = new ArrayList<>();
videoFilters = new ArrayList<>(); videoFilters = new ArrayList<>();
eq = new ArrayList<>(); eq = new ArrayList<>();
recipe = "";
} }
@Override @Override
@ -145,6 +150,7 @@ public class FFmpegCommandBuilder implements Visitor<MediaSession> {
public void visit(OutputFormat p, MediaSession session) { public void visit(OutputFormat p, MediaSession session) {
final String localFilename = session.getInputFile().getName(); final String localFilename = session.getInputFile().getName();
String additionalExtension = ""; String additionalExtension = "";
recipe = "";
switch (p.getValue()) { switch (p.getValue()) {
case OutputFormat.VIDEO -> { case OutputFormat.VIDEO -> {
@ -159,6 +165,11 @@ public class FFmpegCommandBuilder implements Visitor<MediaSession> {
session.setFileType(FileType.AUDIO); session.setFileType(FileType.AUDIO);
additionalExtension = ".mp3"; additionalExtension = ".mp3";
} }
case OutputFormat.AUDIO_SPECTRUM -> {
session.setFileType(FileType.PHOTO);
additionalExtension = ".jpg";
recipe = OutputFormat.AUDIO_SPECTRUM;
}
} }
if (localFilename.toLowerCase(Locale.ENGLISH).endsWith(additionalExtension)) { if (localFilename.toLowerCase(Locale.ENGLISH).endsWith(additionalExtension)) {
@ -167,26 +178,43 @@ public class FFmpegCommandBuilder implements Visitor<MediaSession> {
session.setOutputFile(FilePath.outputFile(localFilename + additionalExtension)); session.setOutputFile(FilePath.outputFile(localFilename + additionalExtension));
} }
private List<String> visitRecipe(String recipe) {
return switch (recipe) {
case OutputFormat.AUDIO_SPECTRUM -> List.of(
"-vn",
"-filter_complex",
Stream.concat(audioFilters.stream(), Stream.of("showspectrumpic=s=1200x640:mode=separate"))
.collect(Collectors.joining(",")),
"-frames:v", "1"
);
default -> List.of();
};
}
public String[] buildCommand(final @NotNull MediaSession session) { public String[] buildCommand(final @NotNull MediaSession session) {
final var commands = new ArrayList<String>(); final var commands = new ArrayList<String>();
commands.addAll(List.of("ffmpeg", "-loglevel", "error", "-stats")); commands.addAll(List.of("ffmpeg", "-loglevel", "error", "-stats"));
commands.addAll(session.getInputParams().asFFmpegCommands()); commands.addAll(session.getInputParams().asFFmpegCommands());
commands.addAll(List.of("-i", FilePath.inputDir() + "/" + session.getInputFile().getName())); commands.addAll(List.of("-i", FilePath.inputDir() + "/" + session.getInputFile().getName()));
if (FileTypes.canContainAudio(session.getFileType())) { if (StringUtils.isNotEmpty(recipe)) {
commands.addAll(audioCommands); commands.addAll(visitRecipe(recipe));
if (!audioFilters.isEmpty()) { } else {
commands.add("-af"); if (FileTypes.canContainAudio(session.getFileType())) {
commands.add(String.join(",", audioFilters)); commands.addAll(audioCommands);
if (!audioFilters.isEmpty()) {
commands.add("-af");
commands.add(String.join(",", audioFilters));
}
} }
} if (FileTypes.canContainVideo(session.getFileType())) {
if (FileTypes.canContainVideo(session.getFileType())) { commands.addAll(videoCommands);
commands.addAll(videoCommands); if (!eq.isEmpty()) {
if (!eq.isEmpty()) { videoFilters.add("eq=" + String.join(":", eq));
videoFilters.add("eq=" + String.join(":", eq)); }
} if (!videoFilters.isEmpty()) {
if (!videoFilters.isEmpty()) { commands.add("-vf");
commands.add("-vf"); commands.add(String.join(",", videoFilters));
commands.add(String.join(",", videoFilters)); }
} }
} }
commands.addAll(List.of("-y", FilePath.outputDir() + "/" + session.getOutputFile().getName())); commands.addAll(List.of("-y", FilePath.outputDir() + "/" + session.getOutputFile().getName()));

View File

@ -34,6 +34,7 @@ public class FilePath {
case ANIMATION, VIDEO, VIDEO_NOTE -> "mp4"; case ANIMATION, VIDEO, VIDEO_NOTE -> "mp4";
case AUDIO -> "mp3"; case AUDIO -> "mp3";
case VOICE -> "ogg"; case VOICE -> "ogg";
case PHOTO -> "jpg";
}; };
} }
} }

View File

@ -2,9 +2,8 @@ package com.annimon.ffmpegbot.parameters;
import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; import com.annimon.ffmpegbot.commands.ffmpeg.Visitor;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.function.Predicate;
import java.util.Objects;
public class OutputFormat extends StringParameter { public class OutputFormat extends StringParameter {
public static final String ID = "output"; public static final String ID = "output";
@ -12,15 +11,17 @@ public class OutputFormat extends StringParameter {
public static final String VIDEO = "VIDEO"; public static final String VIDEO = "VIDEO";
public static final String AUDIO = "AUDIO"; public static final String AUDIO = "AUDIO";
public static final String VIDEO_NOTE = "VIDEO NOTE"; public static final String VIDEO_NOTE = "VIDEO NOTE";
public static final String AUDIO_SPECTRUM = "AUDIO SPECTRUM";
public OutputFormat(List<String> values, String initialValue) { public OutputFormat(List<String> values, String initialValue) {
super(ID, "➡️ Output", values, initialValue); super(ID, "➡️ Output", values, initialValue);
} }
public OutputFormat disableFormat(String format) { public OutputFormat disableFormat(String... formats) {
if (possibleValues.size() <= 1) return this; if (possibleValues.size() <= 1) return this;
final Set<String> set = Set.of(formats);
final var values = possibleValues.stream() final var values = possibleValues.stream()
.filter(f -> !Objects.equals(f, format)) .filter(Predicate.not(set::contains))
.map(Objects::toString) .map(Objects::toString)
.toList(); .toList();
if (possibleValues.size() == values.size()) { if (possibleValues.size() == values.size()) {
@ -29,14 +30,13 @@ public class OutputFormat extends StringParameter {
return new OutputFormat(values, values.get(0)); return new OutputFormat(values, values.get(0));
} }
public OutputFormat enableFormat(String format) { public OutputFormat enableFormat(String... formats) {
boolean contains = possibleValues.stream() final Set<String> newset = new LinkedHashSet<>(possibleValues);
.anyMatch(f -> Objects.equals(f, format)); if (newset.addAll(Set.of(formats))) {
if (contains) return this; final var values = new ArrayList<>(newset);
return new OutputFormat(new ArrayList<>(values), values.get(0));
final var values = new ArrayList<String>(possibleValues); }
values.add(format); return this;
return new OutputFormat(values, values.get(0));
} }
@Override @Override

View File

@ -13,7 +13,7 @@ public class AudioResolver implements ParametersResolver {
@Override @Override
public void resolve(@NotNull Parameters parameters, @NotNull FileInfo fileInfo) { public void resolve(@NotNull Parameters parameters, @NotNull FileInfo fileInfo) {
final boolean hasAudio = switch (fileInfo.fileType()) { final boolean hasAudio = switch (fileInfo.fileType()) {
case ANIMATION -> false; case PHOTO, ANIMATION -> false;
case AUDIO, VOICE -> true; case AUDIO, VOICE -> true;
case VIDEO, VIDEO_NOTE -> true; // TODO: add actual ffprobe check for audio case VIDEO, VIDEO_NOTE -> true; // TODO: add actual ffprobe check for audio
}; };
@ -44,18 +44,29 @@ public class AudioResolver implements ParametersResolver {
Optional<OutputFormat> outputFormat = parameters.findById(OutputFormat.ID, OutputFormat.class); Optional<OutputFormat> outputFormat = parameters.findById(OutputFormat.ID, OutputFormat.class);
if (p.getValueAsPrimitive()) { if (p.getValueAsPrimitive()) {
parameters.disableAll(parameterIds); parameters.disableAll(parameterIds);
outputFormat.ifPresent(par -> parameters.add(par.disableFormat(OutputFormat.AUDIO))); outputFormat.ifPresent(par -> parameters.add(par.disableFormat(OutputFormat.AUDIO, OutputFormat.AUDIO_SPECTRUM)));
} else { } else {
parameters.enableAll(parameterIds); parameters.enableAll(parameterIds);
outputFormat.ifPresent(par -> parameters.add(par.enableFormat(OutputFormat.AUDIO))); outputFormat.ifPresent(par -> parameters.add(par.enableFormat(OutputFormat.AUDIO, OutputFormat.AUDIO_SPECTRUM)));
} }
}); });
parameters.findById(OutputFormat.ID, OutputFormat.class)
.map(Parameter::getValue)
.ifPresent(format -> {
// Audio spectrum ignores audio commands (bitrate)
if (format.equals(OutputFormat.AUDIO_SPECTRUM)) {
parameters.disable(AudioBitrate.ID);
} else {
parameters.enable(AudioBitrate.ID);
}
});
} }
private void disableAudioParam(@NotNull Parameters parameters, @NotNull FileType fileType) { private void disableAudioParam(@NotNull Parameters parameters, @NotNull FileType fileType) {
final boolean canAudioBeDisabled = switch (fileType) { final boolean canAudioBeDisabled = switch (fileType) {
case ANIMATION, AUDIO, VOICE -> false; case ANIMATION, AUDIO, VOICE -> false;
case VIDEO, VIDEO_NOTE -> true; case PHOTO, VIDEO, VIDEO_NOTE -> true;
}; };
if (canAudioBeDisabled) { if (canAudioBeDisabled) {
parameters.add(new DisableAudio()); parameters.add(new DisableAudio());

View File

@ -15,7 +15,7 @@ public class OutputFormatResolver implements ParametersResolver {
public void resolve(@NotNull Parameters parameters, @NotNull FileInfo fileInfo) { public void resolve(@NotNull Parameters parameters, @NotNull FileInfo fileInfo) {
final var outputFormat = switch (fileInfo.fileType()) { final var outputFormat = switch (fileInfo.fileType()) {
case VIDEO -> forVideo(fileInfo); case VIDEO -> forVideo(fileInfo);
case VIDEO_NOTE -> new OutputFormat(List.of(VIDEO_NOTE, VIDEO, AUDIO), VIDEO_NOTE); case VIDEO_NOTE -> new OutputFormat(List.of(VIDEO_NOTE, VIDEO, AUDIO, AUDIO_SPECTRUM), VIDEO_NOTE);
case ANIMATION -> forAnimation(fileInfo); case ANIMATION -> forAnimation(fileInfo);
default -> null; default -> null;
}; };
@ -32,6 +32,7 @@ public class OutputFormatResolver implements ParametersResolver {
types.add(VIDEO_NOTE); types.add(VIDEO_NOTE);
} }
types.add(AUDIO); types.add(AUDIO);
types.add(AUDIO_SPECTRUM);
return new OutputFormat(types, VIDEO); return new OutputFormat(types, VIDEO);
} }

View File

@ -4,7 +4,6 @@ import com.annimon.ffmpegbot.parameters.*;
import com.annimon.ffmpegbot.session.FileInfo; import com.annimon.ffmpegbot.session.FileInfo;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
public class VideoResolver implements ParametersResolver { public class VideoResolver implements ParametersResolver {
@ -41,7 +40,8 @@ public class VideoResolver implements ParametersResolver {
VideoScale.ID, VideoScale.ID,
VideoFrameRate.ID VideoFrameRate.ID
); );
if (Objects.equals(format, OutputFormat.AUDIO)) { Set<String> audioOnly = Set.of(OutputFormat.AUDIO, OutputFormat.AUDIO_SPECTRUM);
if (audioOnly.contains(format)) {
parameters.disableAll(parameterIds); parameters.disableAll(parameterIds);
} else { } else {
parameters.enableAll(parameterIds); parameters.enableAll(parameterIds);

View File

@ -1,6 +1,7 @@
package com.annimon.ffmpegbot.session; package com.annimon.ffmpegbot.session;
public enum FileType { public enum FileType {
PHOTO,
ANIMATION, ANIMATION,
AUDIO, AUDIO,
VIDEO, VIDEO,

View File

@ -57,6 +57,7 @@ public class Resolver {
return switch (fileType) { return switch (fileType) {
case ANIMATION -> Methods.sendAnimation(); case ANIMATION -> Methods.sendAnimation();
case AUDIO -> Methods.sendAudio(); case AUDIO -> Methods.sendAudio();
case PHOTO -> Methods.sendPhoto();
case VIDEO -> Methods.sendVideo(); case VIDEO -> Methods.sendVideo();
case VIDEO_NOTE -> Methods.sendVideoNote(); case VIDEO_NOTE -> Methods.sendVideoNote();
case VOICE -> Methods.sendVoice(); case VOICE -> Methods.sendVoice();
@ -65,6 +66,7 @@ public class Resolver {
public static ActionType resolveAction(@NotNull FileType fileType) { public static ActionType resolveAction(@NotNull FileType fileType) {
return switch (fileType) { return switch (fileType) {
case PHOTO -> ActionType.UPLOAD_PHOTO;
case VIDEO -> ActionType.UPLOAD_VIDEO; case VIDEO -> ActionType.UPLOAD_VIDEO;
case VIDEO_NOTE -> ActionType.UPLOAD_VIDEO_NOTE; case VIDEO_NOTE -> ActionType.UPLOAD_VIDEO_NOTE;
case VOICE -> ActionType.UPLOAD_VOICE; case VOICE -> ActionType.UPLOAD_VOICE;