Zune BT Console: Difference between revisions
| Line 188: | Line 188: | ||
== Zip Ties == | == Zip Ties == | ||
Strain relief for cables and their sockets cannot be over stated as a good thing for DIY projects. | Strain relief for cables and their sockets cannot be over stated as a good thing for DIY projects. | ||
=Software= | |||
== ESP32 Console Code == | |||
<code> | |||
/* Original Attribution */ | |||
/* | |||
Streaming of sound data with Bluetooth to other Bluetooth device. | |||
We generate 2 tones which will be sent to the 2 channels. | |||
Copyright (C) 2020 Phil Schatzmann | |||
This program is free software: you can redistribute it and/or modify | |||
it under the terms of the GNU General Public License as published by | |||
the Free Software Foundation, either version 3 of the License, or | |||
(at your option) any later version. | |||
This program is distributed in the hope that it will be useful, | |||
but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
GNU General Public License for more details. | |||
You should have received a copy of the GNU General Public License | |||
along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
/* Added SSD1306 functionality and I2C functions for the IR sending arduino communication */ | |||
#include <Wire.h> | |||
#include <Adafruit_GFX.h> | |||
#include <Adafruit_SSD1306.h> | |||
#include "AudioTools.h" | |||
#include "AudioTools/AudioLibs/A2DPStream.h" | |||
#define SCREEN_WIDTH 128 // OLED display width, in pixels | |||
#define SCREEN_HEIGHT 64 // OLED display height, in pixels | |||
bool display_active = false; | |||
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) | |||
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); | |||
AudioInfo info24(44100, 2, 24); | |||
BluetoothA2DPSource a2dp_source; | |||
#define I2S_NUM I2S_NUM_0 | |||
#define I2S_BCK 14 | |||
#define I2S_WS 15 | |||
#define I2S_DATA_IN 32 | |||
#define I2S_MCK 0 | |||
// MD0 and MD1 left to float | |||
// FMT set to HIGH | |||
// Clean power need to prevent interference with signal | |||
// The supported audio codec in ESP32 A2DP is SBC. SBC audio stream is encoded | |||
// from PCM data normally formatted as 44.1kHz sampling rate, two-channel 16-bit sample data | |||
I2SStream i2s; | |||
#define SAMPLE_BUFFER_SIZE 128 | |||
/* Receives the two 24bits in 32bit packages from the PCM1808 and shifts the 16bit right */ | |||
/* The shifted values are then stored in the 16bit stereo Frame */ | |||
int32_t get_sound_data(Frame* data, int32_t frameCount) | |||
{ | |||
struct Xrame | |||
{ | |||
uint32_t left_sample; | |||
uint32_t right_sample; | |||
} xrame[SAMPLE_BUFFER_SIZE]; | |||
int bytesRead = i2s.readBytes((uint8_t*)xrame, sizeof(Xrame) * frameCount); | |||
for (int i = 0; i < frameCount; ++i) | |||
{ | |||
data[i].channel1 = xrame[i].left_sample>>16; | |||
data[i].channel2 = xrame[i].right_sample>>16; | |||
} | |||
return frameCount; | |||
} | |||
bool isValid(const char* ssid, esp_bd_addr_t address, int rssi) | |||
{ | |||
if (display_active) | |||
{ | |||
display.clearDisplay(); | |||
display.setCursor(0,0); | |||
display.print(ssid); | |||
display.display(); | |||
} | |||
Serial.print("available SSID: "); | |||
Serial.println(ssid); | |||
/* Test for automatically connecting to my BT speaker */ | |||
if (strcmp(ssid,"CMA3569") == 0) | |||
return true; | |||
else | |||
return false; | |||
} | |||
void setup() | |||
{ | |||
Wire.begin(); | |||
Serial.begin(115200); | |||
Serial.println("Start"); ///Info | |||
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) | |||
{ | |||
Serial.println(F("SSD1306 allocation failed")); | |||
} | |||
else | |||
{ | |||
display_active = true; | |||
display.clearDisplay(); | |||
display.setTextColor(WHITE); | |||
display.setTextSize(1); | |||
display.setCursor(0,0); | |||
display.display(); | |||
} | |||
// I2S configuration | |||
Serial.println("I2S Configure Data Loaded"); /// Info | |||
auto cfg = i2s.defaultConfig(RX_MODE); | |||
cfg.i2s_format = I2S_STD_FORMAT; // or try with I2S_LSB_FORMAT | |||
cfg.copyFrom(info24); | |||
cfg.pin_mck = I2S_MCK; | |||
cfg.pin_bck = I2S_BCK; | |||
cfg.pin_ws = I2S_WS; | |||
cfg.pin_data = I2S_DATA_IN; | |||
cfg.auto_clear = true; | |||
// cfg.use_apll = true; // from your logic: not sure if this works with the new API? | |||
i2s.begin(cfg); | |||
/* Button mapping but no functions implemented */ | |||
pinMode(25,INPUT_PULLUP); | |||
pinMode(26,INPUT_PULLUP); | |||
pinMode(27,INPUT_PULLUP); | |||
pinMode(33,INPUT_PULLUP); | |||
Serial.println("I2S Activate"); /// Info | |||
Serial.println("Activate BT Functions"); /// Info | |||
a2dp_source.set_ssid_callback(isValid); | |||
a2dp_source.set_auto_reconnect(false); | |||
a2dp_source.set_data_callback_in_frames(get_sound_data); | |||
a2dp_source.set_avrc_passthru_command_callback(button_handler); | |||
a2dp_source.start("Quixote"); | |||
} | |||
/* This is referring to the buttons on the BT speaker */ | |||
void button_handler(uint8_t id, bool isReleased) | |||
{ | |||
int IRAddress = 0x55; // This is the address of the IR sending arduino | |||
if (isReleased) { | |||
Serial.print("button id "); | |||
Serial.print(id); | |||
Serial.println(" released"); | |||
if (id == 70) | |||
{ | |||
Wire.beginTransmission(IRAddress); // Start communication with the slave | |||
Wire.write(0); // Send the number 42 | |||
Wire.endTransmission(); // End the transmission | |||
} | |||
if (id == 68) | |||
{ | |||
Wire.beginTransmission(IRAddress); // Start communication with the slave | |||
Wire.write(0); // Send the number 42 | |||
Wire.endTransmission(); // End the transmission | |||
} | |||
if (id == 75) | |||
{ | |||
Wire.beginTransmission(IRAddress); // Start communication with the slave | |||
Wire.write(2); // Send the number 42 | |||
Wire.endTransmission(); // End the transmission | |||
} | |||
if (id == 76) | |||
{ | |||
Wire.beginTransmission(IRAddress); // Start communication with the slave | |||
Wire.write(1); // Send the number 42 | |||
Wire.endTransmission(); // End the transmission | |||
} | |||
} | |||
} | |||
void loop() | |||
{ | |||
delay(1000); | |||
} | |||
</code> | |||
== My inspiring Bluetooth Speaker == | == My inspiring Bluetooth Speaker == | ||
[[File:Zune BT Speaker CMA3569.jpg|Inspiring|left|thumbnail|x400px]] | [[File:Zune BT Speaker CMA3569.jpg|Inspiring|left|thumbnail|x400px]] | ||
Revision as of 12:33, 9 September 2025
Introduction, Problem Statement and the Twist

