From bebc9ce911695ad2ae593a0a02f4fcae993390da Mon Sep 17 00:00:00 2001 From: aNNiMON Date: Sun, 15 Sep 2024 19:53:04 +0300 Subject: [PATCH] Add audio spectrum output --- .../commands/ffmpeg/FFmpegCommandBuilder.java | 56 ++++++++++++++----- .../com/annimon/ffmpegbot/file/FilePath.java | 1 + .../ffmpegbot/parameters/OutputFormat.java | 26 ++++----- .../parameters/resolvers/AudioResolver.java | 19 +++++-- .../resolvers/OutputFormatResolver.java | 3 +- .../parameters/resolvers/VideoResolver.java | 4 +- .../annimon/ffmpegbot/session/FileType.java | 1 + .../annimon/ffmpegbot/session/Resolver.java | 2 + 8 files changed, 78 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java index b1b56d4..9411ec7 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java @@ -5,15 +5,19 @@ import com.annimon.ffmpegbot.file.FilePath; import com.annimon.ffmpegbot.session.FileType; import com.annimon.ffmpegbot.session.FileTypes; import com.annimon.ffmpegbot.session.MediaSession; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class FFmpegCommandBuilder implements Visitor { private boolean discardAudio; + private String recipe; private final List audioCommands; private final List videoCommands; private final List audioFilters; @@ -26,6 +30,7 @@ public class FFmpegCommandBuilder implements Visitor { audioFilters = new ArrayList<>(); videoFilters = new ArrayList<>(); eq = new ArrayList<>(); + recipe = ""; } @Override @@ -145,6 +150,7 @@ public class FFmpegCommandBuilder implements Visitor { public void visit(OutputFormat p, MediaSession session) { final String localFilename = session.getInputFile().getName(); String additionalExtension = ""; + recipe = ""; switch (p.getValue()) { case OutputFormat.VIDEO -> { @@ -159,6 +165,11 @@ public class FFmpegCommandBuilder implements Visitor { session.setFileType(FileType.AUDIO); additionalExtension = ".mp3"; } + case OutputFormat.AUDIO_SPECTRUM -> { + session.setFileType(FileType.PHOTO); + additionalExtension = ".jpg"; + recipe = OutputFormat.AUDIO_SPECTRUM; + } } if (localFilename.toLowerCase(Locale.ENGLISH).endsWith(additionalExtension)) { @@ -167,26 +178,43 @@ public class FFmpegCommandBuilder implements Visitor { session.setOutputFile(FilePath.outputFile(localFilename + additionalExtension)); } + private List 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) { final var commands = new ArrayList(); commands.addAll(List.of("ffmpeg", "-loglevel", "error", "-stats")); commands.addAll(session.getInputParams().asFFmpegCommands()); commands.addAll(List.of("-i", FilePath.inputDir() + "/" + session.getInputFile().getName())); - if (FileTypes.canContainAudio(session.getFileType())) { - commands.addAll(audioCommands); - if (!audioFilters.isEmpty()) { - commands.add("-af"); - commands.add(String.join(",", audioFilters)); + if (StringUtils.isNotEmpty(recipe)) { + commands.addAll(visitRecipe(recipe)); + } else { + if (FileTypes.canContainAudio(session.getFileType())) { + commands.addAll(audioCommands); + if (!audioFilters.isEmpty()) { + commands.add("-af"); + commands.add(String.join(",", audioFilters)); + } } - } - if (FileTypes.canContainVideo(session.getFileType())) { - commands.addAll(videoCommands); - if (!eq.isEmpty()) { - videoFilters.add("eq=" + String.join(":", eq)); - } - if (!videoFilters.isEmpty()) { - commands.add("-vf"); - commands.add(String.join(",", videoFilters)); + if (FileTypes.canContainVideo(session.getFileType())) { + commands.addAll(videoCommands); + if (!eq.isEmpty()) { + videoFilters.add("eq=" + String.join(":", eq)); + } + if (!videoFilters.isEmpty()) { + commands.add("-vf"); + commands.add(String.join(",", videoFilters)); + } } } commands.addAll(List.of("-y", FilePath.outputDir() + "/" + session.getOutputFile().getName())); diff --git a/src/main/java/com/annimon/ffmpegbot/file/FilePath.java b/src/main/java/com/annimon/ffmpegbot/file/FilePath.java index aaa2c5d..5036126 100644 --- a/src/main/java/com/annimon/ffmpegbot/file/FilePath.java +++ b/src/main/java/com/annimon/ffmpegbot/file/FilePath.java @@ -34,6 +34,7 @@ public class FilePath { case ANIMATION, VIDEO, VIDEO_NOTE -> "mp4"; case AUDIO -> "mp3"; case VOICE -> "ogg"; + case PHOTO -> "jpg"; }; } } diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java b/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java index 97fcad6..2d85035 100644 --- a/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java +++ b/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java @@ -2,9 +2,8 @@ package com.annimon.ffmpegbot.parameters; import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.function.Predicate; public class OutputFormat extends StringParameter { 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 AUDIO = "AUDIO"; public static final String VIDEO_NOTE = "VIDEO NOTE"; + public static final String AUDIO_SPECTRUM = "AUDIO SPECTRUM"; public OutputFormat(List values, String initialValue) { super(ID, "➡️ Output", values, initialValue); } - public OutputFormat disableFormat(String format) { + public OutputFormat disableFormat(String... formats) { if (possibleValues.size() <= 1) return this; + final Set set = Set.of(formats); final var values = possibleValues.stream() - .filter(f -> !Objects.equals(f, format)) + .filter(Predicate.not(set::contains)) .map(Objects::toString) .toList(); if (possibleValues.size() == values.size()) { @@ -29,14 +30,13 @@ public class OutputFormat extends StringParameter { return new OutputFormat(values, values.get(0)); } - public OutputFormat enableFormat(String format) { - boolean contains = possibleValues.stream() - .anyMatch(f -> Objects.equals(f, format)); - if (contains) return this; - - final var values = new ArrayList(possibleValues); - values.add(format); - return new OutputFormat(values, values.get(0)); + public OutputFormat enableFormat(String... formats) { + final Set newset = new LinkedHashSet<>(possibleValues); + if (newset.addAll(Set.of(formats))) { + final var values = new ArrayList<>(newset); + return new OutputFormat(new ArrayList<>(values), values.get(0)); + } + return this; } @Override diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/AudioResolver.java b/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/AudioResolver.java index 7a8d30c..e31cec0 100644 --- a/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/AudioResolver.java +++ b/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/AudioResolver.java @@ -13,7 +13,7 @@ public class AudioResolver implements ParametersResolver { @Override public void resolve(@NotNull Parameters parameters, @NotNull FileInfo fileInfo) { final boolean hasAudio = switch (fileInfo.fileType()) { - case ANIMATION -> false; + case PHOTO, ANIMATION -> false; case AUDIO, VOICE -> true; case VIDEO, VIDEO_NOTE -> true; // TODO: add actual ffprobe check for audio }; @@ -44,18 +44,29 @@ public class AudioResolver implements ParametersResolver { Optional outputFormat = parameters.findById(OutputFormat.ID, OutputFormat.class); if (p.getValueAsPrimitive()) { 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 { 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) { final boolean canAudioBeDisabled = switch (fileType) { case ANIMATION, AUDIO, VOICE -> false; - case VIDEO, VIDEO_NOTE -> true; + case PHOTO, VIDEO, VIDEO_NOTE -> true; }; if (canAudioBeDisabled) { parameters.add(new DisableAudio()); diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/OutputFormatResolver.java b/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/OutputFormatResolver.java index caaccf5..3fe41fb 100644 --- a/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/OutputFormatResolver.java +++ b/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/OutputFormatResolver.java @@ -15,7 +15,7 @@ public class OutputFormatResolver implements ParametersResolver { public void resolve(@NotNull Parameters parameters, @NotNull FileInfo fileInfo) { final var outputFormat = switch (fileInfo.fileType()) { 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); default -> null; }; @@ -32,6 +32,7 @@ public class OutputFormatResolver implements ParametersResolver { types.add(VIDEO_NOTE); } types.add(AUDIO); + types.add(AUDIO_SPECTRUM); return new OutputFormat(types, VIDEO); } diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/VideoResolver.java b/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/VideoResolver.java index 9bfd2ca..89bacf3 100644 --- a/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/VideoResolver.java +++ b/src/main/java/com/annimon/ffmpegbot/parameters/resolvers/VideoResolver.java @@ -4,7 +4,6 @@ import com.annimon.ffmpegbot.parameters.*; import com.annimon.ffmpegbot.session.FileInfo; import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.Objects; import java.util.Set; public class VideoResolver implements ParametersResolver { @@ -41,7 +40,8 @@ public class VideoResolver implements ParametersResolver { VideoScale.ID, VideoFrameRate.ID ); - if (Objects.equals(format, OutputFormat.AUDIO)) { + Set audioOnly = Set.of(OutputFormat.AUDIO, OutputFormat.AUDIO_SPECTRUM); + if (audioOnly.contains(format)) { parameters.disableAll(parameterIds); } else { parameters.enableAll(parameterIds); diff --git a/src/main/java/com/annimon/ffmpegbot/session/FileType.java b/src/main/java/com/annimon/ffmpegbot/session/FileType.java index cfa493e..926777a 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/FileType.java +++ b/src/main/java/com/annimon/ffmpegbot/session/FileType.java @@ -1,6 +1,7 @@ package com.annimon.ffmpegbot.session; public enum FileType { + PHOTO, ANIMATION, AUDIO, VIDEO, diff --git a/src/main/java/com/annimon/ffmpegbot/session/Resolver.java b/src/main/java/com/annimon/ffmpegbot/session/Resolver.java index f301cf3..1aaa72f 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/Resolver.java +++ b/src/main/java/com/annimon/ffmpegbot/session/Resolver.java @@ -57,6 +57,7 @@ public class Resolver { return switch (fileType) { case ANIMATION -> Methods.sendAnimation(); case AUDIO -> Methods.sendAudio(); + case PHOTO -> Methods.sendPhoto(); case VIDEO -> Methods.sendVideo(); case VIDEO_NOTE -> Methods.sendVideoNote(); case VOICE -> Methods.sendVoice(); @@ -65,6 +66,7 @@ public class Resolver { public static ActionType resolveAction(@NotNull FileType fileType) { return switch (fileType) { + case PHOTO -> ActionType.UPLOAD_PHOTO; case VIDEO -> ActionType.UPLOAD_VIDEO; case VIDEO_NOTE -> ActionType.UPLOAD_VIDEO_NOTE; case VOICE -> ActionType.UPLOAD_VOICE;