PyShine Wall Clock Tutorial

This tutorial will guide you through creating a wall clock using Python and Pygame. It is designed for beginners and explains the code step by step. By the end, you will have a working digital clock with a tick sound and date display.


Table of Contents

  1. Introduction
  2. Setup
  3. Code Explanation
  4. Main Loop
  5. Complete Code
  6. Conclusion

Introduction

In this tutorial, you will learn how to:

  • Draw a clock face with hour and minute marks
  • Display hour, minute, and second hands
  • Play a tick sound every second
  • Show the current date and day
  • Run a Pygame loop for a live clock

This project is beginner-friendly and helps understand Pygame basics, trigonometry, and working with real-time updates.


Setup

Before running the code, ensure you have Python and Pygame installed. You can install Pygame using pip:

pip install pygame numpy

Code Explanation

Initializing Pygame

We first import required libraries and initialize Pygame.

import pygame
import math
import datetime
import sys
import numpy as np

pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2)
  • pygame.init() initializes all Pygame modules.
  • pygame.mixer.init() sets up the sound system.

Screen and Colors

We define screen dimensions, create a window, and set colors.

WIDTH, HEIGHT = 400, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("PyShine Wall Clock")

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (150, 150, 150)
DARK_GRAY = (50, 50, 50)
  • Portrait mode is used: width 400px, height 600px.
  • Colors are defined using RGB tuples.

Clock Face

We draw the outer circle, hour numbers, and tick marks.

def draw_clock_face():
    pygame.draw.circle(screen, WHITE, (center_x, center_y), clock_radius, 2)
    pygame.draw.circle(screen, DARK_GRAY, (center_x, center_y), clock_radius - 5, 2)

    # Hour marks
    for hour in range(1, 13):
        angle = math.radians(hour * 30 - 90)
        number_x = center_x + (clock_radius - 30) * math.cos(angle) - 10
        number_y = center_y + (clock_radius - 30) * math.sin(angle) - 10
        number_text = font.render(str(hour), True, WHITE)
        screen.blit(number_text, (number_x, number_y))
      
        tick_start_x = center_x + (clock_radius - 15) * math.cos(angle)
        tick_start_y = center_y + (clock_radius - 15) * math.sin(angle)
        tick_end_x = center_x + (clock_radius - 5) * math.cos(angle)
        tick_end_y = center_y + (clock_radius - 5) * math.sin(angle)
        pygame.draw.line(screen, WHITE, (tick_start_x, tick_start_y), (tick_end_x, tick_end_y), 3)

    # Minute marks
    for minute in range(60):
        if minute % 5 != 0:
            angle = math.radians(minute * 6 - 90)
            tick_start_x = center_x + (clock_radius - 10) * math.cos(angle)
            tick_start_y = center_y + (clock_radius - 10) * math.sin(angle)
            tick_end_x = center_x + (clock_radius - 5) * math.cos(angle)
            tick_end_y = center_y + (clock_radius - 5) * math.sin(angle)
            pygame.draw.line(screen, GRAY, (tick_start_x, tick_start_y), (tick_end_x, tick_end_y), 1)
  • math.radians() converts degrees to radians.
  • pygame.draw.circle and pygame.draw.line are used to create the clock face.

Tick Sound

We create a short tick sound using NumPy.

Theory Behind the Tick Sound:

  1. Sample Rate: sample_rate = 44100
    • Determines how many audio samples are played per second.
    • A standard CD-quality sample rate.
  2. Duration: duration = 0.05
    • The tick lasts 50 milliseconds, making it a short click.
  3. Time Array: t = np.linspace(0, duration, n_samples, False)
    • Creates evenly spaced points over the duration for waveform generation.
  4. Envelope: envelope = np.exp(-50 * t)
    • Applies an exponential decay to the waveform.
    • This ensures the tick sound starts loud and fades quickly.
  5. Waveform Generation: waveform = 0.5 * envelope * np.sign(np.sin(2 * np.pi * 1500 * t))
    • Uses a high frequency (1500 Hz) to create a sharp ‘tick’.
    • np.sign converts the sine wave into a square-like waveform, giving a clicking effect.
  6. Integer Conversion: waveform_int16 = np.int16(waveform * 3267)
    • Converts floating-point waveform to 16-bit integers for Pygame playback.
  7. Stereo Sound: sound_array = np.column_stack([waveform_int16, waveform_int16])
    • Creates two channels (left and right) for stereo output.
  8. Make Sound Object: pygame.sndarray.make_sound(sound_array)
    • Converts the NumPy array into a Pygame sound object.
  9. Volume: tick_sound.set_volume(0.5)
    • Sets the playback volume to a moderate level.