The LVL1 Hackerspace in Louisville Kentucky has seen many mp3 players, cell phones and streaming services. But when I, Director of Legal Evil Emeritus, indulge in the Hack-a-Thons, I bring a Zune. And although there are many invasive hacks in wild, I thought I would attempt BT without damaging a device and adding the AVRCP functionality that the common BT fob does not add to the experience. To that end, this is my story/hack, and I am sticking to it.
The Story, Probably Apocryphal
All the cool kids know, “Zune is where its at”, but the whole Bluetooth thing is the next level. This collision of paradigms must be reconciled. My choice was to respect the integrity of the Zune ecosystem while bringing in the new hot topic. So I collected my options and evaluated them as follows:
Whys and Wherefores
There are already Zune mods/hacks for adding Bluetooth. Most are permutations on putting a commercial BT transmitter inside the Zune. These work, but I have a BT speaker with volume, play/pause and skip buttons. These functions can not “inform” the Zune to do anything through a common audio input only BT transmitter. The functions for the button do exist on the Zune and are incorporated in the IR remotes for Zune docks. This mean that a Zune in a dock, connected to a BT transmitter and in line of sight of the dock remote can perform the functions. But I don’t want to keep a remote and line of sight to perform what the speaker already has buttons for as well as headphones with similar buttons.
My spin on the build is to use a BT transmitter that can trap the button functions and relay them to the Zune, via the dock. This means that if I emulate a dock remote based on the AVCRP signals trapped by the BT transmitter then mission accomplished.
My first idea was to use the KCX_BT_Emitter and monitor the serial communications line for the signals and react appropriately. But the KCX doesn’t always show the signals on the serial line. After multiple ways of resetting the module, initializing the connection, and other strategies, it was not consistent.
Next was to use an ESP32 and look for the signal. I found a library that already did what I wanted. I needed to make adjustments for my hardware choices. When I posted about my project, the author discussed some changes and update suggestions.
Now I had the ability to send audio to BT devices and trap the signals from the buttons, I needed to supply directions to the dock. This would be done by emulating a dock remote. This method did not require any hardware changes to the Zune ecosystem hardware. It also allows functioning across the 3 generations of Zune docks and players.
I realize that without the ESP32, this is simply using a BT transmitter and an IR LED bright enough to be seen through walls. And yet, this is a solution with self imposed restraints that allowed me to delve into BT and Zunes. I guess I just had an opportunity to learn and I took it.
Add Bluetooth Function To Zune Usage

