Skip to content

Instantly share code, notes, and snippets.

@undocumented-code
Created September 29, 2019 17:46
Show Gist options
  • Save undocumented-code/3194e0d02a75676f1bab1a4db92184da to your computer and use it in GitHub Desktop.
Save undocumented-code/3194e0d02a75676f1bab1a4db92184da to your computer and use it in GitHub Desktop.
Simplified version of Sven337's UDP Audio Streamer from ESP8266 to dockerized restreaming using FFServer
// https://perso.aquilenet.fr/~sven337/english/2016/07/14/DIY-wifi-baby-monitor.html
// Based on Sven337's Baby Monitor Code which in turn is based on
// Based on ESP_MCP3201_SPI
#include <SPI.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClient.h>
#include <WiFiUdp.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
WiFiUDP udp;
const int udp_recv_port = 45990; // for command&control
const int udp_target_port = 45990; // sound transfer
const IPAddress IP_target_device(10, 0, 0, 217);
const IPAddress IP_target_PC(192, 168, 0, 2);
IPAddress IP_target = IP_target_device;
// Pin definitions:
const int scePin = D8; //15; // SCE - Chip select
/* HW definition of alternate function:
static const uint8_t MOSI = 13; D7 on nodemcu
static const uint8_t MISO = 12; D6 on nodemcu
static const uint8_t SCK = 14; D5 on nodemcu
*/
/* Hardware:
MCP3201 Pin ---------------- ESP8266 Pin
- 1-VREF ---------------- 3,3V
- 2-IN+ ---------------- ANALOG SIGNAL +
- 3-IN- ---------------- ANALOG SIGNAL -
- 4-GND ---------------- GND
- 5-CS ----CS---------- GPIO15/CS (PIN 19)
- 6-Dout(MISO)----MISO-------- GPIO12/MISO (PIN 16)
- 7-CLK ----SCLK-------- GPIO14 (PIN 17)
- 8-VDD ---------------- 3.3V
*/
uint16_t adc_buf[2][700]; // ADC data buffer, double buffered
int current_adc_buf; // which data buffer is being used for the ADC (the other is being sent)
unsigned int adc_buf_pos; // position in the ADC data buffer
int send_samples_now; // flag to signal that a buffer is ready to be sent
#define SILENCE_EMA_WEIGHT 1024
#define ENVELOPE_EMA_WEIGHT 2
int32_t silence_value = 2048; // computed as an exponential moving average of the signal
uint16_t envelope_threshold = 150; // envelope threshold to trigger data sending
uint32_t send_sound_util = 0; // date until sound transmission ends after an envelope threshold has triggered sound transmission
int enable_highpass_filter = 0;
static inline void setDataBits(uint16_t bits) {
const uint32_t mask = ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO));
bits--;
SPI1U1 = ((SPI1U1 & mask) | ((bits << SPILMOSI) | (bits << SPILMISO)));
}
void spiBegin(void)
{
SPI.begin();
SPI.setDataMode(SPI_MODE0);
SPI.setBitOrder(MSBFIRST);
SPI.setClockDivider(SPI_CLOCK_DIV8);
SPI.setHwCs(1);
setDataBits(16);
}
#define ICACHE_RAM_ATTR __attribute__((section(".iram.text")))
/* SPI code based on the SPI library */
static inline ICACHE_RAM_ATTR uint16_t transfer16(void) {
union {
uint16_t val;
struct {
uint8_t lsb;
uint8_t msb;
};
} out;
// Transfer 16 bits at once, leaving HW CS low for the whole 16 bits
while(SPI1CMD & SPIBUSY) {}
SPI1W0 = 0;
SPI1CMD |= SPIBUSY;
while(SPI1CMD & SPIBUSY) {}
/* Follow MCP3201's datasheet: return value looks like this:
xxxBA987 65432101
We want
76543210 0000BA98
So swap the bytes, select 12 bits starting at bit 1, and shift right by one.
*/
out.val = SPI1W0 & 0xFFFF;
uint8_t tmp = out.msb;
out.msb = out.lsb;
out.lsb = tmp;
out.val &= (0x0FFF << 1);
out.val >>= 1;
return out.val;
}
void ICACHE_RAM_ATTR sample_isr(void)
{
uint16_t val;
// Read a sample from ADC
val = transfer16();
adc_buf[current_adc_buf][adc_buf_pos] = val & 0xFFF;
adc_buf_pos++;
// If the buffer is full, signal it's ready to be sent and switch to the other one
if (adc_buf_pos > sizeof(adc_buf[0])/sizeof(adc_buf[0][0])) {
adc_buf_pos = 0;
current_adc_buf = !current_adc_buf;
send_samples_now = 1;
}
}
void ota_onstart(void)
{
// Disable timer when an OTA happens
timer1_detachInterrupt();
timer1_disable();
}
void ota_onprogress(unsigned int sz, unsigned int total)
{
Serial.print("OTA: "); Serial.print(sz); Serial.print("/"); Serial.print(total);
Serial.print("="); Serial.print(100*sz/total); Serial.println("%%");
}
void ota_onerror(ota_error_t err)
{
Serial.print("OTA ERROR:"); Serial.println((int)err);
}
void setup(void)
{
Serial.begin(115200);
Serial.println("I was built on " __DATE__ " at " __TIME__ "");
WiFi.setOutputPower(10); // reduce power to 10dBm = 10mW
WiFi.mode(WIFI_STA);
WiFi.begin("WIFI_SSID_HERE", "WIFI_PASSWORD_HERE");
Serial.print("Connecting to wifi SSID ");
// Wait for connection
int now = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - now) < 10000) {
delay(500);
Serial.print(".");
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("failed to connect to home wifi");
while(1) {}
}
Serial.println ( "" );
Serial.print ( "Connected!" );
Serial.print ( "IP " );
Serial.println ( WiFi.localIP() );
ArduinoOTA.onStart(ota_onstart);
ArduinoOTA.onError(ota_onerror);
ArduinoOTA.onProgress(ota_onprogress);
ArduinoOTA.setHostname("bb-xmit");
ArduinoOTA.begin();
spiBegin();
timer1_isr_init();
timer1_attachInterrupt(sample_isr);
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);
timer1_write(clockCyclesPerMicrosecond() / 16 * 50); //50us = 20kHz sampling freq
Serial.println("setup done");
udp.begin(udp_recv_port);
}
#pragma GCC push_options
#pragma GCC optimize("O3")
uint8_t *delta7_sample(uint16_t last, uint16_t *readptr, uint8_t *writeptr) {
const uint8_t lowbyte1 = *((uint8_t *)readptr);
const uint8_t highbyte1 = *((uint8_t *)readptr+1);
const uint16_t val = *readptr;
const int32_t diff = val - last;
if (diff > -64 && diff < 64) {
// 7bit delta possible
// Encode the delta as "sign and magnitude" format.
// CSMMMMMM (compressed signed magnitude^6)
int8_t out = 0x80 | ((diff < 0) ? 0x40 : 0x0) | abs(diff);
*writeptr++ = out;
} else {
// 7bit delta impossible, output as-is
*writeptr++ = highbyte1;
*writeptr++ = lowbyte1;
}
return writeptr;
}
void loop() {
ArduinoOTA.handle();
if (send_samples_now) {
/* We're ready to send a buffer of samples over wifi. Decide if it has to happen or not,
that is, if the sound level is above a certain threshold. */
// Update silence and envelope computations
uint16_t number_of_samples = sizeof(adc_buf[0])/sizeof(adc_buf[0][0]);
int32_t accum_silence = 0;
int32_t envelope_value = 0;
int32_t now = millis();
uint8_t *writeptr = (uint8_t *)(&adc_buf[!current_adc_buf][0]);
uint16_t *readptr;
uint16_t last = 0;
for (unsigned int i = 0; i < number_of_samples; i++) {
readptr = &adc_buf[!current_adc_buf][i];
int32_t val = *readptr;
int32_t rectified;
if (enable_highpass_filter) {
*readptr = val + 2048;
val = *readptr;
}
rectified = abs(val - silence_value);
accum_silence += val;
envelope_value += rectified;
// delta7-compress the data
writeptr = delta7_sample(last, readptr, writeptr);
last = val;
}
accum_silence /= number_of_samples;
envelope_value /= number_of_samples;
silence_value = (SILENCE_EMA_WEIGHT * silence_value + accum_silence) / (SILENCE_EMA_WEIGHT + 1);
envelope_value = envelope_value;
send_sound_util = millis() + 15000;
if (millis() < send_sound_util) {
udp.beginPacket(IP_target, udp_target_port);
udp.write((const uint8_t *)(&adc_buf[!current_adc_buf][0]), writeptr - (uint8_t *)&adc_buf[!current_adc_buf][0]);
udp.endPacket();
}
send_samples_now = 0;
}
if (udp.parsePacket()) {
// Command and control packets
char buf[32];
char *ptr = &buf[0];
udp.read(&buf[0], 31);
buf[31] = 0;
#define MATCHSTR(X,Y) !strncmp(X, Y, strlen(Y))
udp.beginPacket(udp.remoteIP(), udp.remotePort());
if (MATCHSTR(buf, "target PC")) {
// Direct sound to PC
IP_target = IP_target_PC;
udp.print("target PC");
} else if (MATCHSTR(buf, "target dev")) {
// Direct sound to device
IP_target = IP_target_device;
udp.print("target dev");
} else {
udp.print("unknown command "); udp.println(buf);
}
udp.endPacket();
}
// If not sending anything, add a delay to enable modem sleep
if (millis() > send_sound_util) {
delay(10);
}
}
FROM alpine:latest
WORKDIR /app
COPY udpserver.c .
RUN apk add --no-cache gcc libc-dev
RUN gcc -Ofast udpserver.c -o udpserver
FROM jrottenberg/ffmpeg:3.4-alpine
COPY --from=0 /app/udpserver .
COPY ffserver.conf .
COPY entrypoint.sh .
RUN apk add --no-cache bash
RUN chmod +x entrypoint.sh
EXPOSE 8090
EXPOSE 45990
ENTRYPOINT ["/bin/bash", "-c", "./entrypoint.sh"]
#!/bin/bash
# Start the first process
ffserver -hide_banner -f /ffserver.conf &
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start ffmpeg: $status"
exit $status
fi
# Start the second process
ffmpeg -f s16le -ac 1 -ar 20k -i <(./udpserver) http://localhost:8090/main.ffm &
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start ffserver: $status"
exit $status
fi
# Naive check runs checks once a minute to see if either of the processes exited.
# This illustrates part of the heavy lifting you need to do if you want to run
# more than one service in a container. The container exits with an error
# if it detects that either of the processes has exited.
# Otherwise it loops forever, waking up every 60 seconds
while sleep 60; do
ps aux |grep ffmpeg |grep -q -v grep
PROCESS_1_STATUS=$?
ps aux |grep ffserver |grep -q -v grep
PROCESS_2_STATUS=$?
# If the greps above find anything, they exit with 0 status
# If they are not both 0, then something is wrong
if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
echo "One of the processes has already exited."
exit 1
fi
done
Port 8090
BindAddress 0.0.0.0
MaxHTTPConnections 2000
MaxClients 1000
MaxBandwidth 1000
CustomLog -
NoDaemon
<Feed main.ffm>
File /tmp/x.ffm
FileMaxSize 200k
ACL allow 127.0.0.1
</Feed>
<Stream main.ogg>
Feed main.ffm
AudioBitRate 64
AudioChannels 1
AudioSampleRate 20000
Novideo
</Stream>
<Stream stat.html>
Format status
</Stream>
/*
* udpserver.c - A simple UDP echo server
* https://perso.aquilenet.fr/~sven337/english/2016/07/14/DIY-wifi-baby-monitor.html
* Based on Sven337's Baby Monitor Code which in turn is based on
* usage: udpserver <port>
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFSIZE 2048
const int portno = 45990;
void append_sample(FILE *f, uint16_t sample) {
fwrite(&sample, 2, 1, f);
}
/*
* error - wrapper for perror
*/
void error(char *msg) {
perror(msg);
exit(1);
}
int do_undelta7(const uint8_t *val, int sz, uint16_t *out) {
// Implement delta 7 decompression.
// First bit = 0 <=> uncompressed 15 bits following
// First bit = 1 <=> 7 bits follow representing delta
// must switch to big endian...
uint16_t last = 0;
uint8_t *ptr = (uint8_t *)&out[0];
const uint8_t *start = ptr;
for (int i = 0; i < sz; i++) {
uint16_t *ptr16 = (uint16_t *)ptr;
const int8_t firstbyte = val[i];
if (firstbyte & 0x80) {
// Delta7 compressed
// byte is CSMMMMMM
int8_t delta = firstbyte & 0x3F;
if (firstbyte & 0x40) delta = -delta;
const uint16_t value = last + delta;
*ptr16 = value;
ptr += 2;
last = value;
} else {
// uncompressed -- switch bytes back to LE
*ptr++ = val[i+1];
*ptr++ = val[i];
last = val[i+1] | val[i] << 8;
i++;
}
}
return ptr - start;
}
int main(int argc, char **argv) {
int sockfd; /* socket */
int clientlen; /* byte size of client's address */
struct sockaddr_in serveraddr; /* server's addr */
struct sockaddr_in clientaddr; /* client addr */
struct hostent *hostp; /* client host info */
void* buf = malloc(BUFSIZE); /* message buf */
char *hostaddrp; /* dotted decimal host addr string */
int optval; /* flag value for setsockopt */
int n; /* message byte size */
/*
* socket: create the parent socket
*/
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) error("ERROR opening socket");
/* setsockopt: Handy debugging trick that lets
* us rerun the server immediately after we kill it;
* otherwise we have to wait about 20 secs.
* Eliminates "ERROR on binding: Address already in use" error.
*/
optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
/*
* build the server's Internet address
*/
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons((unsigned short)portno);
/*
* bind: associate the parent socket with a port
*/
if (bind(sockfd, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) < 0) error("ERROR on binding");
/*
* main loop: wait for a datagram, then echo it
*/
clientlen = sizeof(clientaddr);
while (1) {
/*
* recvfrom: receive a UDP datagram from a client
*/
bzero(buf, BUFSIZE);
n = recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *) &clientaddr, &clientlen);
if (n < 0) error("error in recvfrom");
uint16_t decompressed_buffer[n*2];
int sz = do_undelta7(buf, n, decompressed_buffer);
int i;
for (i = 0; i < sz/2; i++) {
uint16_t value = decompressed_buffer[i];
int16_t v = (value - 0x1000/2) << 4;
append_sample(stdout, v);
}
fflush(stdout);
}
free(buf);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment