diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..130320c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Object files +*.o + +# Executables +gal +gal2 +gal3 +opt +synth_optimizer +oscillator_params +rhythm_evolution + +# Distribution files +*.tar.gz +ngal-*/ diff --git a/ACG.cc b/ACG.cc index 9facac5..745a281 100755 --- a/ACG.cc +++ b/ACG.cc @@ -123,7 +123,7 @@ static short randomStateTable[][3] = { // #define RANDOM_PERM_SIZE 64 -_G_uint32_t randomPermutations[RANDOM_PERM_SIZE] = { +uint32_t randomPermutations[RANDOM_PERM_SIZE] = { 0xffffffff, 0x00000000, 0x00000000, 0x00000000, // 3210 0x0000ffff, 0x00ff0000, 0x00000000, 0xff000000, // 2310 0xff0000ff, 0x0000ff00, 0x00000000, 0x00ff0000, // 3120 @@ -149,7 +149,7 @@ _G_uint32_t randomPermutations[RANDOM_PERM_SIZE] = { // SEED_TABLE_SIZE must be a power of 2 // #define SEED_TABLE_SIZE 32 -static _G_uint32_t seedTable[SEED_TABLE_SIZE] = { +static uint32_t seedTable[SEED_TABLE_SIZE] = { 0xbdcc47e5, 0x54aea45d, 0xec0df859, 0xda84637b, 0xc8c6cb4f, 0x35574b01, 0x28260b7d, 0x0d07fdbf, 0x9faaeeb0, 0x613dd169, 0x5ce2d818, 0x85b9e706, @@ -171,15 +171,15 @@ static _G_uint32_t seedTable[SEED_TABLE_SIZE] = { // LC_C = result of a long trial & error series = 3907864577 // -static const _G_uint32_t LC_A = 66049U; -static const _G_uint32_t LC_C = 3907864577U; -static inline _G_uint32_t LCG(_G_uint32_t x) +static const uint32_t LC_A = 66049U; +static const uint32_t LC_C = 3907864577U; +static inline uint32_t LCG(uint32_t x) { return( x * LC_A + LC_C ); } -ACG::ACG(_G_uint32_t seed, int size) +ACG::ACG(uint32_t seed, int size) { register int l; initialSeed = seed; @@ -204,8 +204,8 @@ ACG::ACG(_G_uint32_t seed, int size) // // Allocate the state table & the auxillary table in a single malloc // - - state = new _G_uint32_t[stateSize + auxSize]; + + state = new uint32_t[stateSize + auxSize]; auxState = &state[stateSize]; reset(); @@ -217,7 +217,7 @@ ACG::ACG(_G_uint32_t seed, int size) void ACG::reset() { - register _G_uint32_t u; + register uint32_t u; if (initialSeed < SEED_TABLE_SIZE) { u = seedTable[ initialSeed ]; @@ -246,9 +246,9 @@ ACG::reset() } lcgRecurr = u; - + #if 0 /* Nonsense! */ - assert(sizeof(double) == 2 * sizeof(_G_int32_t)); + assert(sizeof(double) == 2 * sizeof(int32_t)); #endif } @@ -263,24 +263,24 @@ ACG::~ACG() // Returns 32 bits of random information. // -_G_uint32_t +uint32_t ACG::asLong() { - _G_uint32_t result = state[k] + state[j]; + uint32_t result = state[k] + state[j]; state[k] = result; j = (j <= 0) ? (stateSize-1) : (j-1); k = (k <= 0) ? (stateSize-1) : (k-1); - + short int auxIndex = (result >> 24) & (auxSize - 1); - register _G_uint32_t auxACG = auxState[auxIndex]; + register uint32_t auxACG = auxState[auxIndex]; auxState[auxIndex] = lcgRecurr = LCG(lcgRecurr); - + // // 3c is a magic number. We are doing four masks here, so we // do not want to run off the end of the permutation table. // This insures that we have always got four entries left. // - register _G_uint32_t *perm = & randomPermutations[result & 0x3c]; + register uint32_t *perm = & randomPermutations[result & 0x3c]; result = *(perm++) & auxACG; result |= *(perm++) & ((auxACG << 24) diff --git a/ACG.h b/ACG.h index 1a26b42..d3b30cc 100755 --- a/ACG.h +++ b/ACG.h @@ -42,26 +42,26 @@ Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. class ACG : public RNG { - _G_uint32_t initialSeed; // used to reset generator + uint32_t initialSeed; // used to reset generator int initialTableEntry; - _G_uint32_t *state; - _G_uint32_t *auxState; + uint32_t *state; + uint32_t *auxState; short stateSize; short auxSize; - _G_uint32_t lcgRecurr; + uint32_t lcgRecurr; short j; short k; protected: public: - ACG(_G_uint32_t seed = 0, int size = 55); + ACG(uint32_t seed = 0, int size = 55); virtual ~ACG(); // // Return a long-words word of random bits // - virtual _G_uint32_t asLong(); + virtual uint32_t asLong(); virtual void reset(); }; diff --git a/MUSICAL_APPLICATIONS.md b/MUSICAL_APPLICATIONS.md new file mode 100644 index 0000000..1ea75be --- /dev/null +++ b/MUSICAL_APPLICATIONS.md @@ -0,0 +1,293 @@ +# Musical Applications of NGAL + +This document describes how to use the NGAL genetic algorithm library for musical optimization and sound design tasks. + +## Overview + +Genetic algorithms are powerful tools for exploring complex parameter spaces and finding optimal solutions for musical problems. They work by: + +1. **Evolution**: Creating a population of random solutions +2. **Selection**: Evaluating fitness based on musical criteria +3. **Crossover**: Combining good solutions to create offspring +4. **Mutation**: Introducing variation to explore new possibilities +5. **Iteration**: Repeating until convergence or satisfaction + +## Musical Examples Included + +### 1. Waveform Synthesis Optimizer (`synth_optimizer`) + +**Purpose**: Find optimal harmonic amplitudes to approximate a target waveform. + +**How it works**: +- Uses 8 harmonics to synthesize a waveform +- Compares against a target sawtooth wave +- Minimizes RMS error between synthesized and target waveforms + +**Build and run**: +```bash +make synth_optimizer +./synth_optimizer +``` + +**What you'll learn**: +- How genetic algorithms can discover Fourier series coefficients +- Why sawtooth waves need specific harmonic relationships (1, 1/2, 1/3, 1/4...) +- How to optimize continuous parameters for waveform matching + +**Applications**: +- Wavetable synthesis design +- Timbre matching and morphing +- Educational tool for understanding harmonic content + +### 2. Oscillator Parameter Optimizer (`oscillator_params`) + +**Purpose**: Find musically interesting synthesizer parameter combinations. + +**Parameters optimized** (6 dimensions): +- Base frequency (normalized 0-1) +- FM modulation depth (0-1) +- FM modulation ratio (0-16) +- Wave shaping amount (0-1) +- Filter cutoff frequency (0-1) +- Filter resonance (0-1) + +**Fitness criteria**: +- **Spectral richness**: Harmonically complex sounds +- **Musicality**: Avoiding harsh extremes, favoring integer ratios +- **Balance**: Sweet spots in parameter combinations + +**Build and run**: +```bash +make oscillator_params +./oscillator_params +``` + +**What you'll learn**: +- How parameter interactions create interesting timbres +- Why certain FM ratios (integer and simple fractions) sound musical +- The "sweet spots" in synthesis parameter space + +**Applications**: +- Preset generation for synthesizers +- Sound design exploration +- Finding starting points for manual tweaking +- "Two-button parameter tweaker" optimization + +### 3. Rhythm Pattern Evolution (`rhythm_evolution`) + +**Purpose**: Evolve musically interesting rhythmic patterns. + +**Pattern encoding** (16-step sequencer): +- Each step has: active/inactive, velocity (0-127), articulation (0-15) +- Total search space: 2^16 × 128^16 × 16^16 (enormous!) + +**Fitness criteria**: +- **Syncopation**: Off-beat emphasis and weak-beat accents +- **Density**: Ideal number of active steps (6-10 out of 16) +- **Dynamics**: Velocity variation and range +- **Grounding**: Presence of downbeat and backbeats + +**Build and run**: +```bash +make rhythm_evolution +./rhythm_evolution +``` + +**What you'll learn**: +- How complexity emerges from simple fitness rules +- What makes rhythms feel "musical" vs random +- How genetic operators preserve and recombine rhythmic motifs + +**Applications**: +- Drum pattern generation +- Rhythmic accompaniment ideas +- Euclidean rhythm discovery +- Polyrhythm exploration + +## Building All Examples + +```bash +make clean +make +``` + +This will build all three musical examples plus the original library demos. + +## How to Create Your Own Musical Application + +Here's a template for creating custom musical optimizations: + +```cpp +#include "ga.h" + +// 1. Define your parameter encoding +struct MyMusicalParams { + // Your parameters here +}; + +// 2. Decode chromosome into parameters +MyMusicalParams decodeChromosome(CChromosome &chr) { + // Convert genes to musical parameters +} + +// 3. Define fitness function +double CEvaluator::EvaluateFitness(CChromosomeBase &chrbase) { + CChromosome &chr = (CChromosome &)chrbase; + + MyMusicalParams params = decodeChromosome(chr); + + // Calculate musical metrics + double fitness = calculateMusicalQuality(params); + + return fitness; +} + +// 4. Required global instances +CEvaluator &EV = *new CEvaluator; +CChromosomeFactoryBase &CF = *new CChromosomeFactory; + +// 5. Configure and run GA +int main() { + COpMutate opmut(0.02); // Mutation rate + COpCrossover opcross(0.75); // Crossover rate + COpSelectStochastic selector; + + CEngine ga(50, NUM_PARAMS, 1.2); // Population, genes, scale + + ga.SetMutateOp(opmut); + ga.SetCrossoverOp(opcross); + ga.SetSelectionOp(selector); + + for (int i = 0; i < MAX_GENERATIONS; i++) { + ga.Generation(); + // Monitor progress, check convergence + } + + // Extract best solution + CPool &pool = ga.GetPool(); + // Use pool.GetBest() to get optimal parameters +} +``` + +## Tips for Musical GA Design + +### Choosing Chromosome Types + +- `unsigned char` (8 bits): Small parameter ranges (0-255) +- `unsigned short` (16 bits): Medium ranges (0-65535), good for audio +- `unsigned long` (32 bits): Large ranges, floating-point approximation + +### Setting GA Parameters + +**Population size**: +- Larger = better exploration, slower convergence +- Typical: 30-100 individuals +- Musical applications: 40-60 works well + +**Mutation rate**: +- Lower (0.001-0.01): Fine-tuning, refinement +- Medium (0.02-0.04): Balanced exploration +- Higher (0.05-0.1): Maximum variety, slower convergence +- Musical applications: 0.02-0.04 recommended + +**Crossover rate**: +- Higher (0.7-0.9): More recombination +- Lower (0.3-0.5): More preservation +- Musical applications: 0.7-0.8 recommended + +**Scale factor**: +- Higher (1.5-2.0): Aggressive selection pressure +- Lower (1.0-1.2): Gentler, more diverse population +- Musical applications: 1.2-1.5 works well + +### Designing Fitness Functions + +**Multi-objective fitness**: +```cpp +double fitness = (criterion1 * weight1) + + (criterion2 * weight2) + + (criterion3 * weight3); +``` + +**Common musical criteria**: +- Spectral centroid (brightness) +- Harmonic vs inharmonic content +- Rhythmic regularity vs complexity +- Dynamic range +- Timbral variation over time + +**Avoid fitness cliffs**: +- Use smooth, continuous fitness functions +- Prefer gradients over binary pass/fail +- Scale different metrics to similar ranges + +### Convergence Detection + +```cpp +if (pool.GetVariance() < threshold) { + // Population has converged +} +``` + +Small variance means the population has settled on similar solutions. + +## Advanced Techniques + +### Multi-Objective Optimization + +For competing goals (e.g., "bright but not harsh"), use weighted sums or Pareto optimization concepts. + +### Interactive Evolution + +Modify the fitness function to include human feedback: +```cpp +double fitness = automaticMetrics(params) + humanRating * weight; +``` + +### Constraint Handling + +Add penalties for invalid parameters: +```cpp +if (violatesConstraint(params)) { + fitness *= 0.1; // Heavy penalty +} +``` + +### Preserving Diversity + +Keep the best N solutions and present multiple options instead of just the global optimum. + +## Musical Problem Ideas + +Here are more ideas for musical genetic algorithm applications: + +1. **Chord Progression Generator**: Evolve harmonically interesting progressions +2. **Melody Generator**: Optimize for contour, intervals, and cadence +3. **Reverb Parameter Finder**: Match a target spatial characteristic +4. **Compressor Settings**: Optimize dynamics processing for a mix +5. **EQ Curve Matching**: Find EQ settings that match a reference spectrum +6. **Filter Sweep Designer**: Create interesting modulation curves +7. **Arpeggiator Pattern**: Evolve melodic patterns from chord tones +8. **Granular Synthesis Parameters**: Optimize grain size, density, pitch +9. **Sample Slicer**: Find optimal slice points in loops +10. **Microtonal Scale Generator**: Create custom tuning systems + +## Further Reading + +- "The Computer Music Tutorial" by Curtis Roads +- "Evolutionary Computer Music" by Eduardo Reck Miranda +- "Genetic Algorithms in Search, Optimization, and Machine Learning" by Goldberg +- "AI and Music" papers from ICMC and NIME conferences + +## Contributing + +Have you created an interesting musical application with NGAL? Consider sharing it! The genetic algorithm approach is particularly good for: + +- Exploring unknown parameter spaces +- Finding non-obvious solutions +- Creating variation on themes +- Automating tedious parameter searches + +--- + +*Remember: Genetic algorithms find local optima, not guaranteed global optima. But in music, "interesting" is often more valuable than "perfect"!* diff --git a/Makefile b/Makefile index 1f4616c..2704b0f 100755 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CC=g++ CCFLAGS=-Wall -O3 -s VER=1.9.1 MESSAGE="SGAL Simple Genetic Algorithm Library 1.9.0" -PROGS=gal gal2 gal3 opt +PROGS=gal gal2 gal3 opt synth_optimizer oscillator_params rhythm_evolution HEADERS=ACG.h RNG.h chrom.h pool.h operator.h engine.h eval.h pRandom.h ga.h SOURCES=ACG.cc RNG.cc pool.cc operator.cc engine.cc pRandom.cc opt.cc gal.cc gal2.cc gal3.cc OBJS=ACG.o RNG.o pool.o operator.o engine.o pRandom.o @@ -22,6 +22,15 @@ gal3: $(OBJS) gal3.o opt: $(OBJS) opt.o $(CC) $(CCFLAGS) -o $@ $@.o $(OBJS) $(LIBS) +synth_optimizer: $(OBJS) synth_optimizer.o + $(CC) $(CCFLAGS) -o $@ $@.o $(OBJS) $(LIBS) + +oscillator_params: $(OBJS) oscillator_params.o + $(CC) $(CCFLAGS) -o $@ $@.o $(OBJS) $(LIBS) + +rhythm_evolution: $(OBJS) rhythm_evolution.o + $(CC) $(CCFLAGS) -o $@ $@.o $(OBJS) $(LIBS) + $(HEADERS): co $@ diff --git a/RNG.cc b/RNG.cc index 4fb7626..3360ed8 100755 --- a/RNG.cc +++ b/RNG.cc @@ -41,7 +41,7 @@ RNG::RNG() { #if 0 /* Nonsense! */ - assert (sizeof(double) == 2 * sizeof(_G_uint32_t)); + assert (sizeof(double) == 2 * sizeof(uint32_t)); #endif // // The following is a hack that I attribute to diff --git a/RNG.h b/RNG.h index cb3fbfb..8e93145 100755 --- a/RNG.h +++ b/RNG.h @@ -23,16 +23,16 @@ Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #include -#include <_G_config.h> +#include union PrivateRNGSingleType { // used to access floats as unsigneds float s; - _G_uint32_t u; + uint32_t u; }; union PrivateRNGDoubleType { // used to access doubles as unsigneds double d; - _G_uint32_t u[2]; + uint32_t u[2]; }; // @@ -46,7 +46,7 @@ class RNG { // // Return a long-words word of random bits // - virtual _G_uint32_t asLong() = 0; + virtual uint32_t asLong() = 0; virtual void reset() = 0; // // Return random bits converted to either a float or a double diff --git a/oscillator_params.cc b/oscillator_params.cc new file mode 100644 index 0000000..e784c43 --- /dev/null +++ b/oscillator_params.cc @@ -0,0 +1,220 @@ +/***************************************************************** + ** Oscillator Parameter Optimizer + ** + ** Uses NGAL to find optimal oscillator parameters for musical + ** characteristics. This simulates finding "sweet spots" in + ** synthesis parameter space. + ** + ** Example: Finding parameters that maximize harmonic richness + ** while maintaining a certain fundamental frequency relationship. + *****************************************************************/ + +#include +#include +#include +#include +#include +#include "ga.h" + +// Oscillator parameters we're optimizing +struct OscillatorParams { + double frequency; // Base frequency (0.0 - 1.0 normalized) + double modDepth; // FM modulation depth (0.0 - 1.0) + double modRatio; // FM modulation ratio (0.0 - 16.0) + double waveShape; // Wave shaping amount (0.0 - 1.0) + double filterCutoff; // Filter cutoff (0.0 - 1.0) + double resonance; // Filter resonance (0.0 - 1.0) +}; + +// Decode chromosome into oscillator parameters +OscillatorParams decodeChromosome(CChromosome &chr) { + OscillatorParams params; + + // Normalize each gene (0-65535) to appropriate range + params.frequency = (double)chr[0] / 65535.0; + params.modDepth = (double)chr[1] / 65535.0; + params.modRatio = ((double)chr[2] / 65535.0) * 16.0; // 0-16 for FM ratio + params.waveShape = (double)chr[3] / 65535.0; + params.filterCutoff = (double)chr[4] / 65535.0; + params.resonance = (double)chr[5] / 65535.0; + + return params; +} + +// Calculate spectral richness (number and strength of harmonics) +double calculateSpectralRichness(OscillatorParams ¶ms) { + double richness = 0.0; + + // FM synthesis creates sidebands - more modulation = more harmonics + double fmComplexity = params.modDepth * (1.0 + params.modRatio / 16.0); + richness += fmComplexity * 0.5; + + // Wave shaping adds harmonics + richness += params.waveShape * 0.3; + + // Resonance emphasizes certain harmonics + richness += params.resonance * 0.2; + + // Filter cutoff affects harmonic content + // Mid-range cutoffs with resonance are interesting + double filterContribution = params.filterCutoff * params.resonance * 0.3; + richness += filterContribution; + + return richness; +} + +// Calculate "musicality" - subjective criteria for interesting sounds +double calculateMusicality(OscillatorParams ¶ms) { + double score = 0.0; + + // Penalize extreme values (usually harsh) + double extremePenalty = 0.0; + if (params.modDepth > 0.9) extremePenalty += 0.2; + if (params.resonance > 0.85) extremePenalty += 0.2; + + // Reward integer or simple ratio modulation (more harmonic) + double ratioFraction = fmod(params.modRatio, 1.0); + if (ratioFraction < 0.1 || ratioFraction > 0.9) { + score += 0.3; // Close to an integer ratio + } + + // Reward moderate wave shaping (sweet spot) + if (params.waveShape > 0.3 && params.waveShape < 0.7) { + score += 0.2; + } + + // Reward balanced filter settings + double filterBalance = fabs(params.filterCutoff - 0.5) + + fabs(params.resonance - 0.4); + score += (1.0 - filterBalance) * 0.3; + + return score - extremePenalty; +} + +void printParameters(OscillatorParams ¶ms) { + std::cout << " Frequency: " << params.frequency << std::endl; + std::cout << " FM Depth: " << params.modDepth << std::endl; + std::cout << " FM Ratio: " << params.modRatio << std::endl; + std::cout << " Wave Shape: " << params.waveShape << std::endl; + std::cout << " Filter Cutoff: " << params.filterCutoff << std::endl; + std::cout << " Resonance: " << params.resonance << std::endl; +} + +void printBestSolution(CPool &pool) { + CChromosome &best = + (CChromosome &)pool.GetBest(); + + OscillatorParams params = decodeChromosome(best); + + std::cout << "\n=== Best Oscillator Parameters ===" << std::endl; + printParameters(params); + + double richness = calculateSpectralRichness(params); + double musicality = calculateMusicality(params); + + std::cout << "\nSpectral Richness: " << richness << std::endl; + std::cout << "Musicality Score: " << musicality << std::endl; + std::cout << "Combined Fitness: " << best.GetObjective() << std::endl; +} + +void printStatLine(CEngine &ga, CPool &pool) { + std::cout << "[Gen " << ga.nGenerations << "] " + << "Best=" << pool.GetMaxFitness() + << " Avg=" << pool.GetMeanFitness() + << " Variance=" << pool.GetVariance() + << std::endl; +} + +// Global instances required by NGAL +CEvaluator &EV = *new CEvaluator; +CChromosomeFactoryBase &CF = *new CChromosomeFactory; + +// GA operators - tuned for exploration +COpMutate opmut(0.03); // 3% mutation for exploration +COpCrossover opcross(0.80); // 80% crossover +COpSelectStochastic selector; + +int main() { + std::cout << "Oscillator Parameter Optimizer\n"; + std::cout << "===============================\n\n"; + std::cout << "Searching for musically rich oscillator settings...\n\n"; + + // Create GA engine + // 6 parameters: frequency, modDepth, modRatio, waveShape, filterCutoff, resonance + CEngine ga(60, 6, 1.3); + + ga.SetMutateOp(opmut); + ga.SetCrossoverOp(opcross); + ga.SetSelectionOp(selector); + + CPool &pool = ga.GetPool(); + + int converged = 0; + int maxGenerations = 300; + + printStatLine(ga, pool); + + for (int i = 0; i < maxGenerations; i++) { + ga.Generation(); + + // Print stats every 15 generations + if (i % 15 == 0) { + printStatLine(ga, pool); + } + + // Check for convergence + if (pool.GetVariance() < 0.001) { + converged++; + } else { + converged = 0; + } + + if (converged >= 5) { + std::cout << "\nPopulation converged!" << std::endl; + break; + } + } + + std::cout << "\nOptimization complete!" << std::endl; + std::cout << "Generations: " << ga.nGenerations << std::endl; + + printBestSolution(pool); + + // Show top 5 solutions + std::cout << "\n=== Top 5 Parameter Sets ===" << std::endl; + for (int i = 0; i < 5 && i < pool.GetPoolSize(); i++) { + CChromosome &chr = + (CChromosome &)pool[i]; + OscillatorParams params = decodeChromosome(chr); + + std::cout << "\n#" << (i+1) << " (fitness=" << chr.GetFitness() << "):" << std::endl; + std::cout << " FM: depth=" << params.modDepth + << " ratio=" << params.modRatio << std::endl; + std::cout << " Filter: cutoff=" << params.filterCutoff + << " res=" << params.resonance << std::endl; + } + + return 0; +} + +// Fitness evaluation function +double CEvaluator::EvaluateFitness(CChromosomeBase &chrbase) { + CChromosome &chr = + (CChromosome &)chrbase; + + OscillatorParams params = decodeChromosome(chr); + + // Fitness is combination of spectral richness and musicality + double richness = calculateSpectralRichness(params); + double musicality = calculateMusicality(params); + + // Weight them together + double fitness = (richness * 0.6) + (musicality * 0.4); + + // Bonus for having moderate frequency (not too low or high) + if (params.frequency > 0.3 && params.frequency < 0.7) { + fitness += 0.1; + } + + return fitness; +} diff --git a/rhythm_evolution.cc b/rhythm_evolution.cc new file mode 100644 index 0000000..d737ad8 --- /dev/null +++ b/rhythm_evolution.cc @@ -0,0 +1,296 @@ +/***************************************************************** + ** Rhythm Pattern Evolution + ** + ** Uses NGAL to evolve interesting rhythmic patterns. + ** Demonstrates musical pattern generation through genetic algorithms. + ** + ** Each chromosome represents a 16-step rhythm pattern with + ** velocity and articulation information. + *****************************************************************/ + +#include +#include +#include +#include +#include +#include "ga.h" + +const int PATTERN_LENGTH = 16; // 16-step sequencer + +// Decode a chromosome into a rhythm pattern +struct RhythmStep { + bool active; // Is this step triggered? + int velocity; // 0-127 MIDI velocity + int articulation; // Length of note (0-15) +}; + +struct RhythmPattern { + RhythmStep steps[PATTERN_LENGTH]; +}; + +RhythmPattern decodePattern(CChromosome &chr) { + RhythmPattern pattern; + + for (int i = 0; i < PATTERN_LENGTH; i++) { + unsigned short val = chr[i]; + + // Bit 15: active/inactive + pattern.steps[i].active = (val & 0x8000) != 0; + + // Bits 8-14: velocity (0-127) + pattern.steps[i].velocity = (val >> 8) & 0x7F; + + // Bits 0-3: articulation (0-15) + pattern.steps[i].articulation = val & 0x0F; + } + + return pattern; +} + +// Calculate syncopation (off-beat emphasis) +double calculateSyncopation(RhythmPattern &pattern) { + double score = 0.0; + + for (int i = 0; i < PATTERN_LENGTH; i++) { + if (pattern.steps[i].active) { + // Strong beats are 0, 4, 8, 12 + bool isStrongBeat = (i % 4) == 0; + // Weak beats are 2, 6, 10, 14 + bool isWeakBeat = (i % 4) == 2; + // Off-beats are 1, 3, 5, 7, 9, 11, 13, 15 + bool isOffBeat = (i % 2) == 1; + + if (isOffBeat) { + // Reward off-beat hits + score += 0.3; + } + if (isWeakBeat && pattern.steps[i].velocity > 80) { + // Reward accented weak beats + score += 0.4; + } + } + } + + return score; +} + +// Calculate rhythmic density and balance +double calculateDensity(RhythmPattern &pattern) { + int activeSteps = 0; + double avgVelocity = 0.0; + + for (int i = 0; i < PATTERN_LENGTH; i++) { + if (pattern.steps[i].active) { + activeSteps++; + avgVelocity += pattern.steps[i].velocity; + } + } + + if (activeSteps == 0) return 0.0; + + avgVelocity /= activeSteps; + + // Ideal density is around 6-10 steps out of 16 + double densityScore = 0.0; + if (activeSteps >= 6 && activeSteps <= 10) { + densityScore = 1.0; + } else { + densityScore = 1.0 / (1.0 + fabs(8.0 - activeSteps) * 0.2); + } + + // Reward moderate average velocity (60-100) + double velocityScore = 0.0; + if (avgVelocity >= 60 && avgVelocity <= 100) { + velocityScore = 1.0; + } else { + velocityScore = 1.0 / (1.0 + fabs(80.0 - avgVelocity) * 0.05); + } + + return (densityScore + velocityScore) / 2.0; +} + +// Calculate dynamic range and variation +double calculateDynamics(RhythmPattern &pattern) { + int minVel = 127; + int maxVel = 0; + int activeCount = 0; + + for (int i = 0; i < PATTERN_LENGTH; i++) { + if (pattern.steps[i].active) { + activeCount++; + if (pattern.steps[i].velocity < minVel) { + minVel = pattern.steps[i].velocity; + } + if (pattern.steps[i].velocity > maxVel) { + maxVel = pattern.steps[i].velocity; + } + } + } + + if (activeCount == 0) return 0.0; + + // Reward good dynamic range (20-80 points difference) + int range = maxVel - minVel; + double score = 0.0; + if (range >= 20 && range <= 80) { + score = 1.0; + } else { + score = 1.0 / (1.0 + fabs(50.0 - range) * 0.02); + } + + return score; +} + +void printPattern(RhythmPattern &pattern) { + std::cout << "Pattern: "; + for (int i = 0; i < PATTERN_LENGTH; i++) { + if (pattern.steps[i].active) { + // Show velocity as intensity character + if (pattern.steps[i].velocity > 100) std::cout << "X"; + else if (pattern.steps[i].velocity > 70) std::cout << "x"; + else if (pattern.steps[i].velocity > 40) std::cout << "o"; + else std::cout << "."; + } else { + std::cout << "-"; + } + } + std::cout << std::endl; + + std::cout << "Steps: "; + for (int i = 0; i < PATTERN_LENGTH; i++) { + std::cout << (i % 10); + } + std::cout << std::endl; + + std::cout << "Details:" << std::endl; + for (int i = 0; i < PATTERN_LENGTH; i++) { + if (pattern.steps[i].active) { + std::cout << " Step " << i << ": " + << "vel=" << pattern.steps[i].velocity + << " art=" << pattern.steps[i].articulation + << std::endl; + } + } +} + +void printBestSolution(CPool &pool) { + CChromosome &best = + (CChromosome &)pool.GetBest(); + + RhythmPattern pattern = decodePattern(best); + + std::cout << "\n=== Best Rhythm Pattern ===" << std::endl; + printPattern(pattern); + + double syncopation = calculateSyncopation(pattern); + double density = calculateDensity(pattern); + double dynamics = calculateDynamics(pattern); + + std::cout << "\nMetrics:" << std::endl; + std::cout << " Syncopation: " << syncopation << std::endl; + std::cout << " Density: " << density << std::endl; + std::cout << " Dynamics: " << dynamics << std::endl; + std::cout << " Fitness: " << best.GetObjective() << std::endl; +} + +void printStatLine(CEngine &ga, CPool &pool) { + std::cout << "[Gen " << ga.nGenerations << "] " + << "Best=" << pool.GetMaxFitness() + << " Avg=" << pool.GetMeanFitness() + << std::endl; +} + +// Global instances required by NGAL +CEvaluator &EV = *new CEvaluator; +CChromosomeFactoryBase &CF = *new CChromosomeFactory; + +// GA operators +COpMutate opmut(0.04); // 4% mutation for variety +COpCrossover opcross(0.70); // 70% crossover +COpSelectStochastic selector; + +int main() { + std::cout << "Rhythm Pattern Evolution\n"; + std::cout << "========================\n\n"; + std::cout << "Evolving musically interesting 16-step patterns...\n\n"; + + // Create GA engine (16 steps per pattern) + CEngine ga(40, PATTERN_LENGTH, 1.4); + + ga.SetMutateOp(opmut); + ga.SetCrossoverOp(opcross); + ga.SetSelectionOp(selector); + + CPool &pool = ga.GetPool(); + + int maxGenerations = 200; + + printStatLine(ga, pool); + + for (int i = 0; i < maxGenerations; i++) { + ga.Generation(); + + // Print stats every 10 generations + if (i % 10 == 0) { + printStatLine(ga, pool); + } + } + + std::cout << "\nEvolution complete!" << std::endl; + std::cout << "Generations: " << ga.nGenerations << std::endl; + + printBestSolution(pool); + + // Show top 3 patterns + std::cout << "\n=== Top 3 Patterns ===" << std::endl; + for (int i = 0; i < 3 && i < pool.GetPoolSize(); i++) { + CChromosome &chr = + (CChromosome &)pool[i]; + RhythmPattern pattern = decodePattern(chr); + + std::cout << "\n#" << (i+1) << " (fitness=" << chr.GetFitness() << "):" << std::endl; + std::cout << " "; + for (int j = 0; j < PATTERN_LENGTH; j++) { + if (pattern.steps[j].active) { + if (pattern.steps[j].velocity > 100) std::cout << "X"; + else if (pattern.steps[j].velocity > 70) std::cout << "x"; + else std::cout << "o"; + } else { + std::cout << "-"; + } + } + std::cout << std::endl; + } + + return 0; +} + +// Fitness evaluation function +double CEvaluator::EvaluateFitness(CChromosomeBase &chrbase) { + CChromosome &chr = + (CChromosome &)chrbase; + + RhythmPattern pattern = decodePattern(chr); + + // Calculate various musical metrics + double syncopation = calculateSyncopation(pattern); + double density = calculateDensity(pattern); + double dynamics = calculateDynamics(pattern); + + // Weight the metrics + double fitness = (syncopation * 0.3) + + (density * 0.4) + + (dynamics * 0.3); + + // Bonus: reward the downbeat being present (feels grounded) + if (pattern.steps[0].active) { + fitness += 0.2; + } + + // Bonus: reward having at least one strong backbeat (4, 8, 12) + if (pattern.steps[4].active || pattern.steps[8].active || pattern.steps[12].active) { + fitness += 0.1; + } + + return fitness; +} diff --git a/synth_optimizer.cc b/synth_optimizer.cc new file mode 100644 index 0000000..ecdc381 --- /dev/null +++ b/synth_optimizer.cc @@ -0,0 +1,177 @@ +/***************************************************************** + ** Waveform Synthesis Parameter Optimizer + ** + ** Uses NGAL genetic algorithm library to find optimal synthesis + ** parameters that approximate a target waveform. + ** + ** This demonstrates finding parameters for a simple additive + ** synthesizer to match a target sawtooth wave. + *****************************************************************/ + +#include +#include +#include +#include +#include +#include "ga.h" + +// Number of harmonics we can control +const int NUM_HARMONICS = 8; + +// Number of samples in our waveform +const int WAVEFORM_SIZE = 64; + +// Target waveform (we'll generate a sawtooth) +double targetWaveform[WAVEFORM_SIZE]; + +// Helper function to generate our synthesized waveform from parameters +void generateWaveform(unsigned short harmonics[], double output[]) { + for (int i = 0; i < WAVEFORM_SIZE; i++) { + output[i] = 0.0; + double phase = (2.0 * M_PI * i) / WAVEFORM_SIZE; + + // Add each harmonic + for (int h = 0; h < NUM_HARMONICS; h++) { + double amplitude = (double)harmonics[h] / 65535.0; + double harmonic_freq = h + 1; + output[i] += amplitude * sin(phase * harmonic_freq); + } + } +} + +// Calculate RMS error between two waveforms +double calculateError(double waveform1[], double waveform2[]) { + double error = 0.0; + for (int i = 0; i < WAVEFORM_SIZE; i++) { + double diff = waveform1[i] - waveform2[i]; + error += diff * diff; + } + return sqrt(error / WAVEFORM_SIZE); +} + +// Initialize target waveform (sawtooth) +void initializeTarget() { + for (int i = 0; i < WAVEFORM_SIZE; i++) { + // Sawtooth wave: ramps from -1 to 1 + targetWaveform[i] = 2.0 * ((double)i / WAVEFORM_SIZE) - 1.0; + } +} + +void printBestSolution(CPool &pool) { + CChromosome &best = + (CChromosome &)pool.GetBest(); + + std::cout << "\nBest harmonic amplitudes found:" << std::endl; + for (int i = 0; i < NUM_HARMONICS; i++) { + double amplitude = (double)best[i] / 65535.0; + std::cout << " Harmonic " << (i+1) << ": " << amplitude << std::endl; + } + + // Generate and show the waveform error + double synthesized[WAVEFORM_SIZE]; + unsigned short harmonics[NUM_HARMONICS]; + for (int i = 0; i < NUM_HARMONICS; i++) { + harmonics[i] = best[i]; + } + generateWaveform(harmonics, synthesized); + double error = calculateError(targetWaveform, synthesized); + std::cout << "\nFinal RMS error: " << error << std::endl; +} + +void printStatLine(CEngine &ga, CPool &pool, COperator &opmut, + COperator &opcross) { + std::cout << "[Gen " << ga.nGenerations << "] " + << "Best fit=" << pool.GetMaxFitness() + << " Avg=" << pool.GetMeanFitness() + << " Var=" << pool.GetVariance() + << std::endl; +} + +// Global instances required by NGAL +CEvaluator &EV = *new CEvaluator; +CChromosomeFactoryBase &CF = *new CChromosomeFactory; + +// GA operators +COpMutate opmut(0.02); // 2% mutation rate +COpCrossover opcross(0.75); // 75% crossover rate +COpSelectStochastic selector; + +int main() { + std::cout << "Waveform Synthesis Parameter Optimizer\n"; + std::cout << "========================================\n\n"; + std::cout << "Target: Sawtooth wave using " << NUM_HARMONICS + << " harmonics\n\n"; + + // Initialize the target waveform + initializeTarget(); + + // Create GA engine + // Population size: 50, Chromosome length: NUM_HARMONICS, Scale factor: 1.2 + CEngine ga(50, NUM_HARMONICS, 1.2); + + ga.SetMutateOp(opmut); + ga.SetCrossoverOp(opcross); + ga.SetSelectionOp(selector); + + CPool &pool = ga.GetPool(); + + int converged = 0; + int maxGenerations = 500; + + printStatLine(ga, pool, opmut, opcross); + + for (int i = 0; i < maxGenerations; i++) { + ga.Generation(); + + // Print stats every 10 generations + if (i % 10 == 0) { + printStatLine(ga, pool, opmut, opcross); + } + + // Check for convergence + if (pool.GetVariance() < 0.0001) { + converged++; + } else { + converged = 0; + } + + if (converged >= 5) { + std::cout << "\nPopulation converged after " << ga.nGenerations + << " generations" << std::endl; + break; + } + } + + std::cout << "\nOptimization complete!" << std::endl; + std::cout << "Generations: " << ga.nGenerations << std::endl; + std::cout << "Children created: " << ga.nChildren << std::endl; + + printBestSolution(pool); + + return 0; +} + +// Fitness evaluation function +double CEvaluator::EvaluateFitness(CChromosomeBase &chrbase) { + CChromosome &chr = + (CChromosome &)chrbase; + + // Extract harmonic amplitudes from chromosome + unsigned short harmonics[NUM_HARMONICS]; + for (int i = 0; i < NUM_HARMONICS; i++) { + harmonics[i] = chr[i]; + } + + // Generate waveform with these parameters + double synthesized[WAVEFORM_SIZE]; + generateWaveform(harmonics, synthesized); + + // Calculate error compared to target + double error = calculateError(targetWaveform, synthesized); + + // Fitness is inverse of error (lower error = higher fitness) + // Add small constant to avoid division by zero + double fitness = 1.0 / (error + 0.0001); + + return fitness; +}