Reasoning
Not Invasive / No Shell Cracking
- Plug In Commercial BT Module
- Safe, Simple and Finite
Invasive / Shell Cracking
- Remove Commercial BT Module Shell
- Crack Open Zune Shell
- Solder Power and Audio Lines from Zune to Module
- Seal It Up
- Risky Operations / Finite Results
Embrace A Special Kind of Madness
- Build an External BT Sending Device
- Incorporate Zune Response to BT Speaker/ Headphone AVRCP Messages
- Don’t Crack a Single Microsoft Casing
- The Road Less Travelled Has The More Interesting Potholes
Decision
One must build.
Parts

List
| Item | Quantity | Purpose |
|---|---|---|
| ESP32 Wrover | 1 | Bluetooth Transmitter |
| Minimal Arduino | 1 | IR Transmitter via I2C |
| PCM1808 | 1 | I2S ADC |
| SSD1306 | 1 | Info Display |
| IR LED | 1 | IR Transmission |
| IR Photo-Transistor | 1 | IR Receiver |
| Logic Level Shifter | 1 | I2C 3.3V vs 5V Translation |
| Stereo Phono Socket | 1 | Analog Audio In |
| Tactile Switches | 4 | Future Use |
| USB A to micro USB | 1 | Power / Programming |
Notes
Nothing used is unusual or vintage.
Unusual
The minimal Arduino is an old item I purchased from somewhere. It is the ATMEL 328P with a few capacitors and resistors (under the chip) and resonator.
Vintage
Bare perfboard is my goto.
Wiring Components
Signal Wiring

