Skip to content

Instantly share code, notes, and snippets.

@anytizer
Last active September 3, 2026 07:05
Show Gist options
  • Select an option

  • Save anytizer/822339e1391f16861e2d002ee79d0ac7 to your computer and use it in GitHub Desktop.

Select an option

Save anytizer/822339e1391f16861e2d002ee79d0ac7 to your computer and use it in GitHub Desktop.
Sound categorizer for anticipated strengths in beats
// This file is in response to one of the LMMS Discussion thread
// https://github.com/LMMS/lmms/discussions/7653#discussioncomment-18167418
// A temporary attempt to auto classify sound for calculating beat strenghts.
QList<Classifications> classifications = {};
classifications.append(Classifications(Instrument::Unknown, "Unknown", {}, dw->unknown)); // Keep on top
classifications.append(Classifications(
Instrument::Kick,
"Kick Bass Drum",
{
"kick", "bass", "bd", "drum",
"sub", "boom", "thump", "muffle",
// maadal
"dhing", "dhaang", "dhang", "ghan", "ghin",
// tabla
"ghe",
}, dw->kick));
classifications.append(Classifications(
Instrument::Snare,
"Snare",
{
"snare", "snr", "snap", "crack",
"pop", "brush", "sidestick",
"clap",
}, dw->snare));
classifications.append(Classifications(
Instrument::HiHat,
"Hi-Hat",
{
"hat", "hihat", "hh",
"open", "closed", "pedal",
"splash",
// bark, bottle, khat, phat
}, dw->hihat));
classifications.append(Classifications(
Instrument::Tom,
"Tom",
{
"tom", // "tomtom",
//"high tom", "mid tom", "low tom", "floor tom",
"floor", "rack",
// cowbell, conga, clave, rim, shot
}, dw->tom));
classifications.append(Classifications(
Instrument::Click,
"Click",
{
"click", "tick", "rim",
"rimshot", "shot", "stick", "dry",
}, dw->click));
classifications.append(Classifications(
Instrument::Cymbal,
"Cymbal",
{
"cymbal", "crash", "ride",
"china", "splash",
"gong", "sizzle",
"choke", "swell"
}, dw->cymbal));
classifications.append(Classifications(
Instrument::Percussion,
"Percussion",
{
"bongo",
"cowbell", "clave", "conga",
"tambourine", "timbale",
"cabasa",
"maraca",
"triangle",
"wood", "block",
"bell",
"guiro",
"jingle",
"shaker",
}, dw->percussion));
classifications.append(Classifications(
Instrument::Others,
"Others",
{
//"bell", // matched with cowbell :-(
// "whistle",
"fx", "effect", "noise", "misc"
}, dw->others));
// This file is in response to one of the LMMS Discussion thread
// https://github.com/LMMS/lmms/discussions/7653#discussioncomment-18167418
// raw calculations of drum weights (beats placement)
#pragma once
#include <random>
#include <cmath>
#include <algorithm>
#include <QList>
#include <QDebug>
#include <QRandomGenerator>
namespace lmms
{
class DrumWeights
{
public:
QList<int> unknown = {}; // all other unclassifieds
QList<int> kick = {}; // kick, bass, drum
QList<int> snare = {};
QList<int> hihat = {};
QList<int> tom = {};
QList<int> click = {};
QList<int> cymbal = {};
QList<int> percussion = {};
QList<int> others = {}; // sounds explicitly classified as others
DrumWeights(int numerator, int denominator, int tempo, int TOTAL_STEPS)
{
//this->populateUnknowns(TOTAL_STEPS, TOTAL_STEPS/4); // 2, 4, 8, 16
this->populateUnknowns(TOTAL_STEPS, 4);
this->populateProbabilities(numerator, denominator, tempo, TOTAL_STEPS);
}
private:
void populateUnknowns(int TOTAL_STEPS, int howMany)
{
this->unknown.clear();
QList<int> randomIndexes = randomsPicks(TOTAL_STEPS, howMany);
for (int step=0; step<TOTAL_STEPS; ++step)
{
int r_unknown = 0.0f;
if(randomIndexes.contains(step)) r_unknown = 40.0f;
r_unknown = ClampProbability(r_unknown, 0);
this->unknown.append(r_unknown);
}
qDebug() << "Unknowns Plan" << this->unknown;
}
enum Speeds
{
Slower,
Slow,
Medium,
Fast,
Faster
};
void populateProbabilities(int numerator, int denominator, int tempo, int TOTAL_STEPS)
{
// speeds calculator
// // const float secondsPerQuarter = 60.0f / tempo;
// //const float stepSeconds = (60.0f / tempo) * (4.0f / denominator) / 4.0f; // working!! keep!
// const float stepSeconds = (60.0f / tempo) * denominator;
// Speeds speed = Speeds::Medium;
// if (stepSeconds >= 0.200f) speed = Speeds::Slower;
// else if (stepSeconds >= 0.140f) speed = Speeds::Slow;
// else if (stepSeconds >= 0.090f) speed = Speeds::Medium;
// else if (stepSeconds >= 0.060f) speed = Speeds::Fast;
// else speed = Speeds::Faster;
//this->unknown.clear();
this->kick.clear();
this->snare.clear();
this->hihat.clear();
this->tom.clear();
this->click.clear();
this->cymbal.clear();
this->percussion.clear();
this->others.clear();
//float density = Density(tempo); // @todo make use of TOTAL_STEPS as well.
float density = CalculateDensity(tempo, TOTAL_STEPS); // , 16.0f); // 500f
// populate eveything else
for (int step=0; step<TOTAL_STEPS; ++step)
{
int strength = BeatStrength(step, numerator);
// Generate random values per step
//int r_unknown = UnknownRule(step, density);
int r_kick = KickRule(step, density, strength);
int r_snare = SnareRule(step, density, numerator);
int r_hihat = HiHatRule(step, density);
int r_tom = TomRule(step, density);
int r_click = ClickRule(step, density);
int r_cymbal = CymbalRule(step, density);
int r_percussion = PercussionRule(step, density);
int r_others = OtherRule(step, density);
//this->unknown.append(r_unknown);
this->kick.append(r_kick);
this->snare.append(r_snare);
this->hihat.append(r_hihat);
this->tom.append(r_tom);
this->click.append(r_click);
this->cymbal.append(r_cymbal);
this->percussion.append(r_percussion);
this->others.append(r_others);
}
}
/**
PLAN : 4 / 4 @ 140 = 32
unknwn: 83 0 90 0 0 0 72 0 0 0 0 0 0 0 0 51
0 0 0 0 0 0 0 0 40 39 0 0 1 65 0 0
kick : 100 25 43 33 90 9 27 17 98 17 35 25 90 9 27 17
26 0 0 0 26 0 0 0 26 0 0 0 26 0 0 0
snare : 0 4 6 9 100 4 6 9 0 4 6 9 100 4 6 9
0 4 6 9 0 4 6 9 0 4 6 9 0 4 6 9
hihat : 95 45 63 45 95 45 63 45 95 45 63 45 95 45 63 45
95 45 63 45 95 45 63 45 95 45 63 45 95 45 63 45
tom : 52 5 14 5 30 5 14 5 52 5 14 5 30 5 14 5
52 5 14 5 30 5 14 5 52 5 14 5 30 5 14 5
click : 4 4 21 4 37 4 21 4 4 4 21 4 37 4 21 4
4 4 21 4 37 4 21 4 4 4 21 4 37 4 21 4
cymbal: 69 2 2 2 11 2 2 2 28 2 2 2 11 2 2 2
69 2 2 2 11 2 2 2 28 2 2 2 11 2 2 2
percsn: 29 28 52 28 45 28 52 28 16 28 52 28 45 28 52 28
29 28 52 28 45 28 52 28 16 28 52 28 45 28 52 28
others: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
*/
QList<int> randomsPicks(int totalIterations, int maxRandomPicks)
{
// 1. Generate a list of all available indexes (0 to 15)
QList<int> allIndexes;
for (int i = 0; i < totalIterations; ++i) {
allIndexes.append(i);
}
// 2. Shuffle the indexes randomly
// QRandomGenerator::global() provides secure, modern random numbers in Qt 5/6
std::random_device rd;
std::mt19937 generator(rd());
std::shuffle(allIndexes.begin(), allIndexes.end(), generator);
// std::random_shuffle(allIndexes.begin(), allIndexes.end(), [](int i) {
// return QRandomGenerator::global()->bounded(i);
// });
// 3. Pick the first N indexes (up to maxRandomPicks)
QList<int> selectedIndexes;
int picks = qMin(maxRandomPicks, totalIterations);
for (int i = 0; i < picks; ++i) {
selectedIndexes.append(allIndexes[i]);
}
// 4. Overhead for screen print only
std::sort(selectedIndexes.begin(), selectedIndexes.end());
qDebug() << "Selected Random Indexes:" << selectedIndexes;
return selectedIndexes;
}
/**
1. Beat strength
Assign every 16th note a strength value.
For 4/4 (16 steps):
Step: 1 2 3 4 | 5 6 7 8 | 9 10 11 12 |13 14 15 16
Strength: 10 2 4 3 | 8 2 4 3 | 9 2 4 3 | 8 2 4 3
For 3/4 (12 steps):
10 2 4 3 | 8 2 4 3 | 8 2 4 3
For 2/4 (8 steps):
10 2 4 3 | 8 2 4 3
*/
int BeatStrength(int step, int numerator)
{
int beat = step / 4;
int pos = step % 4;
static const int beatAccent4[] = {10,8,9,8};
static const int beatAccent3[] = {10,8,8};
static const int beatAccent2[] = {10,8};
int accent;
switch (numerator)
{
case 2: accent = beatAccent2[beat]; break;
case 3: accent = beatAccent3[beat]; break;
default: accent = beatAccent4[beat];
}
static const int subdivision[] = {0,-8,-6,-7};
return accent + subdivision[pos];
}
/**
* Tempo → Density
* Instead of five speeds (tables), compute a density factor.
*/
float Density(float bpm)
{
if (bpm < 70) return 0.25f;
if (bpm < 100) return 0.45f;
if (bpm < 140) return 0.65f;
if (bpm < 180) return 0.85f;
return 1.00f;
}
/**
* Set maxNotesBaseline to whatever numerical target defines a "full" or standard arrangement in application.
* For example:
* If a standard 3-minute procedural track usually contains around 500 notes, set it to 500f.
* If you are generating short loops or micro-rhythms with only 64 notes, set it to 64f.
*/
float CalculateDensity(float bpm, int TOTAL_STEPS=16)
{
int maxNotesBaseline = 64; // 64 | 16 | TOTAL_STEPS
// 1. Base tempo scaling (faster tempos naturally reduce per-beat multiplier to avoid clutter)
float tempoFactor;
if (bpm < 70) tempoFactor = 0.25f;
else if (bpm < 100) tempoFactor = 0.45f;
else if (bpm < 140) tempoFactor = 0.65f;
else if (bpm < 180) tempoFactor = 0.85f;
else tempoFactor = 1.00f;
// 2. Note load scaling (ratio of desired total notes against a standard baseline)
// Clamp the ratio so extreme note counts don't break your math
float noteFactor = std::clamp((float)TOTAL_STEPS / maxNotesBaseline, 0.1f, 2.0f);
// 3. Combine factors (multiplicative or weighted blend)
float finalDensity = tempoFactor * noteFactor;
// Clamp final output to standard normalized range [0.0f, 1.0f]
//return Mathf.Clamp01(finalDensity);
return std::clamp(finalDensity, 0.00f, 1.00f);
}
/**
Typical 4/4 result at 120 BPM:
100 12 28 16
84 10 25 18
92 12 30 20
84 15 30 18
// 1. Bass Drum: Strong hits on main beats (0, 4, 8, 12), occasional syncopation
// from rule
// bool isMainBeat = (step % 4 == 0);
// double kickChance = isMainBeat ? 0.85 : 0.15;
// add_note = dice < kickChance;
*/
/**
QVector<int> indexes;
for (int i = 0; i < 16; ++i)
indexes.append(i);
std::shuffle(indexes.begin(), indexes.end(), *QRandomGenerator::global());
QVector<int> selected(indexes.begin(), indexes.begin() + 4);
for (int index : selected)
{
qDebug() << "Picked:" << index;
}
*/
int KickRule(int step, float density, int strength)
{
float p = 0.0f;
// Beat strength
p += strength * 8.0f;
// More fills at higher tempos
p += density * 15.0f;
switch (step % 4)
{
case 0: // Quarter note
p += 20.0f;
break;
case 2: // Eighth
p += density * 12.0f;
break;
default: // 16ths
p += density * 6.0f;
break;
}
// Downbeat always wins
if (step == 0)
p = 100;
return ClampProbability(p, 0);
}
/**
Typical 4/4:
0 5 10 15
100 8 12 18
0 5 10 15
100 8 12 18
// 2. Snare: Heavy backbeats on steps 4 and 12 (beats 2 and 4)
// bool isBackbeat = (step == 4 || step == 12);
// double snareChance = isBackbeat ? 0.95 : 0.03;
// add_note = dice < snareChance;
*/
int SnareRule(int step,
float density,
int numerator
)
{
// Main backbeats
if (numerator == 4)
{
if (step == 4 || step == 12)
return 100;
}
else if (numerator == 3)
{
if (step == 4)
return 100;
if (step == 8)
return 90;
}
else if (numerator == 2)
{
if (step == 4)
return 100;
}
// Ghost notes
float p = 0.0f;
switch (step % 4)
{
case 1: p = density * 10; break;
case 2: p = density * 15; break;
case 3: p = density * 20; break;
}
return ClampProbability(p, 0);
}
/**
* Hi-hat density increases almost linearly with tempo.
Slow (density = 0.25)
95 33 54 32
95 33 54 32
95 33 54 32
95 33 54 32
Medium (density = 0.65)
95 60 76 61
95 60 76 61
95 60 76 61
95 60 76 61
Very Fast (density = 1.0)
95 85 95 85
95 85 95 85
95 85 95 85
95 85 95 85
// 3. Hi-Hat: High density driving the 16th-note subdivision
// add_note = dice < 0.65;
*/
int HiHatRule(int step, float density)
{
if (step % 4 == 0)
{
// Quarter notes
return 95;
}
if (step % 2 == 0)
{
// Eighths
return ClampProbability(40 + density * 55, 0);
}
// 16ths
return ClampProbability(15 + density * 70, 0);
}
/**
Density 0.2
T--- ---- T--- ----
Density 0.5
T--- --T- T--- -T--
Density 1.0
T-T- T-T- T-T- -TT-
| Density | Result |
| ------: | ------------------------------------------------------------ |
| **0.0** | Mostly only strong beat accents. |
| **0.5** | Occasional tom hits on quarter and some eighth notes. |
| **1.0** | Fairly active tom groove with rare 16th-note embellishments. |
// 5. Tom: Accent/fill instrument, more likely toward the end of the bar (steps 12 to 15)
// bool isFillArea = (step >= 12);
// int tomChance = isFillArea ? 35 : 5;
// add_note = dice < tomChance;
*/
int TomRule(int step, float density)
{
if (step % 8 == 0)
{
// Strong accents (beats 1 and 3)
return ClampProbability(35 + density * 40, 0);
}
if (step % 4 == 0)
{
// Secondary accents (beats 2 and 4)
return ClampProbability(15 + density * 35, 0);
}
if (step % 2 == 0)
{
// Occasional eighth-note toms
return ClampProbability(5 + density * 20, 0);
}
// Rare 16th-note hits, mostly at high density
return ClampProbability(density * 12, 0);
}
/**
0.0 → Almost silent.
0.5 → Occasional syncopated clicks.
1.0 → Frequent off-beat accents.
*/
int ClickRule(int step, float density)
{
if (step % 8 == 4)
{
// Off-beat accent
return ClampProbability(20 + density * 40, 0);
}
if (step % 4 == 2)
{
// Secondary syncopation
return ClampProbability(10 + density * 25, 0);
}
// Rare ghost clicks
return ClampProbability(density * 10, 0);
}
/**
0.0 → Sparse eighth notes.
0.5 → Steady eighth-note groove with occasional 16ths.
1.0 → Nearly continuous 16th-note shaker.
| Density | Click | Shaker |
| ------: | :----------------: | :----------------: |
| **0.2** | `----C-------C---` | `S-S-S-S-S-S-S-S-` |
| **0.5** | `--C-----C---C---` | `SS-SS-SS-SS-SS-S` |
| **1.0** | `-C-C-C--CC-C-C-C` | `SSSSSSSSSSSSSSSS` |
*/
// int ShakerRule(int step, float density)
// {
// if (step % 2 == 0)
// {
// // Eighth-note pulse
// return ClampProbability(30 + density * 60, 0);
// }
// // 16th-note embellishments
// return ClampProbability(10 + density * 50, 0);
// }
/**
Density 0.2
C--- ---- ---- ----
Density 0.5
C--- ---- C--- ----
Density 1.0
C--- C--- C--- ----
| Density | Typical Pattern |
| ------: | --------------------------------------------------------- |
| **0.0** | Crash mainly at the start of the bar. |
| **0.5** | Start-of-bar crash with occasional mid-bar cymbal. |
| **1.0** | Frequent crashes/rides on strong beats, but still sparse. |
// 6. Cymbal: Strong accent on the very first downbeat (step 0)
// bool isDownbeat = (step == 0);
// int cymbalChance = isDownbeat ? 90 : 2;
// add_note = dice < cymbalChance;
// Cymbal Breakdown by Subdivision
// Quarter Notes: 4 repetitions per measure (hits on counts 1, 2, 3, 4).
// Eighth Notes: 8 repetitions per measure (hits on counts 1 & 2 & 3 & 4 &).
// Sixteenth Notes: 16 repetitions per measure (hits on 1 e & a 2 e &
*/
int CymbalRule(int step, float density)
{
if (step % 16 == 0)
{
// Phrase/bar start (strong crash)
return ClampProbability(50 + density * 45, 0);
}
if (step % 8 == 0)
{
// Mid-bar accent or occasional ride
return ClampProbability(15 + density * 30, 0);
}
if (step % 4 == 0)
{
// Rare quarter-note accents
return ClampProbability(5 + density * 15, 0);
}
// Almost never elsewhere
return ClampProbability(density * 5, 0);
}
/**
Behavior
Steps 0, 16, ...: Occasional phrase accents.
Steps 4, 12, ...: Strong chance for percussion fills.
Steps 2, 6, 10, 14, ...: Good probability for syncopated hits.
Odd sixteenths: Light ghost percussion.
Remaining positions: Low probability.
This produces patterns like:
Kick: X---X---X---X---
Snare: ----X-------X---
Hi-Hat: x-x-x-x-x-x-x-x-
Percussion: --o-o---o-o--o--
Cymbal: C-------r-------
*/
int PercussionRule(int step, float density)
{
if (step % 16 == 0)
{
// Phrase start (conga, cowbell, etc.)
return ClampProbability(20 + density * 20, 0);
}
if (step % 8 == 4)
{
// Backbeat embellishment
return ClampProbability(30 + density * 35, 0);
}
if (step % 4 == 2)
{
// Off-beat groove
return ClampProbability(35 + density * 40, 0);
}
if (step % 2 == 1)
{
// Sixteenth-note syncopation
return ClampProbability(15 + density * 30, 0);
}
// Quarter-note positions
return ClampProbability(10 + density * 15, 0);
}
/**
* For sounds classified as OTHERS
*/
int OtherRule(int step, float density)
{
float p = 0.00f;
return ClampProbability(p, 0);
}
/**
Kick → Foundation
Snare → Backbeat
Hi-Hat → Primary groove
Tom → Fills/accents
Cymbal → Crashes and transitions
Click → Sparse syncopation
Shaker → Continuous high-frequency texture
*/
// int Humanize(int p)
// {
// p += Random(-5, 5); // ±5%
// return std::clamp(p, 0, 100);
// }
// inline int ClampProbability(int probability)
// {
// return std::clamp(
// static_cast<int>(std::round(probability)),
// 0,
// 100
// );
// }
// drum probabilities: combine clamping and humanization into one function:
inline int ClampProbability(float probability, int variation = 0)
{
probability += static_cast<float>(Random(-variation, +variation));
if (probability < 0.0f) probability = 0.0f;
if (probability > 100.0f) probability = 100.0f;
return static_cast<int>(std::round(probability));
}
float Random(const int minimum, const int maximum)
{
static std::random_device rd;
static std::mt19937 generator(rd());
std::uniform_real_distribution<float> distribution(
static_cast<float>(minimum),
static_cast<float>(maximum)
);
return distribution(generator);
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment