# Rust Audio Programming: Oscillator – Handle frequency changes smoothly [PART 2]

In [Part 1](https://blog.paramako.com/rust-audio-programming-oscillator-build-a-sine-wave-part-1), we built a simple sine wave generator and saved it as a .wav file.

Now it’s time to **make it sing** — or at least play a short melody.

We’ll modify our existing code to **switch between different notes over time**, building up something that feels more musical. But we’re also going to hit a snag — and that snag is going to teach us an important concept in audio programming.

Let’s dive in.

We’ll play an **8-second melody**, changing the note **once per second**. We’ll reuse the core logic from [Part 1](https://blog.paramako.com/rust-audio-programming-oscillator-build-a-sine-wave-part-1), but adapt it to loop over an array of frequencies — one for each second.

This is a **naive implementation:**

```rust
use std::f32::consts::PI;

fn main() {
    // WAV file settings
    let spec = hound::WavSpec {
        channels: 1,        // mono
        sample_rate: 44100, // samples per second
        bits_per_sample: 16,
        sample_format: hound::SampleFormat::Int,
    };
    // Create a WAV writer
    let mut writer =
        hound::WavWriter::create("melody.wav", spec).expect("Failed to create WAV file");
    // Sine wave parameters
    let duration_secs = 8.0; // 2 seconds
    let amplitude = i16::MAX as f32; // max volume

    let sample_rate = spec.sample_rate as f32;
    let total_samples = (sample_rate * duration_secs) as usize;

    for t in 0..total_samples {
        let time = t as f32 / sample_rate;

        let freq_hz = match time.ceil() {
            1.0 => 440.0,  // A4
            2.0 => 494.0,  // B4
            3.0 => 523.25, // C5
            4.0 => 587.33, // D5
            5.0 => 659.25, // E5
            6.0 => 698.46, // F5
            7.0 => 783.99, // G5
            _ => 880.0,    // A5
        };

        let sample = (amplitude * (2.0 * PI * freq_hz * time).sin()) as i16;
        writer.write_sample(sample).unwrap();
    }

    writer.finalize().unwrap();
    println!("✅ Melody written to 'melody.wav'");
}
```

Let’s break down what we’ve changed in this version of the code:

In [Part 1](https://blog.paramako.com/rust-audio-programming-oscillator-build-a-sine-wave-part-1), we generated a **single 2-second tone**.

Now we’ve extended the total duration to **8 seconds** so we can play **eight notes**, one per second:

```rust
let duration_secs = 8.0;
```

We keep a single loop running over all the samples:

```rust
for t in 0..total_samples {
    let time = t as f32 / sample_rate;
    let freq_hz = match time.ceil() { ... };
}
```

Instead of generating one note from start to finish, we now **check the current time** and **change the frequency every second**.

This lets us build a full melody from just one continuous time stream.

The rest stays the same — we’re still generating samples, writing them to a .wav file.

## **Let’s play our melody**

Run the program with:

```bash
cargo run
```

You’ll see the file `melody.wav` pop up in your project folder. Go ahead and open it in your favorite audio player — or better yet, drop it into [**Audacity**](https://www.audacityteam.org/) to **listen and inspect** the waveform.

At first, it’ll sound like a simple melody — but listen closely 👂 and you might hear something *unexpected* between the notes…

Those little **clicks or pops** you’re hearing between notes aren’t a bug in your system — they’re a classic issue in audio programming caused by **discontinuities in the waveform**.

Remember, we’re using this formula: `sample = amplitude * sin(2π × frequency × time)`

What that means is:

* At the end of each second, the sine wave is at a certain phase (e.g., somewhere mid-curve).
    
* Then, without warning, we jump to a **new sine wave** — starting at a totally different frequency, *but continuing the same time*.
    

The result? The waveform suddenly **jumps** from one value to another — and your ear **hears that jump** as a **click**.

To be smooth and click-free, a waveform should **connect seamlessly** — one sample to the next, with no sudden jumps in value.

But when we change the frequency without resetting or adjusting the phase, we break that continuity.

So instead of a curve that flows naturally, we get a **sharp corner** in the waveform — and your ear *hates* sharp corners.

## **See it for yourself (in Audacity)**

By default, Audacity might show **beats and measures**, which isn’t helpful for our waveform timing.

To change that:

1. Look at the **timeline above the waveform**.
    
2. Click the little drop-down arrow on the left side of the timeline.
    
3. Select **“Minutes and Seconds”** from the list (instead of “Beats and Measures”).
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1752082936538/6b839395-311c-4fd2-b3ee-886217e4000b.png align="center")

To see the discontinuity clearly:

1. Zoom in until you’re seeing **just a few milliseconds** of waveform per screen.
    
2. Navigate to the **5 to 6 second** mark (between note 6 and 7).
    
3. Look closely at the **transition point** — right around the 6.0s boundary.
    

🎯 You should see a clean, repeating wave suddenly **jump** to a new shape — no smooth transition.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1752082765474/aef65dae-6bad-4dc3-8ae3-296e0ab35011.png align="center")

Or even closer

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1752082817894/cbec67a2-e6e6-407c-b163-30c7042e1975.png align="center")

This is the exact spot where our **phase mismatch** causes a **discontinuity** in the waveform — and that’s what we’ll fix in the next step. You can choose any transition point between notes to inspect — I picked the gap around second 6 just as an example. The same issue will appear wherever the frequency changes abruptly mid-wave.

## **Let’s smooth it out**

We’re going to fix this with as little code change as possible — just enough to demonstrate the concept of **phase continuity.**

We’ll do that by:

* Tracking a persistent phase variable across the entire loop
    
* Using freq only to adjust **how fast** phase advances — not where it starts
    

### **Quick recap**

As we saw earlier, using this formula: `sample = amplitude * sin(2π × frequency × time)` …works great **until the frequency changes**. Then the wave **jumps** because time keeps going, but the math rebuilds a brand new sine wave. The waveform doesn’t connect smoothly — and that sharp transition becomes an audible **click**.

### **The fix**

Here’s our original melody code, with just a few crucial lines added:

```rust
use std::f32::consts::PI;

fn main() {
    let spec = hound::WavSpec {
        channels: 1,
        sample_rate: 44100,
        bits_per_sample: 16,
        sample_format: hound::SampleFormat::Int,
    };

    let mut writer =
        hound::WavWriter::create("melody_fixed.wav", spec).expect("Failed to create WAV file");

    let duration_secs = 8.0;
    let amplitude = i16::MAX as f32;

    let sample_rate = spec.sample_rate as f32;
    let total_samples = (sample_rate * duration_secs) as usize;

    // NEW: keep track of waveform position
    let mut phase = 0.0;

    for t in 0..total_samples {
        let time = t as f32 / sample_rate;

        let freq_hz = match time.ceil() {
            1.0 => 440.0,  // A4
            2.0 => 494.0,  // B4
            3.0 => 523.25, // C5
            4.0 => 587.33, // D5
            5.0 => 659.25, // E5
            6.0 => 698.46, // F5
            7.0 => 783.99, // G5
            _ => 880.0,    // A5
        };

        // NEW: advance phase smoothly based on current frequency
        let phase_increment = 2.0 * PI * freq_hz / sample_rate;
        phase = (phase + phase_increment) % (2.0 * PI);

        // sample now comes from continuous phase
        let sample = (amplitude * phase.sin()) as i16;
        writer.write_sample(sample).unwrap();
    }

    writer.finalize().unwrap();
    println!("✅ Melody written to 'melody_fixed.wav'");
}
```

If you open the new `.wav` file in Audacity, you’ll notice something important:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1767808240689/3fcece35-8925-4ab1-afd5-08b8dac8e159.png align="center")

✅ No more sudden jumps in the waveform.

Even when the **frequency** changes, the wave connects naturally.

If you zoom in to the same transition point as before (say, between second 5 and 6), you’ll see that the waveform now flows **without a visible click**.

That’s **phase continuity** in action.

## **What changed?**

Instead of recalculating the sine wave angle from scratch every time, we now:

* Keep a **phase** that tracks our position inside the waveform
    
* Move it forward a tiny bit for each sample, based on the **current frequency**
    
* Use **sin(phase)** to get the sample value
    
* Wrap it around once it reaches **2π** to avoid overflow
    

### **What is a radian?**

A **radian** is just a unit for measuring angles — like degrees, but more mathematically convenient.

* A full circle is **360°**, right?
    
* In radians, a full circle is **2π radians** (≈ 6.283)
    

So when we talk about **2π**, we’re talking about **one full rotation** — or in waveform terms, **one full sine wave cycle**.

Let’s say we want to generate a **440 Hz** tone (A4). That means we need to complete **440 full sine wave cycles every second**.

Since one full cycle is **2π radians**, we need to sweep through: `2π × 440 = 2764.6` **radians per second**.

That’s the **total angular distance** our sine wave needs to travel per second to hit that pitch.

### **How much to move per sample**

But we don’t generate one sample per second — we generate **44100 samples per second**. So we have to split that angular distance across all **44100 samples**.

