← Back to Blog

Raspberry Pi – Hotspot Music Server with GPIO Controls

Complete embedded music system on Raspberry Pi – WiFi hotspot with web interface, MP3 player with file management, GPIO button controls, and systemd auto-start. Built with Flask, Python, and MPlayer.

11 min read Roman Swetly

Raspberry Pi – Hotspot Music Server with GPIO Controls

Complete embedded music system on Raspberry Pi – WiFi hotspot with web interface, MP3 player with file management, GPIO button controls, and systemd auto-start.


Overview

This project transforms a Raspberry Pi into a self-contained music server with:

  • WiFi Hotspot – Access the music player from any device without external internet.
  • Web Interface – Full music library management (play, download, upload, rename, delete).
  • GPIO Hardware Buttons – Physical controls for playback (play/pause, next, prev, volume).
  • File Management – Upload, delete, rename, and organize MP3 files.
  • Auto-start – Everything starts automatically on boot using systemd.

The system is built on Raspberry Pi OS (Bookworm) with Python, Flask, and MPlayer.


Architecture

self-contained music server

Components

ComponentPurpose
Raspberry PiAny model with GPIO and WiFi (tested on Pi 3/4/Zero 2 W).
Raspberry Pi OSBookworm (Debian 12) with Python 3.11+
Hostapd + DnsmasqWiFi hotspot (PiMusicServer).
Systemd-networkdStatic IP for wlan0.
Python + FlaskWeb interface and GPIO control.
MPlayerCommand-line audio player.
GPIO ButtonsSix physical buttons for track selection.

Quick Start

Step 1: Flash Raspberry Pi OS

# Download Raspberry Pi OS Bookworm
# Flash to SD card using Raspberry Pi Imager
# Enable SSH with the imager (or add empty 'ssh' file to boot partition)

Step 2: Initial Setup

# Update system
sudo apt update
sudo apt upgrade -y

# Install Python and tools
sudo apt install -y python3 python3-pip python3-venv python3-flask python3-flask-cors

# Install MPlayer (audio playback)
sudo apt install -y mplayer

# Install GPIO tools
sudo apt install -y python3-gpiod gpiod

# Install system dependencies
sudo apt install -y hostapd dnsmasq systemd-networkd

WiFi Hotspot Configuration

Network Configuration

# Stop services
sudo systemctl stop hostapd
sudo systemctl stop wpa_supplicant
sudo systemctl stop dnsmasq

# Disable wpa_supplicant
sudo systemctl disable wpa_supplicant
sudo systemctl mask wpa_supplicant

systemd-networkd Configuration

sudo nano /etc/systemd/network/08-wlan0.network
[Match]
Name=wlan0

[Network]
Address=192.168.4.1/24
DHCPServer=yes
IPMasquerade=yes

hostapd Configuration

sudo nano /etc/hostapd/hostapd.conf
interface=wlan0
driver=nl80211
ssid=PiMusicServer
hw_mode=g
channel=7
wpa=2
wpa_passphrase=Music1234
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP

Enable Services

# Configure hostapd
echo 'DAEMON_CONF="/etc/hostapd/hostapd.conf"' | sudo tee /etc/default/hostapd

# Enable IP forwarding
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-hotspot.conf
sudo sysctl --system

# Enable services
sudo systemctl unmask hostapd
sudo systemctl enable hostapd
sudo systemctl enable systemd-networkd
sudo systemctl restart systemd-networkd
sudo systemctl start hostapd

Create Service for wlan0 (Reboot Persistence)

sudo nano /etc/systemd/system/hostapd-wlan0.service
[Unit]
Description=Hostapd with wlan0 ready
Wants=network.target
After=network.target

[Service]
Type=simple
ExecStartPre=/bin/sleep 5
ExecStart=/usr/sbin/hostapd /etc/hostapd/hostapd.conf
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl disable hostapd
sudo systemctl stop hostapd
sudo systemctl daemon-reload
sudo systemctl enable hostapd-wlan0.service
sudo systemctl start hostapd-wlan0.service

Music Server Setup

Project Structure

/home/test/music-server/
├── service1.py          # Web interface (Port 5000)
├── service2.py          # GPIO simulator (Port 5001)
├── requirements.txt     # Python dependencies
├── templates/
│   ├── service1.html    # Music player UI
│   └── service2.html    # GPIO simulator UI
├── static/
│   ├── music/           # MP3 files (uploaded here)
│   └── uploads/         # Temporary uploads
└── venv/                # Python virtual environment

