Created
September 29, 2019 17:46
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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