diff --git a/.gitignore b/.gitignore index 75d2bfa7..40520377 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ libs/ /cdump.cmd /_* sdkconfig +*_history/ diff --git a/components/airplay/airplay_sink.c b/components/airplay/airplay_sink.c deleted file mode 100644 index 8566d08c..00000000 --- a/components/airplay/airplay_sink.c +++ /dev/null @@ -1,65 +0,0 @@ -#include -#include -#include -#include - -#include "mdns.h" -#include "nvs.h" -#include "tcpip_adapter.h" -#include "esp_log.h" -#include "esp_console.h" -#include "esp_pthread.h" -#include "esp_system.h" -#include "freertos/timers.h" -#include "airplay_sink.h" - -#include "trace.h" - -static const char * TAG = "platform"; -extern char current_namespace[]; - -void airplay_sink_init(void) { - const char *hostname; - char *airplay_name, sink_name[32] = CONFIG_AIRPLAY_NAME; - nvs_handle nvs; - - tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &hostname); - - //initialize mDNS - ESP_ERROR_CHECK( mdns_init() ); - ESP_ERROR_CHECK( mdns_hostname_set(hostname) ); - - //structure with TXT records - mdns_txt_item_t serviceTxtData[] = { - {"am", "esp32"}, - {"tp", "UDP"}, - {"sm","false"}, - {"sv","false"}, - {"ek","1"}, - {"et","0,1"}, - {"md","0,1,2"}, - {"cn","0,1"}, - {"ch","2"}, - {"ss","16"}, - {"sr","44100"}, - {"vn","3"}, - {"txtvers","1"}, - }; - - if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) { - size_t len = 31; - nvs_get_str(nvs, "airplay_sink_name", sink_name, &len); - nvs_close(nvs); - } - - // AirPlay wants mDNS name to be MAC@name - uint8_t mac[6]; - esp_read_mac(mac, ESP_MAC_WIFI_STA); - asprintf(&airplay_name, "%02X%02X%02X%02X%02X%02X@%s", mac[3], mac[4], mac[5], mac[3], mac[4], mac[5], sink_name); - - ESP_LOGI(TAG, "mdns hostname set to: [%s] with servicename %s", hostname, sink_name); - - //initialize service - ESP_ERROR_CHECK( mdns_service_add(airplay_name, "_raop", "_tcp", 6000, serviceTxtData, sizeof(serviceTxtData) / sizeof(mdns_txt_item_t)) ); - free(airplay_name); -} diff --git a/components/airplay/airplay_sink.h b/components/airplay/airplay_sink.h deleted file mode 100644 index d6ba163b..00000000 --- a/components/airplay/airplay_sink.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - This example code is in the Public Domain (or CC0 licensed, at your option.) - - Unless required by applicable law or agreed to in writing, this - software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. -*/ - -#ifndef __AIRPLAY_SINK_H__ -#define __AIRPLAY_SINK_H__ - -#include - -//typedef enum { BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE, - //BT_SINK_RATE, BT_SINK_VOLUME, } bt_sink_cmd_t; - -/** - * @brief init sink mode (need to be provided) - */ -void airplay_sink_init(void); - -#endif /* __AIRPLAY_SINK_H__*/ \ No newline at end of file diff --git a/components/driver_bt/bt_app_sink.c b/components/driver_bt/bt_app_sink.c index b2d3536c..3e8cd4c1 100644 --- a/components/driver_bt/bt_app_sink.c +++ b/components/driver_bt/bt_app_sink.c @@ -355,7 +355,7 @@ static void bt_av_hdl_avrc_tg_evt(uint16_t event, void *p_param) } } -void bt_sink_init(void (*cmd_cb)(bt_sink_cmd_t cmd, ...), void (*data_cb)(const uint8_t *data, uint32_t len)) +void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb) { esp_err_t err; diff --git a/components/driver_bt/bt_app_sink.h b/components/driver_bt/bt_app_sink.h index 9c84446e..b3c0d71b 100644 --- a/components/driver_bt/bt_app_sink.h +++ b/components/driver_bt/bt_app_sink.h @@ -13,10 +13,18 @@ typedef enum { BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE, BT_SINK_RATE, BT_SINK_VOLUME, } bt_sink_cmd_t; + +typedef void (*bt_cmd_cb_t)(bt_sink_cmd_t cmd, ...); +typedef void (*bt_data_cb_t)(const uint8_t *data, uint32_t len); /** * @brief init sink mode (need to be provided) */ -void bt_sink_init(void (*cmd_cb)(bt_sink_cmd_t cmd, ...), void (*data_cb)(const uint8_t *data, uint32_t len)); +void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb); + +/** + * @brief local command mode (stop, play, volume ...) + */ +void bt_sink_cmd(bt_sink_cmd_t event, ...); #endif /* __BT_APP_SINK_H__*/ diff --git a/components/raop/alac.c b/components/raop/alac.c new file mode 100644 index 00000000..a4423881 --- /dev/null +++ b/components/raop/alac.c @@ -0,0 +1,1135 @@ +/* + * ALAC (Apple Lossless Audio Codec) decoder + * Copyright (c) 2005 David Hammerton + * All rights reserved. + * + * This is the actual decoder. + * + * http://crazney.net/programs/itunes/alac.html + * + * 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. + * + */ + +#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +static const int host_bigendian = 0; +#else +static const int host_bigendian = 1; +#endif + + +#include +#include +#include +#include + +#include "alac.h" + +extern int _fprintf(FILE *file, ...); + +#define _Swap32(v) do { \ + v = (((v) & 0x000000FF) << 0x18) | \ + (((v) & 0x0000FF00) << 0x08) | \ + (((v) & 0x00FF0000) >> 0x08) | \ + (((v) & 0xFF000000) >> 0x18); } while(0) + +#define _Swap16(v) do { \ + v = (((v) & 0x00FF) << 0x08) | \ + (((v) & 0xFF00) >> 0x08); } while (0) + +struct {signed int x:24;} se_struct_24; +#define SignExtend24(val) (se_struct_24.x = val) + +void allocate_buffers(alac_file *alac) +{ + alac->predicterror_buffer_a = malloc(alac->setinfo_max_samples_per_frame * 4); + alac->predicterror_buffer_b = malloc(alac->setinfo_max_samples_per_frame * 4); + + alac->outputsamples_buffer_a = malloc(alac->setinfo_max_samples_per_frame * 4); + alac->outputsamples_buffer_b = malloc(alac->setinfo_max_samples_per_frame * 4); + + alac->uncompressed_bytes_buffer_a = malloc(alac->setinfo_max_samples_per_frame * 4); + alac->uncompressed_bytes_buffer_b = malloc(alac->setinfo_max_samples_per_frame * 4); +} + +void alac_set_info(alac_file *alac, char *inputbuffer) +{ + char *ptr = inputbuffer; + ptr += 4; /* size */ + ptr += 4; /* frma */ + ptr += 4; /* alac */ + ptr += 4; /* size */ + ptr += 4; /* alac */ + + ptr += 4; /* 0 ? */ + + alac->setinfo_max_samples_per_frame = *(uint32_t*)ptr; /* buffer size / 2 ? */ + if (!host_bigendian) + _Swap32(alac->setinfo_max_samples_per_frame); + ptr += 4; + alac->setinfo_7a = *(uint8_t*)ptr; + ptr += 1; + alac->setinfo_sample_size = *(uint8_t*)ptr; + ptr += 1; + alac->setinfo_rice_historymult = *(uint8_t*)ptr; + ptr += 1; + alac->setinfo_rice_initialhistory = *(uint8_t*)ptr; + ptr += 1; + alac->setinfo_rice_kmodifier = *(uint8_t*)ptr; + ptr += 1; + alac->setinfo_7f = *(uint8_t*)ptr; + ptr += 1; + alac->setinfo_80 = *(uint16_t*)ptr; + if (!host_bigendian) + _Swap16(alac->setinfo_80); + ptr += 2; + alac->setinfo_82 = *(uint32_t*)ptr; + if (!host_bigendian) + _Swap32(alac->setinfo_82); + ptr += 4; + alac->setinfo_86 = *(uint32_t*)ptr; + if (!host_bigendian) + _Swap32(alac->setinfo_86); + ptr += 4; + alac->setinfo_8a_rate = *(uint32_t*)ptr; + if (!host_bigendian) + _Swap32(alac->setinfo_8a_rate); + + allocate_buffers(alac); + +} + +/* stream reading */ + +/* supports reading 1 to 16 bits, in big endian format */ +static uint32_t readbits_16(alac_file *alac, int bits) +{ + uint32_t result; + int new_accumulator; + + result = (alac->input_buffer[0] << 16) | + (alac->input_buffer[1] << 8) | + (alac->input_buffer[2]); + + /* shift left by the number of bits we've already read, + * so that the top 'n' bits of the 24 bits we read will + * be the return bits */ + result = result << alac->input_buffer_bitaccumulator; + + result = result & 0x00ffffff; + + /* and then only want the top 'n' bits from that, where + * n is 'bits' */ + result = result >> (24 - bits); + + new_accumulator = (alac->input_buffer_bitaccumulator + bits); + + /* increase the buffer pointer if we've read over n bytes. */ + alac->input_buffer += (new_accumulator >> 3); + + /* and the remainder goes back into the bit accumulator */ + alac->input_buffer_bitaccumulator = (new_accumulator & 7); + + return result; +} + +/* supports reading 1 to 32 bits, in big endian format */ +static uint32_t readbits(alac_file *alac, int bits) +{ + int32_t result = 0; + + if (bits > 16) + { + bits -= 16; + result = readbits_16(alac, 16) << bits; + } + + result |= readbits_16(alac, bits); + + return result; +} + +/* reads a single bit */ +static int readbit(alac_file *alac) +{ + int result; + int new_accumulator; + + result = alac->input_buffer[0]; + + result = result << alac->input_buffer_bitaccumulator; + + result = result >> 7 & 1; + + new_accumulator = (alac->input_buffer_bitaccumulator + 1); + + alac->input_buffer += (new_accumulator / 8); + + alac->input_buffer_bitaccumulator = (new_accumulator % 8); + + return result; +} + +static void unreadbits(alac_file *alac, int bits) +{ + int new_accumulator = (alac->input_buffer_bitaccumulator - bits); + + alac->input_buffer += (new_accumulator >> 3); + + alac->input_buffer_bitaccumulator = (new_accumulator & 7); + if (alac->input_buffer_bitaccumulator < 0) + alac->input_buffer_bitaccumulator *= -1; +} + +/* various implementations of count_leading_zero: + * the first one is the original one, the simplest and most + * obvious for what it's doing. never use this. + * then there are the asm ones. fill in as necessary + * and finally an unrolled and optimised c version + * to fall back to + */ +#if 0 +/* hideously inefficient. could use a bitmask search, + * alternatively bsr on x86, + */ +static int count_leading_zeros(int32_t input) +{ + int i = 0; + while (!(0x80000000 & input) && i < 32) + { + i++; + input = input << 1; + } + return i; +} +#elif defined(__GNUC__) +/* for some reason the unrolled version (below) is + * actually faster than this. yay intel! + */ +static int count_leading_zeros(int input) +{ + return __builtin_clz(input); +} +#elif (defined(_MSC_VER) || defined (__BORLANDC__)) && defined(_M_IX86) +static int count_leading_zeros(int input) +{ + int output = 0; + if (!input) return 32; + __asm + { + mov eax, input; + mov edx, 0x1f; + bsr ecx, eax; + sub edx, ecx; + mov output, edx; + } + return output; +} +#else +#warning using generic count leading zeroes. You may wish to write one for your CPU / compiler +static int count_leading_zeros(int input) +{ + int output = 0; + int curbyte = 0; + + curbyte = input >> 24; + if (curbyte) goto found; + output += 8; + + curbyte = input >> 16; + if (curbyte & 0xff) goto found; + output += 8; + + curbyte = input >> 8; + if (curbyte & 0xff) goto found; + output += 8; + + curbyte = input; + if (curbyte & 0xff) goto found; + output += 8; + + return output; + +found: + if (!(curbyte & 0xf0)) + { + output += 4; + } + else + curbyte >>= 4; + + if (curbyte & 0x8) + return output; + if (curbyte & 0x4) + return output + 1; + if (curbyte & 0x2) + return output + 2; + if (curbyte & 0x1) + return output + 3; + + /* shouldn't get here: */ + return output + 4; +} +#endif + +#define RICE_THRESHOLD 8 // maximum number of bits for a rice prefix. + +static int32_t entropy_decode_value(alac_file* alac, + int readSampleSize, + int k, + int rice_kmodifier_mask) +{ + int32_t x = 0; // decoded value + + // read x, number of 1s before 0 represent the rice value. + while (x <= RICE_THRESHOLD && readbit(alac)) + { + x++; + } + + if (x > RICE_THRESHOLD) + { + // read the number from the bit stream (raw value) + int32_t value; + + value = readbits(alac, readSampleSize); + + // mask value + value &= (((uint32_t)0xffffffff) >> (32 - readSampleSize)); + + x = value; + } + else + { + if (k != 1) + { + int extraBits = readbits(alac, k); + + // x = x * (2^k - 1) + x *= (((1 << k) - 1) & rice_kmodifier_mask); + + if (extraBits > 1) + x += extraBits - 1; + else + unreadbits(alac, 1); + } + } + + return x; +} + +static void entropy_rice_decode(alac_file* alac, + int32_t* outputBuffer, + int outputSize, + int readSampleSize, + int rice_initialhistory, + int rice_kmodifier, + int rice_historymult, + int rice_kmodifier_mask) +{ + int outputCount; + int history = rice_initialhistory; + int signModifier = 0; + + for (outputCount = 0; outputCount < outputSize; outputCount++) + { + int32_t decodedValue; + int32_t finalValue; + int32_t k; + + k = 31 - rice_kmodifier - count_leading_zeros((history >> 9) + 3); + + if (k < 0) k += rice_kmodifier; + else k = rice_kmodifier; + + // note: don't use rice_kmodifier_mask here (set mask to 0xFFFFFFFF) + decodedValue = entropy_decode_value(alac, readSampleSize, k, 0xFFFFFFFF); + + decodedValue += signModifier; + finalValue = (decodedValue + 1) / 2; // inc by 1 and shift out sign bit + if (decodedValue & 1) // the sign is stored in the low bit + finalValue *= -1; + + outputBuffer[outputCount] = finalValue; + + signModifier = 0; + + // update history + history += (decodedValue * rice_historymult) + - ((history * rice_historymult) >> 9); + + if (decodedValue > 0xFFFF) + history = 0xFFFF; + + // special case, for compressed blocks of 0 + if ((history < 128) && (outputCount + 1 < outputSize)) + { + int32_t blockSize; + + signModifier = 1; + + k = count_leading_zeros(history) + ((history + 16) / 64) - 24; + + // note: blockSize is always 16bit + blockSize = entropy_decode_value(alac, 16, k, rice_kmodifier_mask); + + // got blockSize 0s + if (blockSize > 0) + { + memset(&outputBuffer[outputCount + 1], 0, blockSize * sizeof(*outputBuffer)); + outputCount += blockSize; + } + + if (blockSize > 0xFFFF) + signModifier = 0; + + history = 0; + } + } +} + +#define SIGN_EXTENDED32(val, bits) ((val << (32 - bits)) >> (32 - bits)) + +#define SIGN_ONLY(v) \ + ((v < 0) ? (-1) : \ + ((v > 0) ? (1) : \ + (0))) + +static void predictor_decompress_fir_adapt(int32_t *error_buffer, + int32_t *buffer_out, + int output_size, + int readsamplesize, + int16_t *predictor_coef_table, + int predictor_coef_num, + int predictor_quantitization) +{ + int i; + + /* first sample always copies */ + *buffer_out = *error_buffer; + + if (!predictor_coef_num) + { + if (output_size <= 1) return; + memcpy(buffer_out+1, error_buffer+1, (output_size-1) * 4); + return; + } + + if (predictor_coef_num == 0x1f) /* 11111 - max value of predictor_coef_num */ + { /* second-best case scenario for fir decompression, + * error describes a small difference from the previous sample only + */ + if (output_size <= 1) return; + for (i = 0; i < output_size - 1; i++) + { + int32_t prev_value; + int32_t error_value; + + prev_value = buffer_out[i]; + error_value = error_buffer[i+1]; + buffer_out[i+1] = SIGN_EXTENDED32((prev_value + error_value), readsamplesize); + } + return; + } + + /* read warm-up samples */ + if (predictor_coef_num > 0) + { + int i; + for (i = 0; i < predictor_coef_num; i++) + { + int32_t val; + + val = buffer_out[i] + error_buffer[i+1]; + + val = SIGN_EXTENDED32(val, readsamplesize); + + buffer_out[i+1] = val; + } + } + +#if 0 + /* 4 and 8 are very common cases (the only ones i've seen). these + * should be unrolled and optimised + */ + if (predictor_coef_num == 4) + { + /* FIXME: optimised general case */ + return; + } + + if (predictor_coef_table == 8) + { + /* FIXME: optimised general case */ + return; + } +#endif + + + /* general case */ + if (predictor_coef_num > 0) + { + for (i = predictor_coef_num + 1; + i < output_size; + i++) + { + int j; + int sum = 0; + int outval; + int error_val = error_buffer[i]; + + for (j = 0; j < predictor_coef_num; j++) + { + sum += (buffer_out[predictor_coef_num-j] - buffer_out[0]) * + predictor_coef_table[j]; + } + + outval = (1 << (predictor_quantitization-1)) + sum; + outval = outval >> predictor_quantitization; + outval = outval + buffer_out[0] + error_val; + outval = SIGN_EXTENDED32(outval, readsamplesize); + + buffer_out[predictor_coef_num+1] = outval; + + if (error_val > 0) + { + int predictor_num = predictor_coef_num - 1; + + while (predictor_num >= 0 && error_val > 0) + { + int val = buffer_out[0] - buffer_out[predictor_coef_num - predictor_num]; + int sign = SIGN_ONLY(val); + + predictor_coef_table[predictor_num] -= sign; + + val *= sign; /* absolute value */ + + error_val -= ((val >> predictor_quantitization) * + (predictor_coef_num - predictor_num)); + + predictor_num--; + } + } + else if (error_val < 0) + { + int predictor_num = predictor_coef_num - 1; + + while (predictor_num >= 0 && error_val < 0) + { + int val = buffer_out[0] - buffer_out[predictor_coef_num - predictor_num]; + int sign = - SIGN_ONLY(val); + + predictor_coef_table[predictor_num] -= sign; + + val *= sign; /* neg value */ + + error_val -= ((val >> predictor_quantitization) * + (predictor_coef_num - predictor_num)); + + predictor_num--; + } + } + + buffer_out++; + } + } +} + +static void deinterlace_16(int32_t *buffer_a, int32_t *buffer_b, + int16_t *buffer_out, + int numchannels, int numsamples, + uint8_t interlacing_shift, + uint8_t interlacing_leftweight) +{ + int i; + if (numsamples <= 0) return; + + /* weighted interlacing */ + if (interlacing_leftweight) + { + for (i = 0; i < numsamples; i++) + { + int32_t difference, midright; + int16_t left; + int16_t right; + + midright = buffer_a[i]; + difference = buffer_b[i]; + + + right = midright - ((difference * interlacing_leftweight) >> interlacing_shift); + left = right + difference; + + /* output is always little endian */ + if (host_bigendian) + { + _Swap16(left); + _Swap16(right); + } + + buffer_out[i*numchannels] = left; + buffer_out[i*numchannels + 1] = right; + } + + return; + } + + /* otherwise basic interlacing took place */ + for (i = 0; i < numsamples; i++) + { + int16_t left, right; + + left = buffer_a[i]; + right = buffer_b[i]; + + /* output is always little endian */ + if (host_bigendian) + { + _Swap16(left); + _Swap16(right); + } + + buffer_out[i*numchannels] = left; + buffer_out[i*numchannels + 1] = right; + } +} + +static void deinterlace_24(int32_t *buffer_a, int32_t *buffer_b, + int uncompressed_bytes, + int32_t *uncompressed_bytes_buffer_a, int32_t *uncompressed_bytes_buffer_b, + void *buffer_out, + int numchannels, int numsamples, + uint8_t interlacing_shift, + uint8_t interlacing_leftweight) +{ + int i; + if (numsamples <= 0) return; + + /* weighted interlacing */ + if (interlacing_leftweight) + { + for (i = 0; i < numsamples; i++) + { + int32_t difference, midright; + int32_t left; + int32_t right; + + midright = buffer_a[i]; + difference = buffer_b[i]; + + right = midright - ((difference * interlacing_leftweight) >> interlacing_shift); + left = right + difference; + + if (uncompressed_bytes) + { + uint32_t mask = ~(0xFFFFFFFF << (uncompressed_bytes * 8)); + left <<= (uncompressed_bytes * 8); + right <<= (uncompressed_bytes * 8); + + left |= uncompressed_bytes_buffer_a[i] & mask; + right |= uncompressed_bytes_buffer_b[i] & mask; + } + + ((uint8_t*)buffer_out)[i * numchannels * 3] = (left) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 1] = (left >> 8) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 2] = (left >> 16) & 0xFF; + + ((uint8_t*)buffer_out)[i * numchannels * 3 + 3] = (right) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 4] = (right >> 8) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 5] = (right >> 16) & 0xFF; + } + + return; + } + + /* otherwise basic interlacing took place */ + for (i = 0; i < numsamples; i++) + { + int32_t left, right; + + left = buffer_a[i]; + right = buffer_b[i]; + + if (uncompressed_bytes) + { + uint32_t mask = ~(0xFFFFFFFF << (uncompressed_bytes * 8)); + left <<= (uncompressed_bytes * 8); + right <<= (uncompressed_bytes * 8); + + left |= uncompressed_bytes_buffer_a[i] & mask; + right |= uncompressed_bytes_buffer_b[i] & mask; + } + + ((uint8_t*)buffer_out)[i * numchannels * 3] = (left) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 1] = (left >> 8) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 2] = (left >> 16) & 0xFF; + + ((uint8_t*)buffer_out)[i * numchannels * 3 + 3] = (right) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 4] = (right >> 8) & 0xFF; + ((uint8_t*)buffer_out)[i * numchannels * 3 + 5] = (right >> 16) & 0xFF; + + } + +} + +void decode_frame(alac_file *alac, + unsigned char *inbuffer, + void *outbuffer, int *outputsize) +{ + int channels; + int32_t outputsamples = alac->setinfo_max_samples_per_frame; + + /* setup the stream */ + alac->input_buffer = inbuffer; + alac->input_buffer_bitaccumulator = 0; + + channels = readbits(alac, 3); + + *outputsize = outputsamples * alac->bytespersample; + + switch(channels) + { + case 0: /* 1 channel */ + { + int hassize; + int isnotcompressed; + int readsamplesize; + + int uncompressed_bytes; + int ricemodifier; + + /* 2^result = something to do with output waiting. + * perhaps matters if we read > 1 frame in a pass? + */ + readbits(alac, 4); + + readbits(alac, 12); /* unknown, skip 12 bits */ + + hassize = readbits(alac, 1); /* the output sample size is stored soon */ + + uncompressed_bytes = readbits(alac, 2); /* number of bytes in the (compressed) stream that are not compressed */ + + isnotcompressed = readbits(alac, 1); /* whether the frame is compressed */ + + if (hassize) + { + /* now read the number of samples, + * as a 32bit integer */ + outputsamples = readbits(alac, 32); + *outputsize = outputsamples * alac->bytespersample; + } + + readsamplesize = alac->setinfo_sample_size - (uncompressed_bytes * 8); + + if (!isnotcompressed) + { /* so it is compressed */ + int16_t predictor_coef_table[32]; + int predictor_coef_num; + int prediction_type; + int prediction_quantitization; + int i; + + /* skip 16 bits, not sure what they are. seem to be used in + * two channel case */ + readbits(alac, 8); + readbits(alac, 8); + + prediction_type = readbits(alac, 4); + prediction_quantitization = readbits(alac, 4); + + ricemodifier = readbits(alac, 3); + predictor_coef_num = readbits(alac, 5); + + /* read the predictor table */ + for (i = 0; i < predictor_coef_num; i++) + { + predictor_coef_table[i] = (int16_t)readbits(alac, 16); + } + + if (uncompressed_bytes) + { + int i; + for (i = 0; i < outputsamples; i++) + { + alac->uncompressed_bytes_buffer_a[i] = readbits(alac, uncompressed_bytes * 8); + } + } + + entropy_rice_decode(alac, + alac->predicterror_buffer_a, + outputsamples, + readsamplesize, + alac->setinfo_rice_initialhistory, + alac->setinfo_rice_kmodifier, + ricemodifier * alac->setinfo_rice_historymult / 4, + (1 << alac->setinfo_rice_kmodifier) - 1); + + if (prediction_type == 0) + { /* adaptive fir */ + predictor_decompress_fir_adapt(alac->predicterror_buffer_a, + alac->outputsamples_buffer_a, + outputsamples, + readsamplesize, + predictor_coef_table, + predictor_coef_num, + prediction_quantitization); + } + else + { + _fprintf(stderr, "FIXME: unhandled predicition type: %i\n", prediction_type); + /* i think the only other prediction type (or perhaps this is just a + * boolean?) runs adaptive fir twice.. like: + * predictor_decompress_fir_adapt(predictor_error, tempout, ...) + * predictor_decompress_fir_adapt(predictor_error, outputsamples ...) + * little strange.. + */ + } + + } + else + { /* not compressed, easy case */ + if (alac->setinfo_sample_size <= 16) + { + int i; + for (i = 0; i < outputsamples; i++) + { + int32_t audiobits = readbits(alac, alac->setinfo_sample_size); + + audiobits = SIGN_EXTENDED32(audiobits, alac->setinfo_sample_size); + + alac->outputsamples_buffer_a[i] = audiobits; + } + } + else + { + int i; + for (i = 0; i < outputsamples; i++) + { + int32_t audiobits; + + audiobits = readbits(alac, 16); + /* special case of sign extension.. + * as we'll be ORing the low 16bits into this */ + audiobits = audiobits << (alac->setinfo_sample_size - 16); + audiobits |= readbits(alac, alac->setinfo_sample_size - 16); + audiobits = SignExtend24(audiobits); + + alac->outputsamples_buffer_a[i] = audiobits; + } + } + uncompressed_bytes = 0; // always 0 for uncompressed + } + + switch(alac->setinfo_sample_size) + { + case 16: + { + int i; + for (i = 0; i < outputsamples; i++) + { + int16_t sample = alac->outputsamples_buffer_a[i]; + if (host_bigendian) + _Swap16(sample); + ((int16_t*)outbuffer)[i * alac->numchannels] = sample; + } + break; + } + case 24: + { + int i; + for (i = 0; i < outputsamples; i++) + { + int32_t sample = alac->outputsamples_buffer_a[i]; + + if (uncompressed_bytes) + { + uint32_t mask; + sample = sample << (uncompressed_bytes * 8); + mask = ~(0xFFFFFFFF << (uncompressed_bytes * 8)); + sample |= alac->uncompressed_bytes_buffer_a[i] & mask; + } + + ((uint8_t*)outbuffer)[i * alac->numchannels * 3] = (sample) & 0xFF; + ((uint8_t*)outbuffer)[i * alac->numchannels * 3 + 1] = (sample >> 8) & 0xFF; + ((uint8_t*)outbuffer)[i * alac->numchannels * 3 + 2] = (sample >> 16) & 0xFF; + } + break; + } + case 20: + case 32: + _fprintf(stderr, "FIXME: unimplemented sample size %i\n", alac->setinfo_sample_size); + break; + default: + break; + } + break; + } + case 1: /* 2 channels */ + { + int hassize; + int isnotcompressed; + int readsamplesize; + + int uncompressed_bytes; + + uint8_t interlacing_shift; + uint8_t interlacing_leftweight; + + /* 2^result = something to do with output waiting. + * perhaps matters if we read > 1 frame in a pass? + */ + readbits(alac, 4); + + readbits(alac, 12); /* unknown, skip 12 bits */ + + hassize = readbits(alac, 1); /* the output sample size is stored soon */ + + uncompressed_bytes = readbits(alac, 2); /* the number of bytes in the (compressed) stream that are not compressed */ + + isnotcompressed = readbits(alac, 1); /* whether the frame is compressed */ + + if (hassize) + { + /* now read the number of samples, + * as a 32bit integer */ + outputsamples = readbits(alac, 32); + *outputsize = outputsamples * alac->bytespersample; + } + + readsamplesize = alac->setinfo_sample_size - (uncompressed_bytes * 8) + 1; + + if (!isnotcompressed) + { /* compressed */ + int16_t predictor_coef_table_a[32]; + int predictor_coef_num_a; + int prediction_type_a; + int prediction_quantitization_a; + int ricemodifier_a; + + int16_t predictor_coef_table_b[32]; + int predictor_coef_num_b; + int prediction_type_b; + int prediction_quantitization_b; + int ricemodifier_b; + + int i; + + interlacing_shift = readbits(alac, 8); + interlacing_leftweight = readbits(alac, 8); + + /******** channel 1 ***********/ + prediction_type_a = readbits(alac, 4); + prediction_quantitization_a = readbits(alac, 4); + + ricemodifier_a = readbits(alac, 3); + predictor_coef_num_a = readbits(alac, 5); + + /* read the predictor table */ + for (i = 0; i < predictor_coef_num_a; i++) + { + predictor_coef_table_a[i] = (int16_t)readbits(alac, 16); + } + + /******** channel 2 *********/ + prediction_type_b = readbits(alac, 4); + prediction_quantitization_b = readbits(alac, 4); + + ricemodifier_b = readbits(alac, 3); + predictor_coef_num_b = readbits(alac, 5); + + /* read the predictor table */ + for (i = 0; i < predictor_coef_num_b; i++) + { + predictor_coef_table_b[i] = (int16_t)readbits(alac, 16); + } + + /*********************/ + if (uncompressed_bytes) + { /* see mono case */ + int i; + for (i = 0; i < outputsamples; i++) + { + alac->uncompressed_bytes_buffer_a[i] = readbits(alac, uncompressed_bytes * 8); + alac->uncompressed_bytes_buffer_b[i] = readbits(alac, uncompressed_bytes * 8); + } + } + + /* channel 1 */ + entropy_rice_decode(alac, + alac->predicterror_buffer_a, + outputsamples, + readsamplesize, + alac->setinfo_rice_initialhistory, + alac->setinfo_rice_kmodifier, + ricemodifier_a * alac->setinfo_rice_historymult / 4, + (1 << alac->setinfo_rice_kmodifier) - 1); + + if (prediction_type_a == 0) + { /* adaptive fir */ + predictor_decompress_fir_adapt(alac->predicterror_buffer_a, + alac->outputsamples_buffer_a, + outputsamples, + readsamplesize, + predictor_coef_table_a, + predictor_coef_num_a, + prediction_quantitization_a); + } + else + { /* see mono case */ + _fprintf(stderr, "FIXME: unhandled predicition type: %i\n", prediction_type_a); + } + + /* channel 2 */ + entropy_rice_decode(alac, + alac->predicterror_buffer_b, + outputsamples, + readsamplesize, + alac->setinfo_rice_initialhistory, + alac->setinfo_rice_kmodifier, + ricemodifier_b * alac->setinfo_rice_historymult / 4, + (1 << alac->setinfo_rice_kmodifier) - 1); + + if (prediction_type_b == 0) + { /* adaptive fir */ + predictor_decompress_fir_adapt(alac->predicterror_buffer_b, + alac->outputsamples_buffer_b, + outputsamples, + readsamplesize, + predictor_coef_table_b, + predictor_coef_num_b, + prediction_quantitization_b); + } + else + { + _fprintf(stderr, "FIXME: unhandled predicition type: %i\n", prediction_type_b); + } + } + else + { /* not compressed, easy case */ + if (alac->setinfo_sample_size <= 16) + { + int i; + for (i = 0; i < outputsamples; i++) + { + int32_t audiobits_a, audiobits_b; + + audiobits_a = readbits(alac, alac->setinfo_sample_size); + audiobits_b = readbits(alac, alac->setinfo_sample_size); + + audiobits_a = SIGN_EXTENDED32(audiobits_a, alac->setinfo_sample_size); + audiobits_b = SIGN_EXTENDED32(audiobits_b, alac->setinfo_sample_size); + + alac->outputsamples_buffer_a[i] = audiobits_a; + alac->outputsamples_buffer_b[i] = audiobits_b; + } + } + else + { + int i; + for (i = 0; i < outputsamples; i++) + { + int32_t audiobits_a, audiobits_b; + + audiobits_a = readbits(alac, 16); + audiobits_a = audiobits_a << (alac->setinfo_sample_size - 16); + audiobits_a |= readbits(alac, alac->setinfo_sample_size - 16); + audiobits_a = SignExtend24(audiobits_a); + + audiobits_b = readbits(alac, 16); + audiobits_b = audiobits_b << (alac->setinfo_sample_size - 16); + audiobits_b |= readbits(alac, alac->setinfo_sample_size - 16); + audiobits_b = SignExtend24(audiobits_b); + + alac->outputsamples_buffer_a[i] = audiobits_a; + alac->outputsamples_buffer_b[i] = audiobits_b; + } + } + uncompressed_bytes = 0; // always 0 for uncompressed + interlacing_shift = 0; + interlacing_leftweight = 0; + } + + switch(alac->setinfo_sample_size) + { + case 16: + { + deinterlace_16(alac->outputsamples_buffer_a, + alac->outputsamples_buffer_b, + (int16_t*)outbuffer, + alac->numchannels, + outputsamples, + interlacing_shift, + interlacing_leftweight); + break; + } + case 24: + { + deinterlace_24(alac->outputsamples_buffer_a, + alac->outputsamples_buffer_b, + uncompressed_bytes, + alac->uncompressed_bytes_buffer_a, + alac->uncompressed_bytes_buffer_b, + (int16_t*)outbuffer, + alac->numchannels, + outputsamples, + interlacing_shift, + interlacing_leftweight); + break; + } + case 20: + case 32: + _fprintf(stderr, "FIXME: unimplemented sample size %i\n", alac->setinfo_sample_size); + break; + default: + break; + } + + break; + } + } +} + +alac_file *create_alac(int samplesize, int numchannels) +{ + alac_file *newfile = malloc(sizeof(alac_file)); + + newfile->samplesize = samplesize; + newfile->numchannels = numchannels; + newfile->bytespersample = (samplesize / 8) * numchannels; + + return newfile; +} + +void delete_alac(alac_file *alac) +{ + free(alac->predicterror_buffer_a); + free(alac->predicterror_buffer_b); + + free(alac->outputsamples_buffer_a); + free(alac->outputsamples_buffer_b); + + free(alac->uncompressed_bytes_buffer_a); + free(alac->uncompressed_bytes_buffer_b); + + free(alac); +} + + diff --git a/components/raop/alac.h b/components/raop/alac.h new file mode 100644 index 00000000..382b4286 --- /dev/null +++ b/components/raop/alac.h @@ -0,0 +1,55 @@ +#ifndef __ALAC__DECOMP_H +#define __ALAC__DECOMP_H + +typedef struct alac_file alac_file; + +alac_file *create_alac(int samplesize, int numchannels); +void delete_alac(alac_file *alac); +void decode_frame(alac_file *alac, + unsigned char *inbuffer, + void *outbuffer, int *outputsize); +void alac_set_info(alac_file *alac, char *inputbuffer); +void allocate_buffers(alac_file *alac); + +struct alac_file +{ + unsigned char *input_buffer; + int input_buffer_bitaccumulator; /* used so we can do arbitary + bit reads */ + + int samplesize; + int numchannels; + int bytespersample; + + + /* buffers */ + int32_t *predicterror_buffer_a; + int32_t *predicterror_buffer_b; + + int32_t *outputsamples_buffer_a; + int32_t *outputsamples_buffer_b; + + int32_t *uncompressed_bytes_buffer_a; + int32_t *uncompressed_bytes_buffer_b; + + + + /* stuff from setinfo */ + uint32_t setinfo_max_samples_per_frame; /* 0x1000 = 4096 */ /* max samples per frame? */ + uint8_t setinfo_7a; /* 0x00 */ + uint8_t setinfo_sample_size; /* 0x10 */ + uint8_t setinfo_rice_historymult; /* 0x28 */ + uint8_t setinfo_rice_initialhistory; /* 0x0a */ + uint8_t setinfo_rice_kmodifier; /* 0x0e */ + uint8_t setinfo_7f; /* 0x02 */ + uint16_t setinfo_80; /* 0x00ff */ + uint32_t setinfo_82; /* 0x000020e7 */ /* max sample size?? */ + uint32_t setinfo_86; /* 0x00069fe4 */ /* bit rate (avarge)?? */ + uint32_t setinfo_8a_rate; /* 0x0000ac44 */ + /* end setinfo stuff */ + +}; + + +#endif /* __ALAC__DECOMP_H */ + diff --git a/components/airplay/component.mk b/components/raop/component.mk similarity index 77% rename from components/airplay/component.mk rename to components/raop/component.mk index 06578f47..9da56926 100644 --- a/components/airplay/component.mk +++ b/components/raop/component.mk @@ -7,4 +7,7 @@ # please read the SDK documents if you need to do this. # -CFLAGS += -I$(COMPONENT_PATH)/../tools +CFLAGS += -fstack-usage \ + -I$(COMPONENT_PATH)/../tools \ + -I$(COMPONENT_PATH)/../codecs/inc/alac + diff --git a/components/raop/dmap_parser.c b/components/raop/dmap_parser.c new file mode 100644 index 00000000..d0780ed4 --- /dev/null +++ b/components/raop/dmap_parser.c @@ -0,0 +1,545 @@ +#include "dmap_parser.h" +#include +#include +#include +#include + +#define DMAP_STRINGIFY_(x) #x +#define DMAP_STRINGIFY(x) DMAP_STRINGIFY_(x) + +typedef enum { + DMAP_UNKNOWN, + DMAP_UINT, + DMAP_INT, + DMAP_STR, + DMAP_DATA, + DMAP_DATE, + DMAP_VERS, + DMAP_DICT, + DMAP_ITEM +} DMAP_TYPE; + +typedef struct { + /** + * The four-character code used in the encoded message. + */ + const char *code; + + /** + * The type of data associated with the content code. + */ + DMAP_TYPE type; + + /** + * For listings, the type of their listing item children. + * + * Listing items (mlit) can be of any type, and as with other content codes + * their type information is not encoded in the message. Parsers must + * determine the type of the listing items based on their parent context. + */ + DMAP_TYPE list_item_type; + + /** + * A human-readable name for the content code. + */ + const char *name; +} dmap_field; + +static const dmap_field dmap_fields[] = { + { "abal", DMAP_DICT, DMAP_STR, "daap.browsealbumlisting" }, + { "abar", DMAP_DICT, DMAP_STR, "daap.browseartistlisting" }, + { "abcp", DMAP_DICT, DMAP_STR, "daap.browsecomposerlisting" }, + { "abgn", DMAP_DICT, DMAP_STR, "daap.browsegenrelisting" }, + { "abpl", DMAP_UINT, 0, "daap.baseplaylist" }, + { "abro", DMAP_DICT, 0, "daap.databasebrowse" }, + { "adbs", DMAP_DICT, 0, "daap.databasesongs" }, + { "aeAD", DMAP_DICT, 0, "com.apple.itunes.adam-ids-array" }, + { "aeAI", DMAP_UINT, 0, "com.apple.itunes.itms-artistid" }, + { "aeCD", DMAP_DATA, 0, "com.apple.itunes.flat-chapter-data" }, + { "aeCF", DMAP_UINT, 0, "com.apple.itunes.cloud-flavor-id" }, + { "aeCI", DMAP_UINT, 0, "com.apple.itunes.itms-composerid" }, + { "aeCK", DMAP_UINT, 0, "com.apple.itunes.cloud-library-kind" }, + { "aeCM", DMAP_UINT, 0, "com.apple.itunes.cloud-match-type" }, + { "aeCR", DMAP_STR, 0, "com.apple.itunes.content-rating" } , + { "aeCS", DMAP_UINT, 0, "com.apple.itunes.artworkchecksum" }, + { "aeCU", DMAP_UINT, 0, "com.apple.itunes.cloud-user-id" }, + { "aeCd", DMAP_UINT, 0, "com.apple.itunes.cloud-id" }, + { "aeDE", DMAP_STR, 0, "com.apple.itunes.longest-content-description" }, + { "aeDL", DMAP_UINT, 0, "com.apple.itunes.drm-downloader-user-id" }, + { "aeDP", DMAP_UINT, 0, "com.apple.itunes.drm-platform-id" }, + { "aeDR", DMAP_UINT, 0, "com.apple.itunes.drm-user-id" }, + { "aeDV", DMAP_UINT, 0, "com.apple.itunes.drm-versions" }, + { "aeEN", DMAP_STR, 0, "com.apple.itunes.episode-num-str" }, + { "aeES", DMAP_UINT, 0, "com.apple.itunes.episode-sort" }, + { "aeFA", DMAP_UINT, 0, "com.apple.itunes.drm-family-id" }, + { "aeGD", DMAP_UINT, 0, "com.apple.itunes.gapless-enc-dr" } , + { "aeGE", DMAP_UINT, 0, "com.apple.itunes.gapless-enc-del" }, + { "aeGH", DMAP_UINT, 0, "com.apple.itunes.gapless-heur" }, + { "aeGI", DMAP_UINT, 0, "com.apple.itunes.itms-genreid" }, + { "aeGR", DMAP_UINT, 0, "com.apple.itunes.gapless-resy" }, + { "aeGU", DMAP_UINT, 0, "com.apple.itunes.gapless-dur" }, + { "aeGs", DMAP_UINT, 0, "com.apple.itunes.can-be-genius-seed" }, + { "aeHC", DMAP_UINT, 0, "com.apple.itunes.has-chapter-data" }, + { "aeHD", DMAP_UINT, 0, "com.apple.itunes.is-hd-video" }, + { "aeHV", DMAP_UINT, 0, "com.apple.itunes.has-video" }, + { "aeK1", DMAP_UINT, 0, "com.apple.itunes.drm-key1-id" }, + { "aeK2", DMAP_UINT, 0, "com.apple.itunes.drm-key2-id" }, + { "aeMC", DMAP_UINT, 0, "com.apple.itunes.playlist-contains-media-type-count" }, + { "aeMK", DMAP_UINT, 0, "com.apple.itunes.mediakind" }, + { "aeMX", DMAP_STR, 0, "com.apple.itunes.movie-info-xml" }, + { "aeMk", DMAP_UINT, 0, "com.apple.itunes.extended-media-kind" }, + { "aeND", DMAP_UINT, 0, "com.apple.itunes.non-drm-user-id" }, + { "aeNN", DMAP_STR, 0, "com.apple.itunes.network-name" }, + { "aeNV", DMAP_UINT, 0, "com.apple.itunes.norm-volume" }, + { "aePC", DMAP_UINT, 0, "com.apple.itunes.is-podcast" }, + { "aePI", DMAP_UINT, 0, "com.apple.itunes.itms-playlistid" }, + { "aePP", DMAP_UINT, 0, "com.apple.itunes.is-podcast-playlist" }, + { "aePS", DMAP_UINT, 0, "com.apple.itunes.special-playlist" }, + { "aeRD", DMAP_UINT, 0, "com.apple.itunes.rental-duration" }, + { "aeRP", DMAP_UINT, 0, "com.apple.itunes.rental-pb-start" }, + { "aeRS", DMAP_UINT, 0, "com.apple.itunes.rental-start" }, + { "aeRU", DMAP_UINT, 0, "com.apple.itunes.rental-pb-duration" }, + { "aeRf", DMAP_UINT, 0, "com.apple.itunes.is-featured" }, + { "aeSE", DMAP_UINT, 0, "com.apple.itunes.store-pers-id" }, + { "aeSF", DMAP_UINT, 0, "com.apple.itunes.itms-storefrontid" }, + { "aeSG", DMAP_UINT, 0, "com.apple.itunes.saved-genius" }, + { "aeSI", DMAP_UINT, 0, "com.apple.itunes.itms-songid" }, + { "aeSN", DMAP_STR, 0, "com.apple.itunes.series-name" }, + { "aeSP", DMAP_UINT, 0, "com.apple.itunes.smart-playlist" }, + { "aeSU", DMAP_UINT, 0, "com.apple.itunes.season-num" }, + { "aeSV", DMAP_VERS, 0, "com.apple.itunes.music-sharing-version" }, + { "aeXD", DMAP_STR, 0, "com.apple.itunes.xid" }, + { "aecp", DMAP_STR, 0, "com.apple.itunes.collection-description" }, + { "aels", DMAP_UINT, 0, "com.apple.itunes.liked-state" }, + { "aemi", DMAP_DICT, 0, "com.apple.itunes.media-kind-listing-item" }, + { "aeml", DMAP_DICT, 0, "com.apple.itunes.media-kind-listing" }, + { "agac", DMAP_UINT, 0, "daap.groupalbumcount" }, + { "agma", DMAP_UINT, 0, "daap.groupmatchedqueryalbumcount" }, + { "agmi", DMAP_UINT, 0, "daap.groupmatchedqueryitemcount" }, + { "agrp", DMAP_STR, 0, "daap.songgrouping" }, + { "ajAE", DMAP_UINT, 0, "com.apple.itunes.store.ams-episode-type" }, + { "ajAS", DMAP_UINT, 0, "com.apple.itunes.store.ams-episode-sort-order" }, + { "ajAT", DMAP_UINT, 0, "com.apple.itunes.store.ams-show-type" }, + { "ajAV", DMAP_UINT, 0, "com.apple.itunes.store.is-ams-video" }, + { "ajal", DMAP_UINT, 0, "com.apple.itunes.store.album-liked-state" }, + { "ajcA", DMAP_UINT, 0, "com.apple.itunes.store.show-composer-as-artist" }, + { "ajca", DMAP_UINT, 0, "com.apple.itunes.store.show-composer-as-artist" }, + { "ajuw", DMAP_UINT, 0, "com.apple.itunes.store.use-work-name-as-display-name" }, + { "amvc", DMAP_UINT, 0, "daap.songmovementcount" }, + { "amvm", DMAP_STR, 0, "daap.songmovementname" }, + { "amvn", DMAP_UINT, 0, "daap.songmovementnumber" }, + { "aply", DMAP_DICT, 0, "daap.databaseplaylists" }, + { "aprm", DMAP_UINT, 0, "daap.playlistrepeatmode" }, + { "apro", DMAP_VERS, 0, "daap.protocolversion" }, + { "apsm", DMAP_UINT, 0, "daap.playlistshufflemode" }, + { "apso", DMAP_DICT, 0, "daap.playlistsongs" }, + { "arif", DMAP_DICT, 0, "daap.resolveinfo" }, + { "arsv", DMAP_DICT, 0, "daap.resolve" }, + { "asaa", DMAP_STR, 0, "daap.songalbumartist" }, + { "asac", DMAP_UINT, 0, "daap.songartworkcount" }, + { "asai", DMAP_UINT, 0, "daap.songalbumid" }, + { "asal", DMAP_STR, 0, "daap.songalbum" }, + { "asar", DMAP_STR, 0, "daap.songartist" }, + { "asas", DMAP_UINT, 0, "daap.songalbumuserratingstatus" }, + { "asbk", DMAP_UINT, 0, "daap.bookmarkable" }, + { "asbo", DMAP_UINT, 0, "daap.songbookmark" }, + { "asbr", DMAP_UINT, 0, "daap.songbitrate" }, + { "asbt", DMAP_UINT, 0, "daap.songbeatsperminute" }, + { "ascd", DMAP_UINT, 0, "daap.songcodectype" }, + { "ascm", DMAP_STR, 0, "daap.songcomment" }, + { "ascn", DMAP_STR, 0, "daap.songcontentdescription" }, + { "asco", DMAP_UINT, 0, "daap.songcompilation" }, + { "ascp", DMAP_STR, 0, "daap.songcomposer" }, + { "ascr", DMAP_UINT, 0, "daap.songcontentrating" }, + { "ascs", DMAP_UINT, 0, "daap.songcodecsubtype" }, + { "asct", DMAP_STR, 0, "daap.songcategory" }, + { "asda", DMAP_DATE, 0, "daap.songdateadded" }, + { "asdb", DMAP_UINT, 0, "daap.songdisabled" }, + { "asdc", DMAP_UINT, 0, "daap.songdisccount" }, + { "asdk", DMAP_UINT, 0, "daap.songdatakind" }, + { "asdm", DMAP_DATE, 0, "daap.songdatemodified" }, + { "asdn", DMAP_UINT, 0, "daap.songdiscnumber" }, + { "asdp", DMAP_DATE, 0, "daap.songdatepurchased" }, + { "asdr", DMAP_DATE, 0, "daap.songdatereleased" }, + { "asdt", DMAP_STR, 0, "daap.songdescription" }, + { "ased", DMAP_UINT, 0, "daap.songextradata" }, + { "aseq", DMAP_STR, 0, "daap.songeqpreset" }, + { "ases", DMAP_UINT, 0, "daap.songexcludefromshuffle" }, + { "asfm", DMAP_STR, 0, "daap.songformat" }, + { "asgn", DMAP_STR, 0, "daap.songgenre" }, + { "asgp", DMAP_UINT, 0, "daap.songgapless" }, + { "asgr", DMAP_UINT, 0, "daap.supportsgroups" }, + { "ashp", DMAP_UINT, 0, "daap.songhasbeenplayed" }, + { "askd", DMAP_DATE, 0, "daap.songlastskipdate" }, + { "askp", DMAP_UINT, 0, "daap.songuserskipcount" }, + { "asky", DMAP_STR, 0, "daap.songkeywords" }, + { "aslc", DMAP_STR, 0, "daap.songlongcontentdescription" }, + { "aslr", DMAP_UINT, 0, "daap.songalbumuserrating" }, + { "asls", DMAP_UINT, 0, "daap.songlongsize" }, + { "aspc", DMAP_UINT, 0, "daap.songuserplaycount" }, + { "aspl", DMAP_DATE, 0, "daap.songdateplayed" }, + { "aspu", DMAP_STR, 0, "daap.songpodcasturl" }, + { "asri", DMAP_UINT, 0, "daap.songartistid" }, + { "asrs", DMAP_UINT, 0, "daap.songuserratingstatus" }, + { "asrv", DMAP_INT, 0, "daap.songrelativevolume" }, + { "assa", DMAP_STR, 0, "daap.sortartist" }, + { "assc", DMAP_STR, 0, "daap.sortcomposer" }, + { "assl", DMAP_STR, 0, "daap.sortalbumartist" }, + { "assn", DMAP_STR, 0, "daap.sortname" }, + { "assp", DMAP_UINT, 0, "daap.songstoptime" }, + { "assr", DMAP_UINT, 0, "daap.songsamplerate" }, + { "asss", DMAP_STR, 0, "daap.sortseriesname" }, + { "asst", DMAP_UINT, 0, "daap.songstarttime" }, + { "assu", DMAP_STR, 0, "daap.sortalbum" }, + { "assz", DMAP_UINT, 0, "daap.songsize" }, + { "astc", DMAP_UINT, 0, "daap.songtrackcount" }, + { "astm", DMAP_UINT, 0, "daap.songtime" }, + { "astn", DMAP_UINT, 0, "daap.songtracknumber" }, + { "asul", DMAP_STR, 0, "daap.songdataurl" }, + { "asur", DMAP_UINT, 0, "daap.songuserrating" }, + { "asvc", DMAP_UINT, 0, "daap.songprimaryvideocodec" }, + { "asyr", DMAP_UINT, 0, "daap.songyear" }, + { "ated", DMAP_UINT, 0, "daap.supportsextradata" }, + { "avdb", DMAP_DICT, 0, "daap.serverdatabases" }, + { "awrk", DMAP_STR, 0, "daap.songwork" }, + { "caar", DMAP_UINT, 0, "dacp.availablerepeatstates" }, + { "caas", DMAP_UINT, 0, "dacp.availableshufflestates" }, + { "caci", DMAP_DICT, 0, "caci" }, + { "cafe", DMAP_UINT, 0, "dacp.fullscreenenabled" }, + { "cafs", DMAP_UINT, 0, "dacp.fullscreen" }, + { "caia", DMAP_UINT, 0, "dacp.isactive" }, + { "cana", DMAP_STR, 0, "dacp.nowplayingartist" }, + { "cang", DMAP_STR, 0, "dacp.nowplayinggenre" }, + { "canl", DMAP_STR, 0, "dacp.nowplayingalbum" }, + { "cann", DMAP_STR, 0, "dacp.nowplayingname" }, + { "canp", DMAP_UINT, 0, "dacp.nowplayingids" }, + { "cant", DMAP_UINT, 0, "dacp.nowplayingtime" }, + { "capr", DMAP_VERS, 0, "dacp.protocolversion" }, + { "caps", DMAP_UINT, 0, "dacp.playerstate" }, + { "carp", DMAP_UINT, 0, "dacp.repeatstate" }, + { "cash", DMAP_UINT, 0, "dacp.shufflestate" }, + { "casp", DMAP_DICT, 0, "dacp.speakers" }, + { "cast", DMAP_UINT, 0, "dacp.songtime" }, + { "cavc", DMAP_UINT, 0, "dacp.volumecontrollable" }, + { "cave", DMAP_UINT, 0, "dacp.visualizerenabled" }, + { "cavs", DMAP_UINT, 0, "dacp.visualizer" }, + { "ceJC", DMAP_UINT, 0, "com.apple.itunes.jukebox-client-vote" }, + { "ceJI", DMAP_UINT, 0, "com.apple.itunes.jukebox-current" }, + { "ceJS", DMAP_UINT, 0, "com.apple.itunes.jukebox-score" }, + { "ceJV", DMAP_UINT, 0, "com.apple.itunes.jukebox-vote" }, + { "ceQR", DMAP_DICT, 0, "com.apple.itunes.playqueue-contents-response" }, + { "ceQa", DMAP_STR, 0, "com.apple.itunes.playqueue-album" }, + { "ceQg", DMAP_STR, 0, "com.apple.itunes.playqueue-genre" }, + { "ceQn", DMAP_STR, 0, "com.apple.itunes.playqueue-name" }, + { "ceQr", DMAP_STR, 0, "com.apple.itunes.playqueue-artist" }, + { "cmgt", DMAP_DICT, 0, "dmcp.getpropertyresponse" }, + { "cmmk", DMAP_UINT, 0, "dmcp.mediakind" }, + { "cmpr", DMAP_VERS, 0, "dmcp.protocolversion" }, + { "cmsr", DMAP_UINT, 0, "dmcp.serverrevision" }, + { "cmst", DMAP_DICT, 0, "dmcp.playstatus" }, + { "cmvo", DMAP_UINT, 0, "dmcp.volume" }, + { "f\215ch", DMAP_UINT, 0, "dmap.haschildcontainers" }, + { "ipsa", DMAP_DICT, 0, "dpap.iphotoslideshowadvancedoptions" }, + { "ipsl", DMAP_DICT, 0, "dpap.iphotoslideshowoptions" }, + { "mbcl", DMAP_DICT, 0, "dmap.bag" }, + { "mccr", DMAP_DICT, 0, "dmap.contentcodesresponse" }, + { "mcna", DMAP_STR, 0, "dmap.contentcodesname" }, + { "mcnm", DMAP_UINT, 0, "dmap.contentcodesnumber" }, + { "mcon", DMAP_DICT, 0, "dmap.container" }, + { "mctc", DMAP_UINT, 0, "dmap.containercount" }, + { "mcti", DMAP_UINT, 0, "dmap.containeritemid" }, + { "mcty", DMAP_UINT, 0, "dmap.contentcodestype" }, + { "mdbk", DMAP_UINT, 0, "dmap.databasekind" }, + { "mdcl", DMAP_DICT, 0, "dmap.dictionary" }, + { "mdst", DMAP_UINT, 0, "dmap.downloadstatus" }, + { "meds", DMAP_UINT, 0, "dmap.editcommandssupported" }, + { "meia", DMAP_UINT, 0, "dmap.itemdateadded" }, + { "meip", DMAP_UINT, 0, "dmap.itemdateplayed" }, + { "mext", DMAP_UINT, 0, "dmap.objectextradata" }, + { "miid", DMAP_UINT, 0, "dmap.itemid" }, + { "mikd", DMAP_UINT, 0, "dmap.itemkind" }, + { "mimc", DMAP_UINT, 0, "dmap.itemcount" }, + { "minm", DMAP_STR, 0, "dmap.itemname" }, + { "mlcl", DMAP_DICT, DMAP_DICT, "dmap.listing" }, + { "mlid", DMAP_UINT, 0, "dmap.sessionid" }, + { "mlit", DMAP_ITEM, 0, "dmap.listingitem" }, + { "mlog", DMAP_DICT, 0, "dmap.loginresponse" }, + { "mpco", DMAP_UINT, 0, "dmap.parentcontainerid" }, + { "mper", DMAP_UINT, 0, "dmap.persistentid" }, + { "mpro", DMAP_VERS, 0, "dmap.protocolversion" }, + { "mrco", DMAP_UINT, 0, "dmap.returnedcount" }, + { "mrpr", DMAP_UINT, 0, "dmap.remotepersistentid" }, + { "msal", DMAP_UINT, 0, "dmap.supportsautologout" }, + { "msas", DMAP_UINT, 0, "dmap.authenticationschemes" }, + { "msau", DMAP_UINT, 0, "dmap.authenticationmethod" }, + { "msbr", DMAP_UINT, 0, "dmap.supportsbrowse" }, + { "msdc", DMAP_UINT, 0, "dmap.databasescount" }, + { "msex", DMAP_UINT, 0, "dmap.supportsextensions" }, + { "msix", DMAP_UINT, 0, "dmap.supportsindex" }, + { "mslr", DMAP_UINT, 0, "dmap.loginrequired" }, + { "msma", DMAP_UINT, 0, "dmap.machineaddress" }, + { "msml", DMAP_DICT, 0, "msml" }, + { "mspi", DMAP_UINT, 0, "dmap.supportspersistentids" }, + { "msqy", DMAP_UINT, 0, "dmap.supportsquery" }, + { "msrs", DMAP_UINT, 0, "dmap.supportsresolve" }, + { "msrv", DMAP_DICT, 0, "dmap.serverinforesponse" }, + { "mstc", DMAP_DATE, 0, "dmap.utctime" }, + { "mstm", DMAP_UINT, 0, "dmap.timeoutinterval" }, + { "msto", DMAP_INT, 0, "dmap.utcoffset" }, + { "msts", DMAP_STR, 0, "dmap.statusstring" }, + { "mstt", DMAP_UINT, 0, "dmap.status" }, + { "msup", DMAP_UINT, 0, "dmap.supportsupdate" }, + { "mtco", DMAP_UINT, 0, "dmap.specifiedtotalcount" }, + { "mudl", DMAP_DICT, 0, "dmap.deletedidlisting" }, + { "mupd", DMAP_DICT, 0, "dmap.updateresponse" }, + { "musr", DMAP_UINT, 0, "dmap.serverrevision" }, + { "muty", DMAP_UINT, 0, "dmap.updatetype" }, + { "pasp", DMAP_STR, 0, "dpap.aspectratio" }, + { "pcmt", DMAP_STR, 0, "dpap.imagecomments" }, + { "peak", DMAP_UINT, 0, "com.apple.itunes.photos.album-kind" }, + { "peed", DMAP_DATE, 0, "com.apple.itunes.photos.exposure-date" }, + { "pefc", DMAP_DICT, 0, "com.apple.itunes.photos.faces" }, + { "peki", DMAP_UINT, 0, "com.apple.itunes.photos.key-image-id" }, + { "pekm", DMAP_DICT, 0, "com.apple.itunes.photos.key-image" }, + { "pemd", DMAP_DATE, 0, "com.apple.itunes.photos.modification-date" }, + { "pfai", DMAP_DICT, 0, "dpap.failureids" }, + { "pfdt", DMAP_DICT, 0, "dpap.filedata" }, + { "pfmt", DMAP_STR, 0, "dpap.imageformat" }, + { "phgt", DMAP_UINT, 0, "dpap.imagepixelheight" }, + { "picd", DMAP_DATE, 0, "dpap.creationdate" }, + { "pifs", DMAP_UINT, 0, "dpap.imagefilesize" }, + { "pimf", DMAP_STR, 0, "dpap.imagefilename" }, + { "plsz", DMAP_UINT, 0, "dpap.imagelargefilesize" }, + { "ppro", DMAP_VERS, 0, "dpap.protocolversion" }, + { "prat", DMAP_UINT, 0, "dpap.imagerating" }, + { "pret", DMAP_DICT, 0, "dpap.retryids" }, + { "pwth", DMAP_UINT, 0, "dpap.imagepixelwidth" } +}; +static const size_t dmap_field_count = sizeof(dmap_fields) / sizeof(dmap_field); + +typedef int (*sort_func) (const void *, const void *); + +int dmap_version(void) { + return DMAP_VERSION; +} + +const char *dmap_version_string(void) { + return DMAP_STRINGIFY(DMAP_VERSION_MAJOR) "." + DMAP_STRINGIFY(DMAP_VERSION_MINOR) "." + DMAP_STRINGIFY(DMAP_VERSION_PATCH); +} + +static int dmap_field_sort(const dmap_field *a, const dmap_field *b) { + return memcmp(a->code, b->code, 4); +} + +static const dmap_field *dmap_field_from_code(const char *code) { + dmap_field key; + key.code = code; + return bsearch(&key, dmap_fields, dmap_field_count, sizeof(dmap_field), (sort_func)dmap_field_sort); +} + +const char *dmap_name_from_code(const char *code) { + const dmap_field *field; + if (!code) + return NULL; + + field = dmap_field_from_code(code); + return field ? field->name : NULL; +} + +static uint16_t dmap_read_u16(const char *buf) { + return (uint16_t)(((buf[0] & 0xff) << 8) | (buf[1] & 0xff)); +} + +static int16_t dmap_read_i16(const char *buf) { + return (int16_t)dmap_read_u16(buf); +} + +static uint32_t dmap_read_u32(const char *buf) { + return ((uint32_t)(buf[0] & 0xff) << 24) | + ((uint32_t)(buf[1] & 0xff) << 16) | + ((uint32_t)(buf[2] & 0xff) << 8) | + ((uint32_t)(buf[3] & 0xff)); +} + +static int32_t dmap_read_i32(const char *buf) { + return (int32_t)dmap_read_u32(buf); +} + +static uint64_t dmap_read_u64(const char *buf) { + return ((uint64_t)(buf[0] & 0xff) << 56) | + ((uint64_t)(buf[1] & 0xff) << 48) | + ((uint64_t)(buf[2] & 0xff) << 40) | + ((uint64_t)(buf[3] & 0xff) << 32) | + ((uint64_t)(buf[4] & 0xff) << 24) | + ((uint64_t)(buf[5] & 0xff) << 16) | + ((uint64_t)(buf[6] & 0xff) << 8) | + ((uint64_t)(buf[7] & 0xff)); +} + +static int64_t dmap_read_i64(const char *buf) { + return (int64_t)dmap_read_u64(buf); +} + +static int dmap_parse_internal(const dmap_settings *settings, const char *buf, size_t len, const dmap_field *parent) { + const dmap_field *field; + DMAP_TYPE field_type; + size_t field_len; + const char *field_name; + const char *p = buf; + const char *end = buf + len; + char code[5] = {0}; + + if (!settings || !buf) + return -1; + + while (end - p >= 8) { + memcpy(code, p, 4); + field = dmap_field_from_code(code); + p += 4; + + field_len = dmap_read_u32(p); + p += 4; + + if (p + field_len > end) + return -1; + + if (field) { + field_type = field->type; + field_name = field->name; + + if (field_type == DMAP_ITEM) { + if (parent != NULL && parent->list_item_type) { + field_type = parent->list_item_type; + } else { + field_type = DMAP_DICT; + } + } + } else { + /* Make a best guess of the type */ + field_type = DMAP_UNKNOWN; + field_name = code; + + if (field_len >= 8) { + /* Look for a four char code followed by a length within the current field */ + if (isalpha(p[0] & 0xff) && + isalpha(p[1] & 0xff) && + isalpha(p[2] & 0xff) && + isalpha(p[3] & 0xff)) { + if (dmap_read_u32(p + 4) < field_len) + field_type = DMAP_DICT; + } + } + + if (field_type == DMAP_UNKNOWN) { + size_t i; + int is_string = 1; + for (i=0; i < field_len; i++) { + if (!isprint(p[i] & 0xff)) { + is_string = 0; + break; + } + } + + field_type = is_string ? DMAP_STR : DMAP_UINT; + } + } + + switch (field_type) { + case DMAP_UINT: + /* Determine the integer's type based on its size */ + switch (field_len) { + case 1: + if (settings->on_uint32) + settings->on_uint32(settings->ctx, code, field_name, (unsigned char)*p); + break; + case 2: + if (settings->on_uint32) + settings->on_uint32(settings->ctx, code, field_name, dmap_read_u16(p)); + break; + case 4: + if (settings->on_uint32) + settings->on_uint32(settings->ctx, code, field_name, dmap_read_u32(p)); + break; + case 8: + if (settings->on_uint64) + settings->on_uint64(settings->ctx, code, field_name, dmap_read_u64(p)); + break; + default: + if (settings->on_data) + settings->on_data(settings->ctx, code, field_name, p, field_len); + break; + } + break; + case DMAP_INT: + switch (field_len) { + case 1: + if (settings->on_int32) + settings->on_int32(settings->ctx, code, field_name, *p); + break; + case 2: + if (settings->on_int32) + settings->on_int32(settings->ctx, code, field_name, dmap_read_i16(p)); + break; + case 4: + if (settings->on_int32) + settings->on_int32(settings->ctx, code, field_name, dmap_read_i32(p)); + break; + case 8: + if (settings->on_int64) + settings->on_int64(settings->ctx, code, field_name, dmap_read_i64(p)); + break; + default: + if (settings->on_data) + settings->on_data(settings->ctx, code, field_name, p, field_len); + break; + } + break; + case DMAP_STR: + if (settings->on_string) + settings->on_string(settings->ctx, code, field_name, p, field_len); + break; + case DMAP_DATA: + if (settings->on_data) + settings->on_data(settings->ctx, code, field_name, p, field_len); + break; + case DMAP_DATE: + /* Seconds since epoch */ + if (settings->on_date) + settings->on_date(settings->ctx, code, field_name, dmap_read_u32(p)); + break; + case DMAP_VERS: + if (settings->on_string && field_len >= 4) { + char version[20]; + sprintf(version, "%u.%u", dmap_read_u16(p), dmap_read_u16(p+2)); + settings->on_string(settings->ctx, code, field_name, version, strlen(version)); + } + break; + case DMAP_DICT: + if (settings->on_dict_start) + settings->on_dict_start(settings->ctx, code, field_name); + if (dmap_parse_internal(settings, p, field_len, field) != 0) + return -1; + if (settings->on_dict_end) + settings->on_dict_end(settings->ctx, code, field_name); + break; + case DMAP_ITEM: + /* Unreachable: listing item types are always mapped to another type */ + abort(); + case DMAP_UNKNOWN: + break; + } + + p += field_len; + } + + if (p != end) + return -1; + + return 0; +} + +int dmap_parse(const dmap_settings *settings, const char *buf, size_t len) { + return dmap_parse_internal(settings, buf, len, NULL); +} diff --git a/components/raop/dmap_parser.h b/components/raop/dmap_parser.h new file mode 100644 index 00000000..bf6f31a1 --- /dev/null +++ b/components/raop/dmap_parser.h @@ -0,0 +1,90 @@ +#ifndef dmap_parser_h +#define dmap_parser_h +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define DMAP_VERSION_MAJOR 1 +#define DMAP_VERSION_MINOR 2 +#define DMAP_VERSION_PATCH 1 + +#define DMAP_VERSION (DMAP_VERSION_MAJOR * 1000000 + \ + DMAP_VERSION_MINOR * 1000 + \ + DMAP_VERSION_PATCH) + +/* + * Callbacks invoked during parsing. + * + * @param ctx The context pointer specified in the dmap_settings structure. + * @param code The content code from the message. + * @param name The name associated with the content code, if known. If there is + * no known name this parameter contains the same value as the code + * parameter. + */ +typedef void (*dmap_dict_cb) (void *ctx, const char *code, const char *name); +typedef void (*dmap_int32_cb) (void *ctx, const char *code, const char *name, int32_t value); +typedef void (*dmap_int64_cb) (void *ctx, const char *code, const char *name, int64_t value); +typedef void (*dmap_uint32_cb) (void *ctx, const char *code, const char *name, uint32_t value); +typedef void (*dmap_uint64_cb) (void *ctx, const char *code, const char *name, uint64_t value); +typedef void (*dmap_data_cb) (void *ctx, const char *code, const char *name, const char *buf, size_t len); + +typedef struct { + /* Callbacks to indicate the start and end of dictionary fields. */ + dmap_dict_cb on_dict_start; + dmap_dict_cb on_dict_end; + + /* Callbacks for field data. */ + dmap_int32_cb on_int32; + dmap_int64_cb on_int64; + dmap_uint32_cb on_uint32; + dmap_uint64_cb on_uint64; + dmap_uint32_cb on_date; + dmap_data_cb on_string; + dmap_data_cb on_data; + + /** A context pointer passed to each callback function. */ + void *ctx; +} dmap_settings; + +/** + * Returns the library version number. + * + * The version number format is (major * 1000000) + (minor * 1000) + patch. + * For example, the value for version 1.2.3 is 1002003. + */ +int dmap_version(void); + +/** + * Returns the library version as a string. + */ +const char *dmap_version_string(void); + +/** + * Returns the name associated with the provided content code, or NULL if there + * is no known name. + * + * For example, if given the code "minm" this function returns "dmap.itemname". + */ +const char *dmap_name_from_code(const char *code); + +/** + * Parses a DMAP message buffer using the provided settings. + * + * @param settings A dmap_settings structure populated with the callbacks to + * invoke during parsing. + * @param buf Pointer to a DMAP message buffer. The buffer must contain a + * complete message. + * @param len The length of the DMAP message buffer. + * + * @return 0 if parsing was successful, or -1 if an error occurred. + */ +int dmap_parse(const dmap_settings *settings, const char *buf, size_t len); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/components/raop/log_util.h b/components/raop/log_util.h new file mode 100644 index 00000000..3075dea6 --- /dev/null +++ b/components/raop/log_util.h @@ -0,0 +1,40 @@ +/* + * logging utility + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * (c) Philippe 2016-2017, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef __LOG_UTIL_H +#define __LOG_UTIL_H + +#include "platform.h" + +typedef enum { lERROR = 0, lWARN, lINFO, lDEBUG, lSDEBUG } log_level; + +const char *logtime(void); +void logprint(const char *fmt, ...); +log_level debug2level(char *level); +char *level2debug(log_level level); + +#define LOG_ERROR(fmt, ...) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_WARN(fmt, ...) if (*loglevel >= lWARN) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_INFO(fmt, ...) if (*loglevel >= lINFO) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_DEBUG(fmt, ...) if (*loglevel >= lDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_SDEBUG(fmt, ...) if (*loglevel >= lSDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) + +#endif \ No newline at end of file diff --git a/components/raop/platform.h b/components/raop/platform.h new file mode 100644 index 00000000..82b15807 --- /dev/null +++ b/components/raop/platform.h @@ -0,0 +1,127 @@ +/* + * platform setting definition + * + * (c) Philippe, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef __PLATFORM_H +#define __PLATFORM_H + +#ifdef WIN32 +#define LINUX 0 +#define WIN 1 +#else +#define LINUX 1 +#define WIN 0 +#endif + +#include +#include +#include + +#ifdef WIN32 + +#include +#include +#include +#include +#include + +typedef unsigned __int8 u8_t; +typedef unsigned __int16 u16_t; +typedef unsigned __int32 u32_t; +typedef unsigned __int64 u64_t; +typedef __int16 s16_t; +typedef __int32 s32_t; +typedef __int64 s64_t; + +#define inline __inline + +int gettimeofday(struct timeval *tv, struct timezone *tz); +char *strcasestr(const char *haystack, const char *needle); + +#define usleep(x) Sleep((x)/1000) + #define sleep(x) Sleep((x)*1000) +#define last_error() WSAGetLastError() +#define ERROR_WOULDBLOCK WSAEWOULDBLOCK +#define open _open +#define read _read +#define poll WSAPoll +#define snprintf _snprintf +#define strcasecmp stricmp +#define _random(x) random(x) +#define VALGRIND_MAKE_MEM_DEFINED(x,y) +#define S_ADDR(X) X.S_un.S_addr + +#define in_addr_t u32_t +#define socklen_t int +#define ssize_t int + +#define RTLD_NOW 0 + +#else + +#include +#include +#include +#include +/* +#include +#include +#include +#include +*/ +#include +#include +#include +#include +#include +#include + +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#define max(a,b) (((a) > (b)) ? (a) : (b)) + +typedef int16_t s16_t; +typedef int32_t s32_t; +typedef int64_t s64_t; +typedef uint8_t u8_t; +typedef uint16_t u16_t; +typedef uint32_t u32_t; +typedef unsigned long long u64_t; + +#define last_error() errno +#define ERROR_WOULDBLOCK EWOULDBLOCK + +char *strlwr(char *str); +#define _random(x) random() +#define closesocket(s) close(s) +#define S_ADDR(X) X.s_addr + +#endif + +typedef struct ntp_s { + u32_t seconds; + u32_t fraction; + } ntp_t; + +u64_t timeval_to_ntp(struct timeval tv, struct ntp_s *ntp); +u64_t get_ntp(struct ntp_s *ntp); +// we expect somebody to provide the ms clock, system-wide +u32_t _gettime_ms_(void); +#define gettime_ms _gettime_ms_ + +#endif // __PLATFORM diff --git a/components/raop/raop.c b/components/raop/raop.c new file mode 100644 index 00000000..7554d553 --- /dev/null +++ b/components/raop/raop.c @@ -0,0 +1,823 @@ +/* + * + * (c) Philippe 2019, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include "platform.h" + +#ifdef WIN32 +#include +#include +#include +#include +#include +#include "mdns.h" +#include "mdnsd.h" +#include "mdnssd-itf.h" +#else +#include "esp_pthread.h" +#include "mdns.h" +#include "mbedtls/version.h" +#include +#endif + +#include "util.h" +#include "raop.h" +#include "rtp.h" +#include "dmap_parser.h" +#include "log_util.h" + +typedef struct raop_ctx_s { +#ifdef WIN32 + struct mdns_service *svc; + struct mdnsd *svr; +#endif + struct in_addr host; // IP of bridge + short unsigned port; // RTSP port for AirPlay + int sock; // socket of the above + struct in_addr peer; // IP of the iDevice (airplay sender) + bool running; + bool drift; +#ifdef WIN32 + pthread_t thread, search_thread; +#else + TaskHandle_t thread, search_thread, joiner; +#endif + unsigned char mac[6]; + int latency; + struct { + char *aesiv, *aeskey; + char *fmtp; + } rtsp; + struct rtp_s *rtp; + raop_cmd_cb_t cmd_cb; + raop_data_cb_t data_cb; + /* + struct { + char DACPid[32], id[32]; + struct in_addr host; + u16_t port; + struct mDNShandle_s *handle; + } active_remote; + */ + void *owner; +} raop_ctx_t; + +extern struct mdnsd* glmDNSServer; +extern log_level raop_loglevel; +static log_level *loglevel = &raop_loglevel; + +static void* rtsp_thread(void *arg); +static bool handle_rtsp(raop_ctx_t *ctx, int sock); + +static char* rsa_apply(unsigned char *input, int inlen, int *outlen, int mode); +static int base64_pad(char *src, char **padded); +static int base64_encode(const void *data, int size, char **str); +static int base64_decode(const char *str, void *data); +static void* search_remote(void *args); + +extern char private_key[]; + enum { RSA_MODE_KEY, RSA_MODE_AUTH }; + + static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len); + +/*----------------------------------------------------------------------------*/ +struct raop_ctx_s *raop_create(struct in_addr host, char *name, + unsigned char mac[6], int latency, + raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) { + struct raop_ctx_s *ctx = malloc(sizeof(struct raop_ctx_s)); + struct sockaddr_in addr; + char id[64]; + #ifdef WIN32 + socklen_t nlen = sizeof(struct sockaddr); + char *txt[] = { "am=esp32", "tp=UDP", "sm=false", "sv=false", "ek=1", + "et=0,1", "md=0,1,2", "cn=0,1", "ch=2", + "ss=16", "sr=44100", "vn=3", "txtvers=1", + NULL }; +#else + mdns_txt_item_t txt[] = { + {"am", "esp32"}, + {"tp", "UDP"}, + {"sm","false"}, + {"sv","false"}, + {"ek","1"}, + {"et","0,1"}, + {"md","0,1,2"}, + {"cn","0,1"}, + {"ch","2"}, + {"ss","16"}, + {"sr","44100"}, + {"vn","3"}, + {"txtvers","1"}, + }; + +#endif + + if (!ctx) return NULL; + + // make sure we have a clean context + memset(ctx, 0, sizeof(raop_ctx_t)); + +#ifdef WIN32 + ctx->svr = glmDNSServer; +#endif + ctx->host = host; + ctx->sock = socket(AF_INET, SOCK_STREAM, 0); + ctx->cmd_cb = cmd_cb; + ctx->data_cb = data_cb; + ctx->drift = false; + ctx->latency = min(latency, 44100); + if (ctx->sock == -1) { + LOG_ERROR("Cannot create listening socket", NULL); + free(ctx); + return NULL; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_addr.s_addr = host.s_addr; + addr.sin_family = AF_INET; +#ifdef WIN32 + addr.sin_port = 0; +#else + ctx->port = 5000; + addr.sin_port = htons(ctx->port); +#endif + + if (bind(ctx->sock, (struct sockaddr *) &addr, sizeof(addr)) < 0 || listen(ctx->sock, 1)) { + LOG_ERROR("Cannot bind or listen RTSP listener: %s", strerror(errno)); + free(ctx); + closesocket(ctx->sock); + return NULL; + } + +#ifdef WIN32 + getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen); + ctx->port = ntohs(addr.sin_port); + #endif + + ctx->running = true; + memcpy(ctx->mac, mac, 6); + snprintf(id, 64, "%02X%02X%02X%02X%02X%02X@%s", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], name); + #ifdef WIN32 + // seems that Windows snprintf does not add NULL char if actual size > max + id[63] = '\0'; + ctx->svc = mdnsd_register_svc(ctx->svr, id, "_raop._tcp.local", ctx->port, NULL, (const char**) txt); + pthread_create(&ctx->thread, NULL, &rtsp_thread, ctx); + #else + LOG_INFO("starting mDNS with %s", id); + ESP_ERROR_CHECK( mdns_service_add(id, "_raop", "_tcp", ctx->port, txt, sizeof(txt) / sizeof(mdns_txt_item_t)) ); + xTaskCreate((TaskFunction_t) rtsp_thread, "RTSP_thread", 8*1024, ctx, ESP_TASK_PRIO_MIN + 1, &ctx->thread); + #endif + + return ctx; +} + + +/*----------------------------------------------------------------------------*/ +void raop_delete(struct raop_ctx_s *ctx) { + int sock; + struct sockaddr addr; + socklen_t nlen = sizeof(struct sockaddr); + + if (!ctx) return; + + ctx->running = false; + + // wake-up thread by connecting socket, needed for freeBSD + sock = socket(AF_INET, SOCK_STREAM, 0); + getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen); + connect(sock, (struct sockaddr*) &addr, sizeof(addr)); + closesocket(sock); + +#ifdef WIN32 + pthread_join(ctx->thread, NULL); +#else + ctx->joiner = xTaskGetCurrentTaskHandle(); + xTaskNotifyWait(0, 0, NULL, portMAX_DELAY); +#endif + + rtp_end(ctx->rtp); + +#ifdef WIN32 + shutdown(ctx->sock, SD_BOTH); +#else + shutdown(ctx->sock, SHUT_RDWR); +#endif + closesocket(ctx->sock); + + /* + // terminate search, but do not reclaim memory of pthread if never launched + if (ctx->active_remote.handle) { + close_mDNS(ctx->active_remote.handle); + pthread_join(ctx->search_thread, NULL); + } + */ + + NFREE(ctx->rtsp.aeskey); + NFREE(ctx->rtsp.aesiv); + NFREE(ctx->rtsp.fmtp); + + // stop broadcasting devices +#ifdef WIN32 + mdns_service_remove(ctx->svr, ctx->svc); + mdnsd_stop(ctx->svr); +#endif + + free(ctx); +} + + +/*----------------------------------------------------------------------------*/ +void raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param) { +/* + struct sockaddr_in addr; + int sock; + char *command = NULL; + + // first notify the remote controller (if any) + switch(event) { + case RAOP_PAUSE: + command = strdup("pause"); + break; + case RAOP_PLAY: + command = strdup("play"); + break; + case RAOP_STOP: + command = strdup("stop"); + break; + case RAOP_VOLUME: { + float Volume = *((float*) param); + Volume = Volume ? (Volume - 1) * 30 : -144; + asprintf(&command,"setproperty?dmcp.device-volume=%0.4lf", Volume); + break; + } + default: + break; + } + + // no command to send to remote or no remote found yet + if (!command || !ctx->active_remote.port) { + NFREE(command); + return; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = S_ADDR(ctx->active_remote.host); + addr.sin_port = htons(ctx->active_remote.port); + + if (!connect(sock, (struct sockaddr*) &addr, sizeof(addr))) { + char *method, *buf, resp[512] = ""; + int len; + key_data_t headers[4] = { {NULL, NULL} }; + + asprintf(&method, "GET /ctrl-int/1/%s HTTP/1.0", command); + kd_add(headers, "Active-Remote", ctx->active_remote.id); + kd_add(headers, "Connection", "close"); + + buf = http_send(sock, method, headers); + len = recv(sock, resp, 512, 0); + if (len > 0) resp[len-1] = '\0'; + LOG_INFO("[%p]: sending airplay remote\n%s<== received ==>\n%s", ctx, buf, resp); + + NFREE(method); + NFREE(buf); + kd_free(headers); + } + + free(command); + + closesocket(sock); +*/ + // then notify local system + ctx->cmd_cb(event, param); +} + +/*----------------------------------------------------------------------------*/ +static void *rtsp_thread(void *arg) { + raop_ctx_t *ctx = (raop_ctx_t*) arg; + int sock = -1; + + while (ctx->running) { + fd_set rfds; + struct timeval timeout = {0, 100*1000}; + int n; + bool res = false; + + if (sock == -1) { + struct sockaddr_in peer; + socklen_t addrlen = sizeof(struct sockaddr_in); + + sock = accept(ctx->sock, (struct sockaddr*) &peer, &addrlen); + ctx->peer.s_addr = peer.sin_addr.s_addr; + + if (sock != -1 && ctx->running) { + LOG_INFO("got RTSP connection %u", sock); + } else continue; + } + + FD_ZERO(&rfds); + FD_SET(sock, &rfds); + + n = select(sock + 1, &rfds, NULL, NULL, &timeout); + + if (!n) continue; + + if (n > 0) res = handle_rtsp(ctx, sock); + + if (n < 0 || !res) { + closesocket(sock); + LOG_INFO("RTSP close %u", sock); + sock = -1; + } + } + + if (sock != -1) closesocket(sock); + +#ifndef WIN32 + xTaskNotify(ctx->joiner, 0, eNoAction); + vTaskDelete(NULL); +#endif + + return NULL; +} + + +/*----------------------------------------------------------------------------*/ +static bool handle_rtsp(raop_ctx_t *ctx, int sock) +{ + char *buf = NULL, *body = NULL, method[16] = ""; + key_data_t headers[16], resp[8] = { {NULL, NULL} }; + int len; + bool success = true; + + if (!http_parse(sock, method, headers, &body, &len)) { + NFREE(body); + kd_free(headers); + return false; + } + + if (strcmp(method, "OPTIONS")) { + LOG_INFO("[%p]: received %s", ctx, method); + } + + if ((buf = kd_lookup(headers, "Apple-Challenge")) != NULL) { + int n; + char *buf_pad, *p, *data_b64 = NULL, data[32]; + + LOG_INFO("[%p]: challenge %s", ctx, buf); + + // need to pad the base64 string as apple device don't + base64_pad(buf, &buf_pad); + + p = data + min(base64_decode(buf_pad, data), 32-10); + p = (char*) memcpy(p, &S_ADDR(ctx->host), 4) + 4; + p = (char*) memcpy(p, ctx->mac, 6) + 6; + memset(p, 0, 32 - (p - data)); + p = rsa_apply((unsigned char*) data, 32, &n, RSA_MODE_AUTH); + n = base64_encode(p, n, &data_b64); + + // remove padding as well (seems to be optional now) + for (n = strlen(data_b64) - 1; n > 0 && data_b64[n] == '='; data_b64[n--] = '\0'); + + kd_add(resp, "Apple-Response", data_b64); + + NFREE(p); + NFREE(buf_pad); + NFREE(data_b64); + } + + if (!strcmp(method, "OPTIONS")) { + + kd_add(resp, "Public", "ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER"); + + } else if (!strcmp(method, "ANNOUNCE")) { + char *padded, *p; + + NFREE(ctx->rtsp.aeskey); + NFREE(ctx->rtsp.aesiv); + NFREE(ctx->rtsp.fmtp); + + if ((p = strcasestr(body, "rsaaeskey")) != NULL) { + unsigned char *aeskey; + int len, outlen; + + p = strextract(p, ":", "\r\n"); + base64_pad(p, &padded); + aeskey = malloc(strlen(padded)); + len = base64_decode(padded, aeskey); + ctx->rtsp.aeskey = rsa_apply(aeskey, len, &outlen, RSA_MODE_KEY); + + NFREE(p); + NFREE(aeskey); + NFREE(padded); + } + + if ((p = strcasestr(body, "aesiv")) != NULL) { + p = strextract(p, ":", "\r\n"); + base64_pad(p, &padded); + ctx->rtsp.aesiv = malloc(strlen(padded)); + base64_decode(padded, ctx->rtsp.aesiv); + + NFREE(p); + NFREE(padded); + } + + if ((p = strcasestr(body, "fmtp")) != NULL) { + p = strextract(p, ":", "\r\n"); + ctx->rtsp.fmtp = strdup(p); + NFREE(p); + } + + // on announce, search remote + /* + if ((buf = kd_lookup(headers, "DACP-ID")) != NULL) strcpy(ctx->active_remote.DACPid, buf); + if ((buf = kd_lookup(headers, "Active-Remote")) != NULL) strcpy(ctx->active_remote.id, buf); + + ctx->active_remote.handle = init_mDNS(false, ctx->host); + pthread_create(&ctx->search_thread, NULL, &search_remote, ctx); + */ + + } else if (!strcmp(method, "SETUP") && ((buf = kd_lookup(headers, "Transport")) != NULL)) { + char *p; + rtp_resp_t rtp = { 0 }; + short unsigned tport = 0, cport = 0; + + if ((p = strcasestr(buf, "timing_port")) != NULL) sscanf(p, "%*[^=]=%hu", &tport); + if ((p = strcasestr(buf, "control_port")) != NULL) sscanf(p, "%*[^=]=%hu", &cport); + + rtp = rtp_init(ctx->peer, false, ctx->drift, true, ctx->latency, + ctx->rtsp.aeskey, ctx->rtsp.aesiv, ctx->rtsp.fmtp, + cport, tport, ctx->data_cb); + + ctx->rtp = rtp.ctx; + + if (cport * tport * rtp.cport * rtp.tport * rtp.aport && rtp.ctx) { + char *transport; + asprintf(&transport, "RTP/AVP/UDP;unicast;mode=record;control_port=%u;timing_port=%u;server_port=%u", rtp.cport, rtp.tport, rtp.aport); + LOG_DEBUG("[%p]: audio=(%hu:%hu), timing=(%hu:%hu), control=(%hu:%hu)", ctx, 0, rtp.aport, tport, rtp.tport, cport, rtp.cport); + kd_add(resp, "Transport", transport); + kd_add(resp, "Session", "DEADBEEF"); + free(transport); + } else { + success = false; + LOG_INFO("[%p]: cannot start session, missing ports", ctx); + } + + } else if (!strcmp(method, "RECORD")) { + unsigned short seqno = 0; + unsigned rtptime = 0; + char *p; + + if (ctx->latency) { + char latency[6]; + snprintf(latency, 6, "%u", ctx->latency); + kd_add(resp, "Audio-Latency", latency); + } + + buf = kd_lookup(headers, "RTP-Info"); + if ((p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno); + if ((p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime); + + if (ctx->rtp) rtp_record(ctx->rtp, seqno, rtptime); + + ctx->cmd_cb(RAOP_STREAM, NULL); + + } else if (!strcmp(method, "FLUSH")) { + unsigned short seqno = 0; + unsigned rtptime = 0; + char *p; + + buf = kd_lookup(headers, "RTP-Info"); + if ((p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno); + if ((p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime); + + // only send FLUSH if useful (discards frames above buffer head and top) + if (ctx->rtp && rtp_flush(ctx->rtp, seqno, rtptime)) + ctx->cmd_cb(RAOP_FLUSH, NULL); + + } else if (!strcmp(method, "TEARDOWN")) { + + rtp_end(ctx->rtp); + + ctx->rtp = NULL; + + /* + // need to make sure no search is on-going and reclaim pthread memory + if (ctx->active_remote.handle) close_mDNS(ctx->active_remote.handle); + pthread_join(ctx->search_thread, NULL); + memset(&ctx->active_remote, 0, sizeof(ctx->active_remote)); + */ + + NFREE(ctx->rtsp.aeskey); + NFREE(ctx->rtsp.aesiv); + NFREE(ctx->rtsp.fmtp); + + ctx->cmd_cb(RAOP_STOP, NULL); + + } if (!strcmp(method, "SET_PARAMETER")) { + char *p; + + if ((p = strcasestr(body, "volume")) != NULL) { + float volume; + + sscanf(p, "%*[^:]:%f", &volume); + LOG_INFO("[%p]: SET PARAMETER volume %f", ctx, volume); + volume = (volume == -144.0) ? 0 : (1 + volume / 30); + ctx->cmd_cb(RAOP_VOLUME, &volume); + } +/* + if (((p = kd_lookup(headers, "Content-Type")) != NULL) && !strcasecmp(p, "application/x-dmap-tagged")) { + struct metadata_s metadata; + dmap_settings settings = { + NULL, NULL, NULL, NULL, NULL, NULL, NULL, on_dmap_string, NULL, + NULL + }; + + settings.ctx = &metadata; + memset(&metadata, 0, sizeof(struct metadata_s)); + if (!dmap_parse(&settings, body, len)) { + LOG_INFO("[%p]: received metadata\n\tartist: %s\n\talbum: %s\n\ttitle: %s", + ctx, metadata.artist, metadata.album, metadata.title); + free_metadata(&metadata); + } + } +*/ + } + + // don't need to free "buf" because kd_lookup return a pointer, not a strdup + + kd_add(resp, "Audio-Jack-Status", "connected; type=analog"); + kd_add(resp, "CSeq", kd_lookup(headers, "CSeq")); + + if (success) buf = http_send(sock, "RTSP/1.0 200 OK", resp); + else buf = http_send(sock, "RTSP/1.0 500 ERROR", NULL); + + if (strcmp(method, "OPTIONS")) { + LOG_INFO("[%p]: responding:\n%s", ctx, buf ? buf : ""); + } + + NFREE(body); + NFREE(buf); + kd_free(resp); + kd_free(headers); + + return true; +} + +/*----------------------------------------------------------------------------*/ +/* +bool search_remote_cb(mDNSservice_t *slist, void *cookie, bool *stop) { + mDNSservice_t *s; + raop_ctx_t *ctx = (raop_ctx_t*) cookie; + + // see if we have found an active remote for our ID + for (s = slist; s; s = s->next) { + if (strcasestr(s->name, ctx->active_remote.DACPid)) { + ctx->active_remote.host = s->addr; + ctx->active_remote.port = s->port; + LOG_INFO("[%p]: found ActiveRemote for %s at %s:%u", ctx, ctx->active_remote.DACPid, + inet_ntoa(ctx->active_remote.host), ctx->active_remote.port); + *stop = true; + break; + } + } + + // let caller clear list + return false; +} +*/ + + +/*----------------------------------------------------------------------------*/ +/* +static void* search_remote(void *args) { + raop_ctx_t *ctx = (raop_ctx_t*) args; + + query_mDNS(ctx->active_remote.handle, "_dacp._tcp.local", 0, 0, &search_remote_cb, (void*) ctx); + + return NULL; +} + */ + + /*----------------------------------------------------------------------------*/ +static char *rsa_apply(unsigned char *input, int inlen, int *outlen, int mode) +{ + static char super_secret_key[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n" + "wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n" + "wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n" + "/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n" + "UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n" + "BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n" + "LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n" + "NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n" + "lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n" + "aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n" + "a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n" + "oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n" + "oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n" + "k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n" + "AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n" + "cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n" + "54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n" + "17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n" + "1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n" + "LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n" + "2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n" + "-----END RSA PRIVATE KEY-----"; +#ifdef WIN32 + unsigned char *out; + RSA *rsa; + + BIO *bmem = BIO_new_mem_buf(super_secret_key, -1); + rsa = PEM_read_bio_RSAPrivateKey(bmem, NULL, NULL, NULL); + BIO_free(bmem); + + out = malloc(RSA_size(rsa)); + switch (mode) { + case RSA_MODE_AUTH: + *outlen = RSA_private_encrypt(inlen, input, out, rsa, + RSA_PKCS1_PADDING); + break; + case RSA_MODE_KEY: + *outlen = RSA_private_decrypt(inlen, input, out, rsa, + RSA_PKCS1_OAEP_PADDING); + break; + } + + RSA_free(rsa); + + return (char*) out; +#else + mbedtls_pk_context pkctx; + mbedtls_rsa_context *trsa; + size_t olen; + + /* + we should do entropy initialization & pass a rng function but this + consumes a ton of stack and there is no security concern here. Anyway, + mbedtls takes a lot of stack, unfortunately ... + */ + + mbedtls_pk_init(&pkctx); + mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key, + sizeof(super_secret_key), NULL, 0); + + uint8_t *outbuf = NULL; + trsa = mbedtls_pk_rsa(pkctx); + + switch (mode) { + case RSA_MODE_AUTH: + mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE); + outbuf = malloc(trsa->len); + mbedtls_rsa_pkcs1_encrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, inlen, input, outbuf); + *outlen = trsa->len; + break; + case RSA_MODE_KEY: + mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA1); + outbuf = malloc(trsa->len); + mbedtls_rsa_pkcs1_decrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, &olen, input, outbuf, trsa->len); + *outlen = olen; + break; + } + + mbedtls_pk_free(&pkctx); + + return (char*) outbuf; +#endif +} + +#define DECODE_ERROR 0xffffffff + +static char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/*----------------------------------------------------------------------------*/ +static int base64_pad(char *src, char **padded) +{ + int n; + + n = strlen(src) + strlen(src) % 4; + *padded = malloc(n + 1); + memset(*padded, '=', n); + memcpy(*padded, src, strlen(src)); + (*padded)[n] = '\0'; + + return strlen(*padded); +} + +/*----------------------------------------------------------------------------*/ +static int pos(char c) +{ + char *p; + for (p = base64_chars; *p; p++) + if (*p == c) + return p - base64_chars; + return -1; +} + +/*----------------------------------------------------------------------------*/ +static int base64_encode(const void *data, int size, char **str) +{ + char *s, *p; + int i; + int c; + const unsigned char *q; + + p = s = (char *) malloc(size * 4 / 3 + 4); + if (p == NULL) return -1; + q = (const unsigned char *) data; + i = 0; + for (i = 0; i < size;) { + c = q[i++]; + c *= 256; + if (i < size) c += q[i]; + i++; + c *= 256; + if (i < size) c += q[i]; + i++; + p[0] = base64_chars[(c & 0x00fc0000) >> 18]; + p[1] = base64_chars[(c & 0x0003f000) >> 12]; + p[2] = base64_chars[(c & 0x00000fc0) >> 6]; + p[3] = base64_chars[(c & 0x0000003f) >> 0]; + if (i > size) p[3] = '='; + if (i > size + 1) p[2] = '='; + p += 4; + } + *p = 0; + *str = s; + return strlen(s); +} + +/*----------------------------------------------------------------------------*/ +static unsigned int token_decode(const char *token) +{ + int i; + unsigned int val = 0; + int marker = 0; + if (strlen(token) < 4) + return DECODE_ERROR; + for (i = 0; i < 4; i++) { + val *= 64; + if (token[i] == '=') + marker++; + else if (marker > 0) + return DECODE_ERROR; + else + val += pos(token[i]); + } + if (marker > 2) + return DECODE_ERROR; + return (marker << 24) | val; +} + +/*----------------------------------------------------------------------------*/ +static int base64_decode(const char *str, void *data) +{ + const char *p; + unsigned char *q; + + q = data; + for (p = str; *p && (*p == '=' || strchr(base64_chars, *p)); p += 4) { + unsigned int val = token_decode(p); + unsigned int marker = (val >> 24) & 0xff; + if (val == DECODE_ERROR) + return -1; + *q++ = (val >> 16) & 0xff; + if (marker < 2) + *q++ = (val >> 8) & 0xff; + if (marker < 1) + *q++ = val & 0xff; + } + return q - (unsigned char *) data; +} + +/*----------------------------------------------------------------------------*/ +static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) { + struct metadata_s *metadata = (struct metadata_s *) ctx; + + if (!strcasecmp(code, "asar")) metadata->artist = strndup(buf, len); + else if (!strcasecmp(code, "asal")) metadata->album = strndup(buf, len); + else if (!strcasecmp(code, "minm")) metadata->title = strndup(buf, len); +} + diff --git a/components/raop/raop.h b/components/raop/raop.h new file mode 100644 index 00000000..706fa8d9 --- /dev/null +++ b/components/raop/raop.h @@ -0,0 +1,32 @@ +/* + * AirCast: Chromecast to AirPlay + * + * (c) Philippe 2016-2017, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef __RAOP_H +#define __RAOP_H + +#include "platform.h" +#include "raop_sink.h" + +struct raop_ctx_s* raop_create(struct in_addr host, char *name, unsigned char mac[6], int latency, + raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb); +void raop_delete(struct raop_ctx_s *ctx); +void raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param); + +#endif diff --git a/components/raop/raop_sink.c b/components/raop/raop_sink.c new file mode 100644 index 00000000..3e79f6aa --- /dev/null +++ b/components/raop/raop_sink.c @@ -0,0 +1,68 @@ +#include +#include +#include +#include + +#include "mdns.h" +#include "nvs.h" +#include "tcpip_adapter.h" +#include "esp_log.h" +#include "esp_console.h" +#include "esp_pthread.h" +#include "esp_system.h" +#include "freertos/timers.h" + +#include "raop.h" + +#include "log_util.h" + +#include "trace.h" + +static const char * TAG = "platform"; +extern char current_namespace[]; + +log_level raop_loglevel = lINFO; +log_level util_loglevel; + +static log_level *loglevel = &raop_loglevel; +static struct raop_ctx_s *raop; + +/**************************************************************************************** + * Airplay sink initialization + */ +void raop_sink_init(raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) { + const char *hostname; + char sink_name[64-6] = CONFIG_AIRPLAY_NAME; + nvs_handle nvs; + tcpip_adapter_ip_info_t ipInfo; + struct in_addr host; + + // get various IP info + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo); + tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &hostname); + host.s_addr = ipInfo.ip.addr; + + //initialize mDNS + ESP_ERROR_CHECK( mdns_init() ); + ESP_ERROR_CHECK( mdns_hostname_set(hostname) ); + + if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(sink_name) - 1; + nvs_get_str(nvs, "airplay_sink_name", sink_name, &len); + nvs_close(nvs); + } + + ESP_LOGI(TAG, "mdns hostname set to: [%s] with servicename %s", hostname, sink_name); + + //initialize service + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + raop = raop_create(host, sink_name, mac, 44100, cmd_cb, data_cb); +} + +/**************************************************************************************** + * Airplay local command (stop, start, volume ...) + */ +void raop_sink_cmd(raop_event_t event, void *param) { + raop_cmd(raop, event, param); +} diff --git a/components/raop/raop_sink.h b/components/raop/raop_sink.h new file mode 100644 index 00000000..b60a8ed0 --- /dev/null +++ b/components/raop/raop_sink.h @@ -0,0 +1,31 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#ifndef RAOP_SINK_H +#define RAOP_SINK_H + +#include + +typedef enum { RAOP_STREAM, RAOP_PLAY, RAOP_FLUSH, RAOP_PAUSE, RAOP_STOP, RAOP_VOLUME } raop_event_t ; + +typedef void (*raop_cmd_cb_t)(raop_event_t event, void *param); +typedef void (*raop_data_cb_t)(const u8_t *data, size_t len); + +/** + * @brief init sink mode (need to be provided) + */ + +void raop_sink_init(raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb); + +/** + * @brief init sink mode (need to be provided) + */ + +void raop_sink_cmd(raop_event_t event, void *param); + +#endif /* RAOP_SINK_H*/ \ No newline at end of file diff --git a/components/raop/rtp.c b/components/raop/rtp.c new file mode 100644 index 00000000..3ac9fae5 --- /dev/null +++ b/components/raop/rtp.c @@ -0,0 +1,893 @@ +/* + * HairTunes - RAOP packet handler and slave-clocked replay engine + * Copyright (c) James Laird 2011 + * All rights reserved. + * + * Modularisation: philippe_44@outlook.com, 2019 + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" +#include "rtp.h" +#include "log_util.h" +#include "util.h" + +#ifdef WIN32 +#include +#include "alac.h" +#else +#include "esp_pthread.h" +#include "esp_system.h" +#include +#include +//#include "alac_wrapper.h" +#include "alac.h" +#endif + +#define NTP2MS(ntp) ((((ntp) >> 10) * 1000L) >> 22) +#define MS2NTP(ms) (((((u64_t) (ms)) << 22) / 1000) << 10) +#define NTP2TS(ntp, rate) ((((ntp) >> 16) * (rate)) >> 16) +#define TS2NTP(ts, rate) (((((u64_t) (ts)) << 16) / (rate)) << 16) +#define MS2TS(ms, rate) ((((u64_t) (ms)) * (rate)) / 1000) +#define TS2MS(ts, rate) NTP2MS(TS2NTP(ts,rate)) + + #define GAP_THRES 8 + #define GAP_COUNT 20 + +extern log_level raop_loglevel; +static log_level *loglevel = &raop_loglevel; + +//#define __RTP_STORE + +// default buffer size +#define BUFFER_FRAMES (44100 / 352 + 1) +#define MAX_PACKET 1408 + +#define RTP_SYNC (0x01) +#define NTP_SYNC (0x02) + +#define RESEND_TO 200 + +enum { DATA, CONTROL, TIMING }; + +static const u8_t silence_frame[MAX_PACKET] = { 0 }; + +typedef u16_t seq_t; +typedef struct audio_buffer_entry { // decoded audio packets + int ready; + u32_t rtptime, last_resend; + s16_t *data; + int len; +} abuf_t; + +typedef struct rtp_s { +#ifdef __RTP_STORE + FILE *rtpIN, *rtpOUT; +#endif + bool running; + unsigned char aesiv[16]; +#ifdef WIN32 + AES_KEY aes; +#else + mbedtls_aes_context aes; +#endif + bool decrypt, range; + int frame_size, frame_duration; + int in_frames, out_frames; + struct in_addr host; + struct sockaddr_in rtp_host; + struct { + unsigned short rport, lport; + int sock; + } rtp_sockets[3]; // data, control, timing + struct timing_s { + bool drift; + u64_t local, remote; + u32_t count, gap_count; + s64_t gap_sum, gap_adjust; + } timing; + struct { + u32_t rtp, time; + u8_t status; + bool first, required; + } synchro; + struct { + u32_t time; + seq_t seqno; + u32_t rtptime; + } record; + int latency; // rtp hold depth in samples + u32_t resent_frames; // total recovered frames + u32_t silent_frames; // total silence frames + u32_t filled_frames; // silence frames in current silence episode + int skip; // number of frames to skip to keep sync alignement + abuf_t audio_buffer[BUFFER_FRAMES]; + seq_t ab_read, ab_write; + pthread_mutex_t ab_mutex; +#ifdef WIN32 + pthread_t rtp_thread; +#else + TaskHandle_t rtp_thread, joiner; +#endif + alac_file *alac_codec; + int flush_seqno; + bool playing; + rtp_data_cb_t callback; +} rtp_t; + + +#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) +static void buffer_alloc(abuf_t *audio_buffer, int size); +static void buffer_release(abuf_t *audio_buffer); +static void buffer_reset(abuf_t *audio_buffer); +static void buffer_push_packet(rtp_t *ctx); +static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last); +static bool rtp_request_timing(rtp_t *ctx); +static void* rtp_thread_func(void *arg); +static int seq_order(seq_t a, seq_t b); + +/*---------------------------------------------------------------------------*/ +static alac_file* alac_init(int fmtp[32]) { + alac_file *alac; + int sample_size = fmtp[3]; + + if (sample_size != 16) { + LOG_ERROR("sample size must be 16 %d", sample_size); + return false; + } + + alac = create_alac(sample_size, 2); + + if (!alac) { + LOG_ERROR("cannot create alac codec", NULL); + return NULL; + } + + alac->setinfo_max_samples_per_frame = fmtp[1]; + alac->setinfo_7a = fmtp[2]; + alac->setinfo_sample_size = sample_size; + alac->setinfo_rice_historymult = fmtp[4]; + alac->setinfo_rice_initialhistory = fmtp[5]; + alac->setinfo_rice_kmodifier = fmtp[6]; + alac->setinfo_7f = fmtp[7]; + alac->setinfo_80 = fmtp[8]; + alac->setinfo_82 = fmtp[9]; + alac->setinfo_86 = fmtp[10]; + alac->setinfo_8a_rate = fmtp[11]; + allocate_buffers(alac); + + return alac; +} + +/*---------------------------------------------------------------------------*/ +rtp_resp_t rtp_init(struct in_addr host, bool sync, bool drift, bool range, + int latency, char *aeskey, char *aesiv, char *fmtpstr, + short unsigned pCtrlPort, short unsigned pTimingPort, + rtp_data_cb_t callback) +{ + int i = 0; + char *arg; + int fmtp[12]; + bool rc = true; + rtp_t *ctx = calloc(1, sizeof(rtp_t)); + rtp_resp_t resp = { 0, 0, 0, NULL }; + + if (!ctx) return resp; + + ctx->host = host; + ctx->decrypt = false; + ctx->callback = callback; + ctx->rtp_host.sin_family = AF_INET; + ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; + pthread_mutex_init(&ctx->ab_mutex, 0); + ctx->flush_seqno = -1; + ctx->latency = latency; + ctx->synchro.required = sync; + ctx->timing.drift = drift; + ctx->range = range; + + // write pointer = last written, read pointer = next to read so fill = w-r+1 + ctx->ab_read = ctx->ab_write + 1; + +#ifdef __RTP_STORE + ctx->rtpIN = fopen("airplay.rtpin", "wb"); + ctx->rtpOUT = fopen("airplay.rtpout", "wb"); +#endif + + ctx->rtp_sockets[CONTROL].rport = pCtrlPort; + ctx->rtp_sockets[TIMING].rport = pTimingPort; + + if (aesiv && aeskey) { + memcpy(ctx->aesiv, aesiv, 16); +#ifdef WIN32 + AES_set_decrypt_key((unsigned char*) aeskey, 128, &ctx->aes); +#else + memset(&ctx->aes, 0, sizeof(mbedtls_aes_context)); + mbedtls_aes_setkey_dec(&ctx->aes, (unsigned char*) aeskey, 128); +#endif + ctx->decrypt = true; + } + + memset(fmtp, 0, sizeof(fmtp)); + while ((arg = strsep(&fmtpstr, " \t")) != NULL) fmtp[i++] = atoi(arg); + + ctx->frame_size = fmtp[1]; + ctx->frame_duration = (ctx->frame_size * 1000) / 44100; + + // alac decoder + ctx->alac_codec = alac_init(fmtp); + rc &= ctx->alac_codec != NULL; + + buffer_alloc(ctx->audio_buffer, ctx->frame_size*4); + + // create rtp ports + for (i = 0; i < 3; i++) { + ctx->rtp_sockets[i].sock = bind_socket(&ctx->rtp_sockets[i].lport, SOCK_DGRAM); + rc &= ctx->rtp_sockets[i].sock > 0; + } + + // create http port and start listening + resp.cport = ctx->rtp_sockets[CONTROL].lport; + resp.tport = ctx->rtp_sockets[TIMING].lport; + resp.aport = ctx->rtp_sockets[DATA].lport; + + if (rc) { + ctx->running = true; +#ifdef WIN32 + pthread_create(&ctx->rtp_thread, NULL, rtp_thread_func, (void *) ctx); +#else + xTaskCreate((TaskFunction_t) rtp_thread_func, "RTP_thread", 4096, ctx, configMAX_PRIORITIES - 3, &ctx->rtp_thread); +#endif + } else { + rtp_end(ctx); + ctx = NULL; + } + + resp.ctx = ctx; + + return resp; +} + +/*---------------------------------------------------------------------------*/ +void rtp_end(rtp_t *ctx) +{ + int i; + + if (!ctx) return; + + if (ctx->running) { + ctx->running = false; +#ifdef WIN32 + pthread_join(ctx->rtp_thread, NULL); +#else + ctx->joiner = xTaskGetCurrentTaskHandle(); + xTaskNotifyWait(0, 0, NULL, portMAX_DELAY); +#endif + } + + for (i = 0; i < 3; i++) shutdown_socket(ctx->rtp_sockets[i].sock); + + delete_alac(ctx->alac_codec); + + buffer_release(ctx->audio_buffer); + free(ctx); + +#ifdef __RTP_STORE + fclose(ctx->rtpIN); + fclose(ctx->rtpOUT); +#endif +} + +/*---------------------------------------------------------------------------*/ +bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime) +{ + bool rc = true; + u32_t now = gettime_ms(); + + if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { + rc = false; + LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); + } else { + pthread_mutex_lock(&ctx->ab_mutex); + buffer_reset(ctx->audio_buffer); + ctx->playing = false; + ctx->flush_seqno = seqno; + ctx->synchro.first = false; + pthread_mutex_unlock(&ctx->ab_mutex); + } + + LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); + + return rc; +} + +/*---------------------------------------------------------------------------*/ +void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) +{ + ctx->record.seqno = seqno; + ctx->record.rtptime = rtptime; + ctx->record.time = gettime_ms(); + + LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime); +} + +/*---------------------------------------------------------------------------*/ +static void buffer_alloc(abuf_t *audio_buffer, int size) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) { + audio_buffer[i].data = malloc(size); + audio_buffer[i].ready = 0; + } +} + +/*---------------------------------------------------------------------------*/ +static void buffer_release(abuf_t *audio_buffer) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) { + free(audio_buffer[i].data); + } +} + +/*---------------------------------------------------------------------------*/ +static void buffer_reset(abuf_t *audio_buffer) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) audio_buffer[i].ready = 0; +} + +/*---------------------------------------------------------------------------*/ +// the sequence numbers will wrap pretty often. +// this returns true if the second arg is after the first +static int seq_order(seq_t a, seq_t b) { + s16_t d = b - a; + return d > 0; +} + +/*---------------------------------------------------------------------------*/ +static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, int *outsize) { + unsigned char packet[MAX_PACKET]; + unsigned char iv[16]; + int aeslen; + assert(len<=MAX_PACKET); + + if (ctx->decrypt) { + aeslen = len & ~0xf; + memcpy(iv, ctx->aesiv, sizeof(iv)); +#ifdef WIN32 + AES_cbc_encrypt((unsigned char*)buf, packet, aeslen, &ctx->aes, iv, AES_DECRYPT); +#else + mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, packet); +#endif + memcpy(packet+aeslen, buf+aeslen, len-aeslen); + decode_frame(ctx->alac_codec, packet, dest, outsize); + } else decode_frame(ctx->alac_codec, (unsigned char*) buf, dest, outsize); +} + + +/*---------------------------------------------------------------------------*/ +static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { + abuf_t *abuf = NULL; + + pthread_mutex_lock(&ctx->ab_mutex); + + if (!ctx->playing) { + if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) && + ((ctx->synchro.required && ctx->synchro.first) || !ctx->synchro.required)) { + ctx->ab_write = seqno-1; + ctx->ab_read = seqno; + ctx->skip = 0; + ctx->flush_seqno = -1; + ctx->playing = true; + ctx->synchro.first = false; + ctx->resent_frames = ctx->silent_frames = 0; + } else { + pthread_mutex_unlock(&ctx->ab_mutex); + return; + } + } + + if (seqno == ctx->ab_write+1) { + // expected packet + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->ab_write = seqno; + LOG_SDEBUG("packet expected seqno:%hu rtptime:%u (W:%hu R:%hu)", seqno, rtptime, ctx->ab_write, ctx->ab_read); + } else if (seq_order(ctx->ab_write, seqno)) { + // newer than expected + if (seqno - ctx->ab_write - 1 > ctx->latency / ctx->frame_size) { + // only get rtp latency-1 frames back (last one is seqno) + LOG_WARN("[%p] too many missing frames %hu", ctx, seqno - ctx->ab_write - 1); + ctx->ab_write = seqno - ctx->latency / ctx->frame_size; + } + if (seqno - ctx->ab_read + 1 > ctx->latency / ctx->frame_size) { + // if ab_read is lagging more than http latency, advance it + LOG_WARN("[%p] on hold for too long %hu", ctx, seqno - ctx->ab_read + 1); + ctx->ab_read = seqno - ctx->latency / ctx->frame_size + 1; + } + if (rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1)) { + seq_t i; + u32_t now = gettime_ms(); + for (i = ctx->ab_write + 1; i <= seqno-1; i++) { + ctx->audio_buffer[BUFIDX(i)].rtptime = rtptime - (seqno-i)*ctx->frame_size; + ctx->audio_buffer[BUFIDX(i)].last_resend = now; + } + } + LOG_DEBUG("[%p]: packet newer seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->ab_write = seqno; + } else if (seq_order(ctx->ab_read, seqno + 1)) { + // recovered packet, not yet sent + abuf = ctx->audio_buffer + BUFIDX(seqno); + LOG_DEBUG("[%p]: packet recovered seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + } else { + // too late + LOG_DEBUG("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + } + + if (ctx->in_frames++ > 1000) { + LOG_INFO("[%p]: fill [level:%hd] [W:%hu R:%hu]", ctx, (seq_t) (ctx->ab_write - ctx->ab_read + 1), ctx->ab_write, ctx->ab_read); + ctx->in_frames = 0; + } + + if (abuf) { + alac_decode(ctx, abuf->data, data, len, &abuf->len); + abuf->ready = 1; + // this is the local rtptime when this frame is expected to play + abuf->rtptime = rtptime; +#ifdef __RTP_STORE + fwrite(data, len, 1, ctx->rtpIN); + fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT); +#endif + } + + buffer_push_packet(ctx); + + pthread_mutex_unlock(&ctx->ab_mutex); +} + +/*---------------------------------------------------------------------------*/ +// push as many frames as possible through callback +static void buffer_push_packet(rtp_t *ctx) { + abuf_t *curframe = NULL; + u32_t now, playtime; + int i; + + // not ready to play yet + if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; + + // maybe re-evaluate time in loop in case data callback blocks ... + now = gettime_ms(); + + // there is always at least one frame in the buffer + do { + + curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read); + playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp)) * 1000) / 44100; + + /* + if (now > playtime + ctx->frame_duration) { + //LOG_INFO("[%p]: discarded frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); + } else if (curframe->ready) { + ctx->callback((const u8_t*) curframe->data, curframe->len); + } else if (now >= playtime) { + LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); + ctx->callback(silence_frame, ctx->frame_size * 4); + ctx->silent_frames++; + } else break; + */ + + if (curframe->ready) { + ctx->callback((const u8_t*) curframe->data, curframe->len); + } else if (now >= playtime) { + LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); + ctx->callback(silence_frame, ctx->frame_size * 4); + ctx->silent_frames++; + } else break; + + ctx->ab_read++; + ctx->out_frames++; + + } while (ctx->ab_write - ctx->ab_read + 1 > 0); + + if (ctx->out_frames > 1000) { + LOG_INFO("[%p]: drain [level:%hd gap:%d] [W:%hu R:%hu] [R:%u S:%u F:%u]", + ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read, + ctx->resent_frames, ctx->silent_frames, ctx->filled_frames); + ctx->out_frames = 0; + } + + LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready); + + // each missing packet will be requested up to (latency_frames / 16) times + for (i = 16; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) { + abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); + if (!frame->ready && now - frame->last_resend > RESEND_TO) { + rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); + frame->last_resend = now; + } + } + } + + +/*---------------------------------------------------------------------------*/ +static void *rtp_thread_func(void *arg) { + fd_set fds; + int i, sock = -1; + int count = 0; + bool ntp_sent; + char *packet = malloc(MAX_PACKET); + rtp_t *ctx = (rtp_t*) arg; + + for (i = 0; i < 3; i++) { + if (ctx->rtp_sockets[i].sock > sock) sock = ctx->rtp_sockets[i].sock; + // send synchro requets 3 times + ntp_sent = rtp_request_timing(ctx); + } + + while (ctx->running) { + ssize_t plen; + char type; + socklen_t rtp_client_len = sizeof(struct sockaddr_storage); + int idx = 0; + char *pktp = packet; + struct timeval timeout = {0, 50*1000}; + + FD_ZERO(&fds); + for (i = 0; i < 3; i++) { FD_SET(ctx->rtp_sockets[i].sock, &fds); } + + if (select(sock + 1, &fds, NULL, NULL, &timeout) <= 0) continue; + + for (i = 0; i < 3; i++) + if (FD_ISSET(ctx->rtp_sockets[i].sock, &fds)) idx = i; + + plen = recvfrom(ctx->rtp_sockets[idx].sock, packet, MAX_PACKET, 0, (struct sockaddr*) &ctx->rtp_host, &rtp_client_len); + + if (!ntp_sent) { + LOG_WARN("[%p]: NTP request not send yet", ctx); + ntp_sent = rtp_request_timing(ctx); + } + + if (plen < 0) continue; + assert(plen <= MAX_PACKET); + + type = packet[1] & ~0x80; + pktp = packet; + + switch (type) { + seq_t seqno; + unsigned rtptime; + + // re-sent packet + case 0x56: { + pktp += 4; + plen -= 4; + } + + // data packet + case 0x60: { + seqno = ntohs(*(u16_t*)(pktp+2)); + rtptime = ntohl(*(u32_t*)(pktp+4)); + + // adjust pointer and length + pktp += 12; + plen -= 12; + + LOG_SDEBUG("[%p]: seqno:%hu rtp:%u (type: %x, first: %u)", ctx, seqno, rtptime, type, packet[1] & 0x80); + + // check if packet contains enough content to be reasonable + if (plen < 16) break; + + if ((packet[1] & 0x80) && (type != 0x56)) { + LOG_INFO("[%p]: 1st audio packet received", ctx); + } + + buffer_put_packet(ctx, seqno, rtptime, packet[1] & 0x80, pktp, plen); + + break; + } + + // sync packet + case 0x54: { + u32_t rtp_now_latency = ntohl(*(u32_t*)(pktp+4)); + u64_t remote = (((u64_t) ntohl(*(u32_t*)(pktp+8))) << 32) + ntohl(*(u32_t*)(pktp+12)); + u32_t rtp_now = ntohl(*(u32_t*)(pktp+16)); + + pthread_mutex_lock(&ctx->ab_mutex); + + // re-align timestamp and expected local playback time + if (!ctx->latency) ctx->latency = rtp_now - rtp_now_latency; + ctx->synchro.rtp = rtp_now - ctx->latency; + ctx->synchro.time = ctx->timing.local + (u32_t) NTP2MS(remote - ctx->timing.remote); + + // now we are synced on RTP frames + ctx->synchro.status |= RTP_SYNC; + + // 1st sync packet received (signals a restart of playback) + if (packet[0] & 0x10) { + ctx->synchro.first = true; + LOG_INFO("[%p]: 1st sync packet received", ctx); + } + + pthread_mutex_unlock(&ctx->ab_mutex); + + LOG_DEBUG("[%p]: sync packet rtp_latency:%u rtp:%u remote ntp:%Lx, local time %u (now:%u)", + ctx, rtp_now_latency, rtp_now, remote, ctx->synchro.time, gettime_ms()); + + if (!count--) { + rtp_request_timing(ctx); + count = 3; + } + + break; + } + + // NTP timing packet + case 0x53: { + u64_t expected; + s64_t delta = 0; + u32_t reference = ntohl(*(u32_t*)(pktp+12)); // only low 32 bits in our case + u64_t remote =(((u64_t) ntohl(*(u32_t*)(pktp+16))) << 32) + ntohl(*(u32_t*)(pktp+20)); + u32_t roundtrip = gettime_ms() - reference; + + // better discard sync packets when roundtrip is suspicious + if (roundtrip > 100) { + LOG_WARN("[%p]: discarding NTP roundtrip of %u ms", ctx, roundtrip); + break; + } + + /* + The expected elapsed remote time should be exactly the same as + elapsed local time between the two request, corrected by the + drifting + */ + expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local); + + ctx->timing.remote = remote; + ctx->timing.local = reference; + ctx->timing.count++; + + if (!ctx->timing.drift && (ctx->synchro.status & NTP_SYNC)) { + delta = NTP2MS((s64_t) expected - (s64_t) ctx->timing.remote); + ctx->timing.gap_sum += delta; + + pthread_mutex_lock(&ctx->ab_mutex); + + /* + if expected time is more than remote, then our time is + running faster and we are transmitting frames too quickly, + so we'll run out of frames, need to add one + */ + if (ctx->timing.gap_sum > GAP_THRES && ctx->timing.gap_count++ > GAP_COUNT) { + LOG_INFO("[%p]: Sending packets too fast %Ld [W:%hu R:%hu]", ctx, ctx->timing.gap_sum, ctx->ab_write, ctx->ab_read); + ctx->ab_read--; + ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = 1; + ctx->timing.gap_sum -= GAP_THRES; + ctx->timing.gap_adjust -= GAP_THRES; + /* + if expected time is less than remote, then our time is + running slower and we are transmitting frames too slowly, + so we'll overflow frames buffer, need to remove one + */ + } else if (ctx->timing.gap_sum < -GAP_THRES && ctx->timing.gap_count++ > GAP_COUNT) { + if (seq_order(ctx->ab_read, ctx->ab_write + 1)) { + ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = 0; + ctx->ab_read++; + } else ctx->skip++; + ctx->timing.gap_sum += GAP_THRES; + ctx->timing.gap_adjust += GAP_THRES; + LOG_INFO("[%p]: Sending packets too slow %Ld (skip: %d) [W:%hu R:%hu]", ctx, ctx->timing.gap_sum, ctx->skip, ctx->ab_write, ctx->ab_read); + } + + if (llabs(ctx->timing.gap_sum) < 8) ctx->timing.gap_count = 0; + + pthread_mutex_unlock(&ctx->ab_mutex); + } + + // now we are synced on NTP (mutex not needed) + ctx->synchro.status |= NTP_SYNC; + + LOG_DEBUG("[%p]: Timing references local:%Lu, remote:%Lx (delta:%Ld, sum:%Ld, adjust:%Ld, gaps:%d)", + ctx, ctx->timing.local, ctx->timing.remote, delta, ctx->timing.gap_sum, ctx->timing.gap_adjust, ctx->timing.gap_count); + + break; + } + } + } + + free(packet); + LOG_INFO("[%p]: terminating", ctx); + +#ifndef WIN32 + xTaskNotify(ctx->joiner, 0, eNoAction); + vTaskDelete(NULL); +#endif + + return NULL; +} + +/*---------------------------------------------------------------------------*/ +static bool rtp_request_timing(rtp_t *ctx) { + unsigned char req[32]; + u32_t now = gettime_ms(); + int i; + struct sockaddr_in host; + + LOG_DEBUG("[%p]: timing request now:%u (port: %hu)", ctx, now, ctx->rtp_sockets[TIMING].rport); + + req[0] = 0x80; + req[1] = 0x52|0x80; + *(u16_t*)(req+2) = htons(7); + *(u32_t*)(req+4) = htonl(0); // dummy + for (i = 0; i < 16; i++) req[i+8] = 0; + *(u32_t*)(req+24) = 0; + *(u32_t*)(req+28) = htonl(now); // this is not a real NTP, but a 32 ms counter in the low part of the NTP + + if (ctx->host.s_addr != INADDR_ANY) { + host.sin_family = AF_INET; + host.sin_addr = ctx->host; + } else host = ctx->rtp_host; + + // no address from sender, need to wait for 1st packet to be received + if (host.sin_addr.s_addr == INADDR_ANY) return false; + + host.sin_port = htons(ctx->rtp_sockets[TIMING].rport); + + if (sizeof(req) != sendto(ctx->rtp_sockets[TIMING].sock, req, sizeof(req), 0, (struct sockaddr*) &host, sizeof(host))) { + LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); + } + + return true; +} + +/*---------------------------------------------------------------------------*/ +static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last) { + unsigned char req[8]; // *not* a standard RTCP NACK + + // do not request silly ranges (happens in case of network large blackouts) + if (seq_order(last, first) || last - first > BUFFER_FRAMES / 2) return false; + + ctx->resent_frames += last - first + 1; + + LOG_DEBUG("resend request [W:%hu R:%hu first=%hu last=%hu]", ctx->ab_write, ctx->ab_read, first, last); + + req[0] = 0x80; + req[1] = 0x55|0x80; // Apple 'resend' + *(u16_t*)(req+2) = htons(1); // our seqnum + *(u16_t*)(req+4) = htons(first); // missed seqnum + *(u16_t*)(req+6) = htons(last-first+1); // count + + ctx->rtp_host.sin_port = htons(ctx->rtp_sockets[CONTROL].rport); + + if (sizeof(req) != sendto(ctx->rtp_sockets[CONTROL].sock, req, sizeof(req), 0, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) { + LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); + } + + return true; +} + + +#if 0 +/*---------------------------------------------------------------------------*/ +// get the next frame, when available. return 0 if underrun/stream reset. +static short *_buffer_get_frame(rtp_t *ctx, int *len) { + short buf_fill; + abuf_t *curframe = 0; + int i; + u32_t now, playtime; + + if (!ctx->playing) return NULL; + + // skip frames if we are running late and skip could not be done in SYNC + while (ctx->skip && seq_order(ctx->ab_read, ctx->ab_write + 1)) { + ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = 0; + ctx->ab_read++; + ctx->skip--; + LOG_INFO("[%p]: Sending packets too slow (skip: %d) [W:%hu R:%hu]", ctx, ctx->skip, ctx->ab_write, ctx->ab_read); + } + + buf_fill = ctx->ab_write - ctx->ab_read + 1; + + if (buf_fill >= BUFFER_FRAMES) { + LOG_ERROR("[%p]: Buffer overrun %hu", ctx, buf_fill); + ctx->ab_read = ctx->ab_write - (BUFFER_FRAMES - 64); + buf_fill = ctx->ab_write - ctx->ab_read + 1; + } + + now = gettime_ms(); + curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read); + + // use next frame when buffer is empty or silence continues to be sent + if (!buf_fill) curframe->rtptime = ctx->audio_buffer[BUFIDX(ctx->ab_read - 1)].rtptime + ctx->frame_size; + + playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp))*1000)/44100; + + LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready); + + // wait if not ready but have time, otherwise send silence + if (!buf_fill || ctx->synchro.status != (RTP_SYNC | NTP_SYNC) || (now < playtime && !curframe->ready)) { + LOG_SDEBUG("[%p]: waiting (fill:%hd, W:%hu R:%hu) now:%u, playtime:%u, wait:%d", ctx, buf_fill, ctx->ab_write, ctx->ab_read, now, playtime, playtime - now); + // look for "blocking" frames at the top of the queue and try to catch-up + for (i = 0; i < min(16, buf_fill); i++) { + abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); + if (!frame->ready && now - frame->last_resend > RESEND_TO) { + rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); + frame->last_resend = now; + } + } + return NULL; + } + + // when silence is inserted at the top, need to move write pointer + if (!buf_fill) { + if (!ctx->filled_frames) { + LOG_WARN("[%p]: start silence (late %d ms) [W:%hu R:%hu]", ctx, now - playtime, ctx->ab_write, ctx->ab_read); + } + ctx->ab_write++; + ctx->filled_frames++; + } else ctx->filled_frames = 0; + + if (!(ctx->out_frames++ & 0x1ff)) { + LOG_INFO("[%p]: drain [level:%hd gap:%d] [W:%hu R:%hu] [R:%u S:%u F:%u]", + ctx, buf_fill-1, playtime - now, ctx->ab_write, ctx->ab_read, + ctx->resent_frames, ctx->silent_frames, ctx->filled_frames); + } + + // each missing packet will be requested up to (latency_frames / 16) times + for (i = 16; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) { + abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); + if (!frame->ready && now - frame->last_resend > RESEND_TO) { + rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); + frame->last_resend = now; + } + } + + if (!curframe->ready) { + LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); + memset(curframe->data, 0, ctx->frame_size*4); + curframe->len = ctx->frame_size * 4; + ctx->silent_frames++; + } else { + LOG_SDEBUG("[%p]: prepared frame (fill:%hd, W:%hu R:%hu)", ctx, buf_fill-1, ctx->ab_write, ctx->ab_read); + } + + *len = curframe->len; + curframe->ready = 0; + ctx->ab_read++; + + return curframe->data; +} +#endif + + + + diff --git a/components/raop/rtp.h b/components/raop/rtp.h new file mode 100644 index 00000000..0acb8c6a --- /dev/null +++ b/components/raop/rtp.h @@ -0,0 +1,21 @@ +#ifndef _HAIRTUNES_H_ +#define _HAIRTUNES_H_ + +#include "util.h" + +typedef struct { + unsigned short cport, tport, aport; + struct rtp_s *ctx; +} rtp_resp_t; + +typedef void (*rtp_data_cb_t)(const u8_t *data, size_t len); + +rtp_resp_t rtp_init(struct in_addr host, bool sync, bool drift, bool range, int latency, + char *aeskey, char *aesiv, char *fmtpstr, + short unsigned pCtrlPort, short unsigned pTimingPort, rtp_data_cb_t data_cb); +void rtp_end(struct rtp_s *ctx); +bool rtp_flush(struct rtp_s *ctx, unsigned short seqno, unsigned rtptime); +void rtp_record(struct rtp_s *ctx, unsigned short seqno, unsigned rtptime); +void rtp_metadata(struct rtp_s *ctx, struct metadata_s *metadata); + +#endif diff --git a/components/raop/util.c b/components/raop/util.c new file mode 100644 index 00000000..38da346a --- /dev/null +++ b/components/raop/util.c @@ -0,0 +1,601 @@ +/* + * AirConnect: Chromecast & UPnP to AirPlay + * + * (c) Philippe 2016-2017, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "platform.h" + +#ifdef WIN32 +#include +#else +/* +#include +#include +#include +#include +*/ +#include +#endif + +#include + +#include "pthread.h" +#include "util.h" +#include "log_util.h" + +/*----------------------------------------------------------------------------*/ +/* globals */ +/*----------------------------------------------------------------------------*/ + +extern log_level util_loglevel; + +/*----------------------------------------------------------------------------*/ +/* locals */ +/*----------------------------------------------------------------------------*/ +static log_level *loglevel = &util_loglevel; + +static char *ltrim(char *s); +static int read_line(int fd, char *line, int maxlen, int timeout); + +/*----------------------------------------------------------------------------*/ +/* */ +/* NETWORKING utils */ +/* */ +/*----------------------------------------------------------------------------*/ + +/*---------------------------------------------------------------------------*/ +#define MAX_INTERFACES 256 +#define DEFAULT_INTERFACE 1 +#if !defined(WIN32) +#define INVALID_SOCKET (-1) +#endif +in_addr_t get_localhost(char **name) +{ +#ifdef WIN32 + char buf[256]; + struct hostent *h = NULL; + struct sockaddr_in LocalAddr; + + memset(&LocalAddr, 0, sizeof(LocalAddr)); + + gethostname(buf, 256); + h = gethostbyname(buf); + + if (name) *name = strdup(buf); + + if (h != NULL) { + memcpy(&LocalAddr.sin_addr, h->h_addr_list[0], 4); + return LocalAddr.sin_addr.s_addr; + } + else return INADDR_ANY; +#else + // missing platform here ... + return INADDR_ANY; +#endif +} + + +/*----------------------------------------------------------------------------*/ +#ifdef WIN32 +void winsock_init(void) { + WSADATA wsaData; + WORD wVersionRequested = MAKEWORD(2, 2); + int WSerr = WSAStartup(wVersionRequested, &wsaData); + if (WSerr != 0) { + LOG_ERROR("Bad winsock version", NULL); + exit(1); + } +} + +/*----------------------------------------------------------------------------*/ +void winsock_close(void) { + WSACleanup(); +} +#endif + + +/*----------------------------------------------------------------------------*/ +int shutdown_socket(int sd) +{ + if (sd <= 0) return -1; + +#ifdef WIN32 + shutdown(sd, SD_BOTH); +#else + shutdown(sd, SHUT_RDWR); +#endif + + LOG_DEBUG("closed socket %d", sd); + + return closesocket(sd); +} + + +/*----------------------------------------------------------------------------*/ +int bind_socket(unsigned short *port, int mode) +{ + int sock; + socklen_t len = sizeof(struct sockaddr); + struct sockaddr_in addr; + + if ((sock = socket(AF_INET, mode, 0)) < 0) { + LOG_ERROR("cannot create socket %d", sock); + return sock; + } + + /* Populate socket address structure */ + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(*port); +#ifdef SIN_LEN + si.sin_len = sizeof(si); +#endif + + if (bind(sock, (struct sockaddr*) &addr, sizeof(addr)) < 0) { + closesocket(sock); + LOG_ERROR("cannot bind socket %d", sock); + return -1; + } + + if (!*port) { + getsockname(sock, (struct sockaddr *) &addr, &len); + *port = ntohs(addr.sin_port); + } + + LOG_DEBUG("socket binding %d on port %d", sock, *port); + + return sock; +} + + +/*----------------------------------------------------------------------------*/ +int conn_socket(unsigned short port) +{ + struct sockaddr_in addr; + int sd; + + sd = socket(AF_INET, SOCK_STREAM, 0); + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(port); + + if (sd < 0 || connect(sd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + close(sd); + return -1; + } + + LOG_DEBUG("created socket %d", sd); + + return sd; +} + + + +/*----------------------------------------------------------------------------*/ +/* */ +/* SYSTEM utils */ +/* */ +/*----------------------------------------------------------------------------*/ + +#ifdef WIN32 +/*----------------------------------------------------------------------------*/ +void *dlopen(const char *filename, int flag) { + SetLastError(0); + return LoadLibrary((LPCTSTR)filename); +} + +/*----------------------------------------------------------------------------*/ +void *dlsym(void *handle, const char *symbol) { + SetLastError(0); + return (void *)GetProcAddress(handle, symbol); +} + +/*----------------------------------------------------------------------------*/ +char *dlerror(void) { + static char ret[32]; + int last = GetLastError(); + if (last) { + sprintf(ret, "code: %i", last); + SetLastError(0); + return ret; + } + return NULL; +} +#endif + + +/*----------------------------------------------------------------------------*/ +/* */ +/* STDLIB extensions */ +/* */ +/*----------------------------------------------------------------------------*/ + +#ifdef WIN32 +/*---------------------------------------------------------------------------*/ +char *strcasestr(const char *haystack, const char *needle) { + size_t length_needle; + size_t length_haystack; + size_t i; + + if (!haystack || !needle) + return NULL; + + length_needle = strlen(needle); + length_haystack = strlen(haystack); + + if (length_haystack < length_needle) return NULL; + + length_haystack -= length_needle - 1; + + for (i = 0; i < length_haystack; i++) + { + size_t j; + + for (j = 0; j < length_needle; j++) + { + unsigned char c1; + unsigned char c2; + + c1 = haystack[i+j]; + c2 = needle[j]; + if (toupper(c1) != toupper(c2)) + goto next; + } + return (char *) haystack + i; + next: + ; + } + + return NULL; +} + +/*---------------------------------------------------------------------------*/ +char* strsep(char** stringp, const char* delim) +{ + char* start = *stringp; + char* p; + + p = (start != NULL) ? strpbrk(start, delim) : NULL; + + if (p == NULL) { + *stringp = NULL; + } else { + *p = '\0'; + *stringp = p + 1; + } + + return start; +} + +/*---------------------------------------------------------------------------*/ +char *strndup(const char *s, size_t n) { + char *p = malloc(n + 1); + strncpy(p, s, n); + p[n] = '\0'; + + return p; +} +#endif + + +/*----------------------------------------------------------------------------*/ +char* strextract(char *s1, char *beg, char *end) +{ + char *p1, *p2, *res; + + p1 = strcasestr(s1, beg); + if (!p1) return NULL; + + p1 += strlen(beg); + p2 = strcasestr(p1, end); + if (!p2) return strdup(p1); + + res = malloc(p2 - p1 + 1); + memcpy(res, p1, p2 - p1); + res[p2 - p1] = '\0'; + + return res; +} + + +#ifdef WIN32 +/*----------------------------------------------------------------------------*/ +int asprintf(char **strp, const char *fmt, ...) +{ + va_list args, cp; + int len, ret = 0; + + va_start(args, fmt); + len = vsnprintf(NULL, 0, fmt, args); + *strp = malloc(len + 1); + + if (*strp) ret = vsprintf(*strp, fmt, args); + + va_end(args); + + return ret; +} +#endif + +/*---------------------------------------------------------------------------*/ +static char *ltrim(char *s) +{ + while(isspace((int) *s)) s++; + return s; +} + +/*----------------------------------------------------------------------------*/ +/* */ +/* HTTP management */ +/* */ +/*----------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------------*/ +bool http_parse(int sock, char *method, key_data_t *rkd, char **body, int *len) +{ + char line[256], *dp; + unsigned j; + int i, timeout = 100; + + rkd[0].key = NULL; + + if ((i = read_line(sock, line, sizeof(line), timeout)) <= 0) { + if (i < 0) { + LOG_ERROR("cannot read method", NULL); + } + return false; + } + + if (!sscanf(line, "%s", method)) { + LOG_ERROR("missing method", NULL); + return false; + } + + i = *len = 0; + + while (read_line(sock, line, sizeof(line), timeout) > 0) { + + LOG_SDEBUG("sock: %u, received %s", line); + + // line folding should be deprecated + if (i && rkd[i].key && (line[0] == ' ' || line[0] == '\t')) { + for(j = 0; j < strlen(line); j++) if (line[j] != ' ' && line[j] != '\t') break; + rkd[i].data = realloc(rkd[i].data, strlen(rkd[i].data) + strlen(line + j) + 1); + strcat(rkd[i].data, line + j); + continue; + } + + dp = strstr(line,":"); + + if (!dp){ + LOG_ERROR("Request failed, bad header", NULL); + kd_free(rkd); + return false; + } + + *dp = 0; + rkd[i].key = strdup(line); + rkd[i].data = strdup(ltrim(dp + 1)); + + if (!strcasecmp(rkd[i].key, "Content-Length")) *len = atol(rkd[i].data); + + i++; + rkd[i].key = NULL; + } + + if (*len) { + int size = 0; + + *body = malloc(*len + 1); + while (*body && size < *len) { + int bytes = recv(sock, *body + size, *len - size, 0); + if (bytes <= 0) break; + size += bytes; + } + + (*body)[*len] = '\0'; + + if (!*body || size != *len) { + LOG_ERROR("content length receive error %d %d", *len, size); + } + } + + return true; +} + + +/*----------------------------------------------------------------------------*/ +static int read_line(int fd, char *line, int maxlen, int timeout) +{ + int i,rval; + int count=0; + struct pollfd pfds; + char ch; + + *line = 0; + pfds.fd = fd; + pfds.events = POLLIN; + + for(i = 0; i < maxlen; i++){ + if (poll(&pfds, 1, timeout)) rval=recv(fd, &ch, 1, 0); + else return 0; + + if (rval == -1) { + if (errno == EAGAIN) return 0; + LOG_ERROR("fd: %d read error: %s", fd, strerror(errno)); + return -1; + } + + if (rval == 0) { + LOG_INFO("disconnected on the other end %u", fd); + return 0; + } + + if (ch == '\n') { + *line=0; + return count; + } + + if (ch=='\r') continue; + + *line++=ch; + count++; + if (count >= maxlen-1) break; + } + + *line = 0; + return count; +} + + +/*----------------------------------------------------------------------------*/ +char *http_send(int sock, char *method, key_data_t *rkd) +{ + unsigned sent, len; + char *resp = kd_dump(rkd); + char *data = malloc(strlen(method) + 2 + strlen(resp) + 2 + 1); + + len = sprintf(data, "%s\r\n%s\r\n", method, resp); + NFREE(resp); + + sent = send(sock, data, len, 0); + + if (sent != len) { + LOG_ERROR("HTTP send() error:%s %u (strlen=%u)", data, sent, len); + NFREE(data); + } + + return data; +} + + +/*----------------------------------------------------------------------------*/ +char *kd_lookup(key_data_t *kd, char *key) +{ + int i = 0; + while (kd && kd[i].key){ + if (!strcasecmp(kd[i].key, key)) return kd[i].data; + i++; + } + return NULL; +} + + +/*----------------------------------------------------------------------------*/ +bool kd_add(key_data_t *kd, char *key, char *data) +{ + int i = 0; + while (kd && kd[i].key) i++; + + kd[i].key = strdup(key); + kd[i].data = strdup(data); + kd[i+1].key = NULL; + + return NULL; +} + + +/*----------------------------------------------------------------------------*/ +void kd_free(key_data_t *kd) +{ + int i = 0; + while (kd && kd[i].key){ + free(kd[i].key); + if (kd[i].data) free(kd[i].data); + i++; + } + + kd[0].key = NULL; +} + + +/*----------------------------------------------------------------------------*/ +char *kd_dump(key_data_t *kd) +{ + int i = 0; + int pos = 0, size = 0; + char *str = NULL; + + if (!kd || !kd[0].key) return strdup("\r\n"); + + while (kd && kd[i].key) { + char *buf; + int len; + + len = asprintf(&buf, "%s: %s\r\n", kd[i].key, kd[i].data); + + while (pos + len >= size) { + void *p = realloc(str, size + 1024); + size += 1024; + if (!p) { + free(str); + return NULL; + } + str = p; + } + + memcpy(str + pos, buf, len); + + pos += len; + free(buf); + i++; + } + + str[pos] = '\0'; + + return str; +} + +/*--------------------------------------------------------------------------*/ +void free_metadata(struct metadata_s *metadata) +{ + NFREE(metadata->artist); + NFREE(metadata->album); + NFREE(metadata->title); + NFREE(metadata->genre); + NFREE(metadata->path); + NFREE(metadata->artwork); + NFREE(metadata->remote_title); +} + + /*----------------------------------------------------------------------------*/ + +int _fprintf(FILE *file, ...) +{ + va_list args; + char *fmt; + int n; + + va_start(args, file); + fmt = va_arg(args, char*); + + n = vfprintf(file, fmt, args); + va_end(args); + return n; +} + + + + + + + + + diff --git a/components/raop/util.h b/components/raop/util.h new file mode 100644 index 00000000..3634076e --- /dev/null +++ b/components/raop/util.h @@ -0,0 +1,85 @@ +/* + * Misc utilities + * + * (c) Adrian Smith 2012-2014, triode1@btinternet.com + * (c) Philippe 2016-2017, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef __UTIL_H +#define __UTIL_H + +#include +#include +#include + +#include "platform.h" +#include "pthread.h" + +#define NFREE(p) if (p) { free(p); p = NULL; } + +typedef struct metadata_s { + char *artist; + char *album; + char *title; + char *genre; + char *path; + char *artwork; + char *remote_title; + u32_t track; + u32_t duration; + u32_t track_hash; + u32_t sample_rate; + u8_t sample_size; + u8_t channels; +} metadata_t; + +/* +void free_metadata(struct metadata_s *metadata); +void dup_metadata(struct metadata_s *dst, struct metadata_s *src); +*/ + + u32_t gettime_ms(void); + +#ifdef WIN32 +char* strsep(char** stringp, const char* delim); +char *strndup(const char *s, size_t n); +int asprintf(char **strp, const char *fmt, ...); +void winsock_init(void); +void winsock_close(void); + #else +char *strlwr(char *str); +#endif +char* strextract(char *s1, char *beg, char *end); +in_addr_t get_localhost(char **name); +void get_mac(u8_t mac[]); +int shutdown_socket(int sd); +int bind_socket(short unsigned *port, int mode); +int conn_socket(unsigned short port); typedef struct { + char *key; + char *data; +} key_data_t; + +bool http_parse(int sock, char *method, key_data_t *rkd, char **body, int *len); char* http_send(int sock, char *method, key_data_t *rkd); + +char* kd_lookup(key_data_t *kd, char *key); +bool kd_add(key_data_t *kd, char *key, char *value); +char* kd_dump(key_data_t *kd); +void kd_free(key_data_t *kd); + +int _fprintf(FILE *file, ...); + #endif + diff --git a/components/squeezelite/component.mk b/components/squeezelite/component.mk index 6452d246..0d60996d 100644 --- a/components/squeezelite/component.mk +++ b/components/squeezelite/component.mk @@ -14,7 +14,7 @@ CFLAGS += -O3 -DLINKALL -DLOOPBACK -DNO_FAAD -DRESAMPLE16 -DEMBEDDED -DTREMOR_ON -I$(COMPONENT_PATH)/../codecs/inc/opus \ -I$(COMPONENT_PATH)/../codecs/inc/opusfile \ -I$(COMPONENT_PATH)/../driver_bt \ - -I$(COMPONENT_PATH)/../airplay + -I$(COMPONENT_PATH)/../raop # -I$(COMPONENT_PATH)/../codecs/inc/faad2 diff --git a/components/squeezelite/decode_external.c b/components/squeezelite/decode_external.c index 6cf83fbc..7e720e7d 100644 --- a/components/squeezelite/decode_external.c +++ b/components/squeezelite/decode_external.c @@ -21,7 +21,7 @@ #include "squeezelite.h" #include "bt_app_sink.h" -#include "airplay_sink.h" +#include "raop_sink.h" #define LOCK_O mutex_lock(outputbuf->mutex) #define UNLOCK_O mutex_unlock(outputbuf->mutex) @@ -34,16 +34,19 @@ extern struct buffer *outputbuf; // this is the only system-wide loglevel variable extern log_level loglevel; +static raop_event_t raop_state; +static bool raop_expect_stop = false; + /**************************************************************************************** - * BT sink data handler + * Common sink data handler */ -static void bt_sink_data_handler(const uint8_t *data, uint32_t len) +static void sink_data_handler(const uint8_t *data, uint32_t len) { - size_t bytes; - + size_t bytes, space; + // would be better to lock decoder, but really, it does not matter if (decode.state != DECODE_STOPPED) { - LOG_SDEBUG("Cannot use BT sink while LMS is controlling player"); + LOG_SDEBUG("Cannot use external sink while LMS is controlling player"); return; } @@ -63,13 +66,15 @@ static void bt_sink_data_handler(const uint8_t *data, uint32_t len) } #endif _buf_inc_writep(outputbuf, bytes); + space = _buf_space(outputbuf); + len -= bytes; data += bytes; UNLOCK_O; // allow i2s to empty the buffer if needed - if (len) usleep(50000); + if (len && !space) usleep(50000); } } @@ -81,6 +86,7 @@ static void bt_sink_cmd_handler(bt_sink_cmd_t cmd, ...) va_list args; LOCK_D; + if (decode.state != DECODE_STOPPED) { LOG_WARN("Cannot use BT sink while LMS is controlling player"); UNLOCK_D; @@ -128,14 +134,78 @@ static void bt_sink_cmd_handler(bt_sink_cmd_t cmd, ...) va_end(args); } - + +/**************************************************************************************** + * AirPlay sink command handler + */ +void raop_sink_cmd_handler(raop_event_t event, void *param) +{ + LOCK_D; + + if (decode.state != DECODE_STOPPED) { + LOG_WARN("Cannot use Airplay sink while LMS is controlling player"); + UNLOCK_D; + return; + } + + if (event != RAOP_VOLUME) LOCK_O; + + // this is async, so player might have been deleted + switch (event) { + case RAOP_STREAM: + // a PLAY will come later, so we'll do the load at that time + LOG_INFO("Stream", NULL); + raop_state = event; + output.external = true; + output.current_sample_rate = 44100; + output.state = OUTPUT_BUFFER; + output.threshold = 5; + break; + case RAOP_STOP: + LOG_INFO("Stop", NULL); + output.external = false; + output.state = OUTPUT_OFF; + raop_state = event; + break; + case RAOP_FLUSH: + LOG_INFO("Flush", NULL); + raop_expect_stop = true; + raop_state = event; + output.state = OUTPUT_STOPPED; + break; + case RAOP_PLAY: { + LOG_INFO("Play", NULL); + // this where we need the OUTPUT_START_AT + if (raop_state != RAOP_PLAY) { + output.external = true; + output.state = OUTPUT_RUNNING; + } + raop_state = event; + break; + } + case RAOP_VOLUME: { + float volume = *((float*) param); + LOG_INFO("Volume[0..1] %0.4f", volume); + volume *= 65536; + set_volume((u16_t) volume, (u16_t) volume); + break; + } + default: + break; + } + + if (event != RAOP_VOLUME) UNLOCK_O; + + UNLOCK_D; +} + /**************************************************************************************** * We provide the generic codec register option */ void register_other(void) { #ifdef CONFIG_BT_SINK if (!strcasestr(output.device, "BT ")) { - bt_sink_init(bt_sink_cmd_handler, bt_sink_data_handler); + bt_sink_init(bt_sink_cmd_handler, sink_data_handler); LOG_INFO("Initializing BT sink"); } else { LOG_WARN("Cannot be a BT sink and source"); @@ -143,7 +213,7 @@ void register_other(void) { #endif #ifdef CONFIG_AIRPLAY_SINK if (!strcasestr(output.device, "BT ")) { - airplay_sink_init(); + raop_sink_init(raop_sink_cmd_handler, sink_data_handler); LOG_INFO("Initializing AirPlay sink"); } else { LOG_WARN("Cannot be an AirPlay sink and BT source");