Beyond the Single Note: Implementing Polyphony

In our previous tutorials, our synthesizer was limited to one note at a time. If you pressed a new key, the old sound simply disappeared. Today, we change that by implementing Polyphony—the ability to play chords and manage multiple "Voices" simultaneously.

1. What is a "Voice"?

In professional synthesizer design, we use the Voice Architecture. A "Voice" is a self-contained unit that holds everything needed to produce one sound: an Oscillator and an Envelope.

Instead of the SynthEngine owning a single oscillator, it now owns a list of Voice objects.

2. Managing Objects with std::vector

How do we keep track of notes that are born (Key Down) and die (Release phase finished)? We use the C++ Standard Library’s std::vector.

  • Birth: When a key is pressed, we push_back a new Voice into our vector.

  • Death: We iterate through the vector. If a voice’s envelope has reached the OFF state, we remove it from the vector and free the memory.

3. Thread Safety: The Audio Lock

In audio programming, we have two "threads" running at once: 1. The Main Thread: Watches your keyboard and adds voices to the vector. 2. The Audio Thread: Loops through the vector to calculate the sound.

If the Main Thread adds a voice while the Audio Thread is halfway through reading the vector, the program will crash. To prevent this, we use SDL_LockAudio(). It acts as a "stoplight," ensuring only one thread touches our list of voices at a time.

4. Digital Mixing: The Summing Bus

To hear multiple voices, we simply add their samples together. However, we must be careful. If four voices all produce a signal of 0.5, the sum is 2.0. Since digital audio clips at $1.0$, this results in terrible distortion.

We solve this by providing Headroom:

\[Output = (\sum_{n=1}^{Voices} Voice_{n}) \cdot MasterVolume\]

5. The Keyboard Map

To make the synth playable, we use std::map<SDL_Keycode, int>. This allows us to translate a computer key (like SDLK_a) into a musical frequency using the MIDI standard formula:

\[f = 440 \cdot 2^{\frac{n - 69}{12}}\]

Conclusion

By moving to a polyphonic architecture, we’ve introduced core C++ concepts: Dynamic Memory Management, Thread Synchronization, and Data Structures. Our synth is now a true instrument.

In the next part, we will add Master and Slave Oscillators for / with Hard Sync to give these voices some movement and character!

The Laser Sound: Implementing Hard Sync

In the previous chapters, our oscillators lived in isolation. They knew nothing of each other. Today, we break that wall. We are implementing Hard Sync, a classic synthesis technique famous for creating aggressive, "screaming" lead sounds.

hard-sync.

1. What is Hard Sync?

Hard Sync involves two oscillators: a Master and a Slave. The Master determines the fundamental pitch. The Slave provides the actual sound. However, there is a catch: Every time the Master finishes one cycle (reaches the end of its phase), it forces the Slave to reset its phase to zero instantly.

If the Slave is running at a higher frequency than the Master, it will be "cut off" mid-cycle. This creates sharp, jagged edges in the waveform, resulting in complex, metallic overtones that are perfect for cutting through a mix.

2. Refactoring for Interaction

To make this work, we had to refactor our Oscillator class. Previously, getNextSample() handled both the phase update and the calculation. For Sync, we need to decouple them.

By adding getPhase() and setPhase(), the Voice class can now act as a mediator, checking the Master’s progress and commanding the Slave.

3. The Sync Logic in C++

Inside our Voice::getNextSample(), the magic happens in a few lines of code. We track the "Phase Wrap" of the master:

1
2
3
4
5
6
7
8
float oldPhase = masterOsc->getPhase();
masterOsc->updatePhase();
float newPhase = masterOsc->getPhase();

// If the master wrapped back to 0, reset the slave
if (newPhase < oldPhase) {
    slaveOsc->setPhase(0.0f);
}

This interaction is a great example of Object Composition. The Voice manages the lifecycle of two oscillators and defines how they communicate.

4. Why it Sounds "Angry"

Mathematically, these phase resets introduce discontinuities in the Slave’s waveform. In the frequency domain, these sharp jumps translate to a massive boost in high-frequency harmonics.

Pro Tip: The classic "Sync Sweep" effect is achieved by modulating the Slave’s frequency (usually with an LFO or Envelope) while the Master stays at a constant pitch.

5. Conclusion

Hard Sync moves us away from "Static" synthesis into "Dynamic" synthesis. We are no longer just playing waves; we are creating a system where objects react to one another in real-time.

In the next part, we will look at Digital Aliasing—the "dirt" that these sharp waveforms create—and how we can try to clean it up.