Wednesday, June 3, 2020

Wifi Volume Control

Mostly I play music on my home stereo system via Kodi and the Android Yatse app. Yatse lets me adjust the volume, but if I'm playing something on Youtube or Pandora or even Facebook, there is no way for me to remotely adjust the volume for my outside speakers. So I built a little volume controller and added it to my speaker controller. This turned out to be a fun little project.

First, a couple of pictures:

This shot shows the inside of my speaker controller. There are a lot of parts, I used an old computer case to hold everything together. My original speaker controller is in the middle with all the wires and the blue relays. The box on the right is another speaker switcher, which I kept as part of the system since it does impedance balancing that my controller doesn't do. At the very top are three individual volume knobs, one for each speaker zone around the house. These are manual knobs, there isn't a way to control them remotely. See the wires coming out of the Arduino and going around to the right under the black box? Those are the connections for the volume controller -- power, ground, data, and clock.

This picture shows the volume controller that I built. It's the little green circuit board on the right. It works as a "master" volume in that it adjusts the volume for all three zones at once. This turns out to be fine, it's really the same thing that Yatse does for Kodi, but now I can adjust the volume for other apps as well.


























Here's the layout for the volume controller:

What's missing here???
























I followed the schematic very closely, but I forgot that the speaker connections have TWO wires each, they all have a ground in addition to in/out. Also, the above has the ground from the PT2257 going to the wrong place. So this is a better picture:

This one has connections for the ground wires.
























I added the code to run this to the code for the speaker controller, so it's all in one place. Here are the parts that I added:

