77
88namespace {
99
10- struct ModeSignature {
11- float rms = 0 .0f ;
12- float zeroCrossRate = 0 .0f ;
13- float sideRatio = 0 .0f ;
10+ // Coarse half-octave log-magnitude fingerprint used to assert that the modes and
11+ // scenes stay audibly distinct. Comparing band shape rather than hand-weighted
12+ // scalars (rms/zero-crossings/roughness) keeps the check tied to timbre and
13+ // independent of the shipping parameter defaults, which are a product decision
14+ // and change more often than the synthesis paths do.
15+ constexpr std::size_t kBandCount = 16 ;
16+ constexpr float kLowestBandHz = 60 .0f ;
17+ constexpr std::size_t kAnalysisStart = 1024 ;
18+ constexpr float kMagnitudeFloor = 1 .0e-9f ;
19+
20+ // Minimum spectral distance required between any two modes, and between the
21+ // musical and extreme scenes. Measured worst-case separation at the shipping
22+ // defaults is ~0.117 for modes and ~0.150 for the scene pair, so this leaves
23+ // roughly a 2x margin rather than sitting inside the metric's own scatter.
24+ constexpr float kModeSeparation = 0 .06f ;
25+ constexpr float kSceneSeparation = 0 .06f ;
26+
27+ struct Signature {
28+ std::array<float , kBandCount > bands {};
1429 float peak = 0 .0f ;
15- float roughness = 0 .0f ;
1630};
1731
1832bool nearlyEqual (const float a, const float b, const float epsilon = 1 .0e-5f )
@@ -48,38 +62,95 @@ bool containsMidi(const downspout::gremlin::MidiMessage* events,
4862 return false ;
4963}
5064
51- ModeSignature measureRenderedSignature (const std::array<float , 4096 >& left, const std::array<float , 4096 >& right)
65+ // Goertzel magnitude for one bin over the analysis window.
66+ double bandMagnitude (const std::array<float , 4096 >& left,
67+ const std::array<float , 4096 >& right,
68+ const double frequencyHz,
69+ const double sampleRate)
5270{
53- ModeSignature signature {};
54- int crossings = 0 ;
55- int samples = 0 ;
56- float energy = 0 .0f ;
57- float side = 0 .0f ;
58- float roughness = 0 .0f ;
59- float previous = 0 .0f ;
60- for (std::size_t i = 1024 ; i < left.size (); ++i)
71+ const std::size_t count = left.size () - kAnalysisStart ;
72+ const double omega = 2.0 * 3.14159265358979323846 * frequencyHz / sampleRate;
73+ const double coeff = 2.0 * std::cos (omega);
74+
75+ double q1 = 0.0 ;
76+ double q2 = 0.0 ;
77+ for (std::size_t i = 0 ; i < count; ++i)
6178 {
62- const float mono = 0 .5f * (left[i] + right[i]);
63- if (samples > 0 && ((previous < 0 .0f && mono >= 0 .0f ) || (previous > 0 .0f && mono <= 0 .0f )))
64- ++crossings;
65- if (samples > 0 )
66- roughness += std::fabs (mono - previous);
67- previous = mono;
68-
69- energy += mono * mono;
70- side += std::fabs (left[i] - right[i]);
79+ const double sample = 0.5 * (static_cast <double >(left[kAnalysisStart + i])
80+ + static_cast <double >(right[kAnalysisStart + i]));
81+ const double q0 = coeff * q1 - q2 + sample;
82+ q2 = q1;
83+ q1 = q0;
84+ }
85+
86+ const double power = q1 * q1 + q2 * q2 - coeff * q1 * q2;
87+ return std::sqrt (std::max (0.0 , power)) / static_cast <double >(count);
88+ }
89+
90+ Signature measureRenderedSignature (const std::array<float , 4096 >& left,
91+ const std::array<float , 4096 >& right,
92+ const double sampleRate = 48000.0 )
93+ {
94+ Signature signature {};
95+
96+ for (std::size_t i = kAnalysisStart ; i < left.size (); ++i)
7197 signature.peak = std::max (signature.peak , std::max (std::fabs (left[i]), std::fabs (right[i])));
72- ++samples;
98+
99+ for (std::size_t band = 0 ; band < kBandCount ; ++band)
100+ {
101+ const double frequency = static_cast <double >(kLowestBandHz )
102+ * std::pow (2.0 , static_cast <double >(band) * 0.5 );
103+ const double magnitude = frequency < sampleRate * 0.45
104+ ? bandMagnitude (left, right, frequency, sampleRate)
105+ : 0.0 ;
106+ signature.bands [band] = static_cast <float >(std::log10 (magnitude + kMagnitudeFloor ));
73107 }
74108
75- signature.rms = samples > 0 ? std::sqrt (energy / static_cast <float >(samples)) : 0 .0f ;
76- signature.zeroCrossRate = samples > 0 ? static_cast <float >(crossings) / static_cast <float >(samples) : 0 .0f ;
77- signature.sideRatio = energy > 0 .0f ? side / (std::sqrt (energy) * static_cast <float >(samples)) : 0 .0f ;
78- signature.roughness = samples > 0 ? roughness / static_cast <float >(samples) : 0 .0f ;
79109 return signature;
80110}
81111
82- ModeSignature renderModeSignature (const std::size_t mode)
112+ // Mean-removed cosine distance: 0 means identical band shape, larger means more
113+ // timbral difference. Removing the mean discards overall level so the check is
114+ // about spectrum rather than loudness.
115+ float spectralDistance (const Signature& a, const Signature& b)
116+ {
117+ double meanA = 0.0 ;
118+ double meanB = 0.0 ;
119+ for (std::size_t i = 0 ; i < kBandCount ; ++i)
120+ {
121+ meanA += a.bands [i];
122+ meanB += b.bands [i];
123+ }
124+ meanA /= static_cast <double >(kBandCount );
125+ meanB /= static_cast <double >(kBandCount );
126+
127+ double dot = 0.0 ;
128+ double normA = 0.0 ;
129+ double normB = 0.0 ;
130+ for (std::size_t i = 0 ; i < kBandCount ; ++i)
131+ {
132+ const double x = static_cast <double >(a.bands [i]) - meanA;
133+ const double y = static_cast <double >(b.bands [i]) - meanB;
134+ dot += x * y;
135+ normA += x * x;
136+ normB += y * y;
137+ }
138+
139+ if (normA <= 0.0 || normB <= 0.0 )
140+ return 0 .0f ;
141+
142+ return static_cast <float >(1.0 - dot / std::sqrt (normA * normB));
143+ }
144+
145+ void printSignature (const char * label, const std::size_t index, const Signature& signature)
146+ {
147+ std::cerr << label << ' ' << index << " peak=" << signature.peak << " bands=[" ;
148+ for (std::size_t i = 0 ; i < kBandCount ; ++i)
149+ std::cerr << signature.bands [i] << (i + 1 < kBandCount ? " , " : " " );
150+ std::cerr << " ]\n " ;
151+ }
152+
153+ Signature renderModeSignature (const std::size_t mode)
83154{
84155 using downspout::gremlin::LiveParamId;
85156 using downspout::gremlin::MidiMessage;
@@ -102,7 +173,7 @@ ModeSignature renderModeSignature(const std::size_t mode)
102173 return measureRenderedSignature (left, right);
103174}
104175
105- ModeSignature renderSceneSignature (const downspout::gremlin::SceneId scene)
176+ Signature renderSceneSignature (const downspout::gremlin::SceneId scene)
106177{
107178 using downspout::gremlin::MidiMessage;
108179 using downspout::gremlin::Processor;
@@ -123,17 +194,6 @@ ModeSignature renderSceneSignature(const downspout::gremlin::SceneId scene)
123194 return measureRenderedSignature (left, right);
124195}
125196
126- float signatureDistance (const ModeSignature& a, const ModeSignature& b)
127- {
128- const float rmsA = std::max (a.rms , 1 .0e-6f );
129- const float rmsB = std::max (b.rms , 1 .0e-6f );
130- return std::fabs (std::log (rmsA / rmsB)) * 0 .35f
131- + std::fabs (a.zeroCrossRate - b.zeroCrossRate ) * 2 .2f
132- + std::fabs (a.sideRatio - b.sideRatio ) * 1 .4f
133- + std::fabs (a.peak - b.peak ) * 0 .35f
134- + std::fabs (a.roughness - b.roughness ) * 18 .0f ;
135- }
136-
137197} // namespace
138198
139199int main ()
@@ -218,7 +278,7 @@ int main()
218278 require (modePeak > 0 .005f , " gremlin mode should emit audio after note-on" );
219279 }
220280
221- std::array<ModeSignature , downspout::gremlin::kModeCount > signatures {};
281+ std::array<Signature , downspout::gremlin::kModeCount > signatures {};
222282 for (std::size_t mode = 0 ; mode < downspout::gremlin::kModeCount ; ++mode)
223283 signatures[mode] = renderModeSignature (mode);
224284
@@ -230,35 +290,37 @@ int main()
230290 {
231291 if (mode == other)
232292 continue ;
233- const float distance = signatureDistance (signatures[mode], signatures[other]);
293+ const float distance = spectralDistance (signatures[mode], signatures[other]);
234294 if (distance < nearest)
235295 {
236296 nearest = distance;
237297 nearestMode = other;
238298 }
239299 }
240- if (nearest <= 0 .035f )
241- std::cerr << " nearest signature distance for mode " << mode << " was " << nearest
242- << " against mode " << nearestMode << " \n mode " << mode
243- << " rms=" << signatures[mode].rms
244- << " zc=" << signatures[mode].zeroCrossRate
245- << " side=" << signatures[mode].sideRatio
246- << " peak=" << signatures[mode].peak
247- << " rough=" << signatures[mode].roughness
248- << " \n mode " << nearestMode
249- << " rms=" << signatures[nearestMode].rms
250- << " zc=" << signatures[nearestMode].zeroCrossRate
251- << " side=" << signatures[nearestMode].sideRatio
252- << " peak=" << signatures[nearestMode].peak
253- << " rough=" << signatures[nearestMode].roughness << ' \n ' ;
254- require (nearest > 0 .035f , " gremlin mode signatures should remain separated" );
300+ if (nearest <= kModeSeparation )
301+ {
302+ std::cerr << " nearest spectral distance for mode " << mode << " was " << nearest
303+ << " against mode " << nearestMode << " (threshold " << kModeSeparation << " )\n " ;
304+ printSignature (" mode" , mode, signatures[mode]);
305+ printSignature (" mode" , nearestMode, signatures[nearestMode]);
306+ }
307+ require (nearest > kModeSeparation , " gremlin mode signatures should remain separated" );
255308 }
256309
257- const ModeSignature musicalScene = renderSceneSignature (SceneId::melt);
258- const ModeSignature extremeScene = renderSceneSignature (SceneId::tunnel);
310+ const Signature musicalScene = renderSceneSignature (SceneId::melt);
311+ const Signature extremeScene = renderSceneSignature (SceneId::tunnel);
259312 require (musicalScene.peak > 0 .005f , " gremlin musical scene should emit audio" );
260313 require (extremeScene.peak > 0 .005f , " gremlin extreme scene should emit audio" );
261- require (signatureDistance (musicalScene, extremeScene) > 0 .08f ,
314+
315+ const float sceneDistance = spectralDistance (musicalScene, extremeScene);
316+ if (sceneDistance <= kSceneSeparation )
317+ {
318+ std::cerr << " musical/extreme scene spectral distance was " << sceneDistance
319+ << " (threshold " << kSceneSeparation << " )\n " ;
320+ printSignature (" scene" , 0 , musicalScene);
321+ printSignature (" scene" , 1 , extremeScene);
322+ }
323+ require (sceneDistance > kSceneSeparation ,
262324 " gremlin musical and extreme scenes should remain audibly separated" );
263325
264326 float monoOnly[128 ] {};
0 commit comments