diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f805e81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d47e45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 by bbx10node@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ab6b3c --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# SFX-I2S-web-trigger +ESP8266 Arduino Sound F/X I2S web trigger + +![ESP8266, I2S DAC, speaker, battery](/images/sfx-i2s.png) + +Press the 0 button to play file T0.wav, press the 1 button to play file T1.wav, +etc. The web interface is identical the related project +[SFX-web-trigger](https://github.com/bbx10/SFX-web-trigger). The other project +uses a much more expensive MP3/OGG decoder board. This project uses a $6 I2S +DAC with 3W audio amplifier board. The WAV audio files are stored in the ESP8266 +Flash. + +Most ESP-12s boards and modules come with 4 Mbytes of Flash of which 3 MBbytes is used as a Flash +file system named SPIFFS. To store the WAV files in the SPIFFS file system, +create a data directory in the same directory with the .INO file. Copy WAV +files to the directory with names T0.wav, T1.wav, ... T9.wav. Install the +[SPIFFS upload tool](http://www.esp8266.com/viewtopic.php?f=32&t=10081). Use +the tool to upload the WAV files to the ESP8266 Flash. + +The WAV files should be mono (number of channels = 1) and contain uncompressed +16-bit PCM audio. Samples rates 11025, 22050, and 44100 have been verified to +work. The included test WAV files were generated using Festival (text to +speech) software. + +Upload the ESPI2S.INO application. Connect any web browser to the ESP8266 web +server at http://espsfxtrigger.local. If this does not work use the ESP8266 +IP address. This should bring up the web page shown above. Press buttons +0...9 to play WAV files T0.wav ... T9.wav. + +If the ESP8266 is unable to connect to an Access Point, it becomes an Access +Point. Connect to it by going into your wireless settings. The AP name should +look like ESP\_SFX\_TRIGGER. Once connected the ESP will bring up a web page +where you can enter the SSID and password of your WiFi router. + +How might this device be used? Load up your favorite sound samples such as +elephant trumpet, minion laugh, cat meow, etc. Bury the device in the couch +cushions. Wait for your victim to sit down. While you appear to play Candy +Crush on your phone, remotely trigger your favorite sound F/X! + +## Hardware components + +* [Adafruit I2S 3W Class D Amplifier Breakout - MAX98357A](https://www.adafruit.com/products/3006) +This board converts the digital audio data from the ESP8266 I2S controller to +analog audio and amplifies the signal to drive a speaker. This breakout board is +mono instead of stereo but it is only $6! + +* [Adafruit Feather Huzzah with ESP8266](https://www.adafruit.com/products/2821) +This ESP8266 board was chosen because it has a lithium battery charger. Other +ESP8266 boards should also work. + +* 3 Watt, 4 Ohm speaker + +* Lithium battery + +## Connection Diagram + +Adafruit I2S DAC |ESP8266 | Description +-----------------|-------------------|------------- +LRC |GPIO2/TX1 LRCK | Left/Right audio +BCLK |GPIO15 BCLK | I2S Clock +DIN |GPIO03/RX0 DATA | I2S Data +GAIN |not connected | 9 dB gain +SD |not connected | Stereo average +GND |GND | Ground +Vin |BAT | 3.7V battery power + +If you need more volume you could use a booster to generate 5V from the battery +and drive the DAC board Vin with 5V. Also experiment with the GAIN pin. + +## Software components + +* [WiFiManager](https://github.com/tzapu/WiFiManager) eliminates +the need to store the WiFi SSID and password in the source code. WiFiManager +will switch the ESP8266 to an access point and web server to get the SSID and +password when needed. + +* [SPIFFS](http://esp8266.github.io/Arduino/versions/2.3.0/doc/filesystem.html) +The SPIFFS Flash file system is included with the ESP8266 Arduino board support +package. The header file name is FS.h. + +* ESP8266 I2S support is included with the ESP8266 Arduino board support package. +The header file names are i2s.h and i2s\_reg.h. + +* [WebSockets](https://github.com/Links2004/arduinoWebSockets) + +* Other components are included with the ESP8266 Arduino package. diff --git a/data/T0.wav b/data/T0.wav new file mode 100644 index 0000000..f2806c6 Binary files /dev/null and b/data/T0.wav differ diff --git a/data/T1.wav b/data/T1.wav new file mode 100644 index 0000000..cf93d62 Binary files /dev/null and b/data/T1.wav differ diff --git a/data/T2.wav b/data/T2.wav new file mode 100644 index 0000000..60aa3e9 Binary files /dev/null and b/data/T2.wav differ diff --git a/data/T3.wav b/data/T3.wav new file mode 100644 index 0000000..bdcb322 Binary files /dev/null and b/data/T3.wav differ diff --git a/data/T4.wav b/data/T4.wav new file mode 100644 index 0000000..b9fa88a Binary files /dev/null and b/data/T4.wav differ diff --git a/data/T5.wav b/data/T5.wav new file mode 100644 index 0000000..f592b6e Binary files /dev/null and b/data/T5.wav differ diff --git a/data/T6.wav b/data/T6.wav new file mode 100644 index 0000000..4f4b8cf Binary files /dev/null and b/data/T6.wav differ diff --git a/data/T7.wav b/data/T7.wav new file mode 100644 index 0000000..551f9cf Binary files /dev/null and b/data/T7.wav differ diff --git a/data/T8.wav b/data/T8.wav new file mode 100644 index 0000000..03e3d6a Binary files /dev/null and b/data/T8.wav differ diff --git a/data/T9.wav b/data/T9.wav new file mode 100644 index 0000000..80fb4ab Binary files /dev/null and b/data/T9.wav differ diff --git a/espi2s.ino b/espi2s.ino new file mode 100644 index 0000000..3d8a139 --- /dev/null +++ b/espi2s.ino @@ -0,0 +1,509 @@ +/***************************************************************************** + The MIT License (MIT) + + Copyright (c) 2016 by bbx10node@gmail.com + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + **************************************************************************/ +#include +#include +#include +#include // https://github.com/Links2004/arduinoWebSockets +#include + +#include +#include +#include "wavspiffs.h" + +// Non-blocking I2S write for left and right 16-bit PCM +bool ICACHE_FLASH_ATTR i2s_write_lr_nb(int16_t left, int16_t right){ + int sample = right & 0xFFFF; + sample = sample << 16; + sample |= left & 0xFFFF; + return i2s_write_sample_nb(sample); +} + +struct I2S_status_s { + wavFILE_t wf; + int16_t buffer[512]; + int bufferlen; + int buffer_index; + int playing; +} I2S_WAV; + +void wav_stopPlaying() +{ + i2s_end(); + I2S_WAV.playing = false; + wavClose(&I2S_WAV.wf); +} + +bool wav_playing() +{ + return I2S_WAV.playing; +} + +void wav_setup() +{ + Serial.println(F("wav_setup")); + I2S_WAV.bufferlen = -1; + I2S_WAV.buffer_index = 0; + I2S_WAV.playing = false; +} + +void wav_loop() +{ + bool i2s_full = false; + int rc; + + while (I2S_WAV.playing && !i2s_full) { + while (I2S_WAV.buffer_index < I2S_WAV.bufferlen) { + int16_t pcm = I2S_WAV.buffer[I2S_WAV.buffer_index]; + if (i2s_write_lr_nb(pcm, pcm)) { + I2S_WAV.buffer_index++; + } + else { + i2s_full = true; + break; + } + if ((I2S_WAV.buffer_index & 0x3F) == 0) yield(); + } + if (i2s_full) break; + + rc = wavRead(&I2S_WAV.wf, I2S_WAV.buffer, sizeof(I2S_WAV.buffer)); + if (rc > 0) { + //Serial.printf("wavRead %d\r\n", rc); + I2S_WAV.bufferlen = rc / sizeof(I2S_WAV.buffer[0]); + I2S_WAV.buffer_index = 0; + } + else { + Serial.println(F("Stop playing")); + wav_stopPlaying(); + break; + } + } +} + +void wav_startPlayingFile(const char *wavfilename) +{ + wavProperties_t wProps; + int rc; + + Serial.printf("wav_starPlayingFile(%s)\r\n", wavfilename); + i2s_begin(); + rc = wavOpen(wavfilename, &I2S_WAV.wf, &wProps); + Serial.printf("wavOpen %d\r\n", rc); + if (rc != 0) { + Serial.println("wavOpen failed"); + return; + } + Serial.printf("audioFormat %d\r\n", wProps.audioFormat); + Serial.printf("numChannels %d\r\n", wProps.numChannels); + Serial.printf("sampleRate %d\r\n", wProps.sampleRate); + Serial.printf("byteRate %d\r\n", wProps.byteRate); + Serial.printf("blockAlign %d\r\n", wProps.blockAlign); + Serial.printf("bitsPerSample %d\r\n", wProps.bitsPerSample); + + i2s_set_rate(wProps.sampleRate); + + I2S_WAV.bufferlen = -1; + I2S_WAV.buffer_index = 0; + I2S_WAV.playing = true; + wav_loop(); +} + +/**************************************************************************************/ + +/* + * index.html + */ +// This string holds HTML, CSS, and Javascript for the HTML5 UI. +// The browser must support HTML5 WebSockets which is true for all modern browsers. +static const char PROGMEM INDEX_HTML[] = R"rawliteral( + + + + + +ESP8266 Sound Effects Web Trigger + + + + +