Here’s the math:

```rust
let phase_increment = (2.0 * PI * frequency) / sample_rate;
```

This gives us the **tiny step** we take through the wave with each sample.

Using our **440 Hz** example: `phase_increment ≈ 0.0627` **radians per sample**.

That means **every sample** moves us forward about **0.0627 radians** through the sine wave.

And since one full wave cycle is **2π radians**, we can divide: `2π / 0.0627 ≈ 100` **samples**.

So it takes **around 100 samples** to complete **one full sine wave cycle** at **440 Hz.**

By adjusting the **phase increment**, we control how many samples it takes to go from **0** → **2π** → back to **0**.

### **Why % (2π)**

In our code, we update the phase like this:

```rust
phase = (phase + phase_increment) % (2.0 * PI);
```

So why are we using **modulo 2π** here?

**Because the sine wave is a loop.**

A full sine wave cycle is exactly **2π radians** — and after that, it just repeats.

**That means:**

* sin(0) = sin(2π) = sin(4π)
    
* sin(π/2) = sin(2π + π/2) = sin(4π + π/2)
    
* …and so on
    

So whether the phase is 6.3, 12.6, 18.9, etc — the result of sin(phase) will be the **same shape**, just wrapped around.

By keeping phase in the range **0.0..2π**, we:

* Keep everything tidy
    
* Prevent precision drift
    
* Avoid weird edge cases down the line
    

## **🏁 That’s a wrap for \[PART 2\]**

In this post, we turned our naive melody generator into something **smarter** and **smoother**.

We uncovered and explained a common problem in audio programming (clicks from phase discontinuity), and we fixed it using one of the most fundamental DSP concepts: **tracking phase over time**.

This technique isn’t just a trick — it’s the foundation of how digital sound generators work under the hood.

And even though we haven’t formally built an **oscillator** yet, we’re already using one in spirit — **walking through the waveform cycle sample by sample**.

## **🔜 What’s next?**

In **Part 3**, we’ll finally write our first **true oscillator** and take things further:

* Generate different waveforms
    
* Compare how they sound and look
    
* Learn how waveform shape affects tone and harmonics
    

▶️ **Continue to Part 3:**

[Rust Audio Programming: Oscillator – Exploring the waveforms \[PART 3\]](https://blog.paramako.com/rust-audio-programming-oscillator-exploring-the-waveforms-part-3)

💾 **Source code from this article:**

[**View on GitHub →**](https://github.com/paramako-blog/oscillator/blob/main/examples/part2_frequency_changes.rs)