Create Project Directory

# Create user test (if not exists)
sudo useradd -m -s /bin/bash test
sudo passwd test  # Set password

# Switch to test user
sudo su - test

# Create project structure
mkdir -p ~/music-server/{templates,static/music,static/uploads}
cd ~/music-server

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

requirements.txt

Flask==2.3.3
Flask-CORS==4.0.0
Werkzeug==2.3.7
RPi.GPIO==0.7.1
pip install -r requirements.txt

Service Files

service1.py – Web Interface & File Manager

#!/usr/bin/env python3
from flask import Flask, render_template, jsonify, send_file, send_from_directory, request
from werkzeug.utils import secure_filename
import os
import json
import threading
import time
from pathlib import Path
import shutil

try:
    import RPi.GPIO as GPIO
    GPIO_AVAILABLE = True
except ImportError:
    GPIO_AVAILABLE = False
    print("GPIO running in simulation mode")

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024  # 100MB max upload

# Define paths
BASE_DIR = Path(__file__).parent
MUSIC_FOLDER = BASE_DIR / 'static' / 'music'
UPLOAD_FOLDER = BASE_DIR / 'static' / 'uploads'
TEMPLATE_FOLDER = BASE_DIR / 'templates'

# Create directories if they don't exist
MUSIC_FOLDER.mkdir(parents=True, exist_ok=True)
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
TEMPLATE_FOLDER.mkdir(parents=True, exist_ok=True)

# Allowed file extensions
ALLOWED_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'm4a'}

# GPIO Setup (customizable pins)
BUTTON_PINS = {
    'play_pause': 5,  # GPIO 5 (Pin 29)
    'next': 6,        # GPIO 6 (Pin 31)
    'prev': 16,       # GPIO 16 (Pin 36)
    'vol_up': 25      # GPIO 25 (Pin 22)
}

player_state = {
    'playing': False,
    'current_song': None,
    'volume': 0.5,
    'gpio_available': GPIO_AVAILABLE
}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def get_file_size(filename):
    """Get human readable file size"""
    path = MUSIC_FOLDER / filename
    if path.exists():
        size = path.stat().st_size
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size < 1024.0:
                return f"{size:.1f} {unit}"
            size /= 1024.0
    return "0 B"

def setup_gpio():
    if GPIO_AVAILABLE:
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        for pin in BUTTON_PINS.values():
            GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
        print("GPIO initialized")
    else:
        print("GPIO simulation mode")

def button_monitor():
    if not GPIO_AVAILABLE:
        return
    
    button_states = {pin: False for pin in BUTTON_PINS.values()}
    
    while True:
        try:
            for name, pin in BUTTON_PINS.items():
                current = GPIO.input(pin)
                if current and not button_states[pin]:
                    button_states[pin] = True
                    print(f"GPIO Button pressed: {name} (GPIO{pin})")
                elif not current and button_states[pin]:
                    button_states[pin] = False
        except Exception as e:
            print(f"GPIO error: {e}")
        time.sleep(0.05)

# Routes
@app.route('/')
def index():
    """Main page"""
    return render_template('service1.html', state=player_state)

@app.route('/api/files')
def list_files():
    """List all music files with details"""
    files = []
    for f in MUSIC_FOLDER.iterdir():
        if f.is_file() and f.suffix.lower() in ['.mp3', '.wav', '.ogg', '.flac', '.m4a']:
            stat = f.stat()
            files.append({
                'name': f.name,
                'size': get_file_size(f.name),
                'size_bytes': stat.st_size,
                'modified': stat.st_mtime,
                'url': f'/static/music/{f.name}',
                'download_url': f'/download/{f.name}',
                'delete_url': f'/delete/{f.name}',
                'rename_url': f'/rename/{f.name}'
            })
    
    files.sort(key=lambda x: x['name'].lower())
    return jsonify(files)

@app.route('/static/music/<path:filename>')
def serve_music(filename):
    """Serve music files for playback"""
    return send_from_directory(MUSIC_FOLDER, filename)

@app.route('/download/<filename>')
def download_file(filename):
    """Download a file"""
    return send_from_directory(MUSIC_FOLDER, filename, as_attachment=True)

