Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
== 8.1.0-beta.4 2025-10-10

Improvements:
* Added audio sample rate adjustment to built-in presets.
* Audio sample rates now work similarly to frame rates - when the input has a lower sample rate than the target, the closest standard sample rate is used.
* Standard audio sample rates supported: 8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000 Hz.

== 8.1.0-beta.3 2025-09-30

Improvements:
Expand Down
29 changes: 29 additions & 0 deletions lib/ffmpeg/command_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ module FFMPEG
# args.to_s # "-map 0:v:0 -c:v:0 libx264 -r 30"
class CommandArgs < RawCommandArgs
STANDARD_FRAME_RATES = [12, 24, 25, 30, 50, 60, 90, 120, 240].freeze
STANDARD_AUDIO_SAMPLE_RATES = [
8000, 11_025, 16_000, 22_050, 32_000, 44_100,
48_000, 88_200, 96_000, 176_400, 192_000
].freeze

class << self
# Composes a new instance of CommandArgs with the given media.
Expand Down Expand Up @@ -95,12 +99,23 @@ def audio_bit_rate(target_value, **kwargs)
super(adjusted_audio_bit_rate(target_value), **kwargs)
end

# Sets the audio sample rate to the minimum of the current audio sample rate and the target value.
#
# @param target_value [Integer] The target sample rate.
# @return [self]
def audio_sample_rate(target_value)
return self if target_value.nil?

super(adjusted_audio_sample_rate(target_value))
end

# Returns the minimum of the current frame rate and the target value.
#
# @param target_value [Integer, Float] The target frame rate.
# @return [Numeric]
def adjusted_frame_rate(target_value)
return target_value if media.frame_rate.nil?
return target_value if media.frame_rate <= 0
return target_value if media.frame_rate > target_value

STANDARD_FRAME_RATES.min_by { (_1 - media.frame_rate).abs }
Expand All @@ -126,6 +141,20 @@ def adjusted_audio_bit_rate(target_value)
min_bit_rate(media.audio_bit_rate, target_value)
end

# Returns the minimum of the current audio sample rate and the target value.
# Returns the target value if the current sample rate is nil or zero/negative.
# If the media sample rate is lower than the target, returns the closest standard sample rate.
#
# @param target_value [Integer] The target audio sample rate.
# @return [Integer]
def adjusted_audio_sample_rate(target_value)
return target_value if media.audio_sample_rate.nil?
return target_value if media.audio_sample_rate <= 0
return target_value if media.audio_sample_rate > target_value

STANDARD_AUDIO_SAMPLE_RATES.min_by { (_1 - media.audio_sample_rate).abs }
end

private