ESP SF/X Web Trigger

+
Now Playing
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Track 0
Track 1
Track 2
Track 3
Track 4
Track 5
Track 6
Track 7
Track 8
Track 9
+

+ + +)rawliteral"; + +/* + * Web server and websocket server + */ +ESP8266WebServer server(80); +WebSocketsServer webSocket = WebSocketsServer(81); + +void startPlaying(const char *filename) +{ + char nowPlaying[80] = "nowPlaying="; + + wav_startPlayingFile(filename); + strncat(nowPlaying, filename, sizeof(nowPlaying)-strlen(nowPlaying)-1); + webSocket.broadcastTXT(nowPlaying); +} + +void update_browser() { + if (!wav_playing()) { + webSocket.broadcastTXT("nowPlaying="); + } +} + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + Serial.printf("[%u] Disconnected!\r\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\r\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + Serial.printf("[%u] get Text: %s\r\n", num, payload); + + // Looks for button press "bSFXn=1" messages where n='0'..'9' + if ((length == 7) && + (memcmp((const char *)payload, "bSFX", 4) == 0) && + (payload[6] == '1')) { + switch (payload[4]) { + case '0': + startPlaying("/T0.wav"); + break; + case '1': + startPlaying("/T1.wav"); + break; + case '2': + startPlaying("/T2.wav"); + break; + case '3': + startPlaying("/T3.wav"); + break; + case '4': + startPlaying("/T4.wav"); + break; + case '5': + startPlaying("/T5.wav"); + break; + case '6': + startPlaying("/T6.wav"); + break; + case '7': + startPlaying("/T7.wav"); + break; + case '8': + startPlaying("/T8.wav"); + break; + case '9': + startPlaying("/T9.wav"); + break; + } + } + else { + Serial.printf("Unknown message from client [%s]\r\n", payload); + } + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + Serial.printf("[%u] get binary length: %u\r\n", num, length); + hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + } +} + +void webserver_setup(void) +{ + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + Serial.println("websocket server started"); + + if(MDNS.begin("espsfxtrigger")) { + Serial.println("MDNS responder started. Connect to http://espsfxtrigger.local/"); + } + else { + Serial.println("MDNS responder failed"); + } + + // handle "/" + server.on("/", []() { + server.send_P(200, "text/html", INDEX_HTML); + }); + + server.begin(); + Serial.println("web server started"); + + // Add service to MDNS + MDNS.addService("http", "tcp", 80); + MDNS.addService("ws", "tcp", 81); +} + +inline void webserver_loop(void) { + webSocket.loop(); + server.handleClient(); + update_browser(); +} + +/**************************************************************************************/ + +/* + * WiFiManager (https://github.com/tzapu/WiFiManager) + */ +#include +#include +#include // https://github.com/tzapu/WiFiManager + +void wifiman_setup(void) +{ + //WiFiManager + //Local intialization. Once its business is done, there is no need to keep it around + WiFiManager wifiManager; + //reset saved settings SSID and password + //wifiManager.resetSettings(); + + //fetches ssid and pass from eeprom and tries to connect + //if it does not connect it starts an access point with the specified name + //here "AutoConnectAP" + //and goes into a blocking loop awaiting configuration + wifiManager.autoConnect("ESP_SFX_Trigger"); + + //if you get here you have connected to the WiFi + Serial.println(F("connected")); +} + +void showDir(void) +{ + wavFILE_t wFile; + wavProperties_t wProps; + int rc; + + Dir dir = SPIFFS.openDir("/"); + while (dir.next()) { + Serial.println(dir.fileName()); + rc = wavOpen(dir.fileName().c_str(), &wFile, &wProps); + if (rc == 0) { + Serial.printf(" audioFormat %d\r\n", wProps.audioFormat); + Serial.printf(" numChannels %d\r\n", wProps.numChannels); + Serial.printf(" sampleRate %d\r\n", wProps.sampleRate); + Serial.printf(" byteRate %d\r\n", wProps.byteRate); + Serial.printf(" blockAlign %d\r\n", wProps.blockAlign); + Serial.printf(" bitsPerSample %d\r\n", wProps.bitsPerSample); + Serial.println(); + wavClose(&wFile); + } + } +} + +void setup() +{ + Serial.begin(115200); Serial.println(); + Serial.println(F("\nESP8266 Sound Effects Web Trigger")); + + if (!SPIFFS.begin()) { + Serial.println("SPIFFS.begin() failed"); + return; + } + // Confirm track files are present in SPIFFS + showDir(); + + wifiman_setup(); + wav_setup(); + webserver_setup(); +} + +void loop() +{ + wav_loop(); + webserver_loop(); +} diff --git a/images/sfx-i2s.png b/images/sfx-i2s.png new file mode 100644 index 0000000..81eb39b Binary files /dev/null and b/images/sfx-i2s.png differ diff --git a/wavspiffs.cpp b/wavspiffs.cpp new file mode 100644 index 0000000..5374827 --- /dev/null +++ b/wavspiffs.cpp @@ -0,0 +1,92 @@ +/***************************************************************************** + The MIT License (MIT) + + Copyright (c) 2016 by bbx10node@gmail.com + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + **************************************************************************/ + +/* + * Read WAV/RIFF audio files. Parses header and returns properties. This is + * mostly designed for uncompressed PCM audio. The WAV files must be stored + * in the ESP8266 SPIFFS file system. + */ + +#include "wavspiffs.h" + +#define CCCC(c1, c2, c3, c4) ((c4 << 24) | (c3 << 16) | (c2 << 8) | c1) + +static int readuint32(wavFILE_t *wf, uint32_t *ui32) +{ + int rc; + + rc = wf->f.read((uint8_t *)ui32, sizeof(*ui32)); + //Serial.printf("readuint32 rc=%d val=0x%X\r\n", rc, *ui32); + return rc; +} + +int wavOpen(const char *wavname, wavFILE_t *wf, wavProperties_t *wavProps) +{ + typedef enum headerState_e { + HEADER_INIT, HEADER_RIFF, HEADER_FMT, HEADER_DATA + } headerState_t; + headerState_t state = HEADER_INIT; + uint32_t chunkID, chunkSize; + + wf->f = SPIFFS.open(wavname, "r"); + if (!wf->f) return -1; + Serial.println("SPIFFS.open ok"); + + while (state != HEADER_DATA) { + if (readuint32(wf, &chunkID) != 4) return -1; + if (readuint32(wf, &chunkSize) != 4) return -2; + switch (chunkID) { + case CCCC('R', 'I', 'F', 'F'): + if (readuint32(wf, &chunkID) != 4) return -3; + if (chunkID != CCCC('W', 'A', 'V', 'E')) return -4; + state = HEADER_RIFF; + break; + + case CCCC('f', 'm', 't', ' '): + if (wf->f.read((uint8_t *)wavProps, chunkSize) != chunkSize) return -5; + state = HEADER_FMT; + break; + + case CCCC('d', 'a', 't', 'a'): + state = HEADER_DATA; + break; + default: + if (!wf->f.seek(chunkSize, SeekCur)) return -6; + } + } + if (state == HEADER_DATA) return 0; + wf->f.close(); + return -7; +} + +int wavRead(wavFILE_t *wf, void *buffer, size_t buflen) +{ + return wf->f.read((uint8_t *)buffer, buflen); +} + +int wavClose(wavFILE_t *wf) +{ + wf->f.close(); + return 0; +} diff --git a/wavspiffs.h b/wavspiffs.h new file mode 100644 index 0000000..3b5bf3a --- /dev/null +++ b/wavspiffs.h @@ -0,0 +1,31 @@ +#ifndef __WAVFILE_H__ + +#include + +typedef struct wavFILE_s { + File f; +} wavFILE_t; + +typedef struct wavProperties_s { + uint16_t audioFormat; + uint16_t numChannels; + uint32_t sampleRate; + uint32_t byteRate; + uint16_t blockAlign; + uint16_t bitsPerSample; +} wavProperties_t; + +#ifdef __cplusplus +extern "C" { +#endif + +int wavOpen(const char *wavname, wavFILE_t *, wavProperties_t *wavProps); +int wavRead(wavFILE_t *wf, void *buffer, size_t buflen); +int wavClose(wavFILE_t *wf); + +#ifdef __cplusplus +} +#endif + +#endif +#define __WAVFILE_H__ 1