In the "html head" section, I added a little bit of javascript:

    server.println("<script>");
    server.println("function doit(){");
    server.println("document.getElementById(\"vF\").submit();}");
    server.println("</script>


Then in the main body of the html I added a slider to set the volume:

    server.print("</div><div class='container'><h3>Master Volume</h3><form id='vF' action='/' role='form'>");
    server.print("<input type='range' name='volume' min='0' max='79' value='");
    server.print(volume);
    server.print("' oninput='doit(value)'>");
    server.print("</form>");
    server.print("<form id='mF' action='/' role='form'>");
    server.print("<input type='submit' name='mute' value='Mute'/></form>


To handle the form data, I added these lines to the "handleRequest" method:

    if (request.indexOf("volume=") > -1) {
        int start = request.indexOf("volume=") + 7; // 7 is length of "volume="
        int end = request.indexOf(" ", start);
        volume = request.substring(start, end).toInt();
    } else {
        if (request.indexOf("mute") > -1) {
            volume = 0;
        }
    }



There are several libraries around for running the PT2257, but it's pretty simple and doesn't really need a whole library for what I want to do, so I only needed to add these few methods:

void setVolume(int level) {
    byte bbbaaaa = calculateVolume(level);
    byte aaaa = bbbaaaa & 0b00001111;
    byte bbb = (bbbaaaa >> 4) & 0b00001111;

    Wire.beginTransmission(EVC_ADDR);
    Wire.write(STEP_10 | bbb);
    Wire.write(STEP_1 | aaaa);
    Wire.endTransmission();
}

byte calculateVolume(int level) {
    if (level > 79)
        level = 79;
    if (level < 0)
        level = 0;
    level = 79 - level;     // invert the volume, so 0 is low and 79 is high
    uint8_t b = level / 10; // get the most significant digit (eg. 79 gets 7)
    uint8_t a = level % 10; // get the least significant digit (eg. 79 gets 9)
    b = b & 0b0000111;      // limit the most significant digit to 3 bit (7)
    return (b << 4) | a;    // return both numbers in one byte (0BBBAAAA)
}

void unmute() {
    byte value = MUTE_OFF;
    Wire.beginTransmission(EVC_ADDR);
    Wire.write(value);
    byte error = Wire.endTransmission();
}


Now the webpage looks like this on my phone:



Here is the complete code. I had to remove a few thing and optimize a bit as this barely fits in the memory of the Arduino.


/*
Simple web server to serve a form to turn on or off digital pins. In this case,
the pins are connected to relays to turn speakers on or off. This uses pins
2 through 9 in pairs, so 2 and 3 control the hot tub speakers, 4 and 5 control
the dining room speakers, and 6 and 7 control the portico speakers. 8 and 9 are
available in case I ever find a need to connect one more pair of speakers.
*/

#include <Ethernet.h>
#include <SPI.h>
#include <Wire.h>
#include <string.h>

// MAC address can be anything that is unique within the local network.
byte mac[] = {0x00, 0x1E, 0x2A, 0x76, 0x24, 0x08};

// Some unused IP address on the local network.
byte ip[] = {192, 168, 2, 251};

// web server, nothing fancy, just port 80 for http
EthernetServer server(80);

#define RELAY_ON 0
#define RELAY_OFF 1

// settings for the volume controller
#define EVC_ADDR 0x44       // volume control chip address
#define STEP_1 0b11010000   // 2-Channel, -1dB/step
#define STEP_10 0b11100000  // 2-Channel, -10dB/step
#define MUTE_OFF 0b01111000 // 2-Channel MUTE off

// true, just show speaker status or false, actually change the speaker states
boolean showStatus = false;

// which zones are on or off, initially, all are off
boolean zone1 = false;
boolean zone2 = false;
boolean zone3 = false;
boolean zone4 = false;

// default volume is 25 to start
int volume = 0;

// pin definition, one pin per speaker,
// so the left speaker in zone 1 is pin 2, etc
static const int zone1L = 2;
static const int zone1R = 3;
static const int zone2L = 4;
static const int zone2R = 5;
static const int zone3L = 6;
static const int zone3R = 7;
static const int zone4L = 8;
static const int zone4R = 9;

// set up pins, initially all speakers in all zones are off
void setupPins() {
    digitalWrite(zone1L, RELAY_OFF);
    digitalWrite(zone1R, RELAY_OFF);
    digitalWrite(zone2L, RELAY_OFF);
    digitalWrite(zone2R, RELAY_OFF);
    digitalWrite(zone3L, RELAY_OFF);
    digitalWrite(zone3R, RELAY_OFF);
    digitalWrite(zone4L, RELAY_OFF);
    digitalWrite(zone4R, RELAY_OFF);

    pinMode(zone1L, OUTPUT);
    pinMode(zone1R, OUTPUT);
    pinMode(zone2L, OUTPUT);
    pinMode(zone2R, OUTPUT);
    pinMode(zone3L, OUTPUT);
    pinMode(zone3R, OUTPUT);
    pinMode(zone4L, OUTPUT);
    pinMode(zone4R, OUTPUT);

    delay(2000);
}

void setup() {
    Ethernet.begin(mac, ip);
    server.begin();
    Wire.begin(); // join i2c bus
    setupPins();
    //Serial.begin(115200);
}

// set up buffer for reading web requests
static const int bufferMax = 128;
int bufferSize;
char buffer[bufferMax];

void loop() {
    EthernetClient client = server.available();
    if (client) {
        waitForRequest(client);
        handleRequest();
        client.stop();
    }
}

void waitForRequest(EthernetClient client) {
    bufferSize = 0;

    while (client.connected()) {
        if (client.available()) {
            char c = client.read();
            if (c == '\n') {
                break;
            } else {
                if (bufferSize < bufferMax) {
                    buffer[bufferSize++] = c;
                } else {
                    break;
                }
            }
        }
    }
}

void handleRequest() {
    // Received buffer contains a standard HTTP GET line, something like
    // "GET /?X=X&Y=Y HTTP/1.1".
    // Could have up to 4 parameters, 1=on&2=on&3=on&4=on,
    // one for each set of speakers. All that is necessary here is to
    // extract the query string and check for each of the zone numbers.
    String request = String(buffer);
    //int firstSpace = request.indexOf(" ");                // right after GET
    //int lastSpace = request.indexOf(" ", firstSpace + 1); // just after the query string
    //request = request.substring(firstSpace, lastSpace);
    //Serial.println(request);

    showStatus = request.indexOf("?") == -1;

    if (request.indexOf("1=on") > -1) {
        zone1 = true;
    } else if (request.indexOf("1=off") > -1) {
        zone1 = false;
    }
    if (request.indexOf("2=on") > -1) {
        zone2 = true;
    } else if (request.indexOf("2=off") > -1) {
        zone2 = false;
    }
    if (request.indexOf("3=on") > -1) {
        zone3 = true;
    } else if (request.indexOf("3=off") > -1) {
        zone3 = false;
    }
    if (request.indexOf("4=on") > -1) {
        zone4 = true;
    } else if (request.indexOf("4=off") > -1) {
        zone4 = false;
    }
    if (request.indexOf("volume=") > -1) {
        int start = request.indexOf("volume=") + 7; // 7 is length of "volume="
        int end = request.indexOf(" ", start);
        volume = request.substring(start, end).toInt();
    } else {
        if (request.indexOf("mute") > -1) {
            volume = 0;
        }
    }
    //Serial.println("Volume = " + String(volume));
    if (!showStatus) {
        setSpeakerState();
    }
    sendPage();
}

void setSpeakerState() {
    digitalWrite(zone1L, zone1 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone1R, zone1 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone2L, zone2 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone2R, zone2 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone3L, zone3 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone3R, zone3 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone4L, zone4 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone4R, zone4 ? RELAY_ON : RELAY_OFF);
    unmute();
    setVolume(volume);
}

void sendPage() {
    // http response header
    /*
    int length = 1442;
    if (digitalRead(zone4L) == RELAY_ON)
        length += 18;
    if (digitalRead(zone3L) == RELAY_ON)
        length += 18;
    if (digitalRead(zone2L) == RELAY_ON)
        length += 18;
    if (digitalRead(zone1L) == RELAY_ON)
        length += 18;
    */
    server.println("HTTP/1.1 200 OK");
    server.println("Content-Type: text/html");
    // server.print("Content-Length: ");
    // server.println(length);
    server.println();

    // html head
    server.println("<html><head><meta charset=\"UTF-8\">");
    server.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
    server.println("<link rel=\"stylesheet\" href=\"http://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css\">");
    server.println("<title>Speaker Control</title>");
    server.println("<script>");
    server.println("function doit(){");
    server.println("document.getElementById(\"vF\").submit();}");
    server.println("</script></head><body>");

    // html body
    server.println("<div class='container'><h2>Speaker Control</h2><form action='/' role='form'>");

    int pinState = digitalRead(zone4L);
    printCheckBox(4, pinState, "Hot tub");
    

    pinState = digitalRead(zone3L);
    printCheckBox(3, pinState, "Dining Room");

    pinState = digitalRead(zone2L);
    printCheckBox(2, pinState, "Portico");
    
    /* not used
    pinState = digitalRead(zone1L);
    printCheckBox(1, pinState, "(Empty)");
    */

    server.print("<br/><button type='submit'>Save</button></form></div>");

    server.print("</div><div class='container'><h3>Master Volume</h3><form id='vF' action='/' role='form'>");
    server.print("<input type='range' name='volume' min='0' max='79' value='");
    server.print(volume);
    server.print("' oninput='doit(value)'>");
    server.print("</form>");
    server.print("<form id='mF' action='/' role='form'>");
    server.print("<input type='submit' name='mute' value='Mute'/></form></div></body></html>");
}

void printCheckBox(int zone, int pinState, String label) {
    server.print("<br/><input type='hidden' name='");
    server.print(zone);
    server.print("' value='off'/><div class='checkbox'><label><input type='checkbox' name='");
    server.print(zone);
    server.print("' value='on'");
    server.print(pinState == RELAY_ON ? " checked='checked'" : "");
    server.print(">");
    server.print(label);
    server.print("</label></div>");
}

void setVolume(int level) {
    byte bbbaaaa = calculateVolume(level);
    byte aaaa = bbbaaaa & 0b00001111;
    byte bbb = (bbbaaaa >> 4) & 0b00001111;

    Wire.beginTransmission(EVC_ADDR);
    Wire.write(STEP_10 | bbb);
    Wire.write(STEP_1 | aaaa);
    Wire.endTransmission();
    //Serial.println("set volume to " + (79 - level));
}

byte calculateVolume(int level) {
    if (level > 79)
        level = 79;
    if (level < 0)
        level = 0;
    level = 79 - level;     // invert the volume, so 0 is low and 79 is high
    uint8_t b = level / 10; // get the most significant digit (eg. 79 gets 7)
    uint8_t a = level % 10; // get the least significant digit (eg. 79 gets 9)
    b = b & 0b0000111;      // limit the most significant digit to 3 bit (7)
    return (b << 4) | a;    // return both numbers in one byte (0BBBAAAA)
}

void unmute() {
    byte value = MUTE_OFF;
    Wire.beginTransmission(EVC_ADDR);
    Wire.write(value);
    byte error = Wire.endTransmission();
    //if (error)
    //    Serial.println("mute, error code = " + String(error));
}

No comments: