awaaz 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +15 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GLOSSARY.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +72 -0
- data/Rakefile +8 -0
- data/TODOS.md +3 -0
- data/lib/awaaz/config.rb +123 -0
- data/lib/awaaz/decoders/base_decoder.rb +116 -0
- data/lib/awaaz/decoders/decode.rb +41 -0
- data/lib/awaaz/decoders/decoders.rb +30 -0
- data/lib/awaaz/decoders/mp3_decoder.rb +44 -0
- data/lib/awaaz/decoders/wavefile_decoder.rb +57 -0
- data/lib/awaaz/errors.rb +47 -0
- data/lib/awaaz/extensions/extensions.rb +17 -0
- data/lib/awaaz/extensions/samplerate.rb +114 -0
- data/lib/awaaz/extensions/soundfile.rb +74 -0
- data/lib/awaaz/utils/resample.rb +143 -0
- data/lib/awaaz/utils/shell_command_builder.rb +104 -0
- data/lib/awaaz/utils/sound_config.rb +135 -0
- data/lib/awaaz/utils/soundread.rb +169 -0
- data/lib/awaaz/utils/utils.rb +30 -0
- data/lib/awaaz/utils/via_shell.rb +183 -0
- data/lib/awaaz/version.rb +6 -0
- data/lib/awaaz.rb +25 -0
- data/sig/awaaz.rbs +4 -0
- metadata +115 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awaaz
|
4
|
+
module Decoders
|
5
|
+
##
|
6
|
+
# The WavefileDecoder is responsible for decoding `.wav` audio files
|
7
|
+
# into raw PCM data that can be processed by the Awaaz audio pipeline.
|
8
|
+
#
|
9
|
+
# This decoder supports multiple decoding strategies:
|
10
|
+
#
|
11
|
+
# 1. **Soundread** — a Ruby-level `.wav` file reader.
|
12
|
+
# 2. **Shell-based decoding** — for raw audio extraction.
|
13
|
+
#
|
14
|
+
# The decoder will choose a decoding method based on availability
|
15
|
+
# of decoders and the configured options.
|
16
|
+
#
|
17
|
+
# @see Awaaz::Decoders::BaseDecoder
|
18
|
+
# @see Awaaz::Utils::ViaShell
|
19
|
+
#
|
20
|
+
class WavefileDecoder < BaseDecoder
|
21
|
+
include Utils::ViaShell
|
22
|
+
|
23
|
+
# Sets the available decoding options for this decoder.
|
24
|
+
# Defaults to the base options plus the `:soundread` option.
|
25
|
+
#
|
26
|
+
# @!scope class
|
27
|
+
set_available_options default_available_options + [:soundread]
|
28
|
+
|
29
|
+
##
|
30
|
+
# Loads and decodes a `.wav` file into raw PCM data.
|
31
|
+
#
|
32
|
+
# @raise [AgumentError]
|
33
|
+
# if the file does not have a `.wav` extension.
|
34
|
+
#
|
35
|
+
# @return [Array<(Numo::NArray, Integer, Integer)>]
|
36
|
+
# Returns an array containing:
|
37
|
+
# - samples: A [Numo::NArray] of decoded audio samples
|
38
|
+
# - sample_rate: Integer sample rate (Hz)
|
39
|
+
# - channels: Integer number of audio channels
|
40
|
+
#
|
41
|
+
# @note The decoding method is chosen dynamically:
|
42
|
+
# - If there are no available decoders, or if `:soundread` is enabled,
|
43
|
+
# it will use the {#soundread} method.
|
44
|
+
# - Otherwise, it will use shell based decoding.
|
45
|
+
#
|
46
|
+
def load
|
47
|
+
output_data = if no_decoders? || soundread?
|
48
|
+
soundread
|
49
|
+
else
|
50
|
+
shell_load sox_options: { raw: true }
|
51
|
+
end
|
52
|
+
|
53
|
+
process(*output_data)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/awaaz/errors.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# The Awaaz module serves as the top-level namespace for all components
|
5
|
+
# of the Awaaz gem, which provides audio decoding, resampling, and analysis tools.
|
6
|
+
#
|
7
|
+
# This file defines the core exception classes used throughout the library.
|
8
|
+
#
|
9
|
+
# @see Awaaz::Config for configuration handling
|
10
|
+
# @see Awaaz::Decoders for decoder implementations
|
11
|
+
#
|
12
|
+
module Awaaz
|
13
|
+
##
|
14
|
+
# Raised when no suitable audio decoder is found on the system.
|
15
|
+
#
|
16
|
+
# This error is typically raised when attempting to decode an audio file
|
17
|
+
# but none of the configured or available decoders (e.g., `mpg123`, `ffmpeg`, `sox`)
|
18
|
+
# are detected or usable.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# raise Awaaz::DecoderNotFound, "No decoders available"
|
22
|
+
#
|
23
|
+
class DecoderNotFound < ArgumentError; end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Raised when an error occurs during the resampling process.
|
27
|
+
#
|
28
|
+
# This error generally indicates an issue with the resampling library
|
29
|
+
# or invalid audio data being passed for resampling.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# raise Awaaz::ResamplingError, "Invalid resampling ratio"
|
33
|
+
#
|
34
|
+
class ResamplingError < StandardError; end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Raised when the {https://github.com/beetbox/audioread audioread} backend
|
38
|
+
# encounters an error while decoding audio files.
|
39
|
+
#
|
40
|
+
# This can happen when the file format is unsupported or if
|
41
|
+
# the decoding process fails unexpectedly.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# raise Awaaz::AudioreadError, "Failed to read audio file"
|
45
|
+
#
|
46
|
+
class AudioreadError < StandardError; end
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "soundfile"
|
4
|
+
require_relative "samplerate"
|
5
|
+
|
6
|
+
module Awaaz
|
7
|
+
# The Extensions namespace contains low-level bindings and helper classes
|
8
|
+
# for audio file reading and resampling.
|
9
|
+
#
|
10
|
+
# @note These extensions are generally implemented using {FFI}
|
11
|
+
# and provide direct access to C libraries like `libsndfile` and `libsamplerate`.
|
12
|
+
#
|
13
|
+
# @see Extensions::Soundfile
|
14
|
+
# @see Extensions::Samplerate
|
15
|
+
module Extensions
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awaaz
|
4
|
+
##
|
5
|
+
# The Extensions module is a namespace for FFI-based bindings to external libraries.
|
6
|
+
module Extensions
|
7
|
+
##
|
8
|
+
# The Samplerate module provides Ruby bindings to the `libsamplerate` C library
|
9
|
+
# using the FFI (Foreign Function Interface).
|
10
|
+
#
|
11
|
+
# This module enables high-quality audio resampling directly from Ruby.
|
12
|
+
#
|
13
|
+
# @see https://libsndfile.github.io/libsamplerate/api.html Official libsamplerate API documentation
|
14
|
+
module Samplerate
|
15
|
+
extend FFI::Library
|
16
|
+
|
17
|
+
# Load the libsamplerate shared library
|
18
|
+
ffi_lib "samplerate"
|
19
|
+
|
20
|
+
# rubocop:disable Naming/ClassAndModuleCamelCase
|
21
|
+
|
22
|
+
##
|
23
|
+
# Structure representing the parameters for a sample rate conversion operation.
|
24
|
+
#
|
25
|
+
# Mirrors the C struct `SRC_DATA` from libsamplerate.
|
26
|
+
#
|
27
|
+
# @!attribute [rw] data_in
|
28
|
+
# @return [FFI::Pointer] Pointer to the input audio buffer.
|
29
|
+
# @!attribute [rw] data_out
|
30
|
+
# @return [FFI::Pointer] Pointer to the output audio buffer.
|
31
|
+
# @!attribute [rw] input_frames
|
32
|
+
# @return [Integer] Number of input frames.
|
33
|
+
# @!attribute [rw] output_frames
|
34
|
+
# @return [Integer] Number of output frames allocated.
|
35
|
+
# @!attribute [rw] input_frames_used
|
36
|
+
# @return [Integer] Number of input frames actually used.
|
37
|
+
# @!attribute [rw] output_frames_gen
|
38
|
+
# @return [Integer] Number of output frames generated.
|
39
|
+
# @!attribute [rw] end_of_input
|
40
|
+
# @return [Integer] Flag (0 or 1) indicating whether this is the last block of input data.
|
41
|
+
# @!attribute [rw] src_ratio
|
42
|
+
# @return [Float] Conversion ratio (output_sample_rate / input_sample_rate).
|
43
|
+
class SRC_DATA < FFI::Struct
|
44
|
+
layout :data_in, :pointer,
|
45
|
+
:data_out, :pointer,
|
46
|
+
:input_frames, :long,
|
47
|
+
:output_frames, :long,
|
48
|
+
:input_frames_used, :long,
|
49
|
+
:output_frames_gen, :long,
|
50
|
+
:end_of_input, :int,
|
51
|
+
:src_ratio, :double
|
52
|
+
end
|
53
|
+
|
54
|
+
# rubocop:enable Naming/ClassAndModuleCamelCase
|
55
|
+
|
56
|
+
##
|
57
|
+
# Performs a simple sample rate conversion.
|
58
|
+
#
|
59
|
+
attach_function :src_simple, [SRC_DATA.by_ref, :int, :int], :int
|
60
|
+
|
61
|
+
##
|
62
|
+
# Converts an error code to a human-readable error message.
|
63
|
+
#
|
64
|
+
# @return [String] Human-readable error message.
|
65
|
+
attach_function :src_strerror, [:int], :string
|
66
|
+
|
67
|
+
# --- Converter type constants ---
|
68
|
+
|
69
|
+
# Best quality sinc-based sample rate converter.
|
70
|
+
SRC_SINC_BEST_QUALITY = 0
|
71
|
+
|
72
|
+
# Medium quality sinc-based converter.
|
73
|
+
SRC_SINC_MEDIUM_QUALITY = 1
|
74
|
+
|
75
|
+
# Fastest sinc-based converter (lower quality).
|
76
|
+
SRC_SINC_FASTEST = 2
|
77
|
+
|
78
|
+
# Zero-order hold converter (lowest quality, fastest).
|
79
|
+
SRC_ZERO_ORDER_HOLD = 3
|
80
|
+
|
81
|
+
# Linear interpolation converter (low quality, very fast).
|
82
|
+
SRC_LINEAR = 4
|
83
|
+
|
84
|
+
class << self
|
85
|
+
##
|
86
|
+
# Maps a symbolic or numeric option to a libsamplerate converter type constant.
|
87
|
+
#
|
88
|
+
# @param option [Integer, Symbol] Converter type, either as an integer constant
|
89
|
+
# or a symbol (:sinc_best_quality, :linear, etc.).
|
90
|
+
# @return [Integer] The corresponding converter type constant.
|
91
|
+
# @raise [ArgumentError] If the option is not recognized.
|
92
|
+
#
|
93
|
+
# @example Using symbols
|
94
|
+
# Extensions::Samplerate.resample_option(:sinc_best_quality)
|
95
|
+
# # => 0
|
96
|
+
#
|
97
|
+
# @example Using integers
|
98
|
+
# Extensions::Samplerate.resample_option(:linear)
|
99
|
+
# # => 4
|
100
|
+
def resample_option(option)
|
101
|
+
case option
|
102
|
+
when 0, :sinc_best_quality then SRC_SINC_BEST_QUALITY
|
103
|
+
when 1, :sinc_medium_quality then SRC_SINC_MEDIUM_QUALITY
|
104
|
+
when 2, :sinc_fastest then SRC_SINC_FASTEST
|
105
|
+
when 3, :zero_order_hold then SRC_ZERO_ORDER_HOLD
|
106
|
+
when 4, :linear then SRC_LINEAR
|
107
|
+
else
|
108
|
+
raise ArgumentError, "Not found"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awaaz
|
4
|
+
##
|
5
|
+
# The Extensions module is a namespace for FFI-based bindings to external libraries.
|
6
|
+
module Extensions
|
7
|
+
##
|
8
|
+
# The Soundfile module provides Ruby bindings to the `libsndfile` C library
|
9
|
+
# using the FFI (Foreign Function Interface).
|
10
|
+
#
|
11
|
+
# It allows reading audio file metadata and sample data directly from Ruby.
|
12
|
+
#
|
13
|
+
# @see http://www.mega-nerd.com/libsndfile/ Official libsndfile documentation
|
14
|
+
module Soundfile
|
15
|
+
extend FFI::Library
|
16
|
+
|
17
|
+
# Load the libsndfile shared library
|
18
|
+
ffi_lib "sndfile"
|
19
|
+
|
20
|
+
##
|
21
|
+
# Open mode for reading sound files.
|
22
|
+
# @return [Integer] Bitmask flag for read mode.
|
23
|
+
SFM_READ = 0x10
|
24
|
+
|
25
|
+
# rubocop:disable Naming/ClassAndModuleCamelCase
|
26
|
+
|
27
|
+
##
|
28
|
+
# Structure containing metadata about an audio file.
|
29
|
+
#
|
30
|
+
# Mirrors the C struct `SF_INFO` from libsndfile.
|
31
|
+
#
|
32
|
+
# @!attribute [rw] frames
|
33
|
+
# @return [Integer] Total number of frames in the file.
|
34
|
+
# @!attribute [rw] samplerate
|
35
|
+
# @return [Integer] Sample rate of the audio file (Hz).
|
36
|
+
# @!attribute [rw] channels
|
37
|
+
# @return [Integer] Number of audio channels.
|
38
|
+
# @!attribute [rw] format
|
39
|
+
# @return [Integer] Format identifier of the audio file.
|
40
|
+
# @!attribute [rw] sections
|
41
|
+
# @return [Integer] Number of sections in the file.
|
42
|
+
# @!attribute [rw] seekable
|
43
|
+
# @return [Integer] Whether the file is seekable (1) or not (0).
|
44
|
+
class SF_INFO < FFI::Struct
|
45
|
+
layout :frames, :long_long,
|
46
|
+
:samplerate, :int,
|
47
|
+
:channels, :int,
|
48
|
+
:format, :int,
|
49
|
+
:sections, :int,
|
50
|
+
:seekable, :int
|
51
|
+
end
|
52
|
+
|
53
|
+
# rubocop:enable Naming/ClassAndModuleCamelCase
|
54
|
+
|
55
|
+
##
|
56
|
+
# Opens an audio file and returns a pointer to the file handle.
|
57
|
+
#
|
58
|
+
# @see http://www.mega-nerd.com/libsndfile/api.html#open
|
59
|
+
attach_function :sf_open, %i[string int pointer], :pointer
|
60
|
+
|
61
|
+
##
|
62
|
+
# Reads floating-point audio frames from an open file.
|
63
|
+
#
|
64
|
+
# @return [Integer] Number of frames actually read.
|
65
|
+
attach_function :sf_readf_float, %i[pointer pointer long_long], :long_long
|
66
|
+
|
67
|
+
##
|
68
|
+
# Closes an open audio file.
|
69
|
+
#
|
70
|
+
# @return [Integer] Zero on success, non-zero on error.
|
71
|
+
attach_function :sf_close, [:pointer], :int
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awaaz
|
4
|
+
module Utils
|
5
|
+
##
|
6
|
+
# Resample utilities for audio data represented as Numo::NArray.
|
7
|
+
# Wraps the `libsamplerate` bindings provided by {Extensions::Samplerate}.
|
8
|
+
#
|
9
|
+
# @note This module is intended for internal use, but `read_and_resample_numo`
|
10
|
+
# is public for advanced users who need manual resampling.
|
11
|
+
module Resample
|
12
|
+
class << self
|
13
|
+
##
|
14
|
+
# Resamples a Numo::SFloat array of audio samples from one sample rate to another.
|
15
|
+
#
|
16
|
+
# @param input_samples [Numo::SFloat] The audio samples to resample.
|
17
|
+
# @param input_rate [Integer] The original sample rate (Hz).
|
18
|
+
# @param output_rate [Integer] The desired sample rate (Hz).
|
19
|
+
# @param sampling_option [Symbol, Integer] The resampling quality option.
|
20
|
+
# Can be one of:
|
21
|
+
# * `:sinc_best_quality` (0)
|
22
|
+
# * `:sinc_medium_quality` (1)
|
23
|
+
# * `:sinc_fastest` (2)
|
24
|
+
# * `:zero_order_hold` (3)
|
25
|
+
# * `:linear` (4)
|
26
|
+
#
|
27
|
+
# @return [Numo::SFloat] The resampled audio data.
|
28
|
+
#
|
29
|
+
# @raise [ArgumentError] If inputs are invalid or ratio is out of range.
|
30
|
+
# @raise [Awaaz::ResampleError] If `libsamplerate` returns an error.
|
31
|
+
#
|
32
|
+
# @example Resample 44.1kHz mono audio to 48kHz
|
33
|
+
# samples = Numo::SFloat.new(44100).rand
|
34
|
+
# new_samples = Awaaz::Utils::Resample.read_and_resample_numo(samples, 44100, 48000)
|
35
|
+
def read_and_resample_numo(input_samples, input_rate, output_rate, sampling_option: :sinc_best_quality)
|
36
|
+
validate_inputs(input_samples, input_rate, output_rate)
|
37
|
+
|
38
|
+
ratio = calculate_ratio(input_rate, output_rate)
|
39
|
+
input_ptr, output_ptr, input_frames, output_frames = prepare_memory(input_samples, ratio)
|
40
|
+
|
41
|
+
data = build_src_data(input_ptr, output_ptr, input_frames, output_frames, ratio)
|
42
|
+
perform_resampling(data, sampling_option)
|
43
|
+
|
44
|
+
convert_to_numo(output_ptr, data[:output_frames_gen])
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
##
|
50
|
+
# Validates that the provided inputs are of the correct type and configuration.
|
51
|
+
#
|
52
|
+
# @param samples [Numo::NArray] The input samples.
|
53
|
+
# @param input_rate [Integer]
|
54
|
+
# @param output_rate [Integer]
|
55
|
+
#
|
56
|
+
# @raise [ArgumentError] If samples are not a Numo::SFloat array.
|
57
|
+
def validate_inputs(samples, input_rate, output_rate)
|
58
|
+
return if input_rate != output_rate && samples.is_a?(Numo::NArray)
|
59
|
+
|
60
|
+
raise ArgumentError, "Input must be a Numo::SFloat array" unless samples.is_a?(Numo::NArray)
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Calculates and validates the resampling ratio.
|
65
|
+
#
|
66
|
+
# @param input_rate [Integer]
|
67
|
+
# @param output_rate [Integer]
|
68
|
+
#
|
69
|
+
# @return [Float] The ratio of output_rate to input_rate.
|
70
|
+
# @raise [ArgumentError] If ratio is outside the allowed range.
|
71
|
+
def calculate_ratio(input_rate, output_rate)
|
72
|
+
ratio = output_rate / input_rate.to_f
|
73
|
+
raise ArgumentError, "Bad ratio" if ratio < 1.0 / 256 || ratio > 256
|
74
|
+
|
75
|
+
ratio
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Allocates and prepares FFI memory for the input and output buffers.
|
80
|
+
#
|
81
|
+
# @param input_samples [Numo::NArray]
|
82
|
+
# @param ratio [Float] The resampling ratio.
|
83
|
+
#
|
84
|
+
# @return [Array<FFI::MemoryPointer, FFI::MemoryPointer, Integer, Integer>]
|
85
|
+
def prepare_memory(input_samples, ratio)
|
86
|
+
input_frames = input_samples.size
|
87
|
+
output_frames = (input_frames * ratio).to_i
|
88
|
+
|
89
|
+
input_ptr = FFI::MemoryPointer.new(:float, input_frames)
|
90
|
+
input_ptr.write_bytes(input_samples.to_string)
|
91
|
+
|
92
|
+
output_ptr = FFI::MemoryPointer.new(:float, output_frames)
|
93
|
+
|
94
|
+
[input_ptr, output_ptr, input_frames, output_frames]
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Builds the {Extensions::Samplerate::SRC_DATA} struct for `libsamplerate`.
|
99
|
+
#
|
100
|
+
# @param input_ptr [FFI::MemoryPointer]
|
101
|
+
# @param output_ptr [FFI::MemoryPointer]
|
102
|
+
# @param input_frames [Integer]
|
103
|
+
# @param output_frames [Integer]
|
104
|
+
# @param ratio [Float]
|
105
|
+
#
|
106
|
+
# @return [Extensions::Samplerate::SRC_DATA]
|
107
|
+
def build_src_data(input_ptr, output_ptr, input_frames, output_frames, ratio)
|
108
|
+
Extensions::Samplerate::SRC_DATA.new.tap do |data|
|
109
|
+
data[:data_in] = input_ptr
|
110
|
+
data[:data_out] = output_ptr
|
111
|
+
data[:input_frames] = input_frames
|
112
|
+
data[:output_frames] = output_frames
|
113
|
+
data[:end_of_input] = 1
|
114
|
+
data[:src_ratio] = ratio
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Performs the resampling using `libsamplerate`.
|
120
|
+
#
|
121
|
+
# @param data [Extensions::Samplerate::SRC_DATA]
|
122
|
+
# @param sampling_option [Symbol, Integer]
|
123
|
+
#
|
124
|
+
# @raise [Awaaz::ResampleError] If resampling fails.
|
125
|
+
def perform_resampling(data, sampling_option)
|
126
|
+
err = Extensions::Samplerate.src_simple(data, Extensions::Samplerate.resample_option(sampling_option), 1)
|
127
|
+
raise Awaaz::ResampleError, "Resampling failed: #{Extensions::Samplerate.src_strerror(err)}" if err != 0
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Converts the output FFI pointer back into a Numo::SFloat array.
|
132
|
+
#
|
133
|
+
# @param output_ptr [FFI::MemoryPointer]
|
134
|
+
# @param size [Integer] Number of frames generated.
|
135
|
+
#
|
136
|
+
# @return [Numo::SFloat]
|
137
|
+
def convert_to_numo(output_ptr, size)
|
138
|
+
Numo::SFloat.cast(output_ptr.read_array_of_float(size))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awaaz
|
4
|
+
module Utils
|
5
|
+
##
|
6
|
+
# A utility class to construct shell commands in a safe and composable way.
|
7
|
+
#
|
8
|
+
# This class supports chaining methods to add arguments, flags, and options,
|
9
|
+
# and finally output the full shell command string.
|
10
|
+
#
|
11
|
+
# @example Build a simple FFmpeg command
|
12
|
+
# cmd = ShellCommandBuilder.new(:ffmpeg)
|
13
|
+
# .add_flag("-nostdin")
|
14
|
+
# .add_option("-i", "input.mp3")
|
15
|
+
# .add_option("-ar", 44100)
|
16
|
+
# .command
|
17
|
+
# # => "ffmpeg -nostdin -i input.mp3 -ar 44100"
|
18
|
+
#
|
19
|
+
class ShellCommandBuilder
|
20
|
+
##
|
21
|
+
# Initializes a new shell command builder.
|
22
|
+
#
|
23
|
+
# @param base [String, Symbol, nil] The base command (e.g., `:ffmpeg` or `"ls"`).
|
24
|
+
#
|
25
|
+
def initialize(base = nil)
|
26
|
+
@args = [base.to_s]
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Adds a positional argument to the command.
|
31
|
+
#
|
32
|
+
# @param arg [String, Symbol, Numeric] The argument to add.
|
33
|
+
# @return [ShellCommandBuilder] self, for chaining.
|
34
|
+
#
|
35
|
+
def add_arg(arg)
|
36
|
+
@args << arg.to_s.strip
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Alias for {#add_arg}, used for flags.
|
42
|
+
#
|
43
|
+
# @see #add_arg
|
44
|
+
alias add_flag add_arg
|
45
|
+
|
46
|
+
##
|
47
|
+
# Adds an option with a value to the command.
|
48
|
+
#
|
49
|
+
# @param option [String, Symbol] The option flag (e.g., `-i`).
|
50
|
+
# @param value [String, Symbol, Numeric] The value for the option.
|
51
|
+
# @param with_colon [Boolean] Whether to separate the option and value with a colon (`:`)
|
52
|
+
# instead of a space.
|
53
|
+
# @return [ShellCommandBuilder] self, for chaining.
|
54
|
+
#
|
55
|
+
# @example Space-separated
|
56
|
+
# builder.add_option("-i", "file.mp3")
|
57
|
+
# # => "-i file.mp3"
|
58
|
+
#
|
59
|
+
# @example Colon-separated
|
60
|
+
# builder.add_option("--volume", 10, with_colon: true)
|
61
|
+
# # => "--volume:10"
|
62
|
+
#
|
63
|
+
def add_option(option, value, with_colon: false)
|
64
|
+
@args << "#{option}#{with_colon ? ":" : " "}#{value}"
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Adds multiple arguments or options at once.
|
70
|
+
#
|
71
|
+
# @param args_array [Array<Array>] An array of argument definitions.
|
72
|
+
# Each element should be `[type, args]` where:
|
73
|
+
# - `type` is `:arg` or `:flag` for positional arguments/flags
|
74
|
+
# - otherwise, treated as an option for {#add_option}
|
75
|
+
#
|
76
|
+
# @return [ShellCommandBuilder] self, for chaining.
|
77
|
+
#
|
78
|
+
# @example Adding multiple
|
79
|
+
# builder.add_multiple_args([
|
80
|
+
# [:flag, ["-q"]],
|
81
|
+
# [:option, ["-i", "file.mp3"]],
|
82
|
+
# [:arg, ["extra"]]
|
83
|
+
# ])
|
84
|
+
#
|
85
|
+
def add_multiple_args(args_array)
|
86
|
+
args_array.each do |shell_args|
|
87
|
+
arg_type, args = shell_args
|
88
|
+
arg_type = arg_type.to_sym
|
89
|
+
%i[arg flag].include?(arg_type) ? add_arg(*args) : add_option(*args)
|
90
|
+
end
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Builds and returns the complete shell command string.
|
96
|
+
#
|
97
|
+
# @return [String] The constructed command.
|
98
|
+
#
|
99
|
+
def command
|
100
|
+
@args.join(" ")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Awaaz
|
4
|
+
module Utils
|
5
|
+
##
|
6
|
+
# SoundConfig holds and validates configuration options used for audio processing.
|
7
|
+
#
|
8
|
+
# It ensures that only valid options are passed in and provides convenience
|
9
|
+
# accessors for common audio processing settings such as sample rate, channel count,
|
10
|
+
# amplification factor, and decoder preferences.
|
11
|
+
#
|
12
|
+
# @example Creating a SoundConfig with valid options
|
13
|
+
# valid_keys = [:sample_rate, :mono, :amplification_factor, :decoder]
|
14
|
+
# config = Awaaz::Utils::SoundConfig.new(valid_keys, sample_rate: 44100, mono: true)
|
15
|
+
#
|
16
|
+
class SoundConfig
|
17
|
+
##
|
18
|
+
# Initializes a new SoundConfig instance.
|
19
|
+
#
|
20
|
+
# @param valid_options [Array<Symbol,String>] The list of allowed option keys.
|
21
|
+
# @param options [Hash] The configuration options to store.
|
22
|
+
# @option options [Integer] :sample_rate The audio sample rate in Hz.
|
23
|
+
# @option options [Boolean] :mono Whether to process audio in mono (true) or stereo (false).
|
24
|
+
# @option options [Boolean] :soundread Whether to use soundread for processing.
|
25
|
+
# @option options [Integer] :amplification_factor The amplification factor (default: 32768).
|
26
|
+
# @option options [Symbol,String] :decoder The preferred audio decoder.
|
27
|
+
#
|
28
|
+
# @raise [ArgumentError] If any provided option key is not in +valid_options+.
|
29
|
+
#
|
30
|
+
def initialize(valid_options, **options)
|
31
|
+
@options = options
|
32
|
+
@valid_options = valid_options
|
33
|
+
|
34
|
+
prepare
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# The sample rate for audio processing.
|
39
|
+
#
|
40
|
+
# @return [Integer] The sample rate in Hz (default: 22050).
|
41
|
+
#
|
42
|
+
def sample_rate
|
43
|
+
from_options(:sample_rate) || 22_050
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Whether to process audio in mono.
|
48
|
+
#
|
49
|
+
# @return [Boolean] +true+ if mono, otherwise +false+.
|
50
|
+
#
|
51
|
+
def mono
|
52
|
+
from_options(:mono) || false
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Convenience method to check if audio is mono.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
#
|
60
|
+
def mono?
|
61
|
+
mono
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Convenience method to check if audio is stereo.
|
66
|
+
#
|
67
|
+
# @return [Boolean]
|
68
|
+
#
|
69
|
+
def stereo?
|
70
|
+
!mono?
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Whether to use soundread for audio processing.
|
75
|
+
#
|
76
|
+
# @return [Boolean]
|
77
|
+
#
|
78
|
+
def soundread?
|
79
|
+
from_options(:soundread) == true
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# The number of audio channels.
|
84
|
+
#
|
85
|
+
# @return [Integer] 1 for mono, 2 for stereo.
|
86
|
+
#
|
87
|
+
def num_channels
|
88
|
+
mono? ? 1 : 2
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# The amplification factor for audio processing.
|
93
|
+
#
|
94
|
+
# @return [Integer] Defaults to 32768.
|
95
|
+
#
|
96
|
+
def amplification_factor
|
97
|
+
(from_options(:amplification_factor) || 32_768).to_i
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# The preferred audio decoder.
|
102
|
+
#
|
103
|
+
# @return [Symbol, nil] The decoder symbol if set, otherwise +nil+.
|
104
|
+
#
|
105
|
+
def decoder_option
|
106
|
+
from_options(:decoder)&.to_sym
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
##
|
112
|
+
# Fetches a value from @options using either symbol or string key.
|
113
|
+
#
|
114
|
+
# @param key [Symbol,String] The key to look up.
|
115
|
+
# @return [Object, nil] The value if present, otherwise +nil+.
|
116
|
+
#
|
117
|
+
def from_options(key)
|
118
|
+
@options[key.to_s] || @options[key.to_sym]
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Validates that all provided options are in the allowed list.
|
123
|
+
#
|
124
|
+
# @raise [ArgumentError] If any provided key is invalid.
|
125
|
+
#
|
126
|
+
def prepare
|
127
|
+
@options.each_key do |key|
|
128
|
+
next if @valid_options.include?(key.to_sym) || @valid_options.include?(key.to_s)
|
129
|
+
|
130
|
+
raise ArgumentError, "Invalid key passed: #{key}. Possible keys: #{@valid_options.join(",")}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|