This tutorial guides you through using an external USB speaker on the Uno Q onboard Linux environment. It is divided into two phases:
Phase 1 uses software to synthesize PWM square-wave signals at specified frequencies, testing and verifying the plug-and-play driver compatibility of the USB speaker on the Uno Q.
Phase 2 builds a non-blocking command-line audio player, showing how to upload audio files from a local computer and control playback via the terminal.
The entire workflow uses native Linux tools and underlying runtime environments without relying on desktop GUI windows. If any dependency is missing, install it using the system package manager as instructed in the respective steps.
Special Note: When connected to the USB port or a powered USB hub on the Uno Q, the USB speaker typically plays a short power-on chime. If there is no response and no audio prompt upon connection, check for loose connections or inspect the speaker power supply.
Development Tip: For efficient programming and debugging, connect to the Uno Q remotely via SSH from your computer. Only interact with the Uno Q local terminal directly when troubleshooting network connectivity or low-level hardware issues.
ssh arduino@<Uno_Q_IP>
# Example: ssh arduino@192.168.1.100
sudo modprobe snd-usb-audio
cat /proc/asound/cards
If a sound card node containing USB-Audio (such as card 1) appears alongside the default onboard devices, the driver has successfully identified and loaded the hardware.
cat /proc/asound/cards does not list the USB sound card, or the speaker fails to play the power-on chime.sudo dmesg | grep -i usb.The stock system image does not include command-line audio players, decoders, or multimedia development libraries. Install them via apt:
sudo apt update
sudo apt install -y alsa-utils mpg123 python3-numpy python3-pygame python3-tk
alsa-utils: Provides ALSA low-level streaming commands such as aplay to send raw PCM audio streams directly to the sound card.mpg123: A lightweight command-line MP3 hardware-decoding player with minimal resource overhead.python3-numpy: High-performance numerical computing library for Python, used for waveform matrix processing (optional extended support).python3-pygame: Cross-platform multimedia library (optional extended support).python3-tk: Underlying package for Python Tkinter GUI. Although this tutorial uses terminal text interactions for headless SSH sessions, desktop environments and GUI extensions depend on this package. Installing it in advance avoids ImportError: No module named '_tkinter' errors.
aplay --version && mpg123 --version
python3 -c "import wave, struct; print('Python audio standard libraries are ready')"
If both utilities print version numbers and the Python command executes without errors, the environment is ready. You can also re-run the apt install command; seeing is already the newest version confirms a proper setup.
E: Unable to locate package alsa-utils or network timeout errors.ping -c 3 baidu.com or ping -c 3 google.com to verify network access, then re-run sudo apt update.Create a dedicated project directory for scripts and audio files:
mkdir -p /home/arduino/music_player/mp3
cd /home/arduino/music_player
Because a USB speaker communicates over the digital USB audio class protocol, it cannot be driven directly by microcontroller GPIO PWM signals. In this phase, we use Python standard libraries to compute 50% duty-cycle digital square-wave PCM data (software-simulated PWM) in memory, pack it into a standard WAV audio container, and stream it to the USB speaker using aplay to produce musical scales like a passive buzzer.
This script uses built-in Python standard libraries without requiring pip:
wave (Built-in): Generates and formats RIFF/WAV audio headers.struct (Built-in): Packs calculated integer values into 16-bit little-endian binary PCM byte streams.subprocess (Built-in): Spawns low-level system audio commands.The script relies on the system-level ALSA utility alsa-utils (which provides aplay). If it is not installed yet, run:
sudo apt update
sudo apt install -y alsa-utils
nano pwm_native.py
If you prefer developing in a local IDE (such as VS Code), transfer the file to the Uno Q from your local computer's CMD or Terminal:
scp "C:\your_local_path\pwm_native.py" arduino@<Uno_Q_IP>:/home/arduino/music_player/
Complete source code:
import struct
import subprocess
import wave
# Standard musical pitch reference frequencies (Hz)
NOTES = {
'C4': 262, # Do
'D4': 294, # Re
'E4': 330, # Mi
'F4': 349, # Fa
'G4': 392, # Sol
'A4': 440, # La
'B4': 494, # Si
'C5': 523 # High Do
}
def play_square_wave(freq, duration=0.35, duty=0.5):
"""
Computes 16-bit 44.1kHz PWM square-wave data in memory using standard libraries,
generates a temporary WAV file, and streams it to the USB speaker via aplay.
"""
sample_rate = 44100
total_samples = int(sample_rate * duration)
amplitude = 9000 # Amplitude level (max 32767 for 16-bit signed int; 9000 prevents clipping)
tmp_wav = "/tmp/pwm_tone.wav"
with wave.open(tmp_wav, 'wb') as f:
f.setnchannels(1) # Mono
f.setsampwidth(2) # 16-bit integer (2 bytes)
f.setframerate(sample_rate) # 44.1kHz sample rate
frames = bytearray()
for i in range(total_samples):
t = i / sample_rate
phase = (t * freq) % 1.0
# Square-wave duty cycle logic (simulating hardware PWM)
val = amplitude if phase < duty else -amplitude
# Pack as 16-bit signed little-endian PCM sample
frames += struct.pack('<h', int(val))
f.writeframes(frames)
# Invoke native Linux ALSA playback command
subprocess.run(["aplay", "-q", tmp_wav])
def main():
print("=== Uno Q Native Square-Wave PWM Scale Test ===")
scale = ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5']
for note in scale:
print(f"Playing note: {note} ({NOTES[note]} Hz)")
play_square_wave(NOTES[note], duration=0.3)
print("Scale playback completed!")
if __name__ == "__main__":
main()
In the Uno Q nano editor, press Ctrl + O and Enter to save, then press Ctrl + X to exit.
NOTES = {
'C4': 262, 'D4': 294, 'E4': 330, 'F4': 349,
'G4': 392, 'A4': 440, 'B4': 494, 'C5': 523
}
Explanation: Maps musical note names across an octave to physical audio frequencies (Hz). Subsequent waveform calculations derive period durations from this lookup table.
sample_rate = 44100
total_samples = int(sample_rate * duration)
amplitude = 9000
tmp_wav = "/tmp/pwm_tone.wav"
with wave.open(tmp_wav, 'wb') as f:
f.setnchannels(1) # Mono
f.setsampwidth(2) # 16-bit integer (2 bytes)
f.setframerate(sample_rate) # 44.1kHz sample rate
Function: Initializes the structural parameters of a standard uncompressed audio file.
Explanation: Configures a CD-standard 44.1kHz sampling rate with 16-bit depth. Python's built-in wave module formats a canonical WAV header in /tmp. amplitude = 9000 bounds the dynamic range to ensure sufficient output volume while preventing harmonic clipping and speaker distortion.
frames = bytearray()
for i in range(total_samples):
t = i / sample_rate
phase = (t * freq) % 1.0
val = amplitude if phase < duty else -amplitude
frames += struct.pack('<h', int(val))
f.writeframes(frames)
Function: Synthesizes periodic square-wave samples in software and packs them into a binary PCM stream (simulating hardware PWM duty cycle).
Explanation:
t = i / sample_rate determines the exact timestamp of the current sample point.phase = (t * freq) % 1.0 maps the sample into a normalized single-period phase window (0.0 to 1.0).val = amplitude if phase < duty else -amplitude defines the core square-wave synthesis logic: samples output a positive high value during the active duty cycle (default 50%) and switch to a negative low value for the remainder, reproducing digital level toggling.struct.pack('<h', int(val)) encodes the numeric amplitude into a 2-byte signed 16-bit little-endian binary sample, which is appended to the audio stream.subprocess.run(["aplay", "-q", tmp_wav])
Function: Triggers the Linux audio subsystem to perform digital-to-analog conversion and play sound.
Explanation: Invokes the native ALSA utility aplay in a child process to stream generated WAV data to the default sound card. The -q quiet flag suppresses verbose file header diagnostics in the console.
def main():
print("=== Uno Q Native Square-Wave PWM Scale Test ===")
scale = ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5']
for note in scale:
print(f"Playing note: {note} ({NOTES[note]} Hz)")
play_square_wave(NOTES[note], duration=0.3)
print("Scale playback completed!")
Function: Manages sequential playback of the musical scale.
Explanation: Iterates through the musical scale, passing target frequencies to the synthesis function to execute an automated 8-note scale test.
The following segments drive the physical vibration and audio output of the USB speaker:
val = amplitude if phase < duty else -amplitude
frames += struct.pack('<h', int(val))
Sound is produced by driving the speaker diaphragm back and forth at a target frequency. Alternating between +9000 and -9000 generates binary PCM data that toggles at the designated pitch (e.g., 440Hz).subprocess.run(["aplay", "-q", tmp_wav])
This is the hardware output gateway. Synthesized waveform data remains silent bytes in storage until aplay streams them across the USB bus to the speaker controller. The onboard DAC (Digital-to-Analog Converter) converts these digital frames into analog voltages that move the speaker voice coil, producing audible acoustic waves.Execute the script in your Uno Q terminal:
python3 pwm_native.py
Expected Result: The USB speaker sequentially outputs an 8-note musical scale (Do, Re, Mi, Fa, Sol, La, Ti, Do).
Common Errors and Troubleshooting:
card 1, test it with:aplay -D plughw:1,0 /tmp/pwm_tone.wav
Once audio is verified, update the execution line in your script to subprocess.run(["aplay", "-D", "plughw:1,0", "-q", tmp_wav]).
Compared to single-frequency square-wave tones, using MP3 files allows for playing complete audio tracks. Below is the full workflow for file transfer and interactive playback control.
Use scp from your local computer (Windows PowerShell / CMD or macOS Terminal) to transfer audio files to the Uno Q media folder.
scp "C:\your_local_music_path\*.mp3" arduino@<Uno_Q_IP>:/home/arduino/music_player/mp3/
To upload a pre-sorted folder (e.g., my_songs), use the -r flag:
scp -r "C:\your_local_music_path\my_songs" arduino@<Uno_Q_IP>:/home/arduino/music_player/mp3/
(Note: Replace local file paths and <Uno_Q_IP> with your actual configuration)
ls -lh /home/arduino/music_player/mp3
If uploaded MP3 files appear with valid file sizes, the transfer succeeded.
Permission denied.sudo chown -R arduino:arduino /home/arduino/music_player on the Uno Q to fix ownership.This player uses Python standard libraries for process management and signal routing:
os (Built-in): Scans directory trees for audio files and dispatches POSIX signals.signal (Built-in): Supplies operating system control signals, including SIGSTOP (pause) and SIGCONT (resume).subprocess (Built-in): Asynchronously spawns the background audio decoding process.The backend decoding engine is mpg123. If it is not installed, install it via:
sudo apt update
sudo apt install -y mpg123
Create the script in the project directory:
nano mp3_native.py
You can also write it locally and upload it using SCP:
scp "C:\your_local_path\mp3_native.py" arduino@<Uno_Q_IP>:/home/arduino/music_player/
Complete source code:
import os
import signal
import subprocess
MUSIC_DIR = "/home/arduino/music_player/mp3"
if not os.path.exists(MUSIC_DIR):
print(f"Error: Directory {MUSIC_DIR} does not exist!")
exit(1)
# Scan and sort all MP3 files
playlist = [f for f in os.listdir(MUSIC_DIR) if f.lower().endswith(".mp3")]
playlist.sort()
if not playlist:
print(f"Notice: Please upload .mp3 files to {MUSIC_DIR} first!")
exit(1)
player_proc = None
current_index = 0
is_paused = False
def stop_current():
"""Safely terminates the active audio child process"""
global player_proc, is_paused
if player_proc and player_proc.poll() is None:
player_proc.terminate()
player_proc.wait()
player_proc = None
is_paused = False
def play_track(index):
"""Switches to and plays the track at the specified index"""
global player_proc, current_index, is_paused
stop_current()
current_index = index % len(playlist)
file_path = os.path.join(MUSIC_DIR, playlist[current_index])
# Spawn background mpg123 process for streaming playback
player_proc = subprocess.Popen(
["mpg123", "-q", file_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
is_paused = False
print(f"\n▶ Now Playing [{current_index + 1}/{len(playlist)}]: {playlist[current_index]}")
def toggle_pause():
"""Toggles pause/resume using native Linux process signals"""
global player_proc, is_paused
if not player_proc or player_proc.poll() is not None:
return
if not is_paused:
# Send SIGSTOP to freeze the child process
os.kill(player_proc.pid, signal.SIGSTOP)
is_paused = True
print("⏸ Paused")
else:
# Send SIGCONT to resume the child process
os.kill(player_proc.pid, signal.SIGCONT)
is_paused = False
print("▶ Resumed")
print("=" * 50)
print(" Uno Q USB Music Player (Terminal Edition)")
print("=" * 50)
print("Track List:")
for i, song in enumerate(playlist):
print(f" [{i + 1}] {song}")
print("-" * 50)
print("Controls: [p] Pause/Resume | [n] Next | [b] Previous | [Number] Jump | [q] Quit")
print("=" * 50)
# Automatically start the first track upon launch
play_track(0)
# Interactive control loop
while True:
try:
cmd = input("\nEnter command: ").strip().lower()
if cmd == 'q':
stop_current()
print("Player exited safely.")
break
elif cmd == 'p':
toggle_pause()
elif cmd == 'n':
play_track(current_index + 1)
elif cmd == 'b':
play_track(current_index - 1)
elif cmd.isdigit():
idx = int(cmd) - 1
if 0 <= idx < len(playlist):
play_track(idx)
else:
print("Invalid track number!")
else:
print("Invalid input. Options: p / n / b / Track Number / q")
except (KeyboardInterrupt, EOFError):
stop_current()
break
In the nano editor, press Ctrl + O and Enter to save, then press Ctrl + X to exit.
MUSIC_DIR = "/home/arduino/music_player/mp3"
playlist = [f for f in os.listdir(MUSIC_DIR) if f.lower().endswith(".mp3")]
playlist.sort()
player_proc = None
current_index = 0
is_paused = False
Explanation: Automatically scans the target directory for .mp3 files and sorts them alphabetically to construct an ordered playlist. player_proc maintains a reference to the active decoding subprocess, while is_paused tracks the playback state.
def stop_current():
global player_proc, is_paused
if player_proc and player_proc.poll() is None:
player_proc.terminate()
player_proc.wait()
player_proc = None
is_paused = False
Function: Terminates the active playback subprocess and releases system resources.
Explanation:
player_proc.poll() is None performs a non-blocking check to verify if the subprocess is still running.player_proc.terminate() dispatches a standard SIGTERM signal to the active audio process.player_proc.wait() blocks until the old process exits and frees its hardware lock, preventing overlapping audio streams or zombie processes when switching tracks.def play_track(index):
global player_proc, current_index, is_paused
stop_current()
current_index = index % len(playlist)
file_path = os.path.join(MUSIC_DIR, playlist[current_index])
player_proc = subprocess.Popen(
["mpg123", "-q", file_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
is_paused = False
Function: Switches tracks and starts streaming audio asynchronously in the background.
Explanation:
current_index = index % len(playlist) implements circular playlist wrapping.subprocess.Popen(...) starts mpg123 asynchronously: decoding runs in an independent background process, allowing the main Python thread to remain responsive to terminal inputs.stdout/stderr=subprocess.DEVNULL discards stream decoding diagnostics, keeping the terminal user interface clear.def toggle_pause():
global player_proc, is_paused
if not player_proc or player_proc.poll() is not None:
return
if not is_paused:
os.kill(player_proc.pid, signal.SIGSTOP)
is_paused = True
else:
os.kill(player_proc.pid, signal.SIGCONT)
is_paused = False
Function: Controls the hardware audio stream using low-level POSIX process signals.
Explanation:
os.kill(player_proc.pid, signal.SIGSTOP) sends a freeze signal to the child process, halting decoding and holding the sound card buffer to achieve low-latency pausing.os.kill(player_proc.pid, signal.SIGCONT) unfreezes the child process, resuming stream playback from the exact pause point.while True:
try:
cmd = input("\nEnter command: ").strip().lower()
if cmd == 'q':
stop_current()
break
elif cmd == 'p':
toggle_pause()
elif cmd == 'n':
play_track(current_index + 1)
elif cmd == 'b':
play_track(current_index - 1)
elif cmd.isdigit():
idx = int(cmd) - 1
if 0 <= idx < len(playlist):
play_track(idx)
except (KeyboardInterrupt, EOFError):
stop_current()
break
Function: Listens for terminal keystrokes and routes commands.
Explanation: Uses blocking standard input to receive commands, mapping characters to playback actions. Wrapping the loop in try...except (KeyboardInterrupt, EOFError) ensures that pressing Ctrl + C or disconnecting the terminal triggers stop_current(), preventing orphan background processes from continuing playback.
The primary code paths that control audio playback, pausing, and track switching on the USB speaker are:
player_proc = subprocess.Popen(
["mpg123", "-q", file_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
This is the hardware output gateway for Phase 2. MP3 is a lossy compressed format that cannot be sent directly to a sound card. This line spawns the mpg123 decoder to unpack audio frames in real time and write PCM streams to the ALSA USB speaker device node.os.kill(player_proc.pid, signal.SIGSTOP) # Freezes data transfer; speaker falls silent
os.kill(player_proc.pid, signal.SIGCONT) # Resumes data transfer; speaker outputs sound
Sending SIGSTOP and SIGCONT directly to the decoding process halts or resumes data streaming to the USB speaker with minimal latency.Start the player in your Uno Q terminal:
python3 mp3_native.py
Interactive Controls:
p and press Enter to pause; type p and press Enter again to resume playback.n and press Enter to skip to the next track; type b and press Enter to return to the previous track.2) and press Enter to jump directly to that song.q and press Enter, or press Ctrl + C, to stop playback and exit.Troubleshooting Reference:
Hello from the pygame community... and input commands do not respond
import pygame. Pygame requires an active desktop window to capture keyboard events; over a headless SSH session, it has no GUI focus and cannot read terminal keypresses.python3 mp3_native.py. This script reads standard input via input(), which requires pressing Enter after each command (p, n, b, etc.)._tkinter.TclError: no display name and no $DISPLAY environment variable
mp3_native.py for terminal-based control. If running a GUI script is required, ensure python3-tk is installed and enable X11 forwarding or VNC.No such file or directory: 'mpg123'
mpg123 decoding engine is not installed.sudo apt update && sudo apt install -y mpg123.cat /proc/asound/cards to identify the USB audio index (e.g., card 1), then verify playback directly with mpg123 -a hw:1,0 /home/arduino/music_player/mp3/xxx.mp3.For a visual interface on systems with a display attached or over X11/VNC forwarding, use this combined Tkinter and Pygame script:
import os
import tkinter as tk
from tkinter import messagebox
import pygame
# ==================== Path Configuration ====================
# Specify the folder path here (supports both Windows and Linux formats)
# If left as None or "", the program defaults to the directory containing this script
CUSTOM_DIR = "/home/arduino/music_player/mp3"
# Example: r"C:\Users\Admin\Music" or "/home/arduino/music"
if CUSTOM_DIR and os.path.exists(CUSTOM_DIR):
MUSIC_DIR = CUSTOM_DIR
else:
MUSIC_DIR = os.path.dirname(os.path.abspath(__file__))
# Scan and sort MP3 files
playlist = [f for f in os.listdir(MUSIC_DIR) if f.lower().endswith(".mp3")]
playlist.sort()
current_index = 0
is_paused = False
# Initialize the pygame mixer audio module
pygame.mixer.init()
# ==================== Playback Control Logic ====================
def play_music():
global is_paused
if not playlist:
return
track_path = os.path.join(MUSIC_DIR, playlist[current_index])
try:
pygame.mixer.music.load(track_path)
pygame.mixer.music.play()
is_paused = False
status_label.config(text=f"Playing: {playlist[current_index]}")
btn_pause.config(text="Pause")
listbox.selection_clear(0, tk.END)
listbox.selection_set(current_index)
listbox.see(current_index)
except Exception as e:
messagebox.showerror("Playback Error", f"Cannot play file: {e}")
def toggle_pause():
global is_paused
if not playlist:
return
if is_paused:
pygame.mixer.music.unpause()
is_paused = False
btn_pause.config(text="Pause")
status_label.config(text=f"Playing: {playlist[current_index]}")
else:
pygame.mixer.music.pause()
is_paused = True
btn_pause.config(text="Resume")
status_label.config(text=f"Paused: {playlist[current_index]}")
def prev_music():
global current_index
if not playlist:
return
current_index = (current_index - 1) % len(playlist)
play_music()
def next_music():
global current_index
if not playlist:
return
current_index = (current_index + 1) % len(playlist)
play_music()
def on_select(event):
global current_index
selection = listbox.curselection()
if selection:
current_index = selection[0]
play_music()
def on_close():
pygame.mixer.music.stop()
root.destroy()
# ==================== GUI Construction ====================
root = tk.Tk()
root.title("Music Player")
root.geometry("450x400")
root.protocol("WM_DELETE_WINDOW", on_close)
# Path and status header
path_label = tk.Label(root, text=f"Directory: {MUSIC_DIR}", fg="gray", wraplength=420, font=("Arial", 8))
path_label.pack(pady=(6, 0))
status_label = tk.Label(root, text="Ready", wraplength=420, font=("Arial", 10, "bold"), pady=6)
status_label.pack()
# Track list container
frame_list = tk.Frame(root)
frame_list.pack(fill=tk.BOTH, expand=True, padx=15, pady=5)
scrollbar = tk.Scrollbar(frame_list)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
listbox = tk.Listbox(frame_list, yscrollcommand=scrollbar.set, font=("Arial", 9), selectmode=tk.SINGLE)
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=listbox.yview)
for song in playlist:
listbox.insert(tk.END, song)
listbox.bind("<Double-Button-1>", on_select)
# Control button panel
frame_ctrl = tk.Frame(root, pady=10)
frame_ctrl.pack()
btn_prev = tk.Button(frame_ctrl, text="Prev", width=8, command=prev_music)
btn_prev.grid(row=0, column=0, padx=5)
btn_play = tk.Button(frame_ctrl, text="Play", width=8, command=play_music)
btn_play.grid(row=0, column=1, padx=5)
btn_pause = tk.Button(frame_ctrl, text="Pause", width=8, command=toggle_pause)
btn_pause.grid(row=0, column=2, padx=5)
btn_next = tk.Button(frame_ctrl, text="Next", width=8, command=next_music)
btn_next.grid(row=0, column=3, padx=5)
if not playlist:
status_label.config(text="No .mp3 files found in this directory")
else:
listbox.selection_set(0)
root.mainloop()