By using this method, we can programmatically generate a simple but realistic tick sound without needing an external audio file. Each time the second changes, the tick plays, giving a real wall clock feel.


def create_tick_sound():
    sample_rate = 44100
    duration = 0.05
    n_samples = int(sample_rate * duration)

    t = np.linspace(0, duration, n_samples, False)
    envelope = np.exp(-50 * t)
    waveform = 0.5 * envelope * np.sign(np.sin(2 * np.pi * 1500 * t))
    waveform_int16 = np.int16(waveform * 3267)
    sound_array = np.column_stack([waveform_int16, waveform_int16])

    tick_sound = pygame.sndarray.make_sound(sound_array)
    tick_sound.set_volume(0.5)
    return tick_sound

tick = create_tick_sound()
last_second = -1
  • np.linspace creates a time array.
  • envelope makes the sound decay quickly.
  • pygame.sndarray.make_sound converts the array to a playable sound.

Clock Hands

We calculate angles for hour, minute, and second hands and draw them.

def draw_clock_hands():
    global last_second
    now = datetime.datetime.now()
    hour, minute, second = now.hour % 12, now.minute, now.second

    if second != last_second:
        tick.play()
        last_second = second

    hour_angle = math.radians(hour * 30 + minute * 0.5 - 90)
    minute_angle = math.radians(minute * 6 + second * 0.1 - 90)
    second_angle = math.radians(second * 6 - 90)

    hour_x = center_x + clock_radius * 0.5 * math.cos(hour_angle)
    hour_y = center_y + clock_radius * 0.5 * math.sin(hour_angle)
    pygame.draw.line(screen, WHITE, (center_x, center_y), (hour_x, hour_y), 6)

    minute_x = center_x + clock_radius * 0.7 * math.cos(minute_angle)
    minute_y = center_y + clock_radius * 0.7 * math.sin(minute_angle)
    pygame.draw.line(screen, WHITE, (center_x, center_y), (minute_x, minute_y), 4)

    second_x = center_x + clock_radius * 0.8 * math.cos(second_angle)
    second_y = center_y + clock_radius * 0.8 * math.sin(second_angle)
    pygame.draw.line(screen, RED, (center_x, center_y), (second_x, second_y), 2)

    pygame.draw.circle(screen, RED, (center_x, center_y), 8)
    pygame.draw.circle(screen, WHITE, (center_x, center_y), 8, 2)
    return now
  • The angles are calculated using the current time.
  • pygame.draw.line draws the hands, and circles indicate the pivot.

Date Display

We show the current date and weekday.

def draw_date_display(now, clock_center, clock_radius):
    date_text = date_font.render(now.strftime("%Y-%m-%d"), True, WHITE)
    day_text = date_font.render(now.strftime("%A"), True, WHITE)
    date_rect = date_text.get_rect(midtop=(clock_center[0], clock_center[1] - clock_radius + 70))
    day_rect = day_text.get_rect(midtop=date_rect.midbottom)
    screen.blit(date_text, date_rect)
    screen.blit(day_text, day_rect)
  • strftime formats the date.
  • blit draws the text on the screen.

Main Loop

The main loop handles events, updates the screen, and redraws the clock every frame.

def main():
    clock = pygame.time.Clock()
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
                running = False

        screen.fill(BLACK)
        draw_clock_face()
        now = draw_clock_hands()
        draw_date_display(now, (center_x, center_y), clock_radius)

        pygame.display.flip()
        clock.tick(30)

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()
  • pygame.event.get() captures quit or key press events.
  • screen.fill clears the screen.
  • pygame.display.flip() updates the display.
  • clock.tick(30) limits to 30 frames per second.

Complete Code

# Tutorial and Source Code available: www.pyshine.com
import pygame
import math
import datetime
import sys
import numpy as np

# Initialize Pygame
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2)

# Screen dimensions for portrait mode
WIDTH, HEIGHT = 400, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("PyShine Wall Clock")

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (150, 150, 150)
DARK_GRAY = (50, 50, 50)

# Clock parameters
center_x, center_y = WIDTH // 2, HEIGHT // 2
clock_radius = 150

# Font
font = pygame.font.SysFont('Arial', 24, bold=True)
date_font = pygame.font.SysFont('Arial', 20)

# Generate clock tick sound
def create_tick_sound():
    sample_rate = 44100
    duration = 0.05  # 50ms short tick
    n_samples = int(sample_rate * duration)
  
    # Quick decaying click
    t = np.linspace(0, duration, n_samples, False)
    envelope = np.exp(-50 * t)  # fast decay
    waveform = 0.5 * envelope * np.sign(np.sin(2 * np.pi * 1500 * t))  # high-pitched click
    waveform_int16 = np.int16(waveform * 3267)
    sound_array = np.column_stack([waveform_int16, waveform_int16])
    tick_sound = pygame.sndarray.make_sound(sound_array)
    tick_sound.set_volume(0.5)
    return tick_sound

