Making MDR-V6 wireless, the hard way

In late August, I saw @sophaskins' post about modding her MDR-7506 headphones, and I decided to similarly mod my MDR-V6. But before I got around to it, a new idea sprouted in my mind:

What if I also made them wireless?

A while back, I got an ESP32-A1S from a friend of mine (thank you feelfreelinux); It's a single-module solution to all of your ESP32 + I²S DAC/ADC problems. I didn't want to lock myself to a dedicated Bluetooth Audio chip solution, so an ESP32 was perfect for my needs - it will happily work as an A2DP sink, and I can hack tons of different functionalities in there as I please. An added bonus is the integrated DAC, which means that the whole thing is *smol*.

The first order of business was to make a prototype. I quickly hacked up something with a bunch of wires and alligator clips. It took me a while longer to find a proper library to support the DAC on my A1S module. It turns out that Espressif (or ai-thinker?) chose one DAC to integrate initially (AC101), and then mid-production they changed it a few times to parts that weren't fully drop-in compatible. This (and sparse documentation around the bare module) ate a few hours of my R&D time. But, finally, I had it. A working proto!

Picture 2 - "scratch the case idea, this is perfect"

First version of the FW was heavily based upon the output example from the ArduinoAudioKit library. As a test, and fully expecting it wouldn't work any well, I tried to pipe raw data through WiFi, stealing some code from one of the included ESP32 library examples for a TCP server:

// (…) // "dramatic" recreation of events void loop() { WiFiClient client = server.available(); if(client) { Serial.println("New Client."); while (client.connected()) { if (client.available()) { client.read(buffer, BUFFER_SIZE); kit.write(buffer, BUFFER_SIZE); } } client.stop(); Serial.println("Client Disconnected."); } }

If you've ever done anything related to audio streaming, you know how important is buffering - without it, the DAC can run out of data to play, which results in pops and crackles. Here, we technically have a buffer of BUFFER_SIZE (which was either 1024 or 2048 bytes), but it's not being used properly - `client.read()` is blocking, and so is `kit.write()`. That's bad, because it heavily depends on the whole operation to finish in a constant amount of time, which is *not* guaranteed in case of networks based on Ethernet and TCP/IP.

Anyways, that was a *really* bad idea. But bad ideas have never stopped me, so I continued.

On my PC, I used the following oneliner:

ffmpeg -re -f alsa -ac 2 -ar 48000 -i hw:Loopback,1,0 -f s16le -ar 44100 - | nc hp 1337

... along with the following .asoundrc config:

pcm.mix { type dmix ipc_key 1024 slave { pcm "hw:Loopback,0,0" rate 44100 } bindings { 0 0 1 1 } } pcm.softvol { type softvol slave { pcm "mix" } control { name "meow" card 1 } } pcm.!default { type plug slave.pcm "softvol" } ctl.!default { type hw card Loopback }

This takes the audio output, routes it through a loopback device, it gets captured with ffmpeg, which then resamples it and sends it out to the headphones with ncat. You don't actually need to use `ffmpeg` at all, but it makes it easier to apply audio effects.

And, to my amazement, it worked. Worse even, it worked quite well! Wait, what?

In my excitement, I grabbed a power bank, repurposed the USB->TTL stick as an impromptu voltage regulator and I went for a walk around my apt.

First impressions: less crackling than anticipated, great latency and good range. When it worked, it was a great experience!

Four stages of R&D grief

"I will need to implement A2DP streaming, but this sounds hard"

Bluetooth is - and I can't understate this - a *very* complicated piece of technology. In the beginning, it was designed to replace RS-232 serial ports.[citation needed but I'm too lazy to check] Later, it was extended in *multiple* ways, to do everything from enclosing IP packets, to streaming audio for use in mobile headsets. The latter one also had multiple revisions, each one allowing for better audio quality. Let's say that I wasn't looking forward to implementing all of that.

"Wait, WiFi streaming works better than it has any right to..?"

As I already wrote, I toyed around streaming raw TCP data a bit. After using it for a day or so, I came to the conclusion that it's surprisingly stable for something that really has no right to be (minimal buffers, fluctuating network latency, etc). I saw room for improvement, but I liked how easily I could it integrate with ALSA and all.

I touched grass