@app.route('/delete/<filename>')
def delete_file(filename):
    """Delete a file"""
    try:
        filepath = MUSIC_FOLDER / secure_filename(filename)
        if filepath.exists():
            filepath.unlink()
            return jsonify({'success': True, 'message': f'Deleted {filename}'})
        return jsonify({'success': False, 'message': 'File not found'}), 404
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500

@app.route('/rename/<old_filename>', methods=['POST'])
def rename_file(old_filename):
    """Rename a file"""
    try:
        data = request.get_json()
        if not data or 'new_name' not in data:
            return jsonify({'success': False, 'message': 'New name required'}), 400
        
        new_name = secure_filename(data['new_name'])
        
        if '.' not in new_name:
            old_ext = Path(old_filename).suffix
            new_name = new_name + old_ext
        
        old_path = MUSIC_FOLDER / secure_filename(old_filename)
        new_path = MUSIC_FOLDER / new_name
        
        if not old_path.exists():
            return jsonify({'success': False, 'message': 'File not found'}), 404
        
        if new_path.exists():
            return jsonify({'success': False, 'message': 'File with new name already exists'}), 400
        
        old_path.rename(new_path)
        
        if player_state['current_song'] == old_filename:
            player_state['current_song'] = new_name
        
        return jsonify({
            'success': True, 
            'message': f'Renamed {old_filename} to {new_name}',
            'new_name': new_name
        })
        
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500

@app.route('/upload', methods=['POST'])
def upload_file():
    """Upload new music files"""
    if 'file' not in request.files:
        return jsonify({'success': False, 'message': 'No file part'}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'success': False, 'message': 'No selected file'}), 400
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = MUSIC_FOLDER / filename
        
        counter = 1
        while filepath.exists():
            name, ext = os.path.splitext(filename)
            filename = f"{name}_{counter}{ext}"
            filepath = MUSIC_FOLDER / filename
            counter += 1
        
        file.save(filepath)
        return jsonify({
            'success': True, 
            'message': f'Uploaded {filename}',
            'filename': filename
        })
    
    return jsonify({'success': False, 'message': 'File type not allowed'}), 400

@app.route('/play/<filename>')
def play_file(filename):
    """Set current playing file"""
    filepath = MUSIC_FOLDER / secure_filename(filename)
    if filepath.exists():
        player_state['current_song'] = filename
        player_state['playing'] = True
        return jsonify({'success': True, 'message': f'Playing {filename}'})
    return jsonify({'success': False, 'message': 'File not found'}), 404

@app.route('/api/control/<command>')
def control_player(command):
    """Control player"""
    if command == 'play':
        player_state['playing'] = True
    elif command == 'pause':
        player_state['playing'] = False
    elif command == 'next':
        pass
    elif command == 'prev':
        pass
    
    return jsonify({'success': True, 'state': player_state})

@app.route('/api/status')
def get_status():
    """Get player status"""
    return jsonify(player_state)

@app.route('/storage')
def storage_info():
    """Get storage information"""
    try:
        total, used, free = shutil.disk_usage(MUSIC_FOLDER)
        return jsonify({
            'total': total,
            'used': used,
            'free': free,
            'total_gb': round(total / (1024**3), 2),
            'used_gb': round(used / (1024**3), 2),
            'free_gb': round(free / (1024**3), 2)
        })
    except:
        return jsonify({'error': 'Could not get storage info'}), 500

if __name__ == '__main__':
    setup_gpio()
    
    if GPIO_AVAILABLE:
        btn_thread = threading.Thread(target=button_monitor, daemon=True)
        btn_thread.start()
    
    print(f"Music folder: {MUSIC_FOLDER}")
    print(f"Server starting on http://0.0.0.0:5000")
    print(f"GPIO Available: {GPIO_AVAILABLE}")
    
    app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)

service2.py – GPIO Button Simulator

A second service runs on port 5001 to simulate and test GPIO hardware buttons. See the attached file structure for the complete code (included in the project ZIP).


Systemd Auto-Start Services

service1 Systemd

sudo nano /etc/systemd/system/music-service1.service
[Unit]
Description=Music Server Service 1 (Web Interface)
After=network.target hostapd-wlan0.service

