Class: Vizcore::Analysis::Pipeline

Inherits:
Object
  • Object
show all
Defined in:
lib/vizcore/analysis/pipeline.rb

Overview

End-to-end analysis pipeline from PCM samples to renderer-ready features.

Constant Summary collapse

BEAT_PULSE_DECAY =
0.86
BEAT_PULSE_FLOOR =
0.001
DEFAULT_NOISE_GATE =
0.01
DEFAULT_AUDIO_NORMALIZE =
{ mode: :off }.freeze
DEFAULT_FFT_PREVIEW_BINS =
32
BEATS_PER_BAR =
4
BEATS_PER_PHRASE =
32
BEAT_SUBDIVISIONS =
{ beat_2: 2, beat_4: 4, beat_8: 8, beat_triplet: 3 }.freeze
BAND_KEYS =
%i[sub low mid high].freeze
SILENCE_RESET_FRAMES =
90

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sample_rate: 44_100, fft_size: 1024, window: :hamming, beat_detector: nil, bpm_estimator: nil, smoother: nil, noise_gate: DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: SILENCE_RESET_FRAMES) ⇒ Pipeline

Returns a new instance of Pipeline.

Parameters:

  • sample_rate (Integer) (defaults to: 44_100)
  • fft_size (Integer) (defaults to: 1024)
  • window (Symbol) (defaults to: :hamming)
  • beat_detector (Vizcore::Analysis::BeatDetector, nil) (defaults to: nil)
  • bpm_estimator (Vizcore::Analysis::BPMEstimator, nil) (defaults to: nil)
  • smoother (Vizcore::Analysis::Smoother, nil) (defaults to: nil)
  • noise_gate (Numeric) (defaults to: DEFAULT_NOISE_GATE)

    RMS threshold below which input is treated as silence

  • audio_normalize (Hash, nil) (defaults to: nil)

    optional audio normalization settings

  • bpm (Numeric, nil) (defaults to: nil)

    fixed BPM value used when bpm_lock is true

  • bpm_lock (Boolean) (defaults to: false)

    true when BPM output should stay fixed

  • onset_sensitivity (Numeric) (defaults to: 1.0)

    multiplier applied to positive onset deltas

  • fft_preview_bins (Integer) (defaults to: DEFAULT_FFT_PREVIEW_BINS)

    number of FFT preview bins included in payloads

  • peak_hold_frames (Integer) (defaults to: 0)

    frames to hold per-band peak values

  • silence_reset_frames (Integer) (defaults to: SILENCE_RESET_FRAMES)

    silent frames before tempo state resets



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/vizcore/analysis/pipeline.rb', line 34

def initialize(sample_rate: 44_100, fft_size: 1024, window: :hamming, beat_detector: nil, bpm_estimator: nil, smoother: nil, noise_gate: DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: SILENCE_RESET_FRAMES)
  @fft_processor = FFTProcessor.new(sample_rate: sample_rate, fft_size: fft_size, window: window)
  @band_splitter = BandSplitter.new(sample_rate: sample_rate, fft_size: fft_size)
  @beat_detector = beat_detector || BeatDetector.new
  @analysis_frame_rate = sample_rate.to_f / fft_size.to_f
  @bpm_estimator = bpm_estimator || BPMEstimator.new(frame_rate: @analysis_frame_rate)
  @smoother = smoother || Smoother.new(alpha: 0.35)
  @noise_gate = normalize_noise_gate(noise_gate)
  self.onset_sensitivity = onset_sensitivity
  self.fft_preview_bins = fft_preview_bins
  self.peak_hold_frames = peak_hold_frames
  self.silence_reset_frames = silence_reset_frames
  @beat_pulse = 0.0
  @beat_phase = 0.0
  @last_bpm = 0.0
  self.bpm_lock = { bpm: bpm, locked: bpm_lock }
  self.audio_normalize = audio_normalize
  @silent_frame_count = 0
  @band_peak_state = {}
  @previous_onset_amplitude = 0.0
  @previous_onset_bands = {}
  @previous_flux_spectrum = nil
end

Instance Attribute Details

#band_splitterObject (readonly)

Returns the value of attribute band_splitter.



18
19
20
# File 'lib/vizcore/analysis/pipeline.rb', line 18

def band_splitter
  @band_splitter
end

#beat_detectorObject (readonly)

Returns the value of attribute beat_detector.



18
19
20
# File 'lib/vizcore/analysis/pipeline.rb', line 18

def beat_detector
  @beat_detector
end

#bpm_estimatorObject (readonly)

Returns the value of attribute bpm_estimator.



18
19
20
# File 'lib/vizcore/analysis/pipeline.rb', line 18

def bpm_estimator
  @bpm_estimator
end

#fft_processorObject (readonly)

Returns the value of attribute fft_processor.



18
19
20
# File 'lib/vizcore/analysis/pipeline.rb', line 18

def fft_processor
  @fft_processor
end

#smootherObject (readonly)

Returns the value of attribute smoother.



18
19
20
# File 'lib/vizcore/analysis/pipeline.rb', line 18

def smoother
  @smoother
end

Instance Method Details

#audio_normalize=(settings) ⇒ Hash

Returns normalized settings.

Parameters:

  • settings (Hash, nil)

