summaryrefslogtreecommitdiff
path: root/src/audio/spectral_brush.cc
blob: c6eb64dec2f7003f928b034982e347700c35b8aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
160
161
162
163
164
165
166
167
168
169
170
171
// This file is part of the 64k demo project.
// It implements the "Spectral Brush" primitive for procedural audio generation.
// Implementation of Bezier curves, vertical profiles, and spectrogram rendering.

#include "spectral_brush.h"

#include <cmath>

// Sample rate constant (matches demo audio configuration)
static const float SAMPLE_RATE = 32000.0f;

// Evaluate linear Bezier interpolation between control points
float evaluate_bezier_linear(const float* control_frames,
                              const float* control_values,
                              int n_points,
                              float frame) {
  if (n_points == 0) {
    return 0.0f;
  }
  if (n_points == 1) {
    return control_values[0];
  }

  // Clamp to first/last value if outside range
  if (frame <= control_frames[0]) {
    return control_values[0];
  }
  if (frame >= control_frames[n_points - 1]) {
    return control_values[n_points - 1];
  }

  // Find segment containing frame
  for (int i = 0; i < n_points - 1; ++i) {
    if (frame >= control_frames[i] && frame <= control_frames[i + 1]) {
      // Linear interpolation: value = v0 + (v1 - v0) * t
      const float t =
          (frame - control_frames[i]) / (control_frames[i + 1] - control_frames[i]);
      return control_values[i] * (1.0f - t) + control_values[i + 1] * t;
    }
  }

  // Should not reach here (fallback to last value)
  return control_values[n_points - 1];
}

// Home-brew deterministic RNG (LCG algorithm)
uint32_t spectral_brush_rand(uint32_t seed) {
  // LCG parameters (from Numerical Recipes)
  // X_{n+1} = (a * X_n + c) mod m
  const uint32_t a = 1664525;
  const uint32_t c = 1013904223;
  return a * seed + c;  // Implicit mod 2^32
}

// Evaluate vertical profile at distance from curve center
float evaluate_profile(ProfileType type, float distance, float param1, float param2) {
  switch (type) {
    case PROFILE_GAUSSIAN: {
      // Gaussian: exp(-(dist^2 / sigma^2))
      // param1 = sigma (width in bins)
      const float sigma = param1;
      if (sigma <= 0.0f) {
        return 0.0f;
      }
      return expf(-(distance * distance) / (sigma * sigma));
    }

    case PROFILE_DECAYING_SINUSOID: {
      // Decaying sinusoid: exp(-decay * dist) * cos(omega * dist)
      // param1 = decay rate
      // param2 = oscillation frequency (omega)
      const float decay = param1;
      const float omega = param2;
      const float envelope = expf(-decay * distance);
      const float oscillation = cosf(omega * distance);
      return envelope * oscillation;
    }

    case PROFILE_NOISE: {
      // Random noise: deterministic RNG based on distance
      // param1 = amplitude scale
      // param2 = seed
      const float amplitude = param1;
      const uint32_t seed = (uint32_t)(param2) + (uint32_t)(distance * 1000.0f);
      const uint32_t rand_val = spectral_brush_rand(seed);
      // Map to [0, 1]
      const float normalized = (float)(rand_val % 10000) / 10000.0f;
      return amplitude * normalized;
    }
  }

  return 0.0f;
}

// Internal implementation: Render Bezier curve with profile
static void draw_bezier_curve_impl(float* spectrogram,
                                    int dct_size,
                                    int num_frames,
                                    const float* control_frames,
                                    const float* control_freqs_hz,
                                    const float* control_amps,
                                    int n_control_points,
                                    ProfileType profile_type,
                                    float profile_param1,
                                    float profile_param2,
                                    bool additive) {
  if (n_control_points < 1) {
    return;  // Nothing to draw
  }

  // For each frame in the spectrogram
  for (int f = 0; f < num_frames; ++f) {
    // 1. Evaluate Bezier curve at this frame
    const float freq_hz =
        evaluate_bezier_linear(control_frames, control_freqs_hz, n_control_points, (float)f);
    const float amplitude =
        evaluate_bezier_linear(control_frames, control_amps, n_control_points, (float)f);

    // 2. Convert frequency (Hz) to frequency bin index
    // Nyquist frequency = SAMPLE_RATE / 2
    // bin = (freq_hz / nyquist) * dct_size
    const float nyquist = SAMPLE_RATE / 2.0f;
    const float freq_bin_0 = (freq_hz / nyquist) * dct_size;

    // 3. Apply vertical profile around freq_bin_0
    for (int b = 0; b < dct_size; ++b) {
      const float dist = fabsf(b - freq_bin_0);
      const float profile_val = evaluate_profile(profile_type, dist, profile_param1, profile_param2);
      const float contribution = amplitude * profile_val;

      const int idx = f * dct_size + b;
      if (additive) {
        spectrogram[idx] += contribution;
      } else {
        spectrogram[idx] = contribution;
      }
    }
  }
}

// Draw spectral brush (overwrites spectrogram content)
void draw_bezier_curve(float* spectrogram,
                       int dct_size,
                       int num_frames,
                       const float* control_frames,
                       const float* control_freqs_hz,
                       const float* control_amps,
                       int n_control_points,
                       ProfileType profile_type,
                       float profile_param1,
                       float profile_param2) {
  draw_bezier_curve_impl(spectrogram, dct_size, num_frames, control_frames, control_freqs_hz,
                         control_amps, n_control_points, profile_type, profile_param1,
                         profile_param2, false);
}

// Draw spectral brush (adds to existing spectrogram content)
void draw_bezier_curve_add(float* spectrogram,
                            int dct_size,
                            int num_frames,
                            const float* control_frames,
                            const float* control_freqs_hz,
                            const float* control_amps,
                            int n_control_points,
                            ProfileType profile_type,
                            float profile_param1,
                            float profile_param2) {
  draw_bezier_curve_impl(spectrogram, dct_size, num_frames, control_frames, control_freqs_hz,
                         control_amps, n_control_points, profile_type, profile_param1,
                         profile_param2, true);
}