[Service]
Type=simple
User=test
WorkingDirectory=/home/test/music-server
ExecStart=/home/test/music-server/venv/bin/python /home/test/music-server/service1.py
Restart=always
RestartSec=10
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target

service2 Systemd

sudo nano /etc/systemd/system/music-service2.service
[Unit]
Description=Music Server Service 2 (GPIO Simulator)
After=network.target hostapd-wlan0.service

[Service]
Type=simple
User=test
WorkingDirectory=/home/test/music-server
ExecStart=/home/test/music-server/venv/bin/python /home/test/music-server/service2.py
Restart=always
RestartSec=10
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target

Enable Both Services

sudo systemctl daemon-reload
sudo systemctl enable music-service1
sudo systemctl enable music-service2
sudo systemctl start music-service1
sudo systemctl start music-service2

# Check status
sudo systemctl status music-service1
sudo systemctl status music-service2

GPIO Button Wiring

Pin Mapping

ButtonGPIO (BCM)Physical PinFunction
11711Track 1 (001.mp3)
22713Track 2 (002.mp3)
32215Track 3 (003.mp3)
41019Track 4 (004.mp3)
5921Track 5 (005.mp3)
61123Track 6 (006.mp3)

Wiring Instructions

Button 1: GPIO 17 ──[BTN]── GND
Button 2: GPIO 27 ──[BTN]── GND
Button 3: GPIO 22 ──[BTN]── GND
Button 4: GPIO 10 ──[BTN]── GND
Button 5: GPIO 9  ──[BTN]── GND
Button 6: GPIO 11 ──[BTN]── GND

GND Pins: Use any ground pin (6, 9, 14, 20, 25, 30, 34, 39).


Accessing the Server

ServiceURLPurpose
Music Playerhttp://192.168.4.1:5000Web interface with file management
GPIO Simulatorhttp://192.168.4.1:5001Hardware test and control
SSHssh test@192.168.4.1Remote access (password: 12345)

WiFi Details

  • SSID: PiMusicServer
  • Password: Music1234

Features

FeatureDescription
Play MusicClick any file to play in browser audio player.
Upload FilesDrag and drop or click to upload MP3 files.
Delete FilesRemove unwanted tracks.
Rename FilesDouble-click or right-click to rename.
Download FilesDownload tracks to connected device.
GPIO ButtonsPhysical buttons on the Raspberry Pi.
Keyboard Shortcuts1-6 for tracks, S for stop, +/- for volume.
Auto-StartEverything starts on boot.

Troubleshooting

GPIO Not Working

# Add user to GPIO group
sudo usermod -aG gpio test

# Check GPIO permissions
ls -la /dev/gpio*

# Test GPIO manually
sudo gpioset gpiochip0 17=1
sudo gpioget gpiochip0 17

MPlayer Not Playing

# Check audio device
aplay -l

# Test audio
speaker-test -t sine -f 440 -c 2

# Install MPlayer
sudo apt install mplayer -y

Hotspot Not Starting

# Check status
sudo systemctl status hostapd-wlan0

# Check logs
sudo journalctl -u hostapd-wlan0 -f

# Check interface
ip addr show wlan0
self-contained music server desktop screen

Key Achievements

  • ✅ Complete music server with web interface and file management.
  • ✅ WiFi hotspot with static IP (192.168.4.1).
  • ✅ GPIO hardware controls (6 physical buttons).
  • ✅ Dual-service architecture (web UI + GPIO simulator).
  • ✅ Auto-start on boot via systemd.
  • ✅ File management (upload, delete, rename, download).
  • ✅ Drag-and-drop upload with progress bar.
  • ✅ Keyboard shortcuts for all controls.

Technology Stack

CategoryTechnology
HardwareRaspberry Pi (3/4/Zero 2 W)
OSRaspberry Pi OS Bookworm
Hotspothostapd + systemd-networkd
Web ServerFlask (Python)
Audio PlayerMPlayer
GPIO ControlRPi.GPIO, gpiod
FrontendHTML5, CSS3, JavaScript (Vanilla)
File ManagementPython os, pathlib, shutil


This Raspberry Pi music server is part of my broader Embedded Systems Engineering practice. For a detailed technical walkthrough or custom audio project design, feel free to reach out.

← Previous Post
Embedded Systems – Arduino Pro Mini Ultra-Low Power Standby
Next Post →
Embedded Systems – Arduino Server-Side Compiler

Related Articles