The Idea
The Fruit Jam, with its built‑in audio, Wi‑Fi, HSTX display, SD card, generous memory, and other little goodies, is a fantastic platform for mixing creativity with a bit of technical magic. Add CircuitPython to the mix and you’ve got a wonderfully flexible development environment.
With one of my Fruit Jams, I built a display that shows an analog clock face, rotates through an album of photos of my grandkids as the background, and pulls weather data from Adafruit IO that I can scroll through using an IR remote.
With the built‑in TLV320 DAC, I also wanted it to sound like a clock — a really big clock. I wrote functions to synthesize polyphonic tones that mimic the classic Westminster Quarters every fifteen minutes, and at the top of each hour it plays a WAV file of a single church‑bell toll, repeated once for each hour. It turned into a great little project that let me explore different elements of CircuitPython and many parts of the Fruit Jam board.
It’s been running for months now and has become a charming addition to my office. And the best part is that I can keep extending it whenever a new idea strikes.
I Discovered Copilot Audio Expression
The other day, while playing with Copilot, I noticed a link to Labs at the top of the screen. Clicking through, I found Copilot Audio Expression, described as “an experimental tool designed for effortless audio creation using Copilot’s latest voice‑generation models.” You simply type a word or phrase, and it “speaks” it for you — then you can download the result as an MP3.
A few quick prompts and some experimenting with the different settings sparked an idea: Why not make my Fruit Jam clock talk?
Everything old is new again...
Stringing together audio samples to form phrases is nothing new — people have been doing it for decades, long before digital audio. One of my earliest Learn Guide projects, the Titano Weather Station, used prerecorded samples as alarms (“time for bed”). But once you go beyond a handful of words, the challenge grows quickly. A talking clock or calendar needs nearly a hundred different clips: dates, day names, months, ordinals, and more. Gathering, recording, editing, and normalizing all of that becomes a real chore.
That’s where this new AI tool shines. I can generate clean, consistent audio samples for every word or phrase I need in just a few minutes.
So let’s build a Fruit Jam clock that can announce the time and date.
Step by Step
This article focuses specifically on adding the talking capability to my existing Fruit Jam clock project. The display, weather integration, IR remote navigation, and Westminster chimes are all already in place — and I can cover those in a future article if there’s interest. For now, we’ll walk through the audio‑generation workflow that makes the clock speak.
The overall process breaks down into four main steps:
1. Generate all the required words and phrases using Copilot Audio Expression
2. Convert the MP3 files to WAV format using FFmpeg
3. Organize the files on the SD card and create logical CircuitPython lists
4. Write functions that assemble and play the spoken phrases
Generating the samples
For my project, I needed the Fruit Jam to be able to speak:
- The time (hours, minutes, AM, PM, noon, midnight)
- The date (day name, month name, ordinal date, year)
- The quarter hours (“quarter past”, “half past”, “quarter til”)
- And as a bonus: holidays, special days, and fun extras
Copilot Audio Expression made this surprisingly easy.
Before we dive in, note that Copilot Audio Expression is part of Copilot AI Labs and is marked as experimental. It may change or disappear at any time. The site I used is: Copilot Audio Expression Website.
Copilot Audio Expression Website
The interface is simple and intuitive. You get three main controls to shape the voice output:
- Mode (Emotive, Story and Scripted)
- Voice
- Style
Choosing the right settings
For Mode, I used Scripted. This mode sticks closely to the text you type. The other modes tend to embellish or expand on the prompt. For example, entering “The rain in Spain falls mainly on the plain” in Emotive mode produced a 31‑second commentary — entertaining, but not ideal for a talking clock.
For Voice, I chose Rain, a female voice with a British accent. Fun fact: she sounds exactly like my old boss - so even thought I am happily retired she still speaks to me every day!
For Style, I used Neutral for most clips but switched to Excitement for special phrases like “Happy Birthday”. Have a little fun with it!
Generating the Words
Once your settings are dialed in, the workflow is straightforward:
- Type a word or phrase
- Click Generate
- Listen to the preview
- if you like the sample that was generated, click the download icon to save the MP3
About 95% of the time, the first result was perfect. If something sounded off — pronunciation, pacing, tone — clicking Generate again usually produced a slightly different version. I rarely needed more than one retry.
As you go along, it’s best to rename each MP3 file right after downloading it. I use a simple, consistent pattern:
- all lowercase
- underscores instead of spaces
- descriptive but short
For example:
january.mp3
twenty_sixth.mp3
quarter_past.mp3
happy_birthday.mp3
Handling Tricky Words
Occasionally the model misunderstands. For example, when I typed “AM”, it pronounced it as the word “am”. Trying “A M” (with a space) fixed it immediately. Small variations in spacing or capitalization often help.
Rate Limits
I also discovered there’s a soft limit to how many clips you can generate in one session. After about 30 phrases, I received a “Too Busy – Try Again Later” message. I took a break, came back a couple of hours later, and generated another batch. In total, it took me three short sessions to create all the samples I needed — not bad for a free tool.
Converting MP3 Files to WAV with FFmpeg
Once you’ve generated all your spoken phrases with Copilot Audio Expression, place them all in a single folder containing only these MP3 files. Before we can use them on the Fruit Jam, we need to convert them into WAV format, which is what the FruitJam helper library expects. (Although Fruit Jam can handle MP3s, I’ve found WAV files tend to perform more reliably.)
What Is FFmpeg?
FFmpeg is a free, open‑source command‑line tool used for converting, processing, and analyzing audio and video files. It’s extremely powerful and widely used in everything from video editors to streaming services.
For our purposes, we’re using it for one simple job: Convert MP3 files into WAV files.
FFmpeg is not built into Windows, so you’ll need to install it. It also works on macOS and Linux, so this step is not Windows‑only — but the installation process varies slightly by platform.
Installing FFmpeg on Windows
If you’re on Windows 10 or 11, the easiest method is:
- Download a prebuilt FFmpeg ZIP from a trusted distributor (e.g., Gyan.dev or BtbN).
- Extract the ZIP.
- Add the folder to your system PATH so you can run from any terminal.
(If any of this reads like Greek to you, consult your friendly chat-bot - it should be able to walk you through this).
Once installed, open PowerShell — the built‑in command‑line tool in Windows — and type:
ffmpeg -version
If you see version information, you’re ready to go.
Batch‑Converting MP3s to WAV
With FFmpeg installed and your MP3s neatly organized in one folder, converting them is straightforward.
To convert a single file, open a terminal in that folder and run:
ffmpeg -i input.mp3 output.wav (where 'input' is the file to convert and 'output' is the new file name)
But doing this one file at a time gets old fast. Instead, you can batch‑convert everything in the directory with PowerShell in Windows: In PowerShell window, type:
foreach ($f in Get-ChildItem *.mp3) {ffmpeg -i "$($f.Name)" "$($f.BaseName).wav"}
or in macOS/Linux:
for f in *.mp3; do
ffmpeg -i "$f" "${f%.mp3}.wav"
done
After running the batch conversion, you’ll have a matching set of WAV files with the same names as your MP3s — perfect for use on the Fruit Jam.
Preparing the File Names for CircuitPython Lists
Once all of the WAV files are generated and organized, the next step is getting those filenames into CircuitPython in a clean, structured way. You can type them manually, but with dozens of files it’s far easier — and far less error‑prone — to automate the process.
Here’s the workflow I used to turn a folder full of WAV files into neatly formatted Python list entries.
Generate a Text File Containing All Filenames
First, create a simple text file listing every file in the folder. There are several ways to do this (PowerShell, Command Prompt, Finder/Terminal on macOS), but the goal is the same: produce a list like:
january.wav
february.wav
twenty_six.wav
quarter_past.wav
I save this file with a .csv extension so Excel would open it directly.
Import the CSV into Excel & Create the Full Path for Your Code
Open the CSV in Excel and you’ll see one filename per row in column A. This becomes the foundation for building the full file paths that CircuitPython will use.
The goal is to create a fully qualified filename for each entry that we can paste into a CircuitPython list. Let’s assume the folder on the SD card containing all WAV files is named tvoice. For the list, we need the full path: "/sd/words/filename.wav". For the actual list, each entry needs:
- Four leading spaces (for indentation)
- Single quotes around the file name
- A trailing comma
To generate this automatically, put the following formula in cell B1:
=" "&CHAR(39)&"/sd/tvoice"&A1&CHAR(39)&","
Copy this formula down for all filenames.
Next, select all the generated full paths, Copy, then Paste → Values into a new sheet. This gives you plain text entries you can sort and organize without affecting formulas
Organize the Entries into Logical Groups
To make the audio easy to access in code, I grouped the files into logical lists. Then, when I want a particular file, I can refer to its position in the list.
I use six groupings:
- TIMES
- DAY_NAMES
- MONTHS
- DAY_NUMBERS
- YEARS
- SPECIALS
I create a column for each category (columns C–H) and drag each filename into the appropriate column.
Then I rearrange each column into logical order. For example, the MONTHS list is ordered January → December, matching Python’s structured time values.
So when I want the WAV file for “January,” I simply call
MONTHS[0]
(remember CircuitPython is zero based indexing).
Copy files to your SD card
Create a folder on your SD card (in this example, tvoice) and copy all of your WAV files into it. No subfolders are needed — the organizing happens in code.
Create the File of Lists
Now that the full filenames are neatly sorted, create a .py file containing the lists. Although you can put everything in code.py, I prefer a separate library file due to the size.
Create a blank Python file named voices.py in the lib folder on your Fruit Jam.
Start each list by typing the list name (e.g., TIMES), then an equals sign and an opening bracket. Copy the ordered filenames from Excel and paste them in. Close the list with a closing bracket.
Repeat for each category.
Here is what my voices.py file looks like:
TIMES = [
'/sd/tvoice/the_time_is.wav',
'/sd/tvoice/1oclock.wav',
'/sd/tvoice/2oclock.wav',
'/sd/tvoice/3oclock.wav',
'/sd/tvoice/4oclock.wav',
'/sd/tvoice/5oclock.wav',
'/sd/tvoice/6oclock.wav',
'/sd/tvoice/7oclock.wav',
'/sd/tvoice/8oclock.wav',
'/sd/tvoice/9oclock.wav',
'/sd/tvoice/10oclock.wav',
'/sd/tvoice/11oclock.wav',
'/sd/tvoice/12_oclock.wav',
'/sd/tvoice/midnight.wav',
'/sd/tvoice/noon.wav',
'/sd/tvoice/a_m.wav',
'/sd/tvoice/p_m.wav',
'/sd/tvoice/currently it is.wav',
'/sd/tvoice/currently.wav',
'/sd/tvoice/time_to_wake_up.wav',
'/sd/tvoice/quarter past.wav',
'/sd/tvoice/half_past.wav',
'/sd/tvoice/quarter_til.wav',
'/sd/tvoice/it is.wav',
]
DAY_NAMES = [
'/sd/tvoice/monday.wav',
'/sd/tvoice/tuesday.wav',
'/sd/tvoice/wednesday.wav',
'/sd/tvoice/thursday.wav',
'/sd/tvoice/friday.wav',
'/sd/tvoice/saturday.wav',
'/sd/tvoice/sunday.wav',
'/sd/tvoice/it is.wav',
]
MONTHS = [
'/sd/tvoice/january.wav',
'/sd/tvoice/february.wav',
'/sd/tvoice/march.wav',
'/sd/tvoice/april.wav',
'/sd/tvoice/may.wav',
'/sd/tvoice/june.wav',
'/sd/tvoice/july.wav',
'/sd/tvoice/august.wav',
'/sd/tvoice/september.wav',
'/sd/tvoice/october.wav',
'/sd/tvoice/november.wav',
'/sd/tvoice/december.wav',
]
DAY_NUMBERS = [
'/sd/tvoice/first.wav',
'/sd/tvoice/second.wav',
'/sd/tvoice/third.wav',
'/sd/tvoice/fourth.wav',
'/sd/tvoice/fifth.wav',
'/sd/tvoice/sixth.wav',
'/sd/tvoice/seventh.wav',
'/sd/tvoice/eighth.wav',
'/sd/tvoice/nineth.wav',
'/sd/tvoice/tenth.wav',
'/sd/tvoice/eleventh.wav',
'/sd/tvoice/twelth.wav',
'/sd/tvoice/thirtenth.wav',
'/sd/tvoice/fourteenth.wav',
'/sd/tvoice/fifteenth.wav',
'/sd/tvoice/sixteenth.wav',
'/sd/tvoice/seventeenth.wav',
'/sd/tvoice/eighteenth.wav',
'/sd/tvoice/nineteenth.wav',
'/sd/tvoice/tweentith.wav',
'/sd/tvoice/twenty_first.wav',
'/sd/tvoice/twenty_second.wav',
'/sd/tvoice/twenty_third.wav',
'/sd/tvoice/twenty_fourth.wav',
'/sd/tvoice/twenty_fifth.wav',
'/sd/tvoice/twenty_sixth.wav',
'/sd/tvoice/twenty_seventh.wav',
'/sd/tvoice/twenty_eighth.wav',
'/sd/tvoice/twenty_ninth.wav',
'/sd/tvoice/thirtieth.wav',
'/sd/tvoice/thirty_first.wav',
]
YEARS = [
'/sd/tvoice/2026.wav',
'/sd/tvoice/2027.wav',
'/sd/tvoice/2028.wav',
'/sd/tvoice/2029.wav',
'/sd/tvoice/2030.wav',
]
SPECIALS = [
'/sd/tvoice/christmas.wav',
'/sd/tvoice/happy_birthday.wav',
'/sd/tvoice/happy_christmas.wav',
'/sd/tvoice/happy_new_year.wav',
'/sd/tvoice/its_your_birthday.wav',
'/sd/tvoice/merry_christmas.wav',
]
## First we need to import our lists at the top of the file with the rest of the import statements
from voices import TIMES, DAY_NAMES, MONTHS, DAY_NUMBERS, YEARS, SPECIALS
## Functions to play the samples from the various lists
def play_time_wav(nbr):
print("Playing wav file:", TIMES[nbr])
jam.peripherals.play_file(TIMES[nbr], wait_to_finish=True)
def the_time_is():
print("The Time Is")
jam.peripherals.play_file(TIMES[0], wait_to_finish=True)
def play_year_wav(nbr):
print("Playing year file:", YEARS[nbr])
jam.peripherals.play_file(YEARS[nbr], wait_to_finish=True)
def play_day_number_wav(nbr):
print("Playing day number file:", DAY_NUMBERS[nbr])
jam.peripherals.play_file(DAY_NUMBERS[nbr], wait_to_finish=True)
def play_month_wav(nbr):
print("Playing month file:", MONTHS[nbr])
jam.peripherals.play_file(MONTHS[nbr], wait_to_finish=True)
def play_day_name_wav(nbr):
print("Playing day name file:", DAY_NAMES[nbr])
jam.peripherals.play_file(DAY_NAMES[nbr], wait_to_finish=True)
def play_special_wav(nbr):
print("Playing special file:", SPECIALS[nbr])
jam.peripherals.play_file(SPECIALS[nbr], wait_to_finish=True)
## Functions to make different announcements:
def announce_hour():
t = time.localtime()
hour = t.tm_hour
# Play "the time is"
play_time_wav(0)
# Determine which hour and period to play
if hour == 0:
# Midnight
play_time_wav(12) # 12 o'clock
play_time_wav(13) # midnight
elif hour == 12:
# Noon
play_time_wav(12) # 12 o'clock
play_time_wav(14) # noon
else:
# Regular hour with AM/PM
hour12 = hour if hour < 13 else hour - 12
play_time_wav(hour12) # Play hour (1-12)
if hour < 12:
play_time_wav(15) # AM
else:
play_time_wav(16) # PM
def announce_date():
t = time.localtime()
# Play "It is" (DAY_NAMES[7])
play_day_name_wav(7)
# Get day of week (0=Monday, 6=Sunday in time.localtime())
day_of_week = t.tm_wday
play_day_name_wav(day_of_week)
# Get and play month (0=January in MONTHS, but tm_mon is 1-12)
month = t.tm_mon - 1 # Convert from 1-12 to 0-11
play_month_wav(month)
# Get and play day of month (1st=0 in DAY_NUMBERS, tm_mday is 1-31)
day = t.tm_mday - 1 # Convert from 1-31 to 0-30
play_day_number_wav(day)
# Get and play year (2026=0 in YEARS)
year = t.tm_year
if 2026 <= year <= 2030:
year_index = year - 2026
play_year_wav(year_index)
else:
print(f"Year {year} not in YEARS array (2026-2030)")
def quarter_hour():
play_time_wav(20)
def half_hour():
play_time_wav(21)
def quarter_til():
play_time_wav(22):
With these functions you can add talking features for the different elements already in the code. In the while: loop we are getting the time from the RTC and updating the display, and taking other actions at each quarter hour.
## This function is called in the while loop each minute
def get_formatted_time():
"""
Returns the current time formatted as:
mmm-dd-yyyy \n hh:mm am/pm
Example: Sep-15-2025 \n 10:15 pm
"""
t = time.localtime()
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
month_str = months[t.tm_mon - 1]
day = t.tm_mday
year = t.tm_year
hour = t.tm_hour
minute = t.tm_min
am_pm = "am" if hour < 12 else "pm"
hour12 = hour % 12
if hour12 == 0:
hour12 = 12
formatted = f"{hour12}:{minute:02d} {am_pm}"
if minute == 15: # Detects the quarter hour and runs the function 'qhr()' which handles playing Westminster Quarters and then announces it is the quarter hour
print("Quarter Hour")
qhr()
elif minute == 30:
print("Half Hour") # Same as quarter hour
hhr()
elif minute == 45:
print("Three Quarter Hour") # Same as quarter hour
tqhr()
elif minute == 0:
print("Top of the Hour") # Calls the full hour function to sound the Quarters, ring the bell and announce the time
fhr()
else:
pass
return formatted
## This function is called when the top of the hour is reached
def fhr():
print("On the Hour")
# Announce the time with voice
announce_hour()
# Restart the mixer/synth after voice announcements
jam.peripherals.audio.play(mixer)
mixer.voice[0].play(synth)
mixer.voice[1].play(chorus)
print("Finished announcing hour, now playing tones.")
for i in wc4:
time.sleep(0.01)
n = wcn[i][0]
t = wcn[i][1]
note = synthio.Note(synthio.midi_to_hz(n), bend=lfo)
synth.press(note)
time.sleep(t)
synth.release(note)
if i in (1, 3):
time.sleep(0.5)
print("Finished playing tones for hour.")
# Play the gong for the hour
t = time.localtime()
hour = t.tm_hour
gong(hour if hour < 13 else hour - 12)
In closing
A little more to do... I haven't yet done anything with the SPECIALS. There are slightly longer pauses between each WAV file than ideal (meaning it does not sound like completely normal speech) but is sounds really good to me. I will continue to play around with this - maybe some tweaking can be done to improve the spacing, but overall, I am really happy with how this turned out.
And of course - totally new and different things to try down the road.
This page (Getting my Fruit Jam Clock to Speak) was last updated on January 28, 2026.