Returns:

  • (Hash)

    normalized settings



60
61
62
63
# File 'lib/vizcore/analysis/pipeline.rb', line 60

def audio_normalize=(settings)
  @audio_normalize = normalize_audio_normalize(settings)
  @normalizer = build_normalizer(@audio_normalize)
end

#bpm_lock=(settings) ⇒ Float?

Parameters:

  • settings (Hash)

Returns:

  • (Float, nil)


67
68
69
70
71
# File 'lib/vizcore/analysis/pipeline.rb', line 67

def bpm_lock=(settings)
  values = symbolize_hash(settings)
  @locked_bpm = normalize_locked_bpm(values[:bpm], bpm_lock: values[:locked])
  @last_bpm = @locked_bpm if @locked_bpm
end

#call(samples) ⇒ Hash

Returns normalized analysis payload consumed by frame broadcaster.

Parameters:

  • samples (Array<Numeric>)

    audio frame samples

Returns:

  • (Hash)

    normalized analysis payload consumed by frame broadcaster



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/vizcore/analysis/pipeline.rb', line 99

def call(samples)
  amplitude = rms(samples)
  if silence?(amplitude)
    track_silent_frame(samples)
    return silent_frame(reset_tempo: sustained_silence?)
  end

  @silent_frame_count = 0

  fft = @fft_processor.call(samples)
  bands = @band_splitter.call(fft[:magnitudes])
  beat = @beat_detector.call(samples)
  beat_detected = beat[:beat]
  confidence = beat_confidence(beat)
  @beat_pulse = beat_detected ? 1.0 : @beat_pulse * BEAT_PULSE_DECAY
  @beat_pulse = 0.0 if @beat_pulse < BEAT_PULSE_FLOOR
  bpm = resolve_bpm(beat_detected)
  tempo = tempo_features(beat_detected: beat_detected, beat_count: beat[:beat_count], bpm: bpm)
  peak = peak_level(samples)
  spectrum_preview = preview_spectrum(fft[:magnitudes], bins: @fft_preview_bins)
  spectral = spectral_features(fft[:magnitudes], spectrum_preview)
  normalized = normalize_features(
    amplitude: amplitude,
    bands: bands,
    fft: spectrum_preview
  )
  band_peaks = update_band_peaks(normalized[:bands])
  onsets = detect_onsets(amplitude: normalized[:amplitude], bands: normalized[:bands])
  drums = detect_drum_sources(bands: normalized[:bands], onsets: onsets[:bands])

  {
    amplitude: @smoother.smooth(:amplitude, normalized[:amplitude]),
    peak: peak,
    bands: @smoother.smooth_hash(normalized[:bands], namespace: :bands),
    band_peaks: band_peaks,
    fft: @smoother.smooth_array(normalized[:fft], namespace: :fft),
    onset: onsets[:amplitude],
    onsets: onsets[:bands],
    drums: drums,
    beat: beat_detected,
    beat_confidence: confidence,
    beat_pulse: @beat_pulse,
    beat_count: beat[:beat_count],
    beat_phase: tempo[:beat_phase],
    beat_2: tempo[:beat_2],
    beat_4: tempo[:beat_4],
    beat_8: tempo[:beat_8],
    beat_triplet: tempo[:beat_triplet],
    bar_phase: tempo[:bar_phase],
    bar_count: tempo[:bar_count],
    phrase_count: tempo[:phrase_count],
    bpm: bpm,
    bpm_confidence: bpm_confidence,
    spectral_centroid: spectral[:centroid],
    spectral_rolloff: spectral[:rolloff],
    spectral_flatness: spectral[:flatness],
    spectral_flux: spectral[:flux],
    zero_crossing_rate: zero_crossing_rate(samples),
    peak_frequency: fft[:peak_frequency]
  }
end

#fft_preview_bins=(value) ⇒ Integer

Parameters:

  • value (Integer)

Returns:

  • (Integer)


81
82
83
# File 'lib/vizcore/analysis/pipeline.rb', line 81

def fft_preview_bins=(value)
  @fft_preview_bins = normalize_integer(value, fallback: DEFAULT_FFT_PREVIEW_BINS, min: 8, max: 128)
end

#onset_sensitivity=(value) ⇒ Float

Parameters:

  • value (Numeric)

Returns:

  • (Float)


75
76
77
# File 'lib/vizcore/analysis/pipeline.rb', line 75

def onset_sensitivity=(value)
  @onset_sensitivity = normalize_positive_number(value, fallback: 1.0)
end

#peak_hold_frames=(value) ⇒ Integer

Parameters:

  • value (Integer)

Returns:

  • (Integer)


87
88
89
# File 'lib/vizcore/analysis/pipeline.rb', line 87

def peak_hold_frames=(value)
  @peak_hold_frames = normalize_integer(value, fallback: 0, min: 0, max: 10_000)
end

#silence_reset_frames=(value) ⇒ Integer

Parameters:

  • value (Integer)

Returns:

  • (Integer)


93
94
95
# File 'lib/vizcore/analysis/pipeline.rb', line 93

def silence_reset_frames=(value)
  @silence_reset_frames = normalize_integer(value, fallback: SILENCE_RESET_FRAMES, min: 1, max: 10_000)
end