The most fun (and rapid) way of R&D is trying out the prototype in its intended use - no amount of theory can substitute a quick and violent reality check. In this case, the reality check was going out for a walk to get groceries. Some key points from that venture:

  1. I forgot to charge the battery fully. I was out for maybe 40 minutes, and my headphones managed to die before I got back. The final model needs a new Li-Po/Li-Ion.
  2. 1.4Mbps is too much for 2.4GHz WiFi in crowded areas. I assumed that my phone's hotspot would always perform at this rate or better, but this assumption was incorrect, hard. Some areas in Warsaw where I tested the headset have the 2.4GHz spectrum so overcrowded that I experienced audio dropouts that lasted up to a few seconds. I haven't checked the 5GHz band, but I assume the results would be similar.
  3. I should embrace my weirdness more, having a raw ESP32 strapped to my headphones with wires sticking out was *fun*! ^-^

"I found a library that does all of the work for me. Hooray?"

So, after getting back, I did more research into running an A2DP sink on an ESP32. It turns out that there's a library that does everything I needed. Anyhoo, blame THAT person for making this post significantly less technical :p

Drawing the rest of the f...ing owl

This post is a bit non-linear, but I couldn't find a way to better summarize my adventures. Alas, the build process went something like this:

Picture 3 - I started by disassembling the left headphone to do the base cordless mod. Picture 4, 5 - finished base mod Picture 6, 7, 8 - various states of PCB assembly; featuring: an onboard class-D amp which was later scrapped; ESP32-A1S is loud enough and less noisy Picture 9 - assembled PCB, running on my testbench in the hackerspace Picture 10 - case prototype Picture 11 - me, wearing the prototype; the white tape keeps the things inside from spontaneously falling out

"Finished" product

I put that in big quotes, because virtually all my projects just get to a certain point where they stagnate. Sometimes that's a level of "good enough", other times it's a level of "I wish it could be better, but effort". This one is a mixed bag - the case could be designed a bit better (it's being held onto the headband with zipties, and it's being held together with electrical tape), and I could improve the streaming quality further (currently, there's TCP over WiFi and A2DP streaming implemented; it would be Neat if I took some time to implement UDP streaming).

The current version has USB-C for charging, a satisfying lever on/off switch, and a knob for analog volume control. Inside, it packs a 950mAh Li-Po cell, which lasts for plenty of time (I wish I could tell you how long, but: I have short attention span). It's light enough for me not to notice that it's there - actually, it may be lighter than the cable that it replaced.

Picture 12, 13 - semi-finished product

It's a fun device! It's all I wanted from wireless headphones, and I love using them! ^w^

Thanks to Elia, aviumcaravan and Lili for proofreading this post!


Q/A

nyanpasu64 in the comments below asked: "Does the ESP32-A1S have enough output power to properly drive the headphones?"

Surprisingly, yes! I've been positively surprised with this - the original design has included a stereo amplifier, but the TDA chip I found at the hackerspace turned out to be too low-end, so I quickly scraped that idea.
The DAC is currently limited to 60% volume, and that's plenty loud (my benchmark was riding an electric scooter at around 25km/h; With the volume knob turned 3/4 of the way, the music is above the wind howling loudness).

Something to note is that the higher I set the internal DAC volume to, the more little crackles I hear when the music is off. If your (hypothetical) design doesn't incorporate a physical limiting potentiometer, it may be wise to wire up a pair of low pass + high pass filters on the DAC output.


Support me on ko-fi!

Comments:

nyanpasu64 at 21.11.2022, 16:14:03

Does the ESP32-A1S have enough output power to properly drive the headphones? https://github.com/phkehl/esp32-a1s-audio_hal seems to say the power amp lives on the ESP32 Audio Kit board, not the ESP32-A1S module you used. Though I don't actually know what happens when you make a DAC or op amp drive too much power, whether it overheats/dies or browns out or oscillates. All I've seen is a knockoff Xbox One controller from PowerA whose headphone port lacked bass response, presumably from power line sag.

Rose at 02.01.2023, 16:35:36

I feel like a simpler way to go around this is to use a pre-existing adapter like one for the ATH-M50x and just juryrig it by wires onto the headphones? Not sure but it sounds like an alright idea.

By commenting, you agree for the session cookie to be stored on your device ;p