| ESP32 | PCM1808 | OLED Display | Buttons | Level Shifter |
|---|---|---|---|---|
| 0 | SCK | |||
| 14 | BCK | |||
| 15 | LRC | |||
| 32 | OUT | |||
| 21 | SCL | LV2 | ||
| 22 | SDA | LV1 | ||
| 25 | Green | |||
| 26 | Red | |||
| 27 | White | |||
| 33 | Black |
| Minimal Arduino | IR LED Unit | IR RX Unit | Level Shifter |
|---|---|---|---|
| D3 | DAT | ||
| D5 | DAT | ||
| A4 | HL1 | ||
| A5 | HL2 |
Power Wiring
- ESP32 Wrover provides 5V and 3.3V power.
- The PCM1808 requires 5V and 3.3V with the signals using 3.3V
- The SSD1306 Display requires 3.3V
- The minimal Arduino runs on 5V supplied by the ESP32 Wrover
- A jumper on the Arduino allows it to be isolated from the ESP32 5V line when programming it is necessary.
- The IR LED and IR RX require 5V
- The Level Shifter provides the bridge between the 3.3V signals of the ESP32 I2C and 3.3V I2C of the Arduino
Notes on the Build
PCM1808
When I chose this I2S ADC, I didn’t have much info. I looked at the datasheet and went forth. Most of the implementations on the web seemed to say there where problems, it was peculiar or it just worked.
My experience was complicated by a pcm1808 module that did not work. After some frustrations and verification that the timing signals into the PCM1808 with a scope were in spec, I bought another PCM1808. I plugged it in and it just worked from the standpoint of sending data down the I2S path.
Next came the 24bit output per channel and the need to make it 16bit per channel. This had an interesting twist. The 24bits would be in a left justified 32bit storage. The datasheet for the pcm1808 alluded to this along with a stray web reference. So, 16 bit right shift and storage in the proper variable type yielded success.
IR Send / Receive Library
There are several libraries out there. A couple had the effect of decoding the Zune IR Remotes, but could not duplicate the IR remote when sending. This was annoying. I simply looked for other libraries until I found one that created output that the docks recognized. This libraries and another are identified in the software portion of this write-up.
Software
This was made infinitely more possible by two libraries that handle the heavy lifting:
- A Simple Arduino Bluetooth Music Receiver and Sender for the ESP32
Phil Schatzmann
https://github.com/pschatzmann/ESP32-A2DP
- IRLib2 A Library for Receiving, Decoding and Sending Infrared Signals Using Arduino
Cyborg5 Chris Young
https://github.com/cyborg5/IRLib2
The Future of the Past
This implementation is for only a few dock remote functions. Theoretically, it has a hard limit of implementing all IR remote functions. So be it. The constraints on the Zune ecosystem were known at the beginning. To that end, the project does have an IR decoder capability already wired up. The AVCRP also has limits. So be it. I got this project to do what I wanted. That means success.
But if you have another device to add BT to and an IR option to implement that devices function, then there are other ecosystems to explore. Good Luck and Safe Journey.
The Final Reveal


Wire-Wrap
Wire-wrap is supposed to not require soldering, but component posts are no longer sharp edged and squared off. As a result, I soldered the wrap wires. The future can be so cruel!
Zip Ties
Strain relief for cables and their sockets cannot be over stated as a good thing for DIY projects.
Software
ESP32 Console Code
/* Original Attribution */
/*
Streaming of sound data with Bluetooth to other Bluetooth device.
We generate 2 tones which will be sent to the 2 channels.
Copyright (C) 2020 Phil Schatzmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
- /
/* Added SSD1306 functionality and I2C functions for the IR sending arduino communication */
- include <Wire.h>
- include <Adafruit_GFX.h>
- include <Adafruit_SSD1306.h>
- include "AudioTools.h"
- include "AudioTools/AudioLibs/A2DPStream.h"
- define SCREEN_WIDTH 128 // OLED display width, in pixels
- define SCREEN_HEIGHT 64 // OLED display height, in pixels
bool display_active = false;
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
AudioInfo info24(44100, 2, 24);
BluetoothA2DPSource a2dp_source;
- define I2S_NUM I2S_NUM_0
- define I2S_BCK 14
- define I2S_WS 15
- define I2S_DATA_IN 32
- define I2S_MCK 0
// MD0 and MD1 left to float
// FMT set to HIGH
// Clean power need to prevent interference with signal
// The supported audio codec in ESP32 A2DP is SBC. SBC audio stream is encoded
// from PCM data normally formatted as 44.1kHz sampling rate, two-channel 16-bit sample data
I2SStream i2s;
- define SAMPLE_BUFFER_SIZE 128
/* Receives the two 24bits in 32bit packages from the PCM1808 and shifts the 16bit right */
/* The shifted values are then stored in the 16bit stereo Frame */
int32_t get_sound_data(Frame* data, int32_t frameCount)
{
struct Xrame
{
uint32_t left_sample;
uint32_t right_sample;
} xrame[SAMPLE_BUFFER_SIZE];
int bytesRead = i2s.readBytes((uint8_t*)xrame, sizeof(Xrame) * frameCount);
for (int i = 0; i < frameCount; ++i)
{
data[i].channel1 = xrame[i].left_sample>>16;
data[i].channel2 = xrame[i].right_sample>>16;
}
return frameCount;
}
bool isValid(const char* ssid, esp_bd_addr_t address, int rssi)
{
if (display_active)
{
display.clearDisplay();
display.setCursor(0,0);
display.print(ssid);
display.display();
}
Serial.print("available SSID: ");
Serial.println(ssid);
/* Test for automatically connecting to my BT speaker */
if (strcmp(ssid,"CMA3569") == 0)
return true;
else
return false;
}
void setup()
{
Wire.begin();
Serial.begin(115200);
Serial.println("Start"); ///Info
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C))
{
Serial.println(F("SSD1306 allocation failed"));
}
else
{
display_active = true;
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0,0);
display.display();
}
// I2S configuration
Serial.println("I2S Configure Data Loaded"); /// Info
auto cfg = i2s.defaultConfig(RX_MODE);
cfg.i2s_format = I2S_STD_FORMAT; // or try with I2S_LSB_FORMAT
cfg.copyFrom(info24);
cfg.pin_mck = I2S_MCK;
cfg.pin_bck = I2S_BCK;
cfg.pin_ws = I2S_WS;
cfg.pin_data = I2S_DATA_IN;
cfg.auto_clear = true;
// cfg.use_apll = true; // from your logic: not sure if this works with the new API?
i2s.begin(cfg);
/* Button mapping but no functions implemented */
pinMode(25,INPUT_PULLUP);
pinMode(26,INPUT_PULLUP);
pinMode(27,INPUT_PULLUP);
pinMode(33,INPUT_PULLUP);
Serial.println("I2S Activate"); /// Info
Serial.println("Activate BT Functions"); /// Info
a2dp_source.set_ssid_callback(isValid);
a2dp_source.set_auto_reconnect(false);
a2dp_source.set_data_callback_in_frames(get_sound_data);
a2dp_source.set_avrc_passthru_command_callback(button_handler);
a2dp_source.start("Quixote");
}
/* This is referring to the buttons on the BT speaker */
void button_handler(uint8_t id, bool isReleased)
{
int IRAddress = 0x55; // This is the address of the IR sending arduino
if (isReleased) {
Serial.print("button id ");
Serial.print(id);
Serial.println(" released");
if (id == 70)
{
Wire.beginTransmission(IRAddress); // Start communication with the slave
Wire.write(0); // Send the number 42
Wire.endTransmission(); // End the transmission
}
if (id == 68)
{
Wire.beginTransmission(IRAddress); // Start communication with the slave
Wire.write(0); // Send the number 42
Wire.endTransmission(); // End the transmission
}
if (id == 75)
{
Wire.beginTransmission(IRAddress); // Start communication with the slave
Wire.write(2); // Send the number 42
Wire.endTransmission(); // End the transmission
}
if (id == 76)
{
Wire.beginTransmission(IRAddress); // Start communication with the slave
Wire.write(1); // Send the number 42
Wire.endTransmission(); // End the transmission
}
}
}
void loop()
{
delay(1000);
}
My inspiring Bluetooth Speaker