tick = create_tick_sound()
last_second = -1  # Track last second to play tick

def draw_clock_face():
    pygame.draw.circle(screen, WHITE, (center_x, center_y), clock_radius, 2)
    pygame.draw.circle(screen, DARK_GRAY, (center_x, center_y), clock_radius - 5, 2)
    for hour in range(1, 13):
        angle = math.radians(hour * 30 - 90)
        number_x = center_x + (clock_radius - 30) * math.cos(angle) - 10
        number_y = center_y + (clock_radius - 30) * math.sin(angle) - 10
        number_text = font.render(str(hour), True, WHITE)
        screen.blit(number_text, (number_x, number_y))
        tick_start_x = center_x + (clock_radius - 15) * math.cos(angle)
        tick_start_y = center_y + (clock_radius - 15) * math.sin(angle)
        tick_end_x = center_x + (clock_radius - 5) * math.cos(angle)
        tick_end_y = center_y + (clock_radius - 5) * math.sin(angle)
        pygame.draw.line(screen, WHITE, (tick_start_x, tick_start_y), (tick_end_x, tick_end_y), 3)
    for minute in range(60):
        if minute % 5 != 0:
            angle = math.radians(minute * 6 - 90)
            tick_start_x = center_x + (clock_radius - 10) * math.cos(angle)
            tick_start_y = center_y + (clock_radius - 10) * math.sin(angle)
            tick_end_x = center_x + (clock_radius - 5) * math.cos(angle)
            tick_end_y = center_y + (clock_radius - 5) * math.sin(angle)
            pygame.draw.line(screen, GRAY, (tick_start_x, tick_start_y), (tick_end_x, tick_end_y), 1)

def draw_clock_hands():
    global last_second
    now = datetime.datetime.now()
    hour, minute, second = now.hour % 12, now.minute, now.second
  
    # Play tick sound on every second change
    if second != last_second:
        tick.play()
        last_second = second
  
    hour_angle = math.radians(hour * 30 + minute * 0.5 - 90)
    minute_angle = math.radians(minute * 6 + second * 0.1 - 90)
    second_angle = math.radians(second * 6 - 90)
  
    hour_x = center_x + clock_radius * 0.5 * math.cos(hour_angle)
    hour_y = center_y + clock_radius * 0.5 * math.sin(hour_angle)
    pygame.draw.line(screen, WHITE, (center_x, center_y), (hour_x, hour_y), 6)
  
    minute_x = center_x + clock_radius * 0.7 * math.cos(minute_angle)
    minute_y = center_y + clock_radius * 0.7 * math.sin(minute_angle)
    pygame.draw.line(screen, WHITE, (center_x, center_y), (minute_x, minute_y), 4)
  
    second_x = center_x + clock_radius * 0.8 * math.cos(second_angle)
    second_y = center_y + clock_radius * 0.8 * math.sin(second_angle)
    pygame.draw.line(screen, RED, (center_x, center_y), (second_x, second_y), 2)
  
    pygame.draw.circle(screen, RED, (center_x, center_y), 8)
    pygame.draw.circle(screen, WHITE, (center_x, center_y), 8, 2)
    return now

def draw_date_display(now, clock_center, clock_radius):
    date_text = date_font.render(now.strftime("%Y-%m-%d"), True, WHITE)
    day_text = date_font.render(now.strftime("%A"), True, WHITE)
    date_rect = date_text.get_rect(midtop=(clock_center[0], clock_center[1] - clock_radius + 70))
    day_rect = day_text.get_rect(midtop=date_rect.midbottom)
    screen.blit(date_text, date_rect)
    screen.blit(day_text, day_rect)

def main():
    clock = pygame.time.Clock()
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
                running = False
        screen.fill(BLACK)
        draw_clock_face()
        now = draw_clock_hands()
        draw_date_display(now, (center_x, center_y), clock_radius)
        pygame.display.flip()
        clock.tick(30)
    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()


Conclusion

You have learned how to create a functional wall clock in Python using Pygame. You now understand:

  • How to draw shapes and text
  • How to calculate angles for clock hands
  • How to play sounds
  • How to display real-time updates

You can further enhance this project by:

  • Adding an alarm feature
  • Customizing the clock design
  • Adding themes or color changes

This tutorial gives a solid foundation for beginner-friendly Pygame projects.


Tutorial and Source Code available at www.pyshine.com