Skip to content

Using a DAC to Generate Sounds

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from machine import Pin, I2S
import struct
import math
from utime import sleep_ms, ticks_ms, ticks_diff

# Pin Definitions
BCK_PIN = 16      # Connect to BCK on DAC (Bit Clock)
WS_PIN = 17       # Connect to LCK on DAC (Word Select)
SD_PIN = 18       # Connect to DIN on DAC (Data Input)

# Audio parameters
SAMPLE_RATE = 16000
BUFFER_SIZE = 512  # Larger buffer for smoother playback

# Note frequencies
NOTES = {
    'C4': 261.63,
    'D4': 293.66,
    'E4': 329.63,
    'F4': 349.23,
    'G4': 392.00,
    'A4': 440.00,
    'B4': 493.88,
    'C5': 523.25,
    'REST': 0
}

# Simple melody: each tuple contains (note_name, duration_ms)
MELODY = [
    ('C4', 300), ('E4', 300), ('G4', 300),  # Ascending arpeggio
    ('C5', 600),                            # Hold high note
    ('G4', 200), ('E4', 200), ('C4', 600),  # Descending
    ('REST', 300),                          # Pause
    ('G4', 300), ('F4', 300), ('E4', 300),  # Walking down
    ('D4', 300), ('C4', 600),               # End phrase
]

def apply_envelope(value, i, buffer_size, attack_samples=100, release_samples=100):
    """Apply attack and release envelope to reduce clicks and pops"""
    if i < attack_samples:
        return value * (i / attack_samples)
    elif i > buffer_size - release_samples:
        return value * ((buffer_size - i) / release_samples)
    return value

def make_tone_buffer(frequency):
    """Create a buffer with complete cycles of the sine wave"""
    if frequency == 0:  # For REST
        return bytearray(BUFFER_SIZE * 2)

    # Calculate samples for complete cycles
    samples_per_cycle = SAMPLE_RATE / frequency
    num_cycles = max(1, int(BUFFER_SIZE / samples_per_cycle))
    adjusted_buffer_size = int(num_cycles * samples_per_cycle)
    if adjusted_buffer_size > BUFFER_SIZE:
        adjusted_buffer_size = BUFFER_SIZE

    buffer = bytearray(adjusted_buffer_size * 2)

    # Generate a smoother waveform
    amplitude = 0.15  # Reduced amplitude for cleaner sound

    for i in range(0, adjusted_buffer_size * 2, 2):
        # Basic sine wave
        sample_pos = (i // 2) / samples_per_cycle * 2 * math.pi
        raw_value = math.sin(sample_pos)

        # Apply envelope and amplitude
        value = int(32767 * amplitude * 
                   apply_envelope(raw_value, i//2, adjusted_buffer_size))

        # Pack into buffer
        struct.pack_into("<h", buffer, i, value)

    return buffer

# Configure I2S with higher sample precision
audio_out = I2S(
    0,
    sck=Pin(BCK_PIN),
    ws=Pin(WS_PIN),
    sd=Pin(SD_PIN),
    mode=I2S.TX,
    bits=16,
    format=I2S.MONO,
    rate=SAMPLE_RATE,
    ibuf=2048  # Larger internal buffer
)

print("Playing melody... Press Ctrl+C to stop")

try:
    while True:
        for note_name, duration in MELODY:
            print(f"\nPlaying {note_name} for {duration}ms")

            # Create buffer for this frequency
            frequency = NOTES[note_name]
            buffer = make_tone_buffer(frequency)

            # Play the note for specified duration
            start_time = ticks_ms()
            while ticks_diff(ticks_ms(), start_time) < duration:
                audio_out.write(buffer)
                sleep_ms(10)
                print(".", end="")

            # Longer pause between notes for clearer separation
            sleep_ms(70)

        # Pause between repetitions
        sleep_ms(1000)

except KeyboardInterrupt:
    print("\nStopping...")
finally:
    audio_out.deinit()
    print("Test complete")