def min_bit_rate(*values)
Expand Down
11 changes: 10 additions & 1 deletion lib/ffmpeg/presets/aac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ def aac_128k(
filename: '%<basename>s.m4a',
metadata: nil,
threads: FFMPEG.threads,
audio_sample_rate: 48_000,
&
)
AAC.new(
name:,
filename:,
metadata:,
threads:,
audio_sample_rate:,
audio_bit_rate: '128k',
&
)
Expand All @@ -29,13 +31,15 @@ def aac_192k(
filename: '%<basename>s.m4a',
metadata: nil,
threads: FFMPEG.threads,
audio_sample_rate: 48_000,
&
)
AAC.new(
name:,
filename:,
metadata:,
threads:,
audio_sample_rate:,
audio_bit_rate: '192k',
&
)
Expand All @@ -46,13 +50,15 @@ def aac_320k(
filename: '%<basename>s.m4a',
metadata: nil,
threads: FFMPEG.threads,
audio_sample_rate: 48_000,
&
)
AAC.new(
name:,
filename:,
metadata:,
threads:,
audio_sample_rate:,
audio_bit_rate: '320k',
&
)
Expand All @@ -61,7 +67,7 @@ def aac_320k(

# Preset to encode AAC audio files.
class AAC < Preset
attr_reader :threads, :audio_bit_rate
attr_reader :threads, :audio_bit_rate, :audio_sample_rate

# @param name [String] The name of the preset.
# @param filename [String] The filename format of the output.
Expand All @@ -74,10 +80,12 @@ def initialize(
metadata: nil,
threads: FFMPEG.threads,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
&
)
@threads = threads
@audio_bit_rate = audio_bit_rate
@audio_sample_rate = audio_sample_rate
preset = self

super(name:, filename:, metadata:) do
Expand All @@ -92,6 +100,7 @@ def initialize(

map media.audio_mapping_id do
audio_bit_rate preset.audio_bit_rate
audio_sample_rate preset.audio_sample_rate
end
end
end
Expand Down
11 changes: 10 additions & 1 deletion lib/ffmpeg/presets/dash/aac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def aac_128k(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
audio_sample_rate: 48_000,
&
)
AAC.new(
Expand All @@ -22,6 +23,7 @@ def aac_128k(
metadata:,
threads:,
segment_duration:,
audio_sample_rate:,
audio_bit_rate: '128k',
&
)
Expand All @@ -33,6 +35,7 @@ def aac_192k(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
audio_sample_rate: 48_000,
&
)
AAC.new(
Expand All @@ -41,6 +44,7 @@ def aac_192k(
metadata:,
threads:,
segment_duration:,
audio_sample_rate:,
audio_bit_rate: '192k',
&
)
Expand All @@ -52,6 +56,7 @@ def aac_320k(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
audio_sample_rate: 48_000,
&
)
AAC.new(
Expand All @@ -60,6 +65,7 @@ def aac_320k(
metadata:,
threads:,
segment_duration:,
audio_sample_rate:,
audio_bit_rate: '320k',
&
)
Expand All @@ -68,7 +74,7 @@ def aac_320k(

# Preset to encode DASH AAC audio files.
class AAC < DASH
attr_reader :audio_bit_rate
attr_reader :audio_bit_rate, :audio_sample_rate

# @param name [String] The name of the preset.
# @param filename [String] The filename format of the output.
Expand All @@ -82,9 +88,11 @@ def initialize(
threads: FFMPEG.threads,
segment_duration: 4,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
&
)
@audio_bit_rate = audio_bit_rate
@audio_sample_rate = audio_sample_rate
preset = self

super(
Expand All @@ -102,6 +110,7 @@ def initialize(

map media.audio_mapping_id do
audio_bit_rate preset.audio_bit_rate
audio_sample_rate preset.audio_sample_rate
end
end
end
Expand Down
73 changes: 40 additions & 33 deletions lib/ffmpeg/presets/dash/h264.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def h264_360p(
segment_duration: 4,
keyframe_interval: 2,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
frame_rate: 30,
ld_frame_rate: 24,
&
Expand All @@ -32,11 +33,11 @@ def h264_360p(
segment_duration:,
keyframe_interval:,
h264_presets: [
Presets.h264_360p(audio_bit_rate:, frame_rate:)
Presets.h264_360p(audio_bit_rate:, audio_sample_rate:, frame_rate:)
],
ld_h264_presets: [
Presets.h264_240p(audio_bit_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, frame_rate: ld_frame_rate)
Presets.h264_240p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate)
],
&
)
Expand All @@ -50,6 +51,7 @@ def h264_480p(
segment_duration: 4,
keyframe_interval: 2,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
frame_rate: 30,
ld_frame_rate: 24,
&
Expand All @@ -62,12 +64,12 @@ def h264_480p(
segment_duration:,
keyframe_interval:,
h264_presets: [
Presets.h264_480p(audio_bit_rate:, frame_rate:),
Presets.h264_360p(audio_bit_rate:, frame_rate:)
Presets.h264_480p(audio_bit_rate:, audio_sample_rate:, frame_rate:),
Presets.h264_360p(audio_bit_rate:, audio_sample_rate:, frame_rate:)
],
ld_h264_presets: [
Presets.h264_240p(audio_bit_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, frame_rate: ld_frame_rate)
Presets.h264_240p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate)
],
&
)
Expand All @@ -81,6 +83,7 @@ def h264_720p(
segment_duration: 4,
keyframe_interval: 2,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
ld_frame_rate: 24,
sd_frame_rate: 30,
hd_frame_rate: 30,
Expand All @@ -94,13 +97,13 @@ def h264_720p(
keyframe_interval:,
segment_duration:,
h264_presets: [
Presets.h264_720p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, frame_rate: sd_frame_rate)
Presets.h264_720p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate)
],
ld_h264_presets: [
Presets.h264_240p(audio_bit_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, frame_rate: ld_frame_rate)
Presets.h264_240p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate)
],
&
)
Expand All @@ -114,6 +117,7 @@ def h264_1080p(
segment_duration: 4,
keyframe_interval: 2,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
ld_frame_rate: 24,
sd_frame_rate: 30,
hd_frame_rate: 30,
Expand All @@ -127,14 +131,14 @@ def h264_1080p(
keyframe_interval:,
segment_duration:,
h264_presets: [
Presets.h264_1080p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_720p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, frame_rate: sd_frame_rate)
Presets.h264_1080p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_720p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate)
],
ld_h264_presets: [
Presets.h264_240p(audio_bit_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, frame_rate: ld_frame_rate)
Presets.h264_240p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate)
],
&
)
Expand All @@ -148,6 +152,7 @@ def h264_1440p(
segment_duration: 4,
keyframe_interval: 2,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
ld_frame_rate: 24,
sd_frame_rate: 30,
hd_frame_rate: 30,
Expand All @@ -161,15 +166,15 @@ def h264_1440p(
keyframe_interval:,
segment_duration:,
h264_presets: [
Presets.h264_1440p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_1080p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_720p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, frame_rate: sd_frame_rate)
Presets.h264_1440p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_1080p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_720p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate)
],
ld_h264_presets: [
Presets.h264_240p(audio_bit_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, frame_rate: ld_frame_rate)
Presets.h264_240p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate)
],
&
)
Expand All @@ -183,6 +188,7 @@ def h264_4k(
segment_duration: 4,
keyframe_interval: 2,
audio_bit_rate: '128k',
audio_sample_rate: 48_000,
ld_frame_rate: 24,
sd_frame_rate: 30,
hd_frame_rate: 60,
Expand All @@ -197,16 +203,16 @@ def h264_4k(
segment_duration:,
keyframe_interval:,
h264_presets: [
Presets.h264_4k(audio_bit_rate:, frame_rate: uhd_frame_rate),
Presets.h264_1440p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_1080p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_720p(audio_bit_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, frame_rate: sd_frame_rate)
Presets.h264_4k(audio_bit_rate:, audio_sample_rate:, frame_rate: uhd_frame_rate),
Presets.h264_1440p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_1080p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_720p(audio_bit_rate:, audio_sample_rate:, frame_rate: hd_frame_rate),
Presets.h264_480p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate),
Presets.h264_360p(audio_bit_rate:, audio_sample_rate:, frame_rate: sd_frame_rate)
],
ld_h264_presets: [
Presets.h264_240p(audio_bit_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, frame_rate: ld_frame_rate)
Presets.h264_240p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate),
Presets.h264_144p(audio_bit_rate:, audio_sample_rate:, frame_rate: ld_frame_rate)
],
&
)
Expand Down Expand Up @@ -305,6 +311,7 @@ def initialize(
# Reset the audio stream's timestamps to start from 0.
filter Filter.new(:audio, 'asetpts', expr: 'PTS-STARTPTS')
audio_bit_rate h264_presets.first.audio_bit_rate
audio_sample_rate h264_presets.first.audio_sample_rate
end
end
end
Expand Down
Loading