mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2025-12-11 14:07:11 +03:00
refactoring step 3 - components
squeezelite is a component platform_esp32 is the main
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
set(COMPONENT_ADD_INCLUDEDIRS . )
|
||||
|
||||
set(COMPONENT_SRCS "esp_app_main.c" "platform_esp32.c" "bt_app_core.c" "cmd_wifi.c" "console.c" "nvs_utilities.c" "cmd_squeezelite.c")
|
||||
set(COMPONENT_SRCS "bt_app_core.c" )
|
||||
set(REQUIRES esp_common)
|
||||
set(REQUIRES_COMPONENTS freertos nvs_flash esp32 spi_flash newlib log console )
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
#include "argtable3/argtable3.h"
|
||||
|
||||
#include "bt_app_core.h"
|
||||
|
||||
#include "platform_esp32.h"
|
||||
#include "trace.h"
|
||||
|
||||
static const char * TAG = "platform";
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
# lib(subdirectory_name).a in the build directory. This behaviour is entirely configurable,
|
||||
# please read the SDK documents if you need to do this.
|
||||
#
|
||||
CFLAGS += -D LOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
||||
|
||||
CFLAGS += -I$(COMPONENT_PATH)/../tools
|
||||
#CFLAGS += -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
||||
@@ -1,22 +0,0 @@
|
||||
/* Console example — declarations of command registration functions.
|
||||
|
||||
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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "cmd_system.h"
|
||||
#include "cmd_wifi.h"
|
||||
#include "cmd_nvs.h"
|
||||
#include "cmd_i2ctools.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,129 +0,0 @@
|
||||
//size_t esp_console_split_argv(char *line, char **argv, size_t argv_size);
|
||||
#include "cmd_squeezelite.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "cmd_decl.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_pthread.h"
|
||||
#include "argtable3/argtable3.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "pthread.h"
|
||||
#include "platform_esp32.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
//extern char current_namespace[];
|
||||
static const char * TAG = "squeezelite_cmd";
|
||||
#define SQUEEZELITE_THREAD_STACK_SIZE 8192
|
||||
extern int main(int argc, char **argv);
|
||||
static int launchsqueezelite(int argc, char **argv);
|
||||
pthread_t thread_squeezelite;
|
||||
pthread_t thread_squeezelite_runner;
|
||||
/** Arguments used by 'squeezelite' function */
|
||||
static struct {
|
||||
struct arg_str *parameters;
|
||||
struct arg_end *end;
|
||||
} squeezelite_args;
|
||||
static struct {
|
||||
int argc;
|
||||
char ** argv;
|
||||
} thread_parms ;
|
||||
static void * squeezelite_runner_thread(){
|
||||
ESP_LOGI(TAG ,"Calling squeezelite");
|
||||
main(thread_parms.argc,thread_parms.argv);
|
||||
return NULL;
|
||||
}
|
||||
#define ADDITIONAL_SQUEEZELILTE_ARGS 5
|
||||
static void * squeezelite_thread(){
|
||||
int * exit_code;
|
||||
static bool isRunning=false;
|
||||
if(isRunning) {
|
||||
ESP_LOGE(TAG,"Squeezelite already running. Exiting!");
|
||||
return NULL;
|
||||
}
|
||||
isRunning=true;
|
||||
ESP_LOGI(TAG,"Waiting for WiFi.");
|
||||
while(!wait_for_wifi()){usleep(100000);};
|
||||
ESP_LOGD(TAG ,"Number of args received: %u",thread_parms.argc );
|
||||
ESP_LOGD(TAG ,"Values:");
|
||||
for(int i = 0;i<thread_parms.argc; i++){
|
||||
ESP_LOGD(TAG ," %s",thread_parms.argv[i]);
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG,"Starting Squeezelite runner Thread");
|
||||
esp_pthread_cfg_t cfg = esp_pthread_get_default_config();
|
||||
cfg.thread_name= "squeezelite-run";
|
||||
cfg.inherit_cfg = true;
|
||||
cfg.stack_size = SQUEEZELITE_THREAD_STACK_SIZE ;
|
||||
esp_pthread_set_cfg(&cfg);
|
||||
// no attribute if we want esp stack stack to prevail
|
||||
pthread_create(&thread_squeezelite_runner, NULL, squeezelite_runner_thread,NULL);
|
||||
// Wait for thread completion so we can free up memory.
|
||||
pthread_join(thread_squeezelite_runner,(void **)&exit_code);
|
||||
|
||||
ESP_LOGV(TAG ,"Exited from squeezelite's main(). Freeing argv structure.");
|
||||
for(int i=0;i<thread_parms.argc;i++){
|
||||
ESP_LOGV(TAG ,"Freeing char buffer for parameter %u", i+1);
|
||||
free(thread_parms.argv[i]);
|
||||
}
|
||||
ESP_LOGV(TAG ,"Freeing argv pointer");
|
||||
free(thread_parms.argv);
|
||||
isRunning=false;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int launchsqueezelite(int argc, char **argv)
|
||||
{
|
||||
ESP_LOGV(TAG ,"Begin");
|
||||
|
||||
ESP_LOGD(TAG, "Parameters:");
|
||||
for(int i = 0;i<argc; i++){
|
||||
ESP_LOGD(TAG, " %s",argv[i]);
|
||||
}
|
||||
ESP_LOGV(TAG,"Saving args in thread structure");
|
||||
|
||||
thread_parms.argc=0;
|
||||
thread_parms.argv = malloc(sizeof(char**)*(argc+ADDITIONAL_SQUEEZELILTE_ARGS));
|
||||
memset(thread_parms.argv,'\0',sizeof(char**)*(argc+ADDITIONAL_SQUEEZELILTE_ARGS));
|
||||
|
||||
for(int i=0;i<argc;i++){
|
||||
ESP_LOGD(TAG ,"assigning parm %u : %s",i,argv[i]);
|
||||
thread_parms.argv[thread_parms.argc++]=strdup(argv[i]);
|
||||
}
|
||||
|
||||
if(argc==1){
|
||||
// There isn't a default configuration that would actually work
|
||||
// if no parameter is passed.
|
||||
ESP_LOGV(TAG ,"Adding argv value at %u. Prev value: %s",thread_parms.argc,thread_parms.argv[thread_parms.argc-1]);
|
||||
thread_parms.argv[thread_parms.argc++]=strdup("-?");
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG,"Starting Squeezelite Thread");
|
||||
esp_pthread_cfg_t cfg = esp_pthread_get_default_config();
|
||||
cfg.thread_name= "squeezelite";
|
||||
cfg.inherit_cfg = true;
|
||||
esp_pthread_set_cfg(&cfg);
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_create(&thread_squeezelite, &attr, squeezelite_thread,NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
ESP_LOGD(TAG ,"Back to console thread!");
|
||||
return 0;
|
||||
}
|
||||
void register_squeezelite(){
|
||||
|
||||
squeezelite_args.parameters = arg_str0(NULL, NULL, "<parms>", "command line for squeezelite. -h for help, --defaults to launch with default values.");
|
||||
squeezelite_args.end = arg_end(1);
|
||||
const esp_console_cmd_t launch_squeezelite = {
|
||||
.command = "squeezelite",
|
||||
.help = "Starts squeezelite",
|
||||
.hint = NULL,
|
||||
.func = &launchsqueezelite,
|
||||
.argtable = &squeezelite_args
|
||||
};
|
||||
ESP_ERROR_CHECK( esp_console_cmd_register(&launch_squeezelite) );
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Register WiFi functions
|
||||
void register_squeezelite();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
/* Console example — WiFi commands
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#include "cmd_wifi.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cmd_decl.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_console.h"
|
||||
#include "argtable3/argtable3.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "tcpip_adapter.h"
|
||||
#include "esp_event.h"
|
||||
|
||||
#define JOIN_TIMEOUT_MS (10000)
|
||||
|
||||
static EventGroupHandle_t wifi_event_group;
|
||||
const int CONNECTED_BIT = BIT0;
|
||||
static const char * TAG = "cmd_wifi";
|
||||
/** Arguments used by 'join' function */
|
||||
static struct {
|
||||
struct arg_int *timeout;
|
||||
struct arg_str *ssid;
|
||||
struct arg_str *password;
|
||||
struct arg_end *end;
|
||||
} join_args;
|
||||
|
||||
///** Arguments used by 'join' function */
|
||||
//static struct {
|
||||
// struct arg_int *autoconnect;
|
||||
// struct arg_end *end;
|
||||
//} auto_connect_args;
|
||||
|
||||
static void event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
esp_wifi_connect();
|
||||
xEventGroupClearBits(wifi_event_group, CONNECTED_BIT);
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
xEventGroupSetBits(wifi_event_group, CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
bool wait_for_wifi(){
|
||||
|
||||
bool connected=(xEventGroupGetBits(wifi_event_group) & CONNECTED_BIT)!=0;
|
||||
|
||||
if(!connected){
|
||||
ESP_LOGD(TAG,"Waiting for WiFi...");
|
||||
connected = (xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT,
|
||||
pdFALSE, pdTRUE, JOIN_TIMEOUT_MS / portTICK_PERIOD_MS)& CONNECTED_BIT)!=0;
|
||||
if(!connected){
|
||||
ESP_LOGD(TAG,"wifi timeout.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG,"WiFi Connected!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return connected;
|
||||
|
||||
}
|
||||
static void initialise_wifi(void)
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
tcpip_adapter_init();
|
||||
wifi_event_group = xEventGroupCreate();
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
|
||||
ESP_ERROR_CHECK( esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &event_handler, NULL) );
|
||||
ESP_ERROR_CHECK( esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL) );
|
||||
ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
|
||||
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_NULL) );
|
||||
ESP_ERROR_CHECK( esp_wifi_start() );
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
static bool wifi_join(const char *ssid, const char *pass, int timeout_ms)
|
||||
{
|
||||
initialise_wifi();
|
||||
wifi_config_t wifi_config = { 0 };
|
||||
strncpy((char *) wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
|
||||
if (pass) {
|
||||
strncpy((char *) wifi_config.sta.password, pass, sizeof(wifi_config.sta.password));
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
|
||||
ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) );
|
||||
ESP_ERROR_CHECK( esp_wifi_connect() );
|
||||
|
||||
int bits = xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT,
|
||||
pdFALSE, pdTRUE, timeout_ms / portTICK_PERIOD_MS);
|
||||
return (bits & CONNECTED_BIT) != 0;
|
||||
}
|
||||
|
||||
|
||||
static int set_auto_connect(int argc, char **argv)
|
||||
{
|
||||
// int nerrors = arg_parse(argc, argv, (void **) &join_args);
|
||||
// if (nerrors != 0) {
|
||||
// arg_print_errors(stderr, join_args.end, argv[0]);
|
||||
// return 1;
|
||||
// }
|
||||
// ESP_LOGI(__func__, "Connecting to '%s'",
|
||||
// join_args.ssid->sval[0]);
|
||||
//
|
||||
// /* set default value*/
|
||||
// if (join_args.timeout->count == 0) {
|
||||
// join_args.timeout->ival[0] = JOIN_TIMEOUT_MS;
|
||||
// }
|
||||
//
|
||||
// bool connected = wifi_join(join_args.ssid->sval[0],
|
||||
// join_args.password->sval[0],
|
||||
// join_args.timeout->ival[0]);
|
||||
// if (!connected) {
|
||||
// ESP_LOGW(__func__, "Connection timed out");
|
||||
// return 1;
|
||||
// }
|
||||
// ESP_LOGI(__func__, "Connected");
|
||||
return 0;
|
||||
}
|
||||
static int connect(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **) &join_args);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, join_args.end, argv[0]);
|
||||
return 1;
|
||||
}
|
||||
ESP_LOGI(__func__, "Connecting to '%s'",
|
||||
join_args.ssid->sval[0]);
|
||||
|
||||
/* set default value*/
|
||||
if (join_args.timeout->count == 0) {
|
||||
join_args.timeout->ival[0] = JOIN_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
bool connected = wifi_join(join_args.ssid->sval[0],
|
||||
join_args.password->sval[0],
|
||||
join_args.timeout->ival[0]);
|
||||
if (!connected) {
|
||||
ESP_LOGW(__func__, "Connection timed out");
|
||||
return 1;
|
||||
}
|
||||
ESP_LOGI(__func__, "Connected");
|
||||
return 0;
|
||||
}
|
||||
void register_wifi_join()
|
||||
{
|
||||
join_args.timeout = arg_int0(NULL, "timeout", "<t>", "Connection timeout, ms");
|
||||
join_args.ssid = arg_str1(NULL, NULL, "<ssid>", "SSID of AP");
|
||||
join_args.password = arg_str0(NULL, NULL, "<pass>", "PSK of AP");
|
||||
join_args.end = arg_end(2);
|
||||
|
||||
const esp_console_cmd_t join_cmd = {
|
||||
.command = "join",
|
||||
.help = "Join WiFi AP as a station",
|
||||
.hint = NULL,
|
||||
.func = &connect,
|
||||
.argtable = &join_args
|
||||
};
|
||||
ESP_ERROR_CHECK( esp_console_cmd_register(&join_cmd) );
|
||||
}
|
||||
|
||||
void register_wifi()
|
||||
{
|
||||
register_wifi_join();
|
||||
initialise_wifi();
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/* Console example — declarations of command registration functions.
|
||||
|
||||
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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Register WiFi functions
|
||||
void register_wifi();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
/* Console example
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_vfs_dev.h"
|
||||
#include "driver/uart.h"
|
||||
#include "linenoise/linenoise.h"
|
||||
#include "argtable3/argtable3.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "pthread.h"
|
||||
#include "platform_esp32.h"
|
||||
#include "cmd_decl.h"
|
||||
#include "console.h"
|
||||
|
||||
#include "cmd_squeezelite.h"
|
||||
#include "nvs_utilities.h"
|
||||
pthread_t thread_console;
|
||||
static void * console_thread();
|
||||
void console_start();
|
||||
static const char * TAG = "console";
|
||||
|
||||
/* Prompt to be printed before each line.
|
||||
* This can be customized, made dynamic, etc.
|
||||
*/
|
||||
const char* prompt = LOG_COLOR_I "squeezelite-esp32> " LOG_RESET_COLOR;
|
||||
|
||||
/* Console command history can be stored to and loaded from a file.
|
||||
* The easiest way to do this is to use FATFS filesystem on top of
|
||||
* wear_levelling library.
|
||||
*/
|
||||
|
||||
#define MOUNT_PATH "/data"
|
||||
#define HISTORY_PATH MOUNT_PATH "/history.txt"
|
||||
void run_command(char * line);
|
||||
//optListStruct * getOptionByName(char * option){
|
||||
// optListStruct * curOpt=&optList[0];
|
||||
// while(curOpt->optName !=NULL){
|
||||
// if(!strcmp(curOpt->optName, option)){
|
||||
// return curOpt;
|
||||
// }
|
||||
// curOpt++;
|
||||
// }
|
||||
// return NULL;
|
||||
//}
|
||||
//
|
||||
//static int list_options(int argc, char **argv)
|
||||
//{
|
||||
// nvs_handle nvs;
|
||||
// esp_err_t err;
|
||||
//
|
||||
// err = nvs_open(current_namespace, NVS_READONLY, &nvs);
|
||||
// if (err != ESP_OK) {
|
||||
// return err;
|
||||
// }
|
||||
// //
|
||||
// optListStruct * curOpt=&optList[0];
|
||||
// printf("System Configuration Options.\n");
|
||||
// while(curOpt->optName!=NULL){
|
||||
// printf("Option: %s\n"
|
||||
// " Description: %20s\n"
|
||||
// " Default Value: %20s\n"
|
||||
// " Current Value: ",curOpt->optName, curOpt->description, curOpt->defaultValue);
|
||||
// size_t len;
|
||||
// if ( (nvs_get_str(nvs, curOpt->optName, NULL, &len)) == ESP_OK) {
|
||||
// char *str = (char *)malloc(len);
|
||||
// if ( (nvs_get_str(nvs, curOpt->optName, str, &len)) == ESP_OK) {
|
||||
// printf("%20s\n", str);
|
||||
// }
|
||||
// free(str);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if(store_nvs_value(NVS_TYPE_STR, curOpt->optName,curOpt->defaultValue, strlen(curOpt->defaultValue))==ESP_OK)
|
||||
// {
|
||||
// printf("%20s\n", curOpt->defaultValue);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// printf("Error. Invalid key\n");
|
||||
// }
|
||||
// }
|
||||
// curOpt++;
|
||||
// }
|
||||
// printf("\n");
|
||||
// nvs_close(nvs);
|
||||
// return 0;
|
||||
//}
|
||||
//void register_list_options(){
|
||||
// const esp_console_cmd_t config_list = {
|
||||
// .command = "config-list",
|
||||
// .help = "Lists available configuration options.",
|
||||
// .hint = NULL,
|
||||
// .func = &list_options,
|
||||
// .argtable = NULL
|
||||
// };
|
||||
//
|
||||
// ESP_ERROR_CHECK( esp_console_cmd_register(&config_list) );
|
||||
//
|
||||
//}
|
||||
|
||||
void process_autoexec(){
|
||||
int i=1;
|
||||
char autoexec_name[21]={0};
|
||||
char * autoexec_value=NULL;
|
||||
uint8_t * autoexec_flag=NULL;
|
||||
|
||||
autoexec_flag = get_nvs_value_alloc(NVS_TYPE_U8, "autoexec");
|
||||
|
||||
if(autoexec_flag!=NULL ){
|
||||
ESP_LOGI(TAG,"autoexec flag value found with value %u", *autoexec_flag);
|
||||
if(*autoexec_flag == 1) {
|
||||
do {
|
||||
snprintf(autoexec_name,sizeof(autoexec_name)-1,"autoexec%u",i++);
|
||||
ESP_LOGD(TAG,"Getting command name %s", autoexec_name);
|
||||
autoexec_value= get_nvs_value_alloc(NVS_TYPE_STR, autoexec_name);
|
||||
if(autoexec_value!=NULL ){
|
||||
ESP_LOGI(TAG,"Running command %s = %s", autoexec_name, autoexec_value);
|
||||
run_command(autoexec_value);
|
||||
ESP_LOGD(TAG,"Freeing memory for command %s name", autoexec_name);
|
||||
free(autoexec_value);
|
||||
}
|
||||
else {
|
||||
ESP_LOGD(TAG,"No matching command found for name %s", autoexec_name);
|
||||
break;
|
||||
}
|
||||
} while(1);
|
||||
}
|
||||
free(autoexec_flag);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGD(TAG,"No matching command found for name autoexec. Adding default entries");
|
||||
uint8_t autoexec_dft=0;
|
||||
char autoexec1_dft[]="join MySSID MyPASSWORD";
|
||||
char autoexec2_dft[]="squeezelite -o \"DAC\" -b 500:2000 -d all=debug -M esp32 -r \"44100,4800\" -N \"playername.txt\"";
|
||||
store_nvs_value(NVS_TYPE_U8,"autoexec",&autoexec_dft);
|
||||
store_nvs_value(NVS_TYPE_STR,"autoexec1",autoexec1_dft);
|
||||
store_nvs_value(NVS_TYPE_STR,"autoexec2",autoexec2_dft);
|
||||
}
|
||||
}
|
||||
static void initialize_filesystem() {
|
||||
static wl_handle_t wl_handle;
|
||||
const esp_vfs_fat_mount_config_t mount_config = {
|
||||
.max_files = 10,
|
||||
.format_if_mount_failed = true,
|
||||
.allocation_unit_size = 4096
|
||||
};
|
||||
esp_err_t err = esp_vfs_fat_spiflash_mount(MOUNT_PATH, "storage",
|
||||
&mount_config, &wl_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void initialize_nvs() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
}
|
||||
|
||||
void initialize_console() {
|
||||
|
||||
/* Disable buffering on stdin */
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
|
||||
/* Minicom, screen, idf_monitor send CR when ENTER key is pressed */
|
||||
esp_vfs_dev_uart_set_rx_line_endings(ESP_LINE_ENDINGS_CR);
|
||||
/* Move the caret to the beginning of the next line on '\n' */
|
||||
esp_vfs_dev_uart_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF);
|
||||
|
||||
/* Configure UART. Note that REF_TICK is used so that the baud rate remains
|
||||
* correct while APB frequency is changing in light sleep mode.
|
||||
*/
|
||||
const uart_config_t uart_config = { .baud_rate =
|
||||
CONFIG_CONSOLE_UART_BAUDRATE, .data_bits = UART_DATA_8_BITS,
|
||||
.parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1,
|
||||
.use_ref_tick = true };
|
||||
ESP_ERROR_CHECK(uart_param_config(CONFIG_CONSOLE_UART_NUM, &uart_config));
|
||||
|
||||
/* Install UART driver for interrupt-driven reads and writes */
|
||||
ESP_ERROR_CHECK(
|
||||
uart_driver_install(CONFIG_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0));
|
||||
|
||||
/* Tell VFS to use UART driver */
|
||||
esp_vfs_dev_uart_use_driver(CONFIG_CONSOLE_UART_NUM);
|
||||
|
||||
/* Initialize the console */
|
||||
esp_console_config_t console_config = { .max_cmdline_args = 22,
|
||||
.max_cmdline_length = 256,
|
||||
#if CONFIG_LOG_COLORS
|
||||
.hint_color = atoi(LOG_COLOR_CYAN)
|
||||
#endif
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_console_init(&console_config));
|
||||
|
||||
/* Configure linenoise line completion library */
|
||||
/* Enable multiline editing. If not set, long commands will scroll within
|
||||
* single line.
|
||||
*/
|
||||
linenoiseSetMultiLine(1);
|
||||
|
||||
/* Tell linenoise where to get command completions and hints */
|
||||
linenoiseSetCompletionCallback(&esp_console_get_completion);
|
||||
linenoiseSetHintsCallback((linenoiseHintsCallback*) &esp_console_get_hint);
|
||||
|
||||
/* Set command history size */
|
||||
linenoiseHistorySetMaxLen(100);
|
||||
|
||||
/* Load command history from filesystem */
|
||||
linenoiseHistoryLoad(HISTORY_PATH);
|
||||
}
|
||||
|
||||
void console_start() {
|
||||
initialize_nvs();
|
||||
initialize_filesystem();
|
||||
initialize_console();
|
||||
|
||||
/* Register commands */
|
||||
esp_console_register_help_command();
|
||||
register_system();
|
||||
register_wifi();
|
||||
register_nvs();
|
||||
register_squeezelite();
|
||||
register_i2ctools();
|
||||
printf("\n"
|
||||
"Type 'help' to get the list of commands.\n"
|
||||
"Use UP/DOWN arrows to navigate through command history.\n"
|
||||
"Press TAB when typing command name to auto-complete.\n"
|
||||
"\n"
|
||||
"To automatically execute lines at startup:\n"
|
||||
"\tSet NVS variable autoexec (U8) = 1 to enable, 0 to disable automatic execution.\n"
|
||||
"\tSet NVS variable autoexec[1~9] (string)to a command that should be executed automatically\n"
|
||||
"\n"
|
||||
"\n");
|
||||
|
||||
/* Figure out if the terminal supports escape sequences */
|
||||
int probe_status = linenoiseProbe();
|
||||
if (probe_status) { /* zero indicates success */
|
||||
printf("\n****************************\n"
|
||||
"Your terminal application does not support escape sequences.\n"
|
||||
"Line editing and history features are disabled.\n"
|
||||
"On Windows, try using Putty instead.\n"
|
||||
"****************************\n");
|
||||
linenoiseSetDumbMode(1);
|
||||
#if CONFIG_LOG_COLORS
|
||||
/* Since the terminal doesn't support escape sequences,
|
||||
* don't use color codes in the prompt.
|
||||
*/
|
||||
prompt = "squeezelite-esp32> ";
|
||||
#endif //CONFIG_LOG_COLORS
|
||||
|
||||
}
|
||||
esp_pthread_cfg_t cfg = esp_pthread_get_default_config();
|
||||
cfg.thread_name= "console";
|
||||
cfg.inherit_cfg = true;
|
||||
esp_pthread_set_cfg(&cfg);
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_create(&thread_console, &attr, console_thread, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
void run_command(char * line){
|
||||
/* Try to run the command */
|
||||
int ret;
|
||||
esp_err_t err = esp_console_run(line, &ret);
|
||||
|
||||
if (err == ESP_ERR_NOT_FOUND) {
|
||||
printf("Unrecognized command\n");
|
||||
} else if (err == ESP_ERR_INVALID_ARG) {
|
||||
// command was empty
|
||||
} else if (err == ESP_OK && ret != ESP_OK) {
|
||||
printf("Command returned non-zero error code: 0x%x (%s)\n", ret,
|
||||
esp_err_to_name(err));
|
||||
} else if (err != ESP_OK) {
|
||||
printf("Internal error: %s\n", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
static void * console_thread() {
|
||||
process_autoexec();
|
||||
/* Main loop */
|
||||
while (1) {
|
||||
/* Get a line using linenoise.
|
||||
* The line is returned when ENTER is pressed.
|
||||
*/
|
||||
char* line = linenoise(prompt);
|
||||
if (line == NULL) { /* Ignore empty lines */
|
||||
continue;
|
||||
}
|
||||
/* Add the command to the history */
|
||||
linenoiseHistoryAdd(line);
|
||||
|
||||
/* Save command history to filesystem */
|
||||
linenoiseHistorySave(HISTORY_PATH);
|
||||
printf("\n");
|
||||
run_command(line);
|
||||
/* linenoise allocates line buffer on the heap, so need to free it */
|
||||
linenoiseFree(line);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/* Console example — declarations of command registration functions.
|
||||
|
||||
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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,213 +0,0 @@
|
||||
#include "nvs_utilities.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_vfs_dev.h"
|
||||
#include "driver/uart.h"
|
||||
#include "linenoise/linenoise.h"
|
||||
#include "argtable3/argtable3.h"
|
||||
#include "cmd_decl.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
extern char current_namespace[];
|
||||
static const char * TAG = "platform_esp32";
|
||||
bool isNameValid(char * key){
|
||||
bool bFound=false;
|
||||
nvs_handle nvs;
|
||||
esp_err_t err;
|
||||
int8_t val=0;
|
||||
|
||||
err = nvs_open(current_namespace, NVS_READONLY, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG,"Error opening nvs storage for namespace %s",current_namespace);
|
||||
return false;
|
||||
}
|
||||
err = nvs_get_i8(nvs, key, &val);
|
||||
if(err==ESP_OK || err== ESP_ERR_NVS_INVALID_LENGTH){
|
||||
bFound=true;
|
||||
}
|
||||
else {
|
||||
ESP_LOGD(TAG,"Search for key %s in namespace %s returned %#08X",key,current_namespace,err);
|
||||
}
|
||||
// nvs_iterator_t it = nvs_entry_find(NVS_DEFAULT_PART_NAME, current_namespace, NVS_TYPE_ANY);
|
||||
// while (it != NULL) {
|
||||
// nvs_entry_info_t info;
|
||||
// nvs_entry_info(it, &info);
|
||||
// it = nvs_entry_next(it);
|
||||
// if(!strcmp(info.key,key)){
|
||||
// bFound=true;
|
||||
// }
|
||||
// printf("key '%s', type '%d' \n", info.key, info.type);
|
||||
// };
|
||||
// // Note: no need to release iterator obtained from nvs_entry_find function when
|
||||
// // nvs_entry_find or nvs_entry_next function return NULL, indicating no other
|
||||
// // element for specified criteria was found.
|
||||
nvs_close(nvs);
|
||||
return bFound;
|
||||
}
|
||||
esp_err_t store_nvs_value(nvs_type_t type, const char *key, void * data) {
|
||||
if (type == NVS_TYPE_BLOB)
|
||||
return ESP_ERR_NVS_TYPE_MISMATCH;
|
||||
return store_nvs_value_len(type, key, data,0);
|
||||
}
|
||||
esp_err_t store_nvs_value_len(nvs_type_t type, const char *key, void * data,
|
||||
size_t data_len) {
|
||||
esp_err_t err;
|
||||
nvs_handle nvs;
|
||||
|
||||
if (type == NVS_TYPE_ANY) {
|
||||
return ESP_ERR_NVS_TYPE_MISMATCH;
|
||||
}
|
||||
|
||||
err = nvs_open(current_namespace, NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (type == NVS_TYPE_I8) {
|
||||
err = nvs_set_i8(nvs, key, *(int8_t *) data);
|
||||
} else if (type == NVS_TYPE_U8) {
|
||||
err = nvs_set_u8(nvs, key, *(uint8_t *) data);
|
||||
} else if (type == NVS_TYPE_I16) {
|
||||
err = nvs_set_i16(nvs, key, *(int16_t *) data);
|
||||
} else if (type == NVS_TYPE_U16) {
|
||||
err = nvs_set_u16(nvs, key, *(uint16_t *) data);
|
||||
} else if (type == NVS_TYPE_I32) {
|
||||
err = nvs_set_i32(nvs, key, *(int32_t *) data);
|
||||
} else if (type == NVS_TYPE_U32) {
|
||||
err = nvs_set_u32(nvs, key, *(uint32_t *) data);
|
||||
} else if (type == NVS_TYPE_I64) {
|
||||
err = nvs_set_i64(nvs, key, *(int64_t *) data);
|
||||
} else if (type == NVS_TYPE_U64) {
|
||||
err = nvs_set_u64(nvs, key, *(uint64_t *) data);
|
||||
} else if (type == NVS_TYPE_STR) {
|
||||
err = nvs_set_str(nvs, key, data);
|
||||
} else if (type == NVS_TYPE_BLOB) {
|
||||
err = nvs_set_blob(nvs, key, (void *) data, data_len);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(nvs);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Value stored under key '%s'", key);
|
||||
}
|
||||
}
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
void * get_nvs_value_alloc(nvs_type_t type, const char *key) {
|
||||
nvs_handle nvs;
|
||||
esp_err_t err;
|
||||
void * value=NULL;
|
||||
|
||||
err = nvs_open(current_namespace, NVS_READONLY, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG,"Could not open the nvs storage.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (type == NVS_TYPE_I8) {
|
||||
value=malloc(sizeof(int8_t));
|
||||
err = nvs_get_i8(nvs, key, (int8_t *) value);
|
||||
} else if (type == NVS_TYPE_U8) {
|
||||
value=malloc(sizeof(uint8_t));
|
||||
err = nvs_get_u8(nvs, key, (uint8_t *) value);
|
||||
} else if (type == NVS_TYPE_I16) {
|
||||
value=malloc(sizeof(int16_t));
|
||||
err = nvs_get_i16(nvs, key, (int16_t *) value);
|
||||
} else if (type == NVS_TYPE_U16) {
|
||||
value=malloc(sizeof(uint16_t));
|
||||
err = nvs_get_u16(nvs, key, (uint16_t *) value);
|
||||
} else if (type == NVS_TYPE_I32) {
|
||||
value=malloc(sizeof(int32_t));
|
||||
err = nvs_get_i32(nvs, key, (int32_t *) value);
|
||||
} else if (type == NVS_TYPE_U32) {
|
||||
value=malloc(sizeof(uint32_t));
|
||||
err = nvs_get_u32(nvs, key, (uint32_t *) value);
|
||||
} else if (type == NVS_TYPE_I64) {
|
||||
value=malloc(sizeof(int64_t));
|
||||
err = nvs_get_i64(nvs, key, (int64_t *) value);
|
||||
} else if (type == NVS_TYPE_U64) {
|
||||
value=malloc(sizeof(uint64_t));
|
||||
err = nvs_get_u64(nvs, key, (uint64_t *) value);
|
||||
} else if (type == NVS_TYPE_STR) {
|
||||
size_t len=0;
|
||||
err = nvs_get_str(nvs, key, NULL, &len);
|
||||
if (err == ESP_OK) {
|
||||
value=malloc(len);
|
||||
err = nvs_get_str(nvs, key, value, &len);
|
||||
}
|
||||
} else if (type == NVS_TYPE_BLOB) {
|
||||
size_t len;
|
||||
err = nvs_get_blob(nvs, key, NULL, &len);
|
||||
if (err == ESP_OK) {
|
||||
value=malloc(len+1);
|
||||
err = nvs_get_blob(nvs, key, value, &len);
|
||||
}
|
||||
}
|
||||
if(err!=ESP_OK){
|
||||
ESP_LOGD(TAG,"Value not found for key %s",key);
|
||||
if(value!=NULL)
|
||||
free(value);
|
||||
value=NULL;
|
||||
}
|
||||
nvs_close(nvs);
|
||||
return value;
|
||||
}
|
||||
esp_err_t get_nvs_value(nvs_type_t type, const char *key, void*value, const uint8_t buf_size) {
|
||||
nvs_handle nvs;
|
||||
esp_err_t err;
|
||||
|
||||
err = nvs_open(current_namespace, NVS_READONLY, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (type == NVS_TYPE_I8) {
|
||||
err = nvs_get_i8(nvs, key, (int8_t *) value);
|
||||
} else if (type == NVS_TYPE_U8) {
|
||||
err = nvs_get_u8(nvs, key, (uint8_t *) value);
|
||||
} else if (type == NVS_TYPE_I16) {
|
||||
err = nvs_get_i16(nvs, key, (int16_t *) value);
|
||||
} else if (type == NVS_TYPE_U16) {
|
||||
err = nvs_get_u16(nvs, key, (uint16_t *) value);
|
||||
} else if (type == NVS_TYPE_I32) {
|
||||
err = nvs_get_i32(nvs, key, (int32_t *) value);
|
||||
} else if (type == NVS_TYPE_U32) {
|
||||
err = nvs_get_u32(nvs, key, (uint32_t *) value);
|
||||
} else if (type == NVS_TYPE_I64) {
|
||||
err = nvs_get_i64(nvs, key, (int64_t *) value);
|
||||
} else if (type == NVS_TYPE_U64) {
|
||||
err = nvs_get_u64(nvs, key, (uint64_t *) value);
|
||||
} else if (type == NVS_TYPE_STR) {
|
||||
size_t len;
|
||||
if ((err = nvs_get_str(nvs, key, NULL, &len)) == ESP_OK) {
|
||||
if (len > buf_size) {
|
||||
//ESP_LOGE("Error reading value for %s. Buffer size: %d, Value Length: %d", key, buf_size, len);
|
||||
err = ESP_FAIL;
|
||||
} else {
|
||||
err = nvs_get_str(nvs, key, value, &len);
|
||||
}
|
||||
}
|
||||
} else if (type == NVS_TYPE_BLOB) {
|
||||
size_t len;
|
||||
if ((err = nvs_get_blob(nvs, key, NULL, &len)) == ESP_OK) {
|
||||
|
||||
if (len > buf_size) {
|
||||
//ESP_LOGE("Error reading value for %s. Buffer size: %d, Value Length: %d",
|
||||
// key, buf_size, len);
|
||||
err = ESP_FAIL;
|
||||
} else {
|
||||
err = nvs_get_blob(nvs, key, value, &len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
#include "esp_err.h"
|
||||
#include "nvs.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
bool isNameValid(char * key);
|
||||
esp_err_t store_nvs_value_len(nvs_type_t type, const char *key, void * data, size_t data_len);
|
||||
esp_err_t store_nvs_value(nvs_type_t type, const char *key, void * data);
|
||||
esp_err_t get_nvs_value(nvs_type_t type, const char *key, void*value, const uint8_t buf_size);
|
||||
void * get_nvs_value_alloc(nvs_type_t type, const char *key);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
549
components/squeezelite/alac.c
Normal file
549
components/squeezelite/alac.c
Normal file
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* (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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <alac_wrapper.h>
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
#define ALIGN8(n) (n << 8)
|
||||
#define ALIGN16(n) (n)
|
||||
#define ALIGN24(n) (n >> 8)
|
||||
#define ALIGN32(n) (n >> 16)
|
||||
#else
|
||||
#define ALIGN8(n) (n << 24)
|
||||
#define ALIGN16(n) (n << 16)
|
||||
#define ALIGN24(n) (n << 8)
|
||||
#define ALIGN32(n) (n)
|
||||
#endif
|
||||
|
||||
#define BLOCK_SIZE (4096 * BYTES_PER_FRAME)
|
||||
#define MIN_READ BLOCK_SIZE
|
||||
#define MIN_SPACE (MIN_READ * 4)
|
||||
|
||||
struct chunk_table {
|
||||
u32_t sample, offset;
|
||||
};
|
||||
|
||||
struct alac {
|
||||
void *decoder;
|
||||
u8_t *writebuf;
|
||||
// following used for mp4 only
|
||||
u32_t consume;
|
||||
u32_t pos;
|
||||
u32_t sample;
|
||||
u32_t nextchunk;
|
||||
void *stsc;
|
||||
u32_t skip;
|
||||
u64_t samples;
|
||||
u64_t sttssamples;
|
||||
bool empty;
|
||||
struct chunk_table *chunkinfo;
|
||||
u32_t *block_size, default_block_size, block_index;
|
||||
unsigned sample_rate;
|
||||
unsigned char channels, sample_size;
|
||||
unsigned trak, play;
|
||||
};
|
||||
|
||||
static struct alac *l;
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_O_not_direct if (!decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_not_direct if (!decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_O_not_direct
|
||||
#define UNLOCK_O_not_direct
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
// read mp4 header to extract config data
|
||||
static int read_mp4_header(void) {
|
||||
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
char type[5];
|
||||
u32_t len;
|
||||
|
||||
while (bytes >= 8) {
|
||||
// count trak to find the first playable one
|
||||
u32_t consume;
|
||||
|
||||
len = unpackN((u32_t *)streambuf->readp);
|
||||
memcpy(type, streambuf->readp + 4, 4);
|
||||
type[4] = '\0';
|
||||
|
||||
if (!strcmp(type, "moov")) {
|
||||
l->trak = 0;
|
||||
l->play = 0;
|
||||
}
|
||||
if (!strcmp(type, "trak")) {
|
||||
l->trak++;
|
||||
}
|
||||
|
||||
// extract audio config from within alac
|
||||
if (!strcmp(type, "alac") && bytes > len) {
|
||||
u8_t *ptr = streambuf->readp + 36;
|
||||
l->decoder = alac_create_decoder(len - 36, ptr, &l->sample_size, &l->sample_rate, &l->channels);
|
||||
l->play = l->trak;
|
||||
}
|
||||
|
||||
// extract the total number of samples from stts
|
||||
if (!strcmp(type, "stsz") && bytes > len) {
|
||||
u32_t i;
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
l->default_block_size = unpackN((u32_t *) ptr); ptr += 4;
|
||||
if (!l->default_block_size) {
|
||||
u32_t entries = unpackN((u32_t *)ptr); ptr += 4;
|
||||
l->block_size = malloc((entries + 1)* 4);
|
||||
for (i = 0; i < entries; i++) {
|
||||
l->block_size[i] = unpackN((u32_t *)ptr); ptr += 4;
|
||||
}
|
||||
l->block_size[entries] = 0;
|
||||
LOG_DEBUG("total blocksize contained in stsz %u", entries);
|
||||
} else {
|
||||
LOG_DEBUG("fixed blocksize in stsz %u", l->default_block_size);
|
||||
}
|
||||
}
|
||||
|
||||
// extract the total number of samples from stts
|
||||
if (!strcmp(type, "stts") && bytes > len) {
|
||||
u32_t i;
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
u32_t entries = unpackN((u32_t *)ptr);
|
||||
ptr += 4;
|
||||
for (i = 0; i < entries; ++i) {
|
||||
u32_t count = unpackN((u32_t *)ptr);
|
||||
u32_t size = unpackN((u32_t *)(ptr + 4));
|
||||
l->sttssamples += count * size;
|
||||
ptr += 8;
|
||||
}
|
||||
LOG_DEBUG("total number of samples contained in stts: " FMT_u64, l->sttssamples);
|
||||
}
|
||||
|
||||
// stash sample to chunk info, assume it comes before stco
|
||||
if (!strcmp(type, "stsc") && bytes > len && !l->chunkinfo) {
|
||||
l->stsc = malloc(len - 12);
|
||||
if (l->stsc == NULL) {
|
||||
LOG_WARN("malloc fail");
|
||||
return -1;
|
||||
}
|
||||
memcpy(l->stsc, streambuf->readp + 12, len - 12);
|
||||
}
|
||||
|
||||
// build offsets table from stco and stored stsc
|
||||
if (!strcmp(type, "stco") && bytes > len && l->play == l->trak) {
|
||||
u32_t i;
|
||||
// extract chunk offsets
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
u32_t entries = unpackN((u32_t *)ptr);
|
||||
ptr += 4;
|
||||
l->chunkinfo = malloc(sizeof(struct chunk_table) * (entries + 1));
|
||||
if (l->chunkinfo == NULL) {
|
||||
LOG_WARN("malloc fail");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < entries; ++i) {
|
||||
l->chunkinfo[i].offset = unpackN((u32_t *)ptr);
|
||||
l->chunkinfo[i].sample = 0;
|
||||
ptr += 4;
|
||||
}
|
||||
l->chunkinfo[i].sample = 0;
|
||||
l->chunkinfo[i].offset = 0;
|
||||
// fill in first sample id for each chunk from stored stsc
|
||||
if (l->stsc) {
|
||||
u32_t stsc_entries = unpackN((u32_t *)l->stsc);
|
||||
u32_t sample = 0;
|
||||
u32_t last = 0, last_samples = 0;
|
||||
u8_t *ptr = (u8_t *)l->stsc + 4;
|
||||
while (stsc_entries--) {
|
||||
u32_t first = unpackN((u32_t *)ptr);
|
||||
u32_t samples = unpackN((u32_t *)(ptr + 4));
|
||||
if (last) {
|
||||
for (i = last - 1; i < first - 1; ++i) {
|
||||
l->chunkinfo[i].sample = sample;
|
||||
sample += last_samples;
|
||||
}
|
||||
}
|
||||
if (stsc_entries == 0) {
|
||||
for (i = first - 1; i < entries; ++i) {
|
||||
l->chunkinfo[i].sample = sample;
|
||||
sample += samples;
|
||||
}
|
||||
}
|
||||
last = first;
|
||||
last_samples = samples;
|
||||
ptr += 12;
|
||||
}
|
||||
free(l->stsc);
|
||||
l->stsc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// found media data, advance to start of first chunk and return
|
||||
if (!strcmp(type, "mdat")) {
|
||||
_buf_inc_readp(streambuf, 8);
|
||||
l->pos += 8;
|
||||
bytes -= 8;
|
||||
if (l->play) {
|
||||
LOG_DEBUG("type: mdat len: %u pos: %u", len, l->pos);
|
||||
if (l->chunkinfo && l->chunkinfo[0].offset > l->pos) {
|
||||
u32_t skip = l->chunkinfo[0].offset - l->pos;
|
||||
LOG_DEBUG("skipping: %u", skip);
|
||||
if (skip <= bytes) {
|
||||
_buf_inc_readp(streambuf, skip);
|
||||
l->pos += skip;
|
||||
} else {
|
||||
l->consume = skip;
|
||||
}
|
||||
}
|
||||
l->sample = l->nextchunk = 1;
|
||||
l->block_index = 0;
|
||||
return 1;
|
||||
} else {
|
||||
LOG_DEBUG("type: mdat len: %u, no playable track found", len);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// parse key-value atoms within ilst ---- entries to get encoder padding within iTunSMPB entry for gapless
|
||||
if (!strcmp(type, "----") && bytes > len) {
|
||||
u8_t *ptr = streambuf->readp + 8;
|
||||
u32_t remain = len - 8, size;
|
||||
if (!memcmp(ptr + 4, "mean", 4) && (size = unpackN((u32_t *)ptr)) < remain) {
|
||||
ptr += size; remain -= size;
|
||||
}
|
||||
if (!memcmp(ptr + 4, "name", 4) && (size = unpackN((u32_t *)ptr)) < remain && !memcmp(ptr + 12, "iTunSMPB", 8)) {
|
||||
ptr += size; remain -= size;
|
||||
}
|
||||
if (!memcmp(ptr + 4, "data", 4) && remain > 16 + 48) {
|
||||
// data is stored as hex strings: 0 start end samples
|
||||
u32_t b, c; u64_t d;
|
||||
if (sscanf((const char *)(ptr + 16), "%x %x %x " FMT_x64, &b, &b, &c, &d) == 4) {
|
||||
LOG_DEBUG("iTunSMPB start: %u end: %u samples: " FMT_u64, b, c, d);
|
||||
if (l->sttssamples && l->sttssamples < b + c + d) {
|
||||
LOG_DEBUG("reducing samples as stts count is less");
|
||||
d = l->sttssamples - (b + c);
|
||||
}
|
||||
l->skip = b;
|
||||
l->samples = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to consuming entire box
|
||||
consume = len;
|
||||
|
||||
// read into these boxes so reduce consume
|
||||
if (!strcmp(type, "moov") || !strcmp(type, "trak") || !strcmp(type, "mdia") || !strcmp(type, "minf") || !strcmp(type, "stbl") ||
|
||||
!strcmp(type, "udta") || !strcmp(type, "ilst")) {
|
||||
consume = 8;
|
||||
}
|
||||
// special cases which mix mix data in the enclosing box which we want to read into
|
||||
if (!strcmp(type, "stsd")) consume = 16;
|
||||
if (!strcmp(type, "mp4a")) consume = 36;
|
||||
if (!strcmp(type, "meta")) consume = 12;
|
||||
|
||||
// consume rest of box if it has been parsed (all in the buffer) or is not one we want to parse
|
||||
if (bytes >= consume) {
|
||||
LOG_DEBUG("type: %s len: %u consume: %u", type, len, consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
l->pos += consume;
|
||||
bytes -= consume;
|
||||
} else if ( !(!strcmp(type, "esds") || !strcmp(type, "stts") || !strcmp(type, "stsc") ||
|
||||
!strcmp(type, "stsz") || !strcmp(type, "stco") || !strcmp(type, "----")) ) {
|
||||
LOG_DEBUG("type: %s len: %u consume: %u - partial consume: %u", type, len, consume, bytes);
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
l->pos += bytes;
|
||||
l->consume = consume - bytes;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static decode_state alac_decode(void) {
|
||||
size_t bytes;
|
||||
bool endstream;
|
||||
u8_t *iptr;
|
||||
u32_t frames, block_size;
|
||||
|
||||
LOCK_S;
|
||||
|
||||
// data not reached yet
|
||||
if (l->consume) {
|
||||
u32_t consume = min(l->consume, _buf_used(streambuf));
|
||||
LOG_DEBUG("consume: %u of %u", consume, l->consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
l->pos += consume;
|
||||
l->consume -= consume;
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
if (decode.new_stream) {
|
||||
int found = 0;
|
||||
|
||||
// mp4 - read header
|
||||
found = read_mp4_header();
|
||||
|
||||
if (found == 1) {
|
||||
bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
|
||||
LOG_INFO("setting track_start");
|
||||
LOCK_O;
|
||||
|
||||
output.next_sample_rate = decode_newstream(l->sample_rate, output.supported_rates);
|
||||
output.track_start = outputbuf->writep;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
decode.new_stream = false;
|
||||
|
||||
UNLOCK_O;
|
||||
} else if (found == -1) {
|
||||
LOG_WARN("[%p]: error reading stream header");
|
||||
UNLOCK_S;
|
||||
return DECODE_ERROR;
|
||||
} else {
|
||||
// not finished header parsing come back next time
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
bytes = _buf_used(streambuf);
|
||||
block_size = l->default_block_size ? l->default_block_size : l->block_size[l->block_index];
|
||||
|
||||
// stream terminated
|
||||
if (stream.state <= DISCONNECT && (bytes == 0 || block_size == 0)) {
|
||||
UNLOCK_S;
|
||||
LOG_DEBUG("end of stream");
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
// enough data for coding
|
||||
if (bytes < block_size) {
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
} else if (block_size != l->default_block_size) l->block_index++;
|
||||
|
||||
bytes = min(bytes, _buf_cont_read(streambuf));
|
||||
|
||||
// need to create a buffer with contiguous data
|
||||
if (bytes < block_size) {
|
||||
u8_t *buffer = malloc(block_size);
|
||||
memcpy(buffer, streambuf->readp, bytes);
|
||||
memcpy(buffer + bytes, streambuf->buf, block_size - bytes);
|
||||
iptr = buffer;
|
||||
} else iptr = streambuf->readp;
|
||||
|
||||
if (!alac_to_pcm(l->decoder, iptr, l->writebuf, 2, &frames)) {
|
||||
LOG_ERROR("decode error");
|
||||
UNLOCK_S;
|
||||
return DECODE_ERROR;
|
||||
}
|
||||
|
||||
// and free it
|
||||
if (bytes < block_size) free(iptr);
|
||||
|
||||
LOG_SDEBUG("block of %u bytes (%u frames)", block_size, frames);
|
||||
|
||||
endstream = false;
|
||||
// mp4 end of chunk - skip to next offset
|
||||
if (l->chunkinfo && l->chunkinfo[l->nextchunk].offset && l->sample++ == l->chunkinfo[l->nextchunk].sample) {
|
||||
if (l->chunkinfo[l->nextchunk].offset > l->pos) {
|
||||
u32_t skip = l->chunkinfo[l->nextchunk].offset - l->pos;
|
||||
if (_buf_used(streambuf) >= skip) {
|
||||
_buf_inc_readp(streambuf, skip);
|
||||
l->pos += skip;
|
||||
} else {
|
||||
l->consume = skip;
|
||||
}
|
||||
l->nextchunk++;
|
||||
} else {
|
||||
LOG_ERROR("error: need to skip backwards!");
|
||||
endstream = true;
|
||||
}
|
||||
// mp4 when not at end of chunk
|
||||
} else if (frames) {
|
||||
_buf_inc_readp(streambuf, block_size);
|
||||
l->pos += block_size;
|
||||
} else {
|
||||
endstream = true;
|
||||
}
|
||||
|
||||
UNLOCK_S;
|
||||
|
||||
if (endstream) {
|
||||
LOG_WARN("unable to decode further");
|
||||
return DECODE_ERROR;
|
||||
}
|
||||
|
||||
// now point at the beginning of decoded samples
|
||||
iptr = l->writebuf;
|
||||
|
||||
if (l->skip) {
|
||||
u32_t skip;
|
||||
if (l->empty) {
|
||||
l->empty = false;
|
||||
l->skip -= frames;
|
||||
LOG_DEBUG("gapless: first frame empty, skipped %u frames at start", frames);
|
||||
}
|
||||
skip = min(frames, l->skip);
|
||||
LOG_DEBUG("gapless: skipping %u frames at start", skip);
|
||||
frames -= skip;
|
||||
l->skip -= skip;
|
||||
iptr += skip * l->channels * l->sample_size;
|
||||
}
|
||||
|
||||
if (l->samples) {
|
||||
if (l->samples < frames) {
|
||||
LOG_DEBUG("gapless: trimming %u frames from end", frames - l->samples);
|
||||
frames = (u32_t) l->samples;
|
||||
}
|
||||
l->samples -= frames;
|
||||
}
|
||||
|
||||
LOCK_O_direct;
|
||||
|
||||
while (frames > 0) {
|
||||
size_t f, count;
|
||||
ISAMPLE_T *optr;
|
||||
|
||||
IF_DIRECT(
|
||||
f = min(frames, _buf_cont_write(outputbuf) / BYTES_PER_FRAME);
|
||||
optr = (ISAMPLE_T *)outputbuf->writep;
|
||||
);
|
||||
IF_PROCESS(
|
||||
f = min(frames, process.max_in_frames - process.in_frames);
|
||||
optr = (ISAMPLE_T *)((u8_t *) process.inbuf + process.in_frames * BYTES_PER_FRAME);
|
||||
);
|
||||
|
||||
f = min(f, frames);
|
||||
count = f;
|
||||
|
||||
if (l->sample_size == 8) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN8(*iptr++);
|
||||
*optr++ = ALIGN8(*iptr++);
|
||||
}
|
||||
} else if (l->sample_size == 16) {
|
||||
u16_t *_iptr = (u16_t*) iptr;
|
||||
while (count--) {
|
||||
*optr++ = ALIGN16(*_iptr++);
|
||||
*optr++ = ALIGN16(*_iptr++);
|
||||
}
|
||||
} else if (l->sample_size == 24) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN24(*(u32_t*) iptr);
|
||||
*optr++ = ALIGN24(*(u32_t*) (iptr + 3));
|
||||
iptr += 6;
|
||||
}
|
||||
} else if (l->sample_size == 32) {
|
||||
u32_t *_iptr = (u32_t*) iptr;
|
||||
while (count--) {
|
||||
*optr++ = ALIGN32(*_iptr++);
|
||||
*optr++ = ALIGN32(*_iptr++);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("unsupported bits per sample: %u", l->sample_size);
|
||||
}
|
||||
|
||||
frames -= f;
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames = f;
|
||||
// called only if there is enough space in process buffer
|
||||
if (frames) LOG_ERROR("unhandled case");
|
||||
);
|
||||
}
|
||||
|
||||
UNLOCK_O_direct;
|
||||
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
static void alac_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
|
||||
if (l->decoder) alac_delete_decoder(l->decoder);
|
||||
else l->writebuf = malloc(BLOCK_SIZE * 2);
|
||||
|
||||
if (l->chunkinfo) free(l->chunkinfo);
|
||||
if (l->block_size) free(l->block_size);
|
||||
if (l->stsc) free(l->stsc);
|
||||
l->decoder = l->chunkinfo = l->stsc = l->block_size = NULL;
|
||||
l->skip = 0;
|
||||
l->samples = l->sttssamples = 0;
|
||||
l->empty = false;
|
||||
l->pos = l->consume = l->sample = l->nextchunk = 0;
|
||||
}
|
||||
|
||||
static void alac_close(void) {
|
||||
if (l->decoder) alac_delete_decoder(l->decoder);
|
||||
if (l->chunkinfo) free(l->chunkinfo);
|
||||
if (l->block_size) free(l->block_size);
|
||||
if (l->stsc) free(l->stsc);
|
||||
l->decoder = l->chunkinfo = l->stsc = l->block_size = NULL;
|
||||
free(l->writebuf);
|
||||
}
|
||||
|
||||
struct codec *register_alac(void) {
|
||||
static struct codec ret = {
|
||||
'l', // id
|
||||
"alc", // types
|
||||
MIN_READ, // min read
|
||||
MIN_SPACE, // min space assuming a ratio of 2
|
||||
alac_open, // open
|
||||
alac_close, // close
|
||||
alac_decode, // decode
|
||||
};
|
||||
|
||||
l = malloc(sizeof(struct alac));
|
||||
if (!l) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
l->decoder = l->chunkinfo = l->stsc = l->block_size = NULL;
|
||||
|
||||
LOG_INFO("using alac to decode alc");
|
||||
return &ret;
|
||||
}
|
||||
115
components/squeezelite/buffer.c
Normal file
115
components/squeezelite/buffer.c
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// fifo bufffers
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
// _* called with muxtex locked
|
||||
|
||||
inline unsigned _buf_used(struct buffer *buf) {
|
||||
return buf->writep >= buf->readp ? buf->writep - buf->readp : buf->size - (buf->readp - buf->writep);
|
||||
}
|
||||
|
||||
unsigned _buf_space(struct buffer *buf) {
|
||||
return buf->size - _buf_used(buf) - 1; // reduce by one as full same as empty otherwise
|
||||
}
|
||||
|
||||
unsigned _buf_cont_read(struct buffer *buf) {
|
||||
return buf->writep >= buf->readp ? buf->writep - buf->readp : buf->wrap - buf->readp;
|
||||
}
|
||||
|
||||
unsigned _buf_cont_write(struct buffer *buf) {
|
||||
return buf->writep >= buf->readp ? buf->wrap - buf->writep : buf->readp - buf->writep;
|
||||
}
|
||||
|
||||
void _buf_inc_readp(struct buffer *buf, unsigned by) {
|
||||
buf->readp += by;
|
||||
if (buf->readp >= buf->wrap) {
|
||||
buf->readp -= buf->size;
|
||||
}
|
||||
}
|
||||
|
||||
void _buf_inc_writep(struct buffer *buf, unsigned by) {
|
||||
buf->writep += by;
|
||||
if (buf->writep >= buf->wrap) {
|
||||
buf->writep -= buf->size;
|
||||
}
|
||||
}
|
||||
|
||||
void buf_flush(struct buffer *buf) {
|
||||
mutex_lock(buf->mutex);
|
||||
buf->readp = buf->buf;
|
||||
buf->writep = buf->buf;
|
||||
mutex_unlock(buf->mutex);
|
||||
}
|
||||
|
||||
// adjust buffer to multiple of mod bytes so reading in multiple always wraps on frame boundary
|
||||
void buf_adjust(struct buffer *buf, size_t mod) {
|
||||
size_t size;
|
||||
mutex_lock(buf->mutex);
|
||||
size = ((unsigned)(buf->base_size / mod)) * mod;
|
||||
buf->readp = buf->buf;
|
||||
buf->writep = buf->buf;
|
||||
buf->wrap = buf->buf + size;
|
||||
buf->size = size;
|
||||
mutex_unlock(buf->mutex);
|
||||
}
|
||||
|
||||
// called with mutex locked to resize, does not retain contents, reverts to original size if fails
|
||||
void _buf_resize(struct buffer *buf, size_t size) {
|
||||
free(buf->buf);
|
||||
buf->buf = malloc(size);
|
||||
if (!buf->buf) {
|
||||
size = buf->size;
|
||||
buf->buf= malloc(size);
|
||||
if (!buf->buf) {
|
||||
size = 0;
|
||||
}
|
||||
}
|
||||
buf->readp = buf->buf;
|
||||
buf->writep = buf->buf;
|
||||
buf->wrap = buf->buf + size;
|
||||
buf->size = size;
|
||||
buf->base_size = size;
|
||||
}
|
||||
|
||||
void buf_init(struct buffer *buf, size_t size) {
|
||||
buf->buf = malloc(size);
|
||||
buf->readp = buf->buf;
|
||||
buf->writep = buf->buf;
|
||||
buf->wrap = buf->buf + size;
|
||||
buf->size = size;
|
||||
buf->base_size = size;
|
||||
mutex_create_p(buf->mutex);
|
||||
}
|
||||
|
||||
void buf_destroy(struct buffer *buf) {
|
||||
if (buf->buf) {
|
||||
free(buf->buf);
|
||||
buf->buf = NULL;
|
||||
buf->size = 0;
|
||||
buf->base_size = 0;
|
||||
mutex_destroy(buf->mutex);
|
||||
}
|
||||
}
|
||||
18
components/squeezelite/component.mk
Normal file
18
components/squeezelite/component.mk
Normal file
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# "main" pseudo-component makefile.
|
||||
#
|
||||
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
|
||||
CFLAGS += -O3 -DLINKALL -DLOOPBACK -DNO_FAAD -DRESAMPLE16 -DEMBEDDED -DTREMOR_ONLY -DBYTES_PER_FRAME=4 \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc/mad \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc/alac \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc/helix-aac \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc/vorbis \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc/soxr \
|
||||
-I$(COMPONENT_PATH)/../codecs/inc/resample16 \
|
||||
-I$(COMPONENT_PATH)/../tools
|
||||
|
||||
# -I$(COMPONENT_PATH)/../codecs/inc/faad2
|
||||
|
||||
|
||||
|
||||
308
components/squeezelite/decode.c
Normal file
308
components/squeezelite/decode.c
Normal file
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// decode thread
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct processstate process;
|
||||
|
||||
struct decodestate decode;
|
||||
struct codec *codecs[MAX_CODECS];
|
||||
struct codec *codec;
|
||||
static bool running = true;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_D mutex_lock(decode.mutex);
|
||||
#define UNLOCK_D mutex_unlock(decode.mutex);
|
||||
|
||||
#if PROCESS
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#define MAY_PROCESS(x) { x }
|
||||
#else
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#define MAY_PROCESS(x)
|
||||
#endif
|
||||
|
||||
static void *decode_thread() {
|
||||
|
||||
while (running) {
|
||||
size_t bytes, space, min_space;
|
||||
bool toend;
|
||||
bool ran = false;
|
||||
|
||||
LOCK_S;
|
||||
bytes = _buf_used(streambuf);
|
||||
toend = (stream.state <= DISCONNECT);
|
||||
UNLOCK_S;
|
||||
LOCK_O;
|
||||
space = _buf_space(outputbuf);
|
||||
UNLOCK_O;
|
||||
|
||||
LOCK_D;
|
||||
|
||||
if (decode.state == DECODE_RUNNING && codec) {
|
||||
|
||||
LOG_SDEBUG("streambuf bytes: %u outputbuf space: %u", bytes, space);
|
||||
|
||||
IF_DIRECT(
|
||||
min_space = codec->min_space;
|
||||
);
|
||||
IF_PROCESS(
|
||||
min_space = process.max_out_frames * BYTES_PER_FRAME;
|
||||
);
|
||||
|
||||
if (space > min_space && (bytes > codec->min_read_bytes || toend)) {
|
||||
|
||||
decode.state = codec->decode();
|
||||
|
||||
IF_PROCESS(
|
||||
if (process.in_frames) {
|
||||
process_samples();
|
||||
}
|
||||
|
||||
if (decode.state == DECODE_COMPLETE) {
|
||||
process_drain();
|
||||
}
|
||||
);
|
||||
|
||||
if (decode.state != DECODE_RUNNING) {
|
||||
|
||||
LOG_INFO("decode %s", decode.state == DECODE_COMPLETE ? "complete" : "error");
|
||||
|
||||
LOCK_O;
|
||||
if (output.fade_mode) _checkfade(false);
|
||||
UNLOCK_O;
|
||||
|
||||
wake_controller();
|
||||
}
|
||||
|
||||
ran = true;
|
||||
}
|
||||
}
|
||||
|
||||
UNLOCK_D;
|
||||
|
||||
if (!ran) {
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sort_codecs(int pry, struct codec* ptr) {
|
||||
static int priority[MAX_CODECS];
|
||||
int i, tpry;
|
||||
struct codec* tptr;
|
||||
|
||||
for (i = 0; i < MAX_CODECS; i++) {
|
||||
if (!codecs[i]) {
|
||||
codecs[i] = ptr;
|
||||
priority[i] = pry;
|
||||
return;
|
||||
}
|
||||
if (pry < priority[i]) {
|
||||
tptr = codecs[i];
|
||||
codecs[i] = ptr;
|
||||
ptr = tptr;
|
||||
tpry = priority[i];
|
||||
priority[i] = pry;
|
||||
pry = tpry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static thread_type thread;
|
||||
|
||||
void decode_init(log_level level, const char *include_codecs, const char *exclude_codecs) {
|
||||
int i;
|
||||
char* order_codecs = NULL;
|
||||
|
||||
loglevel = level;
|
||||
|
||||
LOG_INFO("init decode");
|
||||
|
||||
// register codecs
|
||||
// dsf,dff,alc,wma,wmap,wmal,aac,spt,ogg,ogf,flc,aif,pcm,mp3
|
||||
i = 0;
|
||||
|
||||
#if DSD
|
||||
if (!strstr(exclude_codecs, "dsd") && (!include_codecs || (order_codecs = strstr(include_codecs, "dsd"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_dsd());
|
||||
#endif
|
||||
#if FFMPEG
|
||||
if (!strstr(exclude_codecs, "alac") && (!include_codecs || (order_codecs = strstr(include_codecs, "alac"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_ff("alc"));
|
||||
if (!strstr(exclude_codecs, "wma") && (!include_codecs || (order_codecs = strstr(include_codecs, "wma"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_ff("wma"));
|
||||
#else
|
||||
if (!strstr(exclude_codecs, "alac") && (!include_codecs || (order_codecs = strstr(include_codecs, "alac"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_alac());
|
||||
#endif
|
||||
|
||||
#ifndef NO_FAAD
|
||||
if (!strstr(exclude_codecs, "aac") && (!include_codecs || (order_codecs = strstr(include_codecs, "aac"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_faad());
|
||||
#endif
|
||||
|
||||
if (!strstr(exclude_codecs, "aac") && (!include_codecs || (order_codecs = strstr(include_codecs, "aac"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_helixaac());
|
||||
|
||||
if (!strstr(exclude_codecs, "ogg") && (!include_codecs || (order_codecs = strstr(include_codecs, "ogg"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_vorbis());
|
||||
|
||||
if (!strstr(exclude_codecs, "flac") && (!include_codecs || (order_codecs = strstr(include_codecs, "flac"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_flac());
|
||||
|
||||
if (!strstr(exclude_codecs, "pcm") && (!include_codecs || (order_codecs = strstr(include_codecs, "pcm"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_pcm());
|
||||
|
||||
// try mad then mpg for mp3 unless command line option passed
|
||||
if (!(strstr(exclude_codecs, "mp3") || strstr(exclude_codecs, "mad")) &&
|
||||
(!include_codecs || (order_codecs = strstr(include_codecs, "mp3")) || (order_codecs = strstr(include_codecs, "mad"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_mad());
|
||||
|
||||
else if (!(strstr(exclude_codecs, "mp3") || strstr(exclude_codecs, "mpg")) &&
|
||||
(!include_codecs || (order_codecs = strstr(include_codecs, "mp3")) || (order_codecs = strstr(include_codecs, "mpg"))))
|
||||
sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_mpg());
|
||||
|
||||
LOG_DEBUG("include codecs: %s exclude codecs: %s", include_codecs ? include_codecs : "", exclude_codecs);
|
||||
|
||||
mutex_create(decode.mutex);
|
||||
|
||||
#if LINUX || OSX || FREEBSD || EMBEDDED
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
#ifdef PTHREAD_STACK_MIN
|
||||
pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + DECODE_THREAD_STACK_SIZE);
|
||||
#endif
|
||||
pthread_create(&thread, &attr, decode_thread, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
#if HAS_PTHREAD_SETNAME_NP
|
||||
pthread_setname_np(thread, "decode");
|
||||
#endif
|
||||
#endif
|
||||
#if WIN
|
||||
thread = CreateThread(NULL, DECODE_THREAD_STACK_SIZE, (LPTHREAD_START_ROUTINE)&decode_thread, NULL, 0, NULL);
|
||||
#endif
|
||||
|
||||
decode.new_stream = true;
|
||||
decode.state = DECODE_STOPPED;
|
||||
|
||||
MAY_PROCESS(
|
||||
decode.direct = true;
|
||||
decode.process = false;
|
||||
);
|
||||
}
|
||||
|
||||
void decode_close(void) {
|
||||
LOG_INFO("close decode");
|
||||
LOCK_D;
|
||||
if (codec) {
|
||||
codec->close();
|
||||
codec = NULL;
|
||||
}
|
||||
running = false;
|
||||
UNLOCK_D;
|
||||
#if LINUX || OSX || FREEBSD
|
||||
pthread_join(thread, NULL);
|
||||
#endif
|
||||
mutex_destroy(decode.mutex);
|
||||
}
|
||||
|
||||
void decode_flush(void) {
|
||||
LOG_INFO("decode flush");
|
||||
LOCK_D;
|
||||
decode.state = DECODE_STOPPED;
|
||||
IF_PROCESS(
|
||||
process_flush();
|
||||
);
|
||||
UNLOCK_D;
|
||||
}
|
||||
|
||||
unsigned decode_newstream(unsigned sample_rate, unsigned supported_rates[]) {
|
||||
|
||||
// called with O locked to get sample rate for potentially processed output stream
|
||||
// release O mutex during process_newstream as it can take some time
|
||||
|
||||
MAY_PROCESS(
|
||||
if (decode.process) {
|
||||
UNLOCK_O;
|
||||
sample_rate = process_newstream(&decode.direct, sample_rate, supported_rates);
|
||||
LOCK_O;
|
||||
}
|
||||
);
|
||||
|
||||
return sample_rate;
|
||||
}
|
||||
|
||||
void codec_open(u8_t format, u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness) {
|
||||
int i;
|
||||
|
||||
LOG_INFO("codec open: '%c'", format);
|
||||
|
||||
LOCK_D;
|
||||
|
||||
decode.new_stream = true;
|
||||
decode.state = DECODE_STOPPED;
|
||||
|
||||
MAY_PROCESS(
|
||||
decode.direct = true; // potentially changed within codec when processing enabled
|
||||
);
|
||||
|
||||
// find the required codec
|
||||
for (i = 0; i < MAX_CODECS; ++i) {
|
||||
|
||||
if (codecs[i] && codecs[i]->id == format) {
|
||||
|
||||
if (codec && codec != codecs[i]) {
|
||||
LOG_INFO("closing codec: '%c'", codec->id);
|
||||
codec->close();
|
||||
}
|
||||
|
||||
codec = codecs[i];
|
||||
|
||||
codec->open(sample_size, sample_rate, channels, endianness);
|
||||
|
||||
decode.state = DECODE_READY;
|
||||
|
||||
UNLOCK_D;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UNLOCK_D;
|
||||
|
||||
LOG_ERROR("codec not found");
|
||||
}
|
||||
|
||||
@@ -18,27 +18,27 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "squeezelite.h"
|
||||
#include "esp_pthread.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#ifndef QUOTE
|
||||
#define QUOTE(name) #name
|
||||
#endif
|
||||
|
||||
#define ESP_LOG_DEBUG_EVENT(tag,e) ESP_LOGD(tag,"evt: " e)
|
||||
|
||||
extern void run_command(char * line);
|
||||
extern bool wait_for_wifi();
|
||||
extern void console_start();
|
||||
extern pthread_cond_t wifi_connect_suspend_cond;
|
||||
extern pthread_t wifi_connect_suspend_mutex;
|
||||
|
||||
#ifdef __cplusplus
|
||||
void get_mac(u8_t mac[]) {
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
}
|
||||
#endif
|
||||
|
||||
_sig_func_ptr signal(int sig, _sig_func_ptr func) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *audio_calloc(size_t nmemb, size_t size) {
|
||||
return calloc(nmemb, size);
|
||||
}
|
||||
|
||||
int pthread_setname_np(pthread_t thread, const char *name) {
|
||||
esp_pthread_cfg_t cfg = esp_pthread_get_default_config();
|
||||
cfg.thread_name= name;
|
||||
cfg.inherit_cfg = true;
|
||||
return esp_pthread_set_cfg(&cfg);
|
||||
}
|
||||
|
||||
|
||||
22
components/squeezelite/embedded.h
Normal file
22
components/squeezelite/embedded.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef EMBEDDED_H
|
||||
#define EMBEDDED_H
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#define HAS_MUTEX_CREATE_P 0
|
||||
#define HAS_PTHREAD_SETNAME_NP 1
|
||||
|
||||
#ifndef PTHREAD_STACK_MIN
|
||||
#define PTHREAD_STACK_MIN 256
|
||||
#endif
|
||||
|
||||
typedef int16_t s16_t;
|
||||
typedef int32_t s32_t;
|
||||
typedef int64_t s64_t;
|
||||
typedef unsigned long long u64_t;
|
||||
|
||||
#define exit(code) { int ret = code; pthread_exit(&ret); }
|
||||
|
||||
int pthread_setname_np(pthread_t thread, const char *name);
|
||||
|
||||
#endif // EMBEDDED_H
|
||||
659
components/squeezelite/faad.c.nocompile
Normal file
659
components/squeezelite/faad.c.nocompile
Normal file
@@ -0,0 +1,659 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <neaacdec.h>
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
#define ALIGN(n) (n)
|
||||
#else
|
||||
#define ALIGN(n) (n << 8)
|
||||
#endif
|
||||
|
||||
#define WRAPBUF_LEN 2048
|
||||
|
||||
struct chunk_table {
|
||||
u32_t sample, offset;
|
||||
};
|
||||
|
||||
struct faad {
|
||||
NeAACDecHandle hAac;
|
||||
u8_t type;
|
||||
// following used for mp4 only
|
||||
u32_t consume;
|
||||
u32_t pos;
|
||||
u32_t sample;
|
||||
u32_t nextchunk;
|
||||
void *stsc;
|
||||
u32_t skip;
|
||||
u64_t samples;
|
||||
u64_t sttssamples;
|
||||
bool empty;
|
||||
struct chunk_table *chunkinfo;
|
||||
// faad symbols to be dynamically loaded
|
||||
#if !LINKALL
|
||||
NeAACDecConfigurationPtr (* NeAACDecGetCurrentConfiguration)(NeAACDecHandle);
|
||||
unsigned char (* NeAACDecSetConfiguration)(NeAACDecHandle, NeAACDecConfigurationPtr);
|
||||
NeAACDecHandle (* NeAACDecOpen)(void);
|
||||
void (* NeAACDecClose)(NeAACDecHandle);
|
||||
long (* NeAACDecInit)(NeAACDecHandle, unsigned char *, unsigned long, unsigned long *, unsigned char *);
|
||||
char (* NeAACDecInit2)(NeAACDecHandle, unsigned char *pBuffer, unsigned long, unsigned long *, unsigned char *);
|
||||
void *(* NeAACDecDecode)(NeAACDecHandle, NeAACDecFrameInfo *, unsigned char *, unsigned long);
|
||||
char *(* NeAACDecGetErrorMessage)(unsigned char);
|
||||
#endif
|
||||
};
|
||||
|
||||
static struct faad *a;
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
#if LINKALL
|
||||
#define NEAAC(h, fn, ...) (NeAACDec ## fn)(__VA_ARGS__)
|
||||
#else
|
||||
#define NEAAC(h, fn, ...) (h)->NeAACDec##fn(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
// minimal code for mp4 file parsing to extract audio config and find media data
|
||||
|
||||
// adapted from faad2/common/mp4ff
|
||||
u32_t mp4_desc_length(u8_t **buf) {
|
||||
u8_t b;
|
||||
u8_t num_bytes = 0;
|
||||
u32_t length = 0;
|
||||
|
||||
do {
|
||||
b = **buf;
|
||||
*buf += 1;
|
||||
num_bytes++;
|
||||
length = (length << 7) | (b & 0x7f);
|
||||
} while ((b & 0x80) && num_bytes < 4);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
// read mp4 header to extract config data
|
||||
static int read_mp4_header(unsigned long *samplerate_p, unsigned char *channels_p) {
|
||||
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
char type[5];
|
||||
u32_t len;
|
||||
|
||||
while (bytes >= 8) {
|
||||
// count trak to find the first playable one
|
||||
static unsigned trak, play;
|
||||
u32_t consume;
|
||||
|
||||
len = unpackN((u32_t *)streambuf->readp);
|
||||
memcpy(type, streambuf->readp + 4, 4);
|
||||
type[4] = '\0';
|
||||
|
||||
if (!strcmp(type, "moov")) {
|
||||
trak = 0;
|
||||
play = 0;
|
||||
}
|
||||
if (!strcmp(type, "trak")) {
|
||||
trak++;
|
||||
}
|
||||
|
||||
// extract audio config from within esds and pass to DecInit2
|
||||
if (!strcmp(type, "esds") && bytes > len) {
|
||||
unsigned config_len;
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
if (*ptr++ == 0x03) {
|
||||
mp4_desc_length(&ptr);
|
||||
ptr += 4;
|
||||
} else {
|
||||
ptr += 3;
|
||||
}
|
||||
mp4_desc_length(&ptr);
|
||||
ptr += 13;
|
||||
if (*ptr++ != 0x05) {
|
||||
LOG_WARN("error parsing esds");
|
||||
return -1;
|
||||
}
|
||||
config_len = mp4_desc_length(&ptr);
|
||||
if (NEAAC(a, Init2, a->hAac, ptr, config_len, samplerate_p, channels_p) == 0) {
|
||||
LOG_DEBUG("playable aac track: %u", trak);
|
||||
play = trak;
|
||||
}
|
||||
}
|
||||
|
||||
// extract the total number of samples from stts
|
||||
if (!strcmp(type, "stts") && bytes > len) {
|
||||
u32_t i;
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
u32_t entries = unpackN((u32_t *)ptr);
|
||||
ptr += 4;
|
||||
for (i = 0; i < entries; ++i) {
|
||||
u32_t count = unpackN((u32_t *)ptr);
|
||||
u32_t size = unpackN((u32_t *)(ptr + 4));
|
||||
a->sttssamples += count * size;
|
||||
ptr += 8;
|
||||
}
|
||||
LOG_DEBUG("total number of samples contained in stts: " FMT_u64, a->sttssamples);
|
||||
}
|
||||
|
||||
// stash sample to chunk info, assume it comes before stco
|
||||
if (!strcmp(type, "stsc") && bytes > len && !a->chunkinfo) {
|
||||
a->stsc = malloc(len - 12);
|
||||
if (a->stsc == NULL) {
|
||||
LOG_WARN("malloc fail");
|
||||
return -1;
|
||||
}
|
||||
memcpy(a->stsc, streambuf->readp + 12, len - 12);
|
||||
}
|
||||
|
||||
// build offsets table from stco and stored stsc
|
||||
if (!strcmp(type, "stco") && bytes > len && play == trak) {
|
||||
u32_t i;
|
||||
// extract chunk offsets
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
u32_t entries = unpackN((u32_t *)ptr);
|
||||
ptr += 4;
|
||||
a->chunkinfo = malloc(sizeof(struct chunk_table) * (entries + 1));
|
||||
if (a->chunkinfo == NULL) {
|
||||
LOG_WARN("malloc fail");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < entries; ++i) {
|
||||
a->chunkinfo[i].offset = unpackN((u32_t *)ptr);
|
||||
a->chunkinfo[i].sample = 0;
|
||||
ptr += 4;
|
||||
}
|
||||
a->chunkinfo[i].sample = 0;
|
||||
a->chunkinfo[i].offset = 0;
|
||||
// fill in first sample id for each chunk from stored stsc
|
||||
if (a->stsc) {
|
||||
u32_t stsc_entries = unpackN((u32_t *)a->stsc);
|
||||
u32_t sample = 0;
|
||||
u32_t last = 0, last_samples = 0;
|
||||
u8_t *ptr = (u8_t *)a->stsc + 4;
|
||||
while (stsc_entries--) {
|
||||
u32_t first = unpackN((u32_t *)ptr);
|
||||
u32_t samples = unpackN((u32_t *)(ptr + 4));
|
||||
if (last) {
|
||||
for (i = last - 1; i < first - 1; ++i) {
|
||||
a->chunkinfo[i].sample = sample;
|
||||
sample += last_samples;
|
||||
}
|
||||
}
|
||||
if (stsc_entries == 0) {
|
||||
for (i = first - 1; i < entries; ++i) {
|
||||
a->chunkinfo[i].sample = sample;
|
||||
sample += samples;
|
||||
}
|
||||
}
|
||||
last = first;
|
||||
last_samples = samples;
|
||||
ptr += 12;
|
||||
}
|
||||
free(a->stsc);
|
||||
a->stsc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// found media data, advance to start of first chunk and return
|
||||
if (!strcmp(type, "mdat")) {
|
||||
_buf_inc_readp(streambuf, 8);
|
||||
a->pos += 8;
|
||||
bytes -= 8;
|
||||
if (play) {
|
||||
LOG_DEBUG("type: mdat len: %u pos: %u", len, a->pos);
|
||||
if (a->chunkinfo && a->chunkinfo[0].offset > a->pos) {
|
||||
u32_t skip = a->chunkinfo[0].offset - a->pos;
|
||||
LOG_DEBUG("skipping: %u", skip);
|
||||
if (skip <= bytes) {
|
||||
_buf_inc_readp(streambuf, skip);
|
||||
a->pos += skip;
|
||||
} else {
|
||||
a->consume = skip;
|
||||
}
|
||||
}
|
||||
a->sample = a->nextchunk = 1;
|
||||
return 1;
|
||||
} else {
|
||||
LOG_DEBUG("type: mdat len: %u, no playable track found", len);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// parse key-value atoms within ilst ---- entries to get encoder padding within iTunSMPB entry for gapless
|
||||
if (!strcmp(type, "----") && bytes > len) {
|
||||
u8_t *ptr = streambuf->readp + 8;
|
||||
u32_t remain = len - 8, size;
|
||||
if (!memcmp(ptr + 4, "mean", 4) && (size = unpackN((u32_t *)ptr)) < remain) {
|
||||
ptr += size; remain -= size;
|
||||
}
|
||||
if (!memcmp(ptr + 4, "name", 4) && (size = unpackN((u32_t *)ptr)) < remain && !memcmp(ptr + 12, "iTunSMPB", 8)) {
|
||||
ptr += size; remain -= size;
|
||||
}
|
||||
if (!memcmp(ptr + 4, "data", 4) && remain > 16 + 48) {
|
||||
// data is stored as hex strings: 0 start end samples
|
||||
u32_t b, c; u64_t d;
|
||||
if (sscanf((const char *)(ptr + 16), "%x %x %x " FMT_x64, &b, &b, &c, &d) == 4) {
|
||||
LOG_DEBUG("iTunSMPB start: %u end: %u samples: " FMT_u64, b, c, d);
|
||||
if (a->sttssamples && a->sttssamples < b + c + d) {
|
||||
LOG_DEBUG("reducing samples as stts count is less");
|
||||
d = a->sttssamples - (b + c);
|
||||
}
|
||||
a->skip = b;
|
||||
a->samples = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to consuming entire box
|
||||
consume = len;
|
||||
|
||||
// read into these boxes so reduce consume
|
||||
if (!strcmp(type, "moov") || !strcmp(type, "trak") || !strcmp(type, "mdia") || !strcmp(type, "minf") || !strcmp(type, "stbl") ||
|
||||
!strcmp(type, "udta") || !strcmp(type, "ilst")) {
|
||||
consume = 8;
|
||||
}
|
||||
// special cases which mix mix data in the enclosing box which we want to read into
|
||||
if (!strcmp(type, "stsd")) consume = 16;
|
||||
if (!strcmp(type, "mp4a")) consume = 36;
|
||||
if (!strcmp(type, "meta")) consume = 12;
|
||||
|
||||
// consume rest of box if it has been parsed (all in the buffer) or is not one we want to parse
|
||||
if (bytes >= consume) {
|
||||
LOG_DEBUG("type: %s len: %u consume: %u", type, len, consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
a->pos += consume;
|
||||
bytes -= consume;
|
||||
} else if ( !(!strcmp(type, "esds") || !strcmp(type, "stts") || !strcmp(type, "stsc") ||
|
||||
!strcmp(type, "stco") || !strcmp(type, "----")) ) {
|
||||
LOG_DEBUG("type: %s len: %u consume: %u - partial consume: %u", type, len, consume, bytes);
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
a->pos += bytes;
|
||||
a->consume = consume - bytes;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static decode_state faad_decode(void) {
|
||||
size_t bytes_total;
|
||||
size_t bytes_wrap;
|
||||
static NeAACDecFrameInfo info;
|
||||
ISAMPLE_T *iptr;
|
||||
bool endstream;
|
||||
frames_t frames;
|
||||
|
||||
LOCK_S;
|
||||
bytes_total = _buf_used(streambuf);
|
||||
bytes_wrap = min(bytes_total, _buf_cont_read(streambuf));
|
||||
|
||||
if (stream.state <= DISCONNECT && !bytes_total) {
|
||||
UNLOCK_S;
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
if (a->consume) {
|
||||
u32_t consume = min(a->consume, bytes_wrap);
|
||||
LOG_DEBUG("consume: %u of %u", consume, a->consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
a->pos += consume;
|
||||
a->consume -= consume;
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
if (decode.new_stream) {
|
||||
int found = 0;
|
||||
static unsigned char channels;
|
||||
static unsigned long samplerate;
|
||||
|
||||
if (a->type == '2') {
|
||||
|
||||
// adts stream - seek for header
|
||||
while (bytes_wrap >= 2 && (*(streambuf->readp) != 0xFF || (*(streambuf->readp + 1) & 0xF6) != 0xF0)) {
|
||||
_buf_inc_readp(streambuf, 1);
|
||||
bytes_total--;
|
||||
bytes_wrap--;
|
||||
}
|
||||
|
||||
if (bytes_wrap >= 2) {
|
||||
long n = NEAAC(a, Init, a->hAac, streambuf->readp, bytes_wrap, &samplerate, &channels);
|
||||
if (n < 0) {
|
||||
found = -1;
|
||||
} else {
|
||||
_buf_inc_readp(streambuf, n);
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// mp4 - read header
|
||||
found = read_mp4_header(&samplerate, &channels);
|
||||
}
|
||||
|
||||
if (found == 1) {
|
||||
|
||||
LOG_INFO("samplerate: %u channels: %u", samplerate, channels);
|
||||
bytes_total = _buf_used(streambuf);
|
||||
bytes_wrap = min(bytes_total, _buf_cont_read(streambuf));
|
||||
|
||||
LOCK_O;
|
||||
LOG_INFO("setting track_start");
|
||||
output.next_sample_rate = decode_newstream(samplerate, output.supported_rates);
|
||||
IF_DSD( output.next_fmt = PCM; )
|
||||
output.track_start = outputbuf->writep;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
decode.new_stream = false;
|
||||
UNLOCK_O;
|
||||
|
||||
} else if (found == -1) {
|
||||
|
||||
LOG_WARN("error reading stream header");
|
||||
UNLOCK_S;
|
||||
return DECODE_ERROR;
|
||||
|
||||
} else {
|
||||
|
||||
// not finished header parsing come back next time
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes_wrap < WRAPBUF_LEN && bytes_total > WRAPBUF_LEN) {
|
||||
|
||||
// make a local copy of frames which may have wrapped round the end of streambuf
|
||||
static u8_t buf[WRAPBUF_LEN];
|
||||
memcpy(buf, streambuf->readp, bytes_wrap);
|
||||
memcpy(buf + bytes_wrap, streambuf->buf, WRAPBUF_LEN - bytes_wrap);
|
||||
|
||||
iptr = NEAAC(a, Decode, a->hAac, &info, buf, WRAPBUF_LEN);
|
||||
|
||||
} else {
|
||||
|
||||
iptr = NEAAC(a, Decode, a->hAac, &info, streambuf->readp, bytes_wrap);
|
||||
}
|
||||
|
||||
if (info.error) {
|
||||
LOG_WARN("error: %u %s", info.error, NEAAC(a, GetErrorMessage, info.error));
|
||||
}
|
||||
|
||||
endstream = false;
|
||||
|
||||
// mp4 end of chunk - skip to next offset
|
||||
if (a->chunkinfo && a->chunkinfo[a->nextchunk].offset && a->sample++ == a->chunkinfo[a->nextchunk].sample) {
|
||||
|
||||
if (a->chunkinfo[a->nextchunk].offset > a->pos) {
|
||||
u32_t skip = a->chunkinfo[a->nextchunk].offset - a->pos;
|
||||
if (skip != info.bytesconsumed) {
|
||||
LOG_DEBUG("skipping to next chunk pos: %u consumed: %u != skip: %u", a->pos, info.bytesconsumed, skip);
|
||||
}
|
||||
if (bytes_total >= skip) {
|
||||
_buf_inc_readp(streambuf, skip);
|
||||
a->pos += skip;
|
||||
} else {
|
||||
a->consume = skip;
|
||||
}
|
||||
a->nextchunk++;
|
||||
} else {
|
||||
LOG_ERROR("error: need to skip backwards!");
|
||||
endstream = true;
|
||||
}
|
||||
|
||||
// adts and mp4 when not at end of chunk
|
||||
} else if (info.bytesconsumed != 0) {
|
||||
|
||||
_buf_inc_readp(streambuf, info.bytesconsumed);
|
||||
a->pos += info.bytesconsumed;
|
||||
|
||||
// error which doesn't advance streambuf - end
|
||||
} else {
|
||||
endstream = true;
|
||||
}
|
||||
|
||||
UNLOCK_S;
|
||||
|
||||
if (endstream) {
|
||||
LOG_WARN("unable to decode further");
|
||||
return DECODE_ERROR;
|
||||
}
|
||||
|
||||
if (!info.samples) {
|
||||
a->empty = true;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
frames = info.samples / info.channels;
|
||||
|
||||
if (a->skip) {
|
||||
u32_t skip;
|
||||
if (a->empty) {
|
||||
a->empty = false;
|
||||
a->skip -= frames;
|
||||
LOG_DEBUG("gapless: first frame empty, skipped %u frames at start", frames);
|
||||
}
|
||||
skip = min(frames, a->skip);
|
||||
LOG_DEBUG("gapless: skipping %u frames at start", skip);
|
||||
frames -= skip;
|
||||
a->skip -= skip;
|
||||
iptr += skip * info.channels;
|
||||
}
|
||||
|
||||
if (a->samples) {
|
||||
if (a->samples < frames) {
|
||||
LOG_DEBUG("gapless: trimming %u frames from end", frames - a->samples);
|
||||
frames = (frames_t)a->samples;
|
||||
}
|
||||
a->samples -= frames;
|
||||
}
|
||||
|
||||
LOG_SDEBUG("write %u frames", frames);
|
||||
|
||||
LOCK_O_direct;
|
||||
|
||||
while (frames > 0) {
|
||||
frames_t f;
|
||||
frames_t count;
|
||||
ISAMPLE_T *optr;
|
||||
|
||||
IF_DIRECT(
|
||||
f = _buf_cont_write(outputbuf) / BYTES_PER_FRAME;
|
||||
optr = (ISAMPLE_T *)outputbuf->writep;
|
||||
);
|
||||
IF_PROCESS(
|
||||
f = process.max_in_frames;
|
||||
optr = (ISAMPLE_T *)process.inbuf;
|
||||
);
|
||||
|
||||
f = min(f, frames);
|
||||
count = f;
|
||||
|
||||
if (info.channels == 2) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
memcpy(optr, iptr, count * BYTES_PER_FRAME);
|
||||
iptr += count * 2;
|
||||
#else
|
||||
while (count--) {
|
||||
*optr++ = *iptr++ << 8;
|
||||
*optr++ = *iptr++ << 8;
|
||||
}
|
||||
#endif
|
||||
} else if (info.channels == 1) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN(*iptr);
|
||||
*optr++ = ALIGN(*iptr++);
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("unsupported number of channels");
|
||||
}
|
||||
|
||||
frames -= f;
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames = f;
|
||||
if (frames) LOG_ERROR("unhandled case");
|
||||
);
|
||||
}
|
||||
|
||||
UNLOCK_O_direct;
|
||||
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
static void faad_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
|
||||
NeAACDecConfigurationPtr conf;
|
||||
|
||||
LOG_INFO("opening %s stream", size == '2' ? "adts" : "mp4");
|
||||
|
||||
a->type = size;
|
||||
a->pos = a->consume = a->sample = a->nextchunk = 0;
|
||||
|
||||
if (a->chunkinfo) {
|
||||
free(a->chunkinfo);
|
||||
}
|
||||
if (a->stsc) {
|
||||
free(a->stsc);
|
||||
}
|
||||
a->chunkinfo = NULL;
|
||||
a->stsc = NULL;
|
||||
a->skip = 0;
|
||||
a->samples = 0;
|
||||
a->sttssamples = 0;
|
||||
a->empty = false;
|
||||
|
||||
if (a->hAac) {
|
||||
NEAAC(a, Close, a->hAac);
|
||||
}
|
||||
a->hAac = NEAAC(a, Open);
|
||||
|
||||
conf = NEAAC(a, GetCurrentConfiguration, a->hAac);
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
conf->outputFormat = FAAD_FMT_16BIT;
|
||||
#else
|
||||
conf->outputFormat = FAAD_FMT_24BIT;
|
||||
#endif
|
||||
conf->defSampleRate = 44100;
|
||||
conf->downMatrix = 1;
|
||||
|
||||
if (!NEAAC(a, SetConfiguration, a->hAac, conf)) {
|
||||
LOG_WARN("error setting config");
|
||||
};
|
||||
}
|
||||
|
||||
static void faad_close(void) {
|
||||
NEAAC(a, Close, a->hAac);
|
||||
a->hAac = NULL;
|
||||
if (a->chunkinfo) {
|
||||
free(a->chunkinfo);
|
||||
a->chunkinfo = NULL;
|
||||
}
|
||||
if (a->stsc) {
|
||||
free(a->stsc);
|
||||
a->stsc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_faad() {
|
||||
#if !LINKALL
|
||||
void *handle = dlopen(LIBFAAD, RTLD_NOW);
|
||||
char *err;
|
||||
|
||||
if (!handle) {
|
||||
LOG_INFO("dlerror: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
a->NeAACDecGetCurrentConfiguration = dlsym(handle, "NeAACDecGetCurrentConfiguration");
|
||||
a->NeAACDecSetConfiguration = dlsym(handle, "NeAACDecSetConfiguration");
|
||||
a->NeAACDecOpen = dlsym(handle, "NeAACDecOpen");
|
||||
a->NeAACDecClose = dlsym(handle, "NeAACDecClose");
|
||||
a->NeAACDecInit = dlsym(handle, "NeAACDecInit");
|
||||
a->NeAACDecInit2 = dlsym(handle, "NeAACDecInit2");
|
||||
a->NeAACDecDecode = dlsym(handle, "NeAACDecDecode");
|
||||
a->NeAACDecGetErrorMessage = dlsym(handle, "NeAACDecGetErrorMessage");
|
||||
|
||||
if ((err = dlerror()) != NULL) {
|
||||
LOG_INFO("dlerror: %s", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("loaded "LIBFAAD"");
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct codec *register_faad(void) {
|
||||
static struct codec ret = {
|
||||
'a', // id
|
||||
"aac", // types
|
||||
WRAPBUF_LEN, // min read
|
||||
20480, // min space
|
||||
faad_open, // open
|
||||
faad_close, // close
|
||||
faad_decode, // decode
|
||||
};
|
||||
|
||||
a = malloc(sizeof(struct faad));
|
||||
if (!a) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
a->hAac = NULL;
|
||||
a->chunkinfo = NULL;
|
||||
a->stsc = NULL;
|
||||
|
||||
if (!load_faad()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_INFO("using faad to decode aac");
|
||||
return &ret;
|
||||
}
|
||||
308
components/squeezelite/flac.c
Normal file
308
components/squeezelite/flac.c
Normal file
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezeplay emulator for linux
|
||||
*
|
||||
* (c) Adrian Smith 2012, triode1@btinternet.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <FLAC/stream_decoder.h>
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
#define ALIGN8(n) (n << 8)
|
||||
#define ALIGN16(n) (n)
|
||||
#define ALIGN24(n) (n >> 8)
|
||||
#define ALIGN32(n) (n >> 16)
|
||||
#else
|
||||
#define ALIGN8(n) (n << 24)
|
||||
#define ALIGN16(n) (n << 16)
|
||||
#define ALIGN24(n) (n << 8)
|
||||
#define ALIGN32(n) (n)
|
||||
#endif
|
||||
|
||||
struct flac {
|
||||
FLAC__StreamDecoder *decoder;
|
||||
#if !LINKALL
|
||||
// FLAC symbols to be dynamically loaded
|
||||
const char **FLAC__StreamDecoderErrorStatusString;
|
||||
const char **FLAC__StreamDecoderStateString;
|
||||
FLAC__StreamDecoder * (* FLAC__stream_decoder_new)(void);
|
||||
FLAC__bool (* FLAC__stream_decoder_reset)(FLAC__StreamDecoder *decoder);
|
||||
void (* FLAC__stream_decoder_delete)(FLAC__StreamDecoder *decoder);
|
||||
FLAC__StreamDecoderInitStatus (* FLAC__stream_decoder_init_stream)(
|
||||
FLAC__StreamDecoder *decoder,
|
||||
FLAC__StreamDecoderReadCallback read_callback,
|
||||
FLAC__StreamDecoderSeekCallback seek_callback,
|
||||
FLAC__StreamDecoderTellCallback tell_callback,
|
||||
FLAC__StreamDecoderLengthCallback length_callback,
|
||||
FLAC__StreamDecoderEofCallback eof_callback,
|
||||
FLAC__StreamDecoderWriteCallback write_callback,
|
||||
FLAC__StreamDecoderMetadataCallback metadata_callback,
|
||||
FLAC__StreamDecoderErrorCallback error_callback,
|
||||
void *client_data
|
||||
);
|
||||
FLAC__bool (* FLAC__stream_decoder_process_single)(FLAC__StreamDecoder *decoder);
|
||||
FLAC__StreamDecoderState (* FLAC__stream_decoder_get_state)(const FLAC__StreamDecoder *decoder);
|
||||
#endif
|
||||
};
|
||||
|
||||
static struct flac *f;
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
#if LINKALL
|
||||
#define FLAC(h, fn, ...) (FLAC__ ## fn)(__VA_ARGS__)
|
||||
#define FLAC_A(h, a) (FLAC__ ## a)
|
||||
#else
|
||||
#define FLAC(h, fn, ...) (h)->FLAC__##fn(__VA_ARGS__)
|
||||
#define FLAC_A(h, a) (h)->FLAC__ ## a
|
||||
#endif
|
||||
|
||||
static FLAC__StreamDecoderReadStatus read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *want, void *client_data) {
|
||||
size_t bytes;
|
||||
bool end;
|
||||
|
||||
LOCK_S;
|
||||
bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
bytes = min(bytes, *want);
|
||||
end = (stream.state <= DISCONNECT && bytes == 0);
|
||||
|
||||
memcpy(buffer, streambuf->readp, bytes);
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
UNLOCK_S;
|
||||
|
||||
*want = bytes;
|
||||
|
||||
return end ? FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM : FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
|
||||
}
|
||||
|
||||
static FLAC__StreamDecoderWriteStatus write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame,
|
||||
const FLAC__int32 *const buffer[], void *client_data) {
|
||||
|
||||
size_t frames = frame->header.blocksize;
|
||||
unsigned bits_per_sample = frame->header.bits_per_sample;
|
||||
unsigned channels = frame->header.channels;
|
||||
|
||||
FLAC__int32 *lptr = (FLAC__int32 *)buffer[0];
|
||||
FLAC__int32 *rptr = (FLAC__int32 *)buffer[channels > 1 ? 1 : 0];
|
||||
|
||||
if (decode.new_stream) {
|
||||
LOCK_O;
|
||||
LOG_INFO("setting track_start");
|
||||
output.track_start = outputbuf->writep;
|
||||
decode.new_stream = false;
|
||||
|
||||
#if DSD
|
||||
#if SL_LITTLE_ENDIAN
|
||||
#define MARKER_OFFSET 2
|
||||
#else
|
||||
#define MARKER_OFFSET 1
|
||||
#endif
|
||||
if (bits_per_sample == 24 && is_stream_dop(((u8_t *)lptr) + MARKER_OFFSET, ((u8_t *)rptr) + MARKER_OFFSET, 4, frames)) {
|
||||
LOG_INFO("file contains DOP");
|
||||
if (output.dsdfmt == DOP_S24_LE || output.dsdfmt == DOP_S24_3LE)
|
||||
output.next_fmt = output.dsdfmt;
|
||||
else
|
||||
output.next_fmt = DOP;
|
||||
output.next_sample_rate = frame->header.sample_rate;
|
||||
output.fade = FADE_INACTIVE;
|
||||
} else {
|
||||
output.next_sample_rate = decode_newstream(frame->header.sample_rate, output.supported_rates);
|
||||
output.next_fmt = PCM;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
}
|
||||
#else
|
||||
output.next_sample_rate = decode_newstream(frame->header.sample_rate, output.supported_rates);
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
#endif
|
||||
|
||||
UNLOCK_O;
|
||||
}
|
||||
|
||||
LOCK_O_direct;
|
||||
|
||||
while (frames > 0) {
|
||||
frames_t f;
|
||||
frames_t count;
|
||||
ISAMPLE_T *optr;
|
||||
|
||||
IF_DIRECT(
|
||||
optr = (ISAMPLE_T *)outputbuf->writep;
|
||||
f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
|
||||
);
|
||||
IF_PROCESS(
|
||||
optr = (ISAMPLE_T *)process.inbuf;
|
||||
f = process.max_in_frames;
|
||||
);
|
||||
|
||||
f = min(f, frames);
|
||||
|
||||
count = f;
|
||||
|
||||
if (bits_per_sample == 8) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN8(*lptr++);
|
||||
*optr++ = ALIGN8(*rptr++);
|
||||
}
|
||||
} else if (bits_per_sample == 16) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN16(*lptr++);
|
||||
*optr++ = ALIGN16(*rptr++);
|
||||
}
|
||||
} else if (bits_per_sample == 24) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN24(*lptr++);
|
||||
*optr++ = ALIGN24(*rptr++);
|
||||
}
|
||||
} else if (bits_per_sample == 32) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN32(*lptr++);
|
||||
*optr++ = ALIGN32(*rptr++);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("unsupported bits per sample: %u", bits_per_sample);
|
||||
}
|
||||
|
||||
frames -= f;
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames = f;
|
||||
if (frames) LOG_ERROR("unhandled case");
|
||||
);
|
||||
}
|
||||
|
||||
UNLOCK_O_direct;
|
||||
|
||||
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
|
||||
}
|
||||
|
||||
static void error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) {
|
||||
LOG_INFO("flac error: %s", FLAC_A(f, StreamDecoderErrorStatusString)[status]);
|
||||
}
|
||||
|
||||
static void flac_open(u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness) {
|
||||
if (f->decoder) {
|
||||
FLAC(f, stream_decoder_reset, f->decoder);
|
||||
} else {
|
||||
f->decoder = FLAC(f, stream_decoder_new);
|
||||
}
|
||||
FLAC(f, stream_decoder_init_stream, f->decoder, &read_cb, NULL, NULL, NULL, NULL, &write_cb, NULL, &error_cb, NULL);
|
||||
}
|
||||
|
||||
static void flac_close(void) {
|
||||
FLAC(f, stream_decoder_delete, f->decoder);
|
||||
f->decoder = NULL;
|
||||
}
|
||||
|
||||
static decode_state flac_decode(void) {
|
||||
bool ok = FLAC(f, stream_decoder_process_single, f->decoder);
|
||||
FLAC__StreamDecoderState state = FLAC(f, stream_decoder_get_state, f->decoder);
|
||||
|
||||
if (!ok && state != FLAC__STREAM_DECODER_END_OF_STREAM) {
|
||||
LOG_INFO("flac error: %s", FLAC_A(f, StreamDecoderStateString)[state]);
|
||||
};
|
||||
|
||||
if (state == FLAC__STREAM_DECODER_END_OF_STREAM) {
|
||||
return DECODE_COMPLETE;
|
||||
} else if (state > FLAC__STREAM_DECODER_END_OF_STREAM) {
|
||||
return DECODE_ERROR;
|
||||
} else {
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_flac() {
|
||||
#if !LINKALL
|
||||
void *handle = dlopen(LIBFLAC, RTLD_NOW);
|
||||
char *err;
|
||||
|
||||
if (!handle) {
|
||||
LOG_INFO("dlerror: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
f->FLAC__StreamDecoderErrorStatusString = dlsym(handle, "FLAC__StreamDecoderErrorStatusString");
|
||||
f->FLAC__StreamDecoderStateString = dlsym(handle, "FLAC__StreamDecoderStateString");
|
||||
f->FLAC__stream_decoder_new = dlsym(handle, "FLAC__stream_decoder_new");
|
||||
f->FLAC__stream_decoder_reset = dlsym(handle, "FLAC__stream_decoder_reset");
|
||||
f->FLAC__stream_decoder_delete = dlsym(handle, "FLAC__stream_decoder_delete");
|
||||
f->FLAC__stream_decoder_init_stream = dlsym(handle, "FLAC__stream_decoder_init_stream");
|
||||
f->FLAC__stream_decoder_process_single = dlsym(handle, "FLAC__stream_decoder_process_single");
|
||||
f->FLAC__stream_decoder_get_state = dlsym(handle, "FLAC__stream_decoder_get_state");
|
||||
|
||||
if ((err = dlerror()) != NULL) {
|
||||
LOG_INFO("dlerror: %s", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("loaded "LIBFLAC);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct codec *register_flac(void) {
|
||||
static struct codec ret = {
|
||||
'f', // id
|
||||
"flc", // types
|
||||
16384, // min read
|
||||
204800, // min space
|
||||
flac_open, // open
|
||||
flac_close, // close
|
||||
flac_decode, // decode
|
||||
};
|
||||
|
||||
f = malloc(sizeof(struct flac));
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
f->decoder = NULL;
|
||||
|
||||
if (!load_flac()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_INFO("using flac to decode flc");
|
||||
return &ret;
|
||||
}
|
||||
658
components/squeezelite/helix-aac.c
Normal file
658
components/squeezelite/helix-aac.c
Normal file
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.com
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <aacdec.h>
|
||||
|
||||
// AAC_MAX_SAMPLES is the number of samples for one channel
|
||||
#define FRAME_BUF (AAC_MAX_NSAMPS*2)
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
#define ALIGN(n) (n)
|
||||
#else
|
||||
#define ALIGN(n) (n << 8)
|
||||
#endif
|
||||
|
||||
#define WRAPBUF_LEN 2048
|
||||
|
||||
static unsigned rates[] = { 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 };
|
||||
|
||||
struct chunk_table {
|
||||
u32_t sample, offset;
|
||||
};
|
||||
|
||||
struct helixaac {
|
||||
HAACDecoder hAac;
|
||||
u8_t type;
|
||||
u8_t *write_buf;
|
||||
// following used for mp4 only
|
||||
u32_t consume;
|
||||
u32_t pos;
|
||||
u32_t sample;
|
||||
u32_t nextchunk;
|
||||
void *stsc;
|
||||
u32_t skip;
|
||||
u64_t samples;
|
||||
u64_t sttssamples;
|
||||
bool empty;
|
||||
struct chunk_table *chunkinfo;
|
||||
#if !LINKALL
|
||||
#endif
|
||||
};
|
||||
|
||||
static struct helixaac *a;
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
#if LINKALL
|
||||
#define HAAC(h, fn, ...) (AAC ## fn)(__VA_ARGS__)
|
||||
#else
|
||||
#define HAAC(h, fn, ...) (h)->AAC##fn(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
// minimal code for mp4 file parsing to extract audio config and find media data
|
||||
|
||||
// adapted from faad2/common/mp4ff
|
||||
u32_t mp4_desc_length(u8_t **buf) {
|
||||
u8_t b;
|
||||
u8_t num_bytes = 0;
|
||||
u32_t length = 0;
|
||||
|
||||
do {
|
||||
b = **buf;
|
||||
*buf += 1;
|
||||
num_bytes++;
|
||||
length = (length << 7) | (b & 0x7f);
|
||||
} while ((b & 0x80) && num_bytes < 4);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
// read mp4 header to extract config data
|
||||
static int read_mp4_header(unsigned long *samplerate_p, unsigned char *channels_p) {
|
||||
size_t bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
char type[5];
|
||||
u32_t len;
|
||||
|
||||
while (bytes >= 8) {
|
||||
// count trak to find the first playable one
|
||||
static unsigned trak, play;
|
||||
u32_t consume;
|
||||
|
||||
len = unpackN((u32_t *)streambuf->readp);
|
||||
memcpy(type, streambuf->readp + 4, 4);
|
||||
type[4] = '\0';
|
||||
|
||||
if (!strcmp(type, "moov")) {
|
||||
trak = 0;
|
||||
play = 0;
|
||||
}
|
||||
if (!strcmp(type, "trak")) {
|
||||
trak++;
|
||||
}
|
||||
|
||||
// extract audio config from within esds and pass to DecInit2
|
||||
if (!strcmp(type, "esds") && bytes > len) {
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
AACFrameInfo info;
|
||||
if (*ptr++ == 0x03) {
|
||||
mp4_desc_length(&ptr);
|
||||
ptr += 4;
|
||||
} else {
|
||||
ptr += 3;
|
||||
}
|
||||
mp4_desc_length(&ptr);
|
||||
ptr += 13;
|
||||
if (*ptr++ != 0x05) {
|
||||
LOG_WARN("error parsing esds");
|
||||
return -1;
|
||||
}
|
||||
mp4_desc_length(&ptr);
|
||||
info.profile = *ptr >> 3;
|
||||
info.sampRateCore = (*ptr++ & 0x07) << 1;
|
||||
info.sampRateCore |= (*ptr >> 7) & 0x01;
|
||||
info.sampRateCore = rates[info.sampRateCore];
|
||||
info.nChans = *ptr >> 3;
|
||||
*channels_p = info.nChans;
|
||||
*samplerate_p = info.sampRateCore;
|
||||
HAAC(a, SetRawBlockParams, a->hAac, 0, &info);
|
||||
LOG_DEBUG("playable aac track: %u (p:%x, r:%d, c:%d)", trak, info.profile, info.sampRateCore, info.nChans);
|
||||
play = trak;
|
||||
}
|
||||
|
||||
// extract the total number of samples from stts
|
||||
if (!strcmp(type, "stts") && bytes > len) {
|
||||
u32_t i;
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
u32_t entries = unpackN((u32_t *)ptr);
|
||||
ptr += 4;
|
||||
for (i = 0; i < entries; ++i) {
|
||||
u32_t count = unpackN((u32_t *)ptr);
|
||||
u32_t size = unpackN((u32_t *)(ptr + 4));
|
||||
a->sttssamples += count * size;
|
||||
ptr += 8;
|
||||
}
|
||||
LOG_DEBUG("total number of samples contained in stts: " FMT_u64, a->sttssamples);
|
||||
}
|
||||
|
||||
// stash sample to chunk info, assume it comes before stco
|
||||
if (!strcmp(type, "stsc") && bytes > len && !a->chunkinfo) {
|
||||
a->stsc = malloc(len - 12);
|
||||
if (a->stsc == NULL) {
|
||||
LOG_WARN("malloc fail");
|
||||
return -1;
|
||||
}
|
||||
memcpy(a->stsc, streambuf->readp + 12, len - 12);
|
||||
}
|
||||
|
||||
// build offsets table from stco and stored stsc
|
||||
if (!strcmp(type, "stco") && bytes > len && play == trak) {
|
||||
u32_t i;
|
||||
// extract chunk offsets
|
||||
u8_t *ptr = streambuf->readp + 12;
|
||||
u32_t entries = unpackN((u32_t *)ptr);
|
||||
ptr += 4;
|
||||
a->chunkinfo = malloc(sizeof(struct chunk_table) * (entries + 1));
|
||||
if (a->chunkinfo == NULL) {
|
||||
LOG_WARN("malloc fail");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < entries; ++i) {
|
||||
a->chunkinfo[i].offset = unpackN((u32_t *)ptr);
|
||||
a->chunkinfo[i].sample = 0;
|
||||
ptr += 4;
|
||||
}
|
||||
a->chunkinfo[i].sample = 0;
|
||||
a->chunkinfo[i].offset = 0;
|
||||
// fill in first sample id for each chunk from stored stsc
|
||||
if (a->stsc) {
|
||||
u32_t stsc_entries = unpackN((u32_t *)a->stsc);
|
||||
u32_t sample = 0;
|
||||
u32_t last = 0, last_samples = 0;
|
||||
u8_t *ptr = (u8_t *)a->stsc + 4;
|
||||
while (stsc_entries--) {
|
||||
u32_t first = unpackN((u32_t *)ptr);
|
||||
u32_t samples = unpackN((u32_t *)(ptr + 4));
|
||||
if (last) {
|
||||
for (i = last - 1; i < first - 1; ++i) {
|
||||
a->chunkinfo[i].sample = sample;
|
||||
sample += last_samples;
|
||||
}
|
||||
}
|
||||
if (stsc_entries == 0) {
|
||||
for (i = first - 1; i < entries; ++i) {
|
||||
a->chunkinfo[i].sample = sample;
|
||||
sample += samples;
|
||||
}
|
||||
}
|
||||
last = first;
|
||||
last_samples = samples;
|
||||
ptr += 12;
|
||||
}
|
||||
free(a->stsc);
|
||||
a->stsc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// found media data, advance to start of first chunk and return
|
||||
if (!strcmp(type, "mdat")) {
|
||||
_buf_inc_readp(streambuf, 8);
|
||||
a->pos += 8;
|
||||
bytes -= 8;
|
||||
if (play) {
|
||||
LOG_DEBUG("type: mdat len: %u pos: %u", len, a->pos);
|
||||
if (a->chunkinfo && a->chunkinfo[0].offset > a->pos) {
|
||||
u32_t skip = a->chunkinfo[0].offset - a->pos;
|
||||
LOG_DEBUG("skipping: %u", skip);
|
||||
if (skip <= bytes) {
|
||||
_buf_inc_readp(streambuf, skip);
|
||||
a->pos += skip;
|
||||
} else {
|
||||
a->consume = skip;
|
||||
}
|
||||
}
|
||||
a->sample = a->nextchunk = 1;
|
||||
return 1;
|
||||
} else {
|
||||
LOG_DEBUG("type: mdat len: %u, no playable track found", len);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// parse key-value atoms within ilst ---- entries to get encoder padding within iTunSMPB entry for gapless
|
||||
if (!strcmp(type, "----") && bytes > len) {
|
||||
u8_t *ptr = streambuf->readp + 8;
|
||||
u32_t remain = len - 8, size;
|
||||
if (!memcmp(ptr + 4, "mean", 4) && (size = unpackN((u32_t *)ptr)) < remain) {
|
||||
ptr += size; remain -= size;
|
||||
}
|
||||
if (!memcmp(ptr + 4, "name", 4) && (size = unpackN((u32_t *)ptr)) < remain && !memcmp(ptr + 12, "iTunSMPB", 8)) {
|
||||
ptr += size; remain -= size;
|
||||
}
|
||||
if (!memcmp(ptr + 4, "data", 4) && remain > 16 + 48) {
|
||||
// data is stored as hex strings: 0 start end samples
|
||||
u32_t b, c; u64_t d;
|
||||
if (sscanf((const char *)(ptr + 16), "%x %x %x " FMT_x64, &b, &b, &c, &d) == 4) {
|
||||
LOG_DEBUG("iTunSMPB start: %u end: %u samples: " FMT_u64, b, c, d);
|
||||
if (a->sttssamples && a->sttssamples < b + c + d) {
|
||||
LOG_DEBUG("reducing samples as stts count is less");
|
||||
d = a->sttssamples - (b + c);
|
||||
}
|
||||
a->skip = b;
|
||||
a->samples = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to consuming entire box
|
||||
consume = len;
|
||||
|
||||
// read into these boxes so reduce consume
|
||||
if (!strcmp(type, "moov") || !strcmp(type, "trak") || !strcmp(type, "mdia") || !strcmp(type, "minf") || !strcmp(type, "stbl") ||
|
||||
!strcmp(type, "udta") || !strcmp(type, "ilst")) {
|
||||
consume = 8;
|
||||
}
|
||||
// special cases which mix mix data in the enclosing box which we want to read into
|
||||
if (!strcmp(type, "stsd")) consume = 16;
|
||||
if (!strcmp(type, "mp4a")) consume = 36;
|
||||
if (!strcmp(type, "meta")) consume = 12;
|
||||
|
||||
// consume rest of box if it has been parsed (all in the buffer) or is not one we want to parse
|
||||
if (bytes >= consume) {
|
||||
LOG_DEBUG("type: %s len: %u consume: %u", type, len, consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
a->pos += consume;
|
||||
bytes -= consume;
|
||||
} else if ( !(!strcmp(type, "esds") || !strcmp(type, "stts") || !strcmp(type, "stsc") ||
|
||||
!strcmp(type, "stco") || !strcmp(type, "----")) ) {
|
||||
LOG_DEBUG("type: %s len: %u consume: %u - partial consume: %u", type, len, consume, bytes);
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
a->pos += bytes;
|
||||
a->consume = consume - bytes;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static decode_state helixaac_decode(void) {
|
||||
size_t bytes_total, bytes_wrap;
|
||||
int res, bytes;
|
||||
static AACFrameInfo info;
|
||||
ISAMPLE_T *iptr;
|
||||
u8_t *sptr;
|
||||
bool endstream;
|
||||
frames_t frames;
|
||||
|
||||
LOCK_S;
|
||||
bytes_total = _buf_used(streambuf);
|
||||
bytes_wrap = min(bytes_total, _buf_cont_read(streambuf));
|
||||
|
||||
if (stream.state <= DISCONNECT && !bytes_total) {
|
||||
UNLOCK_S;
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
if (a->consume) {
|
||||
u32_t consume = min(a->consume, bytes_wrap);
|
||||
LOG_DEBUG("consume: %u of %u", consume, a->consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
a->pos += consume;
|
||||
a->consume -= consume;
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
if (decode.new_stream) {
|
||||
int found = 0;
|
||||
static unsigned char channels;
|
||||
static unsigned long samplerate;
|
||||
|
||||
if (a->type == '2') {
|
||||
|
||||
// adts stream - seek for header
|
||||
long n = AACFindSyncWord(streambuf->readp, bytes_wrap);
|
||||
|
||||
LOG_DEBUG("Sync search in %d bytes %d", bytes_wrap, n);
|
||||
|
||||
if (n >= 0) {
|
||||
u8_t *p = streambuf->readp + n;
|
||||
int bytes = bytes_wrap - n;
|
||||
|
||||
if (!HAAC(a, Decode, a->hAac, &p, &bytes, (short*) a->write_buf)) {
|
||||
HAAC(a, GetLastFrameInfo, a->hAac, &info);
|
||||
channels = info.nChans;
|
||||
samplerate = info.sampRateOut;
|
||||
found = 1;
|
||||
}
|
||||
|
||||
HAAC(a, FlushCodec, a->hAac);
|
||||
|
||||
bytes_total -= n;
|
||||
bytes_wrap -= n;
|
||||
_buf_inc_readp(streambuf, n);
|
||||
} else {
|
||||
found = -1;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// mp4 - read header
|
||||
found = read_mp4_header(&samplerate, &channels);
|
||||
}
|
||||
|
||||
if (found == 1) {
|
||||
|
||||
LOG_INFO("samplerate: %u channels: %u", samplerate, channels);
|
||||
bytes_total = _buf_used(streambuf);
|
||||
bytes_wrap = min(bytes_total, _buf_cont_read(streambuf));
|
||||
|
||||
LOCK_O;
|
||||
LOG_INFO("setting track_start");
|
||||
output.next_sample_rate = decode_newstream(samplerate, output.supported_rates);
|
||||
IF_DSD( output.next_fmt = PCM; )
|
||||
output.track_start = outputbuf->writep;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
decode.new_stream = false;
|
||||
UNLOCK_O;
|
||||
|
||||
} else if (found == -1) {
|
||||
|
||||
LOG_WARN("error reading stream header");
|
||||
UNLOCK_S;
|
||||
return DECODE_ERROR;
|
||||
|
||||
} else {
|
||||
|
||||
// not finished header parsing come back next time
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes_wrap < WRAPBUF_LEN && bytes_total > WRAPBUF_LEN) {
|
||||
|
||||
// make a local copy of frames which may have wrapped round the end of streambuf
|
||||
static u8_t buf[WRAPBUF_LEN];
|
||||
memcpy(buf, streambuf->readp, bytes_wrap);
|
||||
memcpy(buf + bytes_wrap, streambuf->buf, WRAPBUF_LEN - bytes_wrap);
|
||||
|
||||
sptr = buf;
|
||||
bytes = bytes_wrap = WRAPBUF_LEN;
|
||||
} else {
|
||||
|
||||
sptr = streambuf->readp;
|
||||
bytes = bytes_wrap;
|
||||
}
|
||||
|
||||
// decode function changes iptr, so can't use streambuf->readp (same for bytes)
|
||||
res = HAAC(a, Decode, a->hAac, &sptr, &bytes, (short*) a->write_buf);
|
||||
if (res < 0) {
|
||||
LOG_WARN("AAC decode error %d", res);
|
||||
}
|
||||
|
||||
HAAC(a, GetLastFrameInfo, a->hAac, &info);
|
||||
iptr = (ISAMPLE_T *) a->write_buf;
|
||||
bytes = bytes_wrap - bytes;
|
||||
endstream = false;
|
||||
|
||||
// mp4 end of chunk - skip to next offset
|
||||
if (a->chunkinfo && a->chunkinfo[a->nextchunk].offset && a->sample++ == a->chunkinfo[a->nextchunk].sample) {
|
||||
|
||||
if (a->chunkinfo[a->nextchunk].offset > a->pos) {
|
||||
u32_t skip = a->chunkinfo[a->nextchunk].offset - a->pos;
|
||||
if (skip != bytes) {
|
||||
LOG_DEBUG("skipping to next chunk pos: %u consumed: %u != skip: %u", a->pos, bytes, skip);
|
||||
}
|
||||
if (bytes_total >= skip) {
|
||||
_buf_inc_readp(streambuf, skip);
|
||||
a->pos += skip;
|
||||
} else {
|
||||
a->consume = skip;
|
||||
}
|
||||
a->nextchunk++;
|
||||
} else {
|
||||
LOG_ERROR("error: need to skip backwards!");
|
||||
endstream = true;
|
||||
}
|
||||
|
||||
// adts and mp4 when not at end of chunk
|
||||
} else if (bytes > 0) {
|
||||
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
a->pos += bytes;
|
||||
|
||||
// error which doesn't advance streambuf - end
|
||||
} else {
|
||||
endstream = true;
|
||||
}
|
||||
|
||||
UNLOCK_S;
|
||||
|
||||
if (endstream) {
|
||||
LOG_WARN("unable to decode further");
|
||||
return DECODE_ERROR;
|
||||
}
|
||||
|
||||
if (!info.outputSamps) {
|
||||
a->empty = true;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
frames = info.outputSamps / info.nChans;
|
||||
|
||||
if (a->skip) {
|
||||
u32_t skip;
|
||||
if (a->empty) {
|
||||
a->empty = false;
|
||||
a->skip -= frames;
|
||||
LOG_DEBUG("gapless: first frame empty, skipped %u frames at start", frames);
|
||||
}
|
||||
skip = min(frames, a->skip);
|
||||
LOG_DEBUG("gapless: skipping %u frames at start", skip);
|
||||
frames -= skip;
|
||||
a->skip -= skip;
|
||||
iptr += skip * info.nChans;
|
||||
}
|
||||
|
||||
if (a->samples) {
|
||||
if (a->samples < frames) {
|
||||
LOG_DEBUG("gapless: trimming %u frames from end", frames - a->samples);
|
||||
frames = (frames_t)a->samples;
|
||||
}
|
||||
a->samples -= frames;
|
||||
}
|
||||
|
||||
LOG_SDEBUG("write %u frames", frames);
|
||||
|
||||
LOCK_O_direct;
|
||||
|
||||
while (frames > 0) {
|
||||
frames_t f;
|
||||
frames_t count;
|
||||
ISAMPLE_T *optr;
|
||||
|
||||
IF_DIRECT(
|
||||
f = _buf_cont_write(outputbuf) / BYTES_PER_FRAME;
|
||||
optr = (ISAMPLE_T *)outputbuf->writep;
|
||||
);
|
||||
IF_PROCESS(
|
||||
f = process.max_in_frames;
|
||||
optr = (ISAMPLE_T *)process.inbuf;
|
||||
);
|
||||
|
||||
f = min(f, frames);
|
||||
count = f;
|
||||
|
||||
if (info.nChans == 2) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
memcpy(optr, iptr, count * BYTES_PER_FRAME);
|
||||
iptr += count * 2;
|
||||
#else
|
||||
while (count--) {
|
||||
*optr++ = *iptr++ << 8;
|
||||
*optr++ = *iptr++ << 8;
|
||||
}
|
||||
#endif
|
||||
} else if (info.nChans == 1) {
|
||||
while (count--) {
|
||||
*optr++ = ALIGN(*iptr);
|
||||
*optr++ = ALIGN(*iptr++);
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("unsupported number of channels");
|
||||
}
|
||||
|
||||
frames -= f;
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames = f;
|
||||
if (frames) LOG_ERROR("unhandled case");
|
||||
);
|
||||
}
|
||||
|
||||
UNLOCK_O_direct;
|
||||
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
static void helixaac_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
|
||||
LOG_INFO("opening %s stream", size == '2' ? "adts" : "mp4");
|
||||
|
||||
a->type = size;
|
||||
a->pos = a->consume = a->sample = a->nextchunk = 0;
|
||||
|
||||
if (a->chunkinfo) {
|
||||
free(a->chunkinfo);
|
||||
}
|
||||
if (a->stsc) {
|
||||
free(a->stsc);
|
||||
}
|
||||
a->chunkinfo = NULL;
|
||||
a->stsc = NULL;
|
||||
a->skip = 0;
|
||||
a->samples = 0;
|
||||
a->sttssamples = 0;
|
||||
a->empty = false;
|
||||
|
||||
if (a->hAac) {
|
||||
HAAC(a, FlushCodec, a->hAac);
|
||||
} else {
|
||||
a->hAac = HAAC(a, InitDecoder);
|
||||
a->write_buf = malloc(FRAME_BUF * BYTES_PER_FRAME);
|
||||
}
|
||||
}
|
||||
|
||||
static void helixaac_close(void) {
|
||||
HAAC(a, FreeDecoder, a->hAac);
|
||||
a->hAac = NULL;
|
||||
if (a->chunkinfo) {
|
||||
free(a->chunkinfo);
|
||||
a->chunkinfo = NULL;
|
||||
}
|
||||
if (a->stsc) {
|
||||
free(a->stsc);
|
||||
a->stsc = NULL;
|
||||
}
|
||||
free(a->write_buf);
|
||||
}
|
||||
|
||||
static bool load_helixaac() {
|
||||
#if !LINKALL
|
||||
void *handle = dlopen(LIBHELIX-AAC, RTLD_NOW);
|
||||
char *err;
|
||||
|
||||
if (!handle) {
|
||||
LOG_INFO("dlerror: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
// load symbols here
|
||||
|
||||
if ((err = dlerror()) != NULL) {
|
||||
LOG_INFO("dlerror: %s", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("loaded "LIBHELIX-AAC"");
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct codec *register_helixaac(void) {
|
||||
static struct codec ret = {
|
||||
'a', // id
|
||||
"aac", // types
|
||||
WRAPBUF_LEN, // min read
|
||||
20480, // min space
|
||||
helixaac_open, // open
|
||||
helixaac_close, // close
|
||||
helixaac_decode, // decode
|
||||
};
|
||||
|
||||
a = malloc(sizeof(struct helixaac));
|
||||
if (!a) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
a->hAac = NULL;
|
||||
a->chunkinfo = NULL;
|
||||
a->stsc = NULL;
|
||||
|
||||
if (!load_helixaac()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_INFO("using helix-aac to decode aac");
|
||||
return &ret;
|
||||
}
|
||||
419
components/squeezelite/mad.c
Normal file
419
components/squeezelite/mad.c
Normal file
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <mad.h>
|
||||
|
||||
#define MAD_DELAY 529
|
||||
|
||||
#define READBUF_SIZE 2048 // local buffer used by decoder: FIXME merge with any other decoders needing one?
|
||||
|
||||
struct mad {
|
||||
u8_t *readbuf;
|
||||
unsigned readbuf_len;
|
||||
struct mad_stream stream;
|
||||
struct mad_frame frame;
|
||||
struct mad_synth synth;
|
||||
enum mad_error last_error;
|
||||
// for lame gapless processing
|
||||
int checktags;
|
||||
u32_t consume;
|
||||
u32_t skip;
|
||||
u64_t samples;
|
||||
u32_t padding;
|
||||
#if !LINKALL
|
||||
// mad symbols to be dynamically loaded
|
||||
void (* mad_stream_init)(struct mad_stream *);
|
||||
void (* mad_frame_init)(struct mad_frame *);
|
||||
void (* mad_synth_init)(struct mad_synth *);
|
||||
void (* mad_frame_finish)(struct mad_frame *);
|
||||
void (* mad_stream_finish)(struct mad_stream *);
|
||||
void (* mad_stream_buffer)(struct mad_stream *, unsigned char const *, unsigned long);
|
||||
int (* mad_frame_decode)(struct mad_frame *, struct mad_stream *);
|
||||
void (* mad_synth_frame)(struct mad_synth *, struct mad_frame const *);
|
||||
char const *(* mad_stream_errorstr)(struct mad_stream const *);
|
||||
#endif
|
||||
};
|
||||
|
||||
static struct mad *m;
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
#if LINKALL
|
||||
#define MAD(h, fn, ...) (mad_ ## fn)(__VA_ARGS__)
|
||||
#else
|
||||
#define MAD(h, fn, ...) (h)->mad_##fn(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
// based on libmad minimad.c scale
|
||||
static inline ISAMPLE_T scale(mad_fixed_t sample) {
|
||||
sample += (1L << (MAD_F_FRACBITS - 24));
|
||||
|
||||
if (sample >= MAD_F_ONE)
|
||||
sample = MAD_F_ONE - 1;
|
||||
else if (sample < -MAD_F_ONE)
|
||||
sample = -MAD_F_ONE;
|
||||
#if BYTES_PER_FRAME == 4
|
||||
return (ISAMPLE_T)((sample >> (MAD_F_FRACBITS + 1 - 24)) >> 8);
|
||||
#else
|
||||
return (ISAMPLE_T)((sample >> (MAD_F_FRACBITS + 1 - 24)) << 8);
|
||||
#endif
|
||||
}
|
||||
|
||||
// check for id3.2 tag at start of file - http://id3.org/id3v2.4.0-structure, return length
|
||||
static unsigned _check_id3_tag(size_t bytes) {
|
||||
u8_t *ptr = streambuf->readp;
|
||||
u32_t size = 0;
|
||||
|
||||
if (bytes > 10 && *ptr == 'I' && *(ptr+1) == 'D' && *(ptr+2) == '3') {
|
||||
// size is encoded as syncsafe integer, add 10 if footer present
|
||||
if (*(ptr+6) < 0x80 && *(ptr+7) < 0x80 && *(ptr+8) < 0x80 && *(ptr+9) < 0x80) {
|
||||
size = 10 + (*(ptr+6) << 21) + (*(ptr+7) << 14) + (*(ptr+8) << 7) + *(ptr+9) + ((*(ptr+5) & 0x10) ? 10 : 0);
|
||||
LOG_DEBUG("id3.2 tag len: %u", size);
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
// check for lame gapless params, don't advance streambuf
|
||||
static void _check_lame_header(size_t bytes) {
|
||||
u8_t *ptr = streambuf->readp;
|
||||
|
||||
if (*ptr == 0xff && (*(ptr+1) & 0xf0) == 0xf0 && bytes > 180) {
|
||||
|
||||
u32_t frame_count = 0, enc_delay = 0, enc_padding = 0;
|
||||
u8_t flags;
|
||||
|
||||
// 2 channels
|
||||
if (!memcmp(ptr + 36, "Xing", 4) || !memcmp(ptr + 36, "Info", 4)) {
|
||||
ptr += 36 + 7;
|
||||
// mono
|
||||
} else if (!memcmp(ptr + 21, "Xing", 4) || !memcmp(ptr + 21, "Info", 4)) {
|
||||
ptr += 21 + 7;
|
||||
}
|
||||
|
||||
flags = *ptr;
|
||||
|
||||
if (flags & 0x01) {
|
||||
frame_count = unpackN((u32_t *)(ptr + 1));
|
||||
ptr += 4;
|
||||
}
|
||||
if (flags & 0x02) ptr += 4;
|
||||
if (flags & 0x04) ptr += 100;
|
||||
if (flags & 0x08) ptr += 4;
|
||||
|
||||
if (!!memcmp(ptr+1, "LAME", 4)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ptr += 22;
|
||||
|
||||
enc_delay = (*ptr << 4 | *(ptr + 1) >> 4) + MAD_DELAY;
|
||||
enc_padding = (*(ptr + 1) & 0xF) << 8 | *(ptr + 2);
|
||||
enc_padding = enc_padding > MAD_DELAY ? enc_padding - MAD_DELAY : 0;
|
||||
|
||||
// add one frame to initial skip for this (empty) frame
|
||||
m->skip = enc_delay + 1152;
|
||||
m->samples = frame_count * 1152 - enc_delay - enc_padding;
|
||||
m->padding = enc_padding;
|
||||
|
||||
LOG_INFO("gapless: skip: %u samples: " FMT_u64 " delay: %u padding: %u", m->skip, m->samples, enc_delay, enc_padding);
|
||||
}
|
||||
}
|
||||
|
||||
static decode_state mad_decode(void) {
|
||||
size_t bytes;
|
||||
bool eos = false;
|
||||
|
||||
LOCK_S;
|
||||
bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
|
||||
if (m->checktags) {
|
||||
if (m->checktags == 1) {
|
||||
m->consume = _check_id3_tag(bytes);
|
||||
m->checktags = 2;
|
||||
}
|
||||
if (m->consume) {
|
||||
u32_t consume = min(m->consume, bytes);
|
||||
LOG_DEBUG("consume: %u of %u", consume, m->consume);
|
||||
_buf_inc_readp(streambuf, consume);
|
||||
m->consume -= consume;
|
||||
UNLOCK_S;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
if (m->checktags == 2) {
|
||||
if (!stream.meta_interval) {
|
||||
_check_lame_header(bytes);
|
||||
}
|
||||
m->checktags = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (m->stream.next_frame && m->readbuf_len) {
|
||||
m->readbuf_len -= m->stream.next_frame - m->readbuf;
|
||||
memmove(m->readbuf, m->stream.next_frame, m->readbuf_len);
|
||||
}
|
||||
|
||||
bytes = min(bytes, READBUF_SIZE - m->readbuf_len);
|
||||
memcpy(m->readbuf + m->readbuf_len, streambuf->readp, bytes);
|
||||
m->readbuf_len += bytes;
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
|
||||
if (stream.state <= DISCONNECT && _buf_used(streambuf) == 0) {
|
||||
eos = true;
|
||||
LOG_DEBUG("end of stream");
|
||||
memset(m->readbuf + m->readbuf_len, 0, MAD_BUFFER_GUARD);
|
||||
m->readbuf_len += MAD_BUFFER_GUARD;
|
||||
}
|
||||
|
||||
UNLOCK_S;
|
||||
|
||||
MAD(m, stream_buffer, &m->stream, m->readbuf, m->readbuf_len);
|
||||
|
||||
while (true) {
|
||||
size_t frames;
|
||||
s32_t *iptrl;
|
||||
s32_t *iptrr;
|
||||
unsigned max_frames;
|
||||
|
||||
if (MAD(m, frame_decode, &m->frame, &m->stream) == -1) {
|
||||
decode_state ret;
|
||||
if (!eos && m->stream.error == MAD_ERROR_BUFLEN) {
|
||||
ret = DECODE_RUNNING;
|
||||
} else if (eos && (m->stream.error == MAD_ERROR_BUFLEN || m->stream.error == MAD_ERROR_LOSTSYNC
|
||||
|| m->stream.error == MAD_ERROR_BADBITRATE)) {
|
||||
ret = DECODE_COMPLETE;
|
||||
} else if (!MAD_RECOVERABLE(m->stream.error)) {
|
||||
LOG_INFO("mad_frame_decode error: %s - stopping decoder", MAD(m, stream_errorstr, &m->stream));
|
||||
ret = DECODE_COMPLETE;
|
||||
} else {
|
||||
if (m->stream.error != m->last_error) {
|
||||
// suppress repeat error messages
|
||||
LOG_DEBUG("mad_frame_decode error: %s", MAD(m, stream_errorstr, &m->stream));
|
||||
}
|
||||
ret = DECODE_RUNNING;
|
||||
}
|
||||
m->last_error = m->stream.error;
|
||||
return ret;
|
||||
};
|
||||
|
||||
MAD(m, synth_frame, &m->synth, &m->frame);
|
||||
|
||||
if (decode.new_stream) {
|
||||
LOCK_O;
|
||||
LOG_INFO("setting track_start");
|
||||
output.next_sample_rate = decode_newstream(m->synth.pcm.samplerate, output.supported_rates);
|
||||
IF_DSD( output.next_fmt = PCM; )
|
||||
output.track_start = outputbuf->writep;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
decode.new_stream = false;
|
||||
UNLOCK_O;
|
||||
}
|
||||
|
||||
LOCK_O_direct;
|
||||
|
||||
IF_DIRECT(
|
||||
max_frames = _buf_space(outputbuf) / BYTES_PER_FRAME;
|
||||
);
|
||||
IF_PROCESS(
|
||||
max_frames = process.max_in_frames - process.in_frames;
|
||||
);
|
||||
|
||||
if (m->synth.pcm.length > max_frames) {
|
||||
LOG_WARN("too many samples - dropping samples");
|
||||
m->synth.pcm.length = max_frames;
|
||||
}
|
||||
|
||||
frames = m->synth.pcm.length;
|
||||
iptrl = m->synth.pcm.samples[0];
|
||||
iptrr = m->synth.pcm.samples[ m->synth.pcm.channels - 1 ];
|
||||
|
||||
if (m->skip) {
|
||||
u32_t skip = min(m->skip, frames);
|
||||
LOG_DEBUG("gapless: skipping %u frames at start", skip);
|
||||
frames -= skip;
|
||||
m->skip -= skip;
|
||||
iptrl += skip;
|
||||
iptrr += skip;
|
||||
}
|
||||
|
||||
if (m->samples) {
|
||||
if (m->samples < frames) {
|
||||
LOG_DEBUG("gapless: trimming %u frames from end", frames - m->samples);
|
||||
frames = (size_t)m->samples;
|
||||
}
|
||||
m->samples -= frames;
|
||||
if (m->samples > 0 && eos && !(m->stream.next_frame[0] == 0xff && (m->stream.next_frame[1] & 0xf0) == 0xf0)) {
|
||||
// this is the last frame to be decoded, but more samples expected so we must have skipped, remove padding
|
||||
// note this only works if the padding is less than one frame of 1152 bytes otherswise some gap will remain
|
||||
LOG_DEBUG("gapless: early end - trimming padding from end");
|
||||
if (frames >= m->padding) {
|
||||
frames -= m->padding;
|
||||
} else {
|
||||
frames = 0;
|
||||
}
|
||||
m->samples = 0;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_SDEBUG("write %u frames", frames);
|
||||
|
||||
while (frames > 0) {
|
||||
size_t f, count;
|
||||
ISAMPLE_T *optr;
|
||||
|
||||
IF_DIRECT(
|
||||
f = min(frames, _buf_cont_write(outputbuf) / BYTES_PER_FRAME);
|
||||
optr = (ISAMPLE_T *)outputbuf->writep;
|
||||
);
|
||||
IF_PROCESS(
|
||||
f = min(frames, process.max_in_frames - process.in_frames);
|
||||
optr = (ISAMPLE_T *)((u8_t *)process.inbuf + process.in_frames * BYTES_PER_FRAME);
|
||||
);
|
||||
|
||||
count = f;
|
||||
|
||||
while (count--) {
|
||||
*optr++ = scale(*iptrl++);
|
||||
*optr++ = scale(*iptrr++);
|
||||
}
|
||||
|
||||
frames -= f;
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames += f;
|
||||
);
|
||||
}
|
||||
|
||||
UNLOCK_O_direct;
|
||||
}
|
||||
|
||||
return eos ? DECODE_COMPLETE : DECODE_RUNNING;
|
||||
}
|
||||
|
||||
static void mad_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
|
||||
if (!m->readbuf) {
|
||||
m->readbuf = malloc(READBUF_SIZE + MAD_BUFFER_GUARD);
|
||||
}
|
||||
m->checktags = 1;
|
||||
m->consume = 0;
|
||||
m->skip = MAD_DELAY;
|
||||
m->samples = 0;
|
||||
m->readbuf_len = 0;
|
||||
m->last_error = MAD_ERROR_NONE;
|
||||
MAD(m, stream_init, &m->stream);
|
||||
MAD(m, frame_init, &m->frame);
|
||||
MAD(m, synth_init, &m->synth);
|
||||
}
|
||||
|
||||
static void mad_close(void) {
|
||||
mad_synth_finish(&m->synth); // macro only in current version
|
||||
MAD(m, frame_finish, &m->frame);
|
||||
MAD(m, stream_finish, &m->stream);
|
||||
free(m->readbuf);
|
||||
m->readbuf = NULL;
|
||||
}
|
||||
|
||||
static bool load_mad() {
|
||||
#if !LINKALL
|
||||
void *handle = dlopen(LIBMAD, RTLD_NOW);
|
||||
char *err;
|
||||
|
||||
if (!handle) {
|
||||
LOG_INFO("dlerror: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
m->mad_stream_init = dlsym(handle, "mad_stream_init");
|
||||
m->mad_frame_init = dlsym(handle, "mad_frame_init");
|
||||
m->mad_synth_init = dlsym(handle, "mad_synth_init");
|
||||
m->mad_frame_finish = dlsym(handle, "mad_frame_finish");
|
||||
m->mad_stream_finish = dlsym(handle, "mad_stream_finish");
|
||||
m->mad_stream_buffer = dlsym(handle, "mad_stream_buffer");
|
||||
m->mad_frame_decode = dlsym(handle, "mad_frame_decode");
|
||||
m->mad_synth_frame = dlsym(handle, "mad_synth_frame");
|
||||
m->mad_stream_errorstr = dlsym(handle, "mad_stream_errorstr");
|
||||
|
||||
if ((err = dlerror()) != NULL) {
|
||||
LOG_INFO("dlerror: %s", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("loaded "LIBMAD);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct codec *register_mad(void) {
|
||||
static struct codec ret = {
|
||||
'm', // id
|
||||
"mp3", // types
|
||||
READBUF_SIZE, // min read
|
||||
206800, // min space
|
||||
mad_open, // open
|
||||
mad_close, // close
|
||||
mad_decode, // decode
|
||||
};
|
||||
|
||||
m = malloc(sizeof(struct mad));
|
||||
if (!m) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
m->readbuf = NULL;
|
||||
m->readbuf_len = 0;
|
||||
|
||||
if (!load_mad()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_INFO("using mad to decode mp3");
|
||||
return &ret;
|
||||
}
|
||||
848
components/squeezelite/main.c
Normal file
848
components/squeezelite/main.c
Normal file
@@ -0,0 +1,848 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additions (c) Paul Hermann, 2015-2017 under the same license terms
|
||||
* -Control of Raspberry pi GPIO for amplifier power
|
||||
* -Launch script on power status change from LMS
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#define TITLE "Squeezelite " VERSION ", Copyright 2012-2015 Adrian Smith, 2015-2019 Ralph Irving."
|
||||
|
||||
#define CODECS_BASE "flac,pcm,mp3,ogg"
|
||||
#if NO_FAAD
|
||||
#define CODECS_AAC ""
|
||||
#else
|
||||
#define CODECS_AAC ",aac"
|
||||
#endif
|
||||
#if FFMPEG
|
||||
#define CODECS_FF ",wma,alac"
|
||||
#else
|
||||
#define CODECS_FF ""
|
||||
#endif
|
||||
#if DSD
|
||||
#define CODECS_DSD ",dsd"
|
||||
#else
|
||||
#define CODECS_DSD ""
|
||||
#endif
|
||||
#define CODECS_MP3 " (mad,mpg for specific mp3 codec)"
|
||||
|
||||
#define CODECS CODECS_BASE CODECS_AAC CODECS_FF CODECS_DSD CODECS_MP3
|
||||
|
||||
static void usage(const char *argv0) {
|
||||
printf(TITLE " See -t for license terms\n"
|
||||
"Usage: %s [options]\n"
|
||||
" -s <server>[:<port>]\tConnect to specified server, otherwise uses autodiscovery to find server\n"
|
||||
" -o <output device>\tSpecify output device, default \"default\", - = output to stdout\n"
|
||||
" -l \t\t\tList output devices\n"
|
||||
#if ALSA
|
||||
" -a <b>:<p>:<f>:<m>\tSpecify ALSA params to open output device, b = buffer time in ms or size in bytes, p = period count or size in bytes, f sample format (16|24|24_3|32), m = use mmap (0|1)\n"
|
||||
#endif
|
||||
#if PORTAUDIO
|
||||
#if PA18API
|
||||
" -a <frames>:<buffers>\tSpecify output target 4 byte frames per buffer, number of buffers\n"
|
||||
#elif OSX && !defined(OSXPPC)
|
||||
" -a <l>:<r>\t\tSpecify Portaudio params to open output device, l = target latency in ms, r = allow OSX to resample (0|1)\n"
|
||||
#elif WIN
|
||||
" -a <l>:<e>\t\tSpecify Portaudio params to open output device, l = target latency in ms, e = use exclusive mode for WASAPI (0|1)\n"
|
||||
#else
|
||||
" -a <l>\t\tSpecify Portaudio params to open output device, l = target latency in ms\n"
|
||||
#endif
|
||||
#endif
|
||||
" -a <f>\t\tSpecify sample format (16|24|32) of output file when using -o - to output samples to stdout (interleaved little endian only)\n"
|
||||
" -b <stream>:<output>\tSpecify internal Stream and Output buffer sizes in Kbytes\n"
|
||||
" -c <codec1>,<codec2>\tRestrict codecs to those specified, otherwise load all available codecs; known codecs: " CODECS "\n"
|
||||
" \t\t\tCodecs reported to LMS in order listed, allowing codec priority refinement.\n"
|
||||
" -C <timeout>\t\tClose output device when idle after timeout seconds, default is to keep it open while player is 'on'\n"
|
||||
#if !IR
|
||||
" -d <log>=<level>\tSet logging level, logs: all|slimproto|stream|decode|output, level: info|debug|sdebug\n"
|
||||
#else
|
||||
" -d <log>=<level>\tSet logging level, logs: all|slimproto|stream|decode|output|ir, level: info|debug|sdebug\n"
|
||||
#endif
|
||||
#if defined(GPIO) && defined(RPI)
|
||||
" -G <Rpi GPIO#>:<H/L>\tSpecify the BCM GPIO# to use for Amp Power Relay and if the output should be Active High or Low\n"
|
||||
#endif
|
||||
" -e <codec1>,<codec2>\tExplicitly exclude native support of one or more codecs; known codecs: " CODECS "\n"
|
||||
" -f <logfile>\t\tWrite debug to logfile\n"
|
||||
#if IR
|
||||
" -i [<filename>]\tEnable lirc remote control support (lirc config file ~/.lircrc used if filename not specified)\n"
|
||||
#endif
|
||||
" -m <mac addr>\t\tSet mac address, format: ab:cd:ef:12:34:56\n"
|
||||
" -M <modelname>\tSet the squeezelite player model name sent to the server (default: " MODEL_NAME_STRING ")\n"
|
||||
" -n <name>\t\tSet the player name\n"
|
||||
" -N <filename>\t\tStore player name in filename to allow server defined name changes to be shared between servers (not supported with -n)\n"
|
||||
" -W\t\t\tRead wave and aiff format from header, ignore server parameters\n"
|
||||
#if ALSA
|
||||
" -p <priority>\t\tSet real time priority of output thread (1-99)\n"
|
||||
#endif
|
||||
#if LINUX || FREEBSD || SUN
|
||||
" -P <filename>\t\tStore the process id (PID) in filename\n"
|
||||
#endif
|
||||
" -r <rates>[:<delay>]\tSample rates supported, allows output to be off when squeezelite is started; rates = <maxrate>|<minrate>-<maxrate>|<rate1>,<rate2>,<rate3>; delay = optional delay switching rates in ms\n"
|
||||
#if GPIO
|
||||
" -S <Power Script>\tAbsolute path to script to launch on power commands from LMS\n"
|
||||
#endif
|
||||
#if RESAMPLE
|
||||
" -R -u [params]\tResample, params = <recipe>:<flags>:<attenuation>:<precision>:<passband_end>:<stopband_start>:<phase_response>,\n"
|
||||
" \t\t\t recipe = (v|h|m|l|q)(L|I|M)(s) [E|X], E = exception - resample only if native rate not supported, X = async - resample to max rate for device, otherwise to max sync rate\n"
|
||||
" \t\t\t flags = num in hex,\n"
|
||||
" \t\t\t attenuation = attenuation in dB to apply (default is -1db if not explicitly set),\n"
|
||||
" \t\t\t precision = number of bits precision (NB. HQ = 20. VHQ = 28),\n"
|
||||
" \t\t\t passband_end = number in percent (0dB pt. bandwidth to preserve. nyquist = 100%%),\n"
|
||||
" \t\t\t stopband_start = number in percent (Aliasing/imaging control. > passband_end),\n"
|
||||
" \t\t\t phase_response = 0-100 (0 = minimum / 50 = linear / 100 = maximum)\n"
|
||||
#endif
|
||||
#if RESAMPLE16
|
||||
" -R -u [params]\tResample, params = (b|l|m)[:i],\n"
|
||||
" \t\t\t b = basic linear interpolation, l = 13 taps, m = 21 taps, i = interpolate filter coefficients\n"
|
||||
#endif
|
||||
#if DSD
|
||||
#if ALSA
|
||||
" -D [delay][:format]\tOutput device supports DSD, delay = optional delay switching between PCM and DSD in ms\n"
|
||||
" \t\t\t format = dop (default if not specified), u8, u16le, u16be, u32le or u32be.\n"
|
||||
#else
|
||||
" -D [delay]\t\tOutput device supports DSD over PCM (DoP), delay = optional delay switching between PCM and DoP in ms\n"
|
||||
#endif
|
||||
#endif
|
||||
#if VISEXPORT
|
||||
" -v \t\t\tVisualiser support\n"
|
||||
#endif
|
||||
# if ALSA
|
||||
" -O <mixer device>\tSpecify mixer device, defaults to 'output device'\n"
|
||||
" -L \t\t\tList volume controls for output device\n"
|
||||
" -U <control>\t\tUnmute ALSA control and set to full volume (not supported with -V)\n"
|
||||
" -V <control>\t\tUse ALSA control for volume adjustment, otherwise use software volume adjustment\n"
|
||||
" -X \t\t\tUse linear volume adjustments instead of in terms of dB (only for hardware volume control)\n"
|
||||
#endif
|
||||
#if LINUX || FREEBSD || SUN
|
||||
" -z \t\t\tDaemonize\n"
|
||||
#endif
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
" -Z <rate>\t\tReport rate to server in helo as the maximum sample rate we can support\n"
|
||||
#endif
|
||||
" -t \t\t\tLicense terms\n"
|
||||
" -? \t\t\tDisplay this help text\n"
|
||||
"\n"
|
||||
"Build options:"
|
||||
#if SUN
|
||||
" SOLARIS"
|
||||
#elif LINUX
|
||||
" LINUX"
|
||||
#endif
|
||||
#if WIN
|
||||
" WIN"
|
||||
#endif
|
||||
#if OSX
|
||||
" OSX"
|
||||
#endif
|
||||
#if OSXPPC
|
||||
"PPC"
|
||||
#endif
|
||||
#if FREEBSD
|
||||
" FREEBSD"
|
||||
#endif
|
||||
#if ALSA
|
||||
" ALSA"
|
||||
#endif
|
||||
#if PORTAUDIO
|
||||
" PORTAUDIO"
|
||||
#if PA18API
|
||||
"18"
|
||||
#endif
|
||||
#endif
|
||||
#if EMBEDDED
|
||||
" EMBEDDED"
|
||||
#endif
|
||||
#if EVENTFD
|
||||
" EVENTFD"
|
||||
#endif
|
||||
#if SELFPIPE
|
||||
" SELFPIPE"
|
||||
#endif
|
||||
#if LOOPBACK
|
||||
" LOOPBACK"
|
||||
#endif
|
||||
#if WINEVENT
|
||||
" WINEVENT"
|
||||
#endif
|
||||
#if RESAMPLE_MP
|
||||
" RESAMPLE_MP"
|
||||
#else
|
||||
#if RESAMPLE
|
||||
" RESAMPLE"
|
||||
#endif
|
||||
#if RESAMPLE16
|
||||
" RESAMPLE16"
|
||||
#endif
|
||||
#endif
|
||||
#if FFMPEG
|
||||
" FFMPEG"
|
||||
#endif
|
||||
#if NO_FAAD
|
||||
" NO_FAAD"
|
||||
#endif
|
||||
#if VISEXPORT
|
||||
" VISEXPORT"
|
||||
#endif
|
||||
#if IR
|
||||
" IR"
|
||||
#endif
|
||||
#if GPIO
|
||||
" GPIO"
|
||||
#endif
|
||||
#if RPI
|
||||
" RPI"
|
||||
#endif
|
||||
#if DSD
|
||||
" DSD"
|
||||
#endif
|
||||
#if USE_SSL
|
||||
" SSL"
|
||||
#endif
|
||||
#if LINKALL
|
||||
" LINKALL"
|
||||
#endif
|
||||
#if STATUSHACK
|
||||
" STATUSHACK"
|
||||
#endif
|
||||
"\n\n",
|
||||
argv0);
|
||||
}
|
||||
|
||||
static void license(void) {
|
||||
printf(TITLE "\n\n"
|
||||
"This program is free software: you can redistribute it and/or modify\n"
|
||||
"it under the terms of the GNU General Public License as published by\n"
|
||||
"the Free Software Foundation, either version 3 of the License, or\n"
|
||||
"(at your option) any later version.\n\n"
|
||||
"This program is distributed in the hope that it will be useful,\n"
|
||||
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
|
||||
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
|
||||
"GNU General Public License for more details.\n\n"
|
||||
"You should have received a copy of the GNU General Public License\n"
|
||||
"along with this program. If not, see <http://www.gnu.org/licenses/>.\n"
|
||||
#if DSD
|
||||
"\nContains dsd2pcm library Copyright 2009, 2011 Sebastian Gesemann which\n"
|
||||
"is subject to its own license.\n"
|
||||
"\nContains the Daphile Project full dsd patch Copyright 2013-2017 Daphile,\n"
|
||||
"which is subject to its own license.\n"
|
||||
#endif
|
||||
"\nOption to allow server side upsampling for PCM streams (-W) from\n"
|
||||
"squeezelite-R2 (c) Marco Curti 2015, marcoc1712@gmail.com.\n"
|
||||
#if GPIO
|
||||
"\nAdditions (c) Paul Hermann, 2015, 2017 under the same license terms\n"
|
||||
"- Launch a script on power status change\n"
|
||||
"- Control of Raspberry pi GPIO for amplifier power\n"
|
||||
#endif
|
||||
#if RPI
|
||||
"\nContains wiringpi GPIO Interface library Copyright (c) 2012-2017\n"
|
||||
"Gordon Henderson, which is subject to its own license.\n"
|
||||
#endif
|
||||
#if FFMPEG
|
||||
"\nThis software uses libraries from the FFmpeg project under\n"
|
||||
"the LGPLv2.1 and its source can be downloaded from\n"
|
||||
"<https://sourceforge.net/projects/lmsclients/files/source/>\n"
|
||||
#endif
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
|
||||
static void sighandler(int signum) {
|
||||
slimproto_stop();
|
||||
|
||||
// remove ourselves in case above does not work, second SIGINT will cause non gracefull shutdown
|
||||
signal(signum, SIG_DFL);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char *server = NULL;
|
||||
char *output_device = "default";
|
||||
char *include_codecs = NULL;
|
||||
char *exclude_codecs = "";
|
||||
char *name = NULL;
|
||||
char *namefile = NULL;
|
||||
char *modelname = NULL;
|
||||
extern bool pcm_check_header;
|
||||
extern bool user_rates;
|
||||
char *logfile = NULL;
|
||||
u8_t mac[6];
|
||||
unsigned stream_buf_size = STREAMBUF_SIZE;
|
||||
unsigned output_buf_size = 0; // set later
|
||||
unsigned rates[MAX_SUPPORTED_SAMPLERATES] = { 0 };
|
||||
unsigned rate_delay = 0;
|
||||
char *resample = NULL;
|
||||
char *output_params = NULL;
|
||||
unsigned idle = 0;
|
||||
#if LINUX || FREEBSD || SUN
|
||||
bool daemonize = false;
|
||||
char *pidfile = NULL;
|
||||
FILE *pidfp = NULL;
|
||||
#endif
|
||||
#if ALSA
|
||||
unsigned rt_priority = OUTPUT_RT_PRIORITY;
|
||||
char *mixer_device = output_device;
|
||||
char *output_mixer = NULL;
|
||||
bool output_mixer_unmute = false;
|
||||
bool linear_volume = false;
|
||||
#endif
|
||||
#if DSD
|
||||
unsigned dsd_delay = 0;
|
||||
dsd_format dsd_outfmt = PCM;
|
||||
#endif
|
||||
#if VISEXPORT
|
||||
bool visexport = false;
|
||||
#endif
|
||||
#if IR
|
||||
char *lircrc = NULL;
|
||||
#endif
|
||||
|
||||
log_level log_output = lWARN;
|
||||
log_level log_stream = lWARN;
|
||||
log_level log_decode = lWARN;
|
||||
log_level log_slimproto = lWARN;
|
||||
#if IR
|
||||
log_level log_ir = lWARN;
|
||||
#endif
|
||||
|
||||
int maxSampleRate = 0;
|
||||
|
||||
char *optarg = NULL;
|
||||
int optind = 1;
|
||||
int i;
|
||||
|
||||
#define MAXCMDLINE 512
|
||||
char cmdline[MAXCMDLINE] = "";
|
||||
|
||||
get_mac(mac);
|
||||
|
||||
for (i = 0; i < argc && (strlen(argv[i]) + strlen(cmdline) + 2 < MAXCMDLINE); i++) {
|
||||
strcat(cmdline, argv[i]);
|
||||
strcat(cmdline, " ");
|
||||
}
|
||||
|
||||
while (optind < argc && strlen(argv[optind]) >= 2 && argv[optind][0] == '-') {
|
||||
char *opt = argv[optind] + 1;
|
||||
if (strstr("oabcCdefmMnNpPrs"
|
||||
#if ALSA
|
||||
"UVO"
|
||||
#endif
|
||||
/*
|
||||
* only allow '-Z <rate>' override of maxSampleRate
|
||||
* reported by client if built with the capability to resample!
|
||||
*/
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
"Z"
|
||||
#endif
|
||||
, opt) && optind < argc - 1) {
|
||||
optarg = argv[optind + 1];
|
||||
optind += 2;
|
||||
} else if (strstr("ltz?W"
|
||||
#if ALSA
|
||||
"LX"
|
||||
#endif
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
"uR"
|
||||
#endif
|
||||
#if DSD
|
||||
"D"
|
||||
#endif
|
||||
#if VISEXPORT
|
||||
"v"
|
||||
#endif
|
||||
#if IR
|
||||
"i"
|
||||
#endif
|
||||
#if defined(GPIO) && defined(RPI)
|
||||
"G"
|
||||
#endif
|
||||
#if GPIO
|
||||
"S"
|
||||
#endif
|
||||
|
||||
, opt)) {
|
||||
optarg = NULL;
|
||||
optind += 1;
|
||||
} else {
|
||||
fprintf(stderr, "\nOption error: -%s\n\n", opt);
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
switch (opt[0]) {
|
||||
case 'o':
|
||||
output_device = optarg;
|
||||
#if ALSA
|
||||
mixer_device = optarg;
|
||||
#endif
|
||||
break;
|
||||
case 'a':
|
||||
output_params = optarg;
|
||||
break;
|
||||
case 'b':
|
||||
{
|
||||
char *s = next_param(optarg, ':');
|
||||
char *o = next_param(NULL, ':');
|
||||
if (s) stream_buf_size = atoi(s) * 1024;
|
||||
if (o) output_buf_size = atoi(o) * 1024;
|
||||
}
|
||||
break;
|
||||
case 'c':
|
||||
include_codecs = optarg;
|
||||
break;
|
||||
case 'C':
|
||||
if (atoi(optarg) > 0) {
|
||||
idle = atoi(optarg) * 1000;
|
||||
}
|
||||
break;
|
||||
case 'e':
|
||||
exclude_codecs = optarg;
|
||||
break;
|
||||
case 'd':
|
||||
{
|
||||
char *l = strtok(optarg, "=");
|
||||
char *v = strtok(NULL, "=");
|
||||
log_level new = lWARN;
|
||||
if (l && v) {
|
||||
if (!strcmp(v, "info")) new = lINFO;
|
||||
if (!strcmp(v, "debug")) new = lDEBUG;
|
||||
if (!strcmp(v, "sdebug")) new = lSDEBUG;
|
||||
if (!strcmp(l, "all") || !strcmp(l, "slimproto")) log_slimproto = new;
|
||||
if (!strcmp(l, "all") || !strcmp(l, "stream")) log_stream = new;
|
||||
if (!strcmp(l, "all") || !strcmp(l, "decode")) log_decode = new;
|
||||
if (!strcmp(l, "all") || !strcmp(l, "output")) log_output = new;
|
||||
#if IR
|
||||
if (!strcmp(l, "all") || !strcmp(l, "ir")) log_ir = new;
|
||||
#endif
|
||||
} else {
|
||||
fprintf(stderr, "\nDebug settings error: -d %s\n\n", optarg);
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
logfile = optarg;
|
||||
break;
|
||||
case 'm':
|
||||
{
|
||||
int byte = 0;
|
||||
char *tmp;
|
||||
if (!strncmp(optarg, "00:04:20", 8)) {
|
||||
LOG_ERROR("ignoring mac address from hardware player range 00:04:20:**:**:**");
|
||||
} else {
|
||||
char *t = strtok(optarg, ":");
|
||||
while (t && byte < 6) {
|
||||
mac[byte++] = (u8_t)strtoul(t, &tmp, 16);
|
||||
t = strtok(NULL, ":");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'M':
|
||||
modelname = optarg;
|
||||
break;
|
||||
case 'r':
|
||||
{
|
||||
char *rstr = next_param(optarg, ':');
|
||||
char *dstr = next_param(NULL, ':');
|
||||
if (rstr && strstr(rstr, ",")) {
|
||||
// parse sample rates and sort them
|
||||
char *r = next_param(rstr, ',');
|
||||
unsigned tmp[MAX_SUPPORTED_SAMPLERATES] = { 0 };
|
||||
int i, j;
|
||||
int last = 999999;
|
||||
for (i = 0; r && i < MAX_SUPPORTED_SAMPLERATES; ++i) {
|
||||
tmp[i] = atoi(r);
|
||||
r = next_param(NULL, ',');
|
||||
}
|
||||
for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) {
|
||||
int largest = 0;
|
||||
for (j = 0; j < MAX_SUPPORTED_SAMPLERATES; ++j) {
|
||||
if (tmp[j] > largest && tmp[j] < last) {
|
||||
largest = tmp[j];
|
||||
}
|
||||
}
|
||||
rates[i] = last = largest;
|
||||
}
|
||||
} else if (rstr) {
|
||||
// optstr is <min>-<max> or <max>, extract rates from test rates within this range
|
||||
unsigned ref[] TEST_RATES;
|
||||
char *str1 = next_param(rstr, '-');
|
||||
char *str2 = next_param(NULL, '-');
|
||||
unsigned max = str2 ? atoi(str2) : (str1 ? atoi(str1) : ref[0]);
|
||||
unsigned min = str1 && str2 ? atoi(str1) : 0;
|
||||
unsigned tmp;
|
||||
int i, j;
|
||||
if (max < min) { tmp = max; max = min; min = tmp; }
|
||||
rates[0] = max;
|
||||
for (i = 0, j = 1; i < MAX_SUPPORTED_SAMPLERATES; ++i) {
|
||||
if (ref[i] < rates[j-1] && ref[i] >= min) {
|
||||
rates[j++] = ref[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dstr) {
|
||||
rate_delay = atoi(dstr);
|
||||
}
|
||||
if (rates[0]) {
|
||||
user_rates = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
server = optarg;
|
||||
break;
|
||||
case 'n':
|
||||
name = optarg;
|
||||
break;
|
||||
case 'N':
|
||||
namefile = optarg;
|
||||
break;
|
||||
case 'W':
|
||||
pcm_check_header = true;
|
||||
break;
|
||||
#if ALSA
|
||||
case 'p':
|
||||
rt_priority = atoi(optarg);
|
||||
if (rt_priority > 99 || rt_priority < 1) {
|
||||
fprintf(stderr, "\nError: invalid priority: %s\n\n", optarg);
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#if LINUX || FREEBSD || SUN
|
||||
case 'P':
|
||||
pidfile = optarg;
|
||||
break;
|
||||
#endif
|
||||
#ifndef EMBEDDED
|
||||
case 'l':
|
||||
list_devices();
|
||||
exit(0);
|
||||
break;
|
||||
#endif
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
case 'u':
|
||||
case 'R':
|
||||
if (optind < argc && argv[optind] && argv[optind][0] != '-') {
|
||||
resample = argv[optind++];
|
||||
} else {
|
||||
resample = "";
|
||||
}
|
||||
break;
|
||||
case 'Z':
|
||||
maxSampleRate = atoi(optarg);
|
||||
break;
|
||||
#endif
|
||||
#if DSD
|
||||
case 'D':
|
||||
dsd_outfmt = DOP;
|
||||
if (optind < argc && argv[optind] && argv[optind][0] != '-') {
|
||||
char *dstr = next_param(argv[optind++], ':');
|
||||
char *fstr = next_param(NULL, ':');
|
||||
dsd_delay = dstr ? atoi(dstr) : 0;
|
||||
if (fstr) {
|
||||
if (!strcmp(fstr, "dop")) dsd_outfmt = DOP;
|
||||
if (!strcmp(fstr, "u8")) dsd_outfmt = DSD_U8;
|
||||
if (!strcmp(fstr, "u16le")) dsd_outfmt = DSD_U16_LE;
|
||||
if (!strcmp(fstr, "u32le")) dsd_outfmt = DSD_U32_LE;
|
||||
if (!strcmp(fstr, "u16be")) dsd_outfmt = DSD_U16_BE;
|
||||
if (!strcmp(fstr, "u32be")) dsd_outfmt = DSD_U32_BE;
|
||||
if (!strcmp(fstr, "dop24")) dsd_outfmt = DOP_S24_LE;
|
||||
if (!strcmp(fstr, "dop24_3")) dsd_outfmt = DOP_S24_3LE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#if VISEXPORT
|
||||
case 'v':
|
||||
visexport = true;
|
||||
break;
|
||||
#endif
|
||||
#if ALSA
|
||||
case 'O':
|
||||
mixer_device = optarg;
|
||||
break;
|
||||
case 'L':
|
||||
list_mixers(mixer_device);
|
||||
exit(0);
|
||||
break;
|
||||
case 'X':
|
||||
linear_volume = true;
|
||||
break;
|
||||
case 'U':
|
||||
output_mixer_unmute = true;
|
||||
case 'V':
|
||||
if (output_mixer) {
|
||||
fprintf(stderr, "-U and -V option should not be used at same time\n");
|
||||
exit(1);
|
||||
}
|
||||
output_mixer = optarg;
|
||||
break;
|
||||
#endif
|
||||
#if IR
|
||||
case 'i':
|
||||
if (optind < argc && argv[optind] && argv[optind][0] != '-') {
|
||||
lircrc = argv[optind++];
|
||||
} else {
|
||||
lircrc = "~/.lircrc"; // liblirc_client will expand ~/
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#if defined(GPIO) && defined(RPI)
|
||||
case 'G':
|
||||
if (power_script != NULL){
|
||||
fprintf(stderr, "-G and -S options cannot be used together \n\n" );
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
if (optind < argc && argv[optind] && argv[optind][0] != '-') {
|
||||
char *gp = next_param(argv[optind++], ':');
|
||||
char *go = next_param (NULL, ':');
|
||||
gpio_pin = atoi(gp);
|
||||
if (go != NULL){
|
||||
if ((strcmp(go, "H")==0)|(strcmp(go, "h")==0)){
|
||||
gpio_active_low=false;
|
||||
}else if((strcmp(go, "L")==0)|(strcmp(go, "l")==0)){
|
||||
gpio_active_low=true;
|
||||
}else{
|
||||
fprintf(stderr,"Must set output to be active High or Low i.e. -G18:H or -G18:L\n");
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
}else{
|
||||
fprintf(stderr,"-G Option Error\n");
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
gpio_active = true;
|
||||
relay(0);
|
||||
|
||||
} else {
|
||||
fprintf(stderr, "Error in GPIO Pin assignment.\n");
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#if GPIO
|
||||
case 'S':
|
||||
if (gpio_active){
|
||||
fprintf(stderr, "-G and -S options cannot be used together \n\n" );
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
if (optind < argc && argv[optind] && argv[optind][0] != '-') {
|
||||
power_script = argv[optind++];
|
||||
if( access( power_script, R_OK|X_OK ) == -1 ) {
|
||||
// file doesn't exist
|
||||
fprintf(stderr, "Script %s, not found\n\n", argv[optind-1]);
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "No Script Name Given.\n\n");
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
relay_script(0);
|
||||
break;
|
||||
#endif
|
||||
#if LINUX || FREEBSD || SUN
|
||||
case 'z':
|
||||
daemonize = true;
|
||||
#if SUN
|
||||
init_daemonize();
|
||||
#endif /* SUN */
|
||||
break;
|
||||
#endif
|
||||
case 't':
|
||||
license();
|
||||
exit(0);
|
||||
case '?':
|
||||
usage(argv[0]);
|
||||
exit(0);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Arg error: %s\n", argv[optind]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// warn if command line includes something which isn't parsed
|
||||
if (optind < argc) {
|
||||
fprintf(stderr, "\nError: command line argument error\n\n");
|
||||
usage(argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
signal(SIGINT, sighandler);
|
||||
signal(SIGTERM, sighandler);
|
||||
#if defined(SIGQUIT)
|
||||
signal(SIGQUIT, sighandler);
|
||||
#endif
|
||||
#if defined(SIGHUP)
|
||||
signal(SIGHUP, sighandler);
|
||||
#endif
|
||||
|
||||
#if USE_SSL && !LINKALL
|
||||
ssl_loaded = load_ssl_symbols();
|
||||
#endif
|
||||
|
||||
// set the output buffer size if not specified on the command line, take account of resampling
|
||||
if (!output_buf_size) {
|
||||
output_buf_size = OUTPUTBUF_SIZE;
|
||||
if (resample) {
|
||||
unsigned scale = 8;
|
||||
if (rates[0]) {
|
||||
scale = rates[0] / 44100;
|
||||
if (scale > 8) scale = 8;
|
||||
if (scale < 1) scale = 1;
|
||||
}
|
||||
output_buf_size *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
if (logfile) {
|
||||
if (!freopen(logfile, "a", stderr)) {
|
||||
fprintf(stderr, "error opening logfile %s: %s\n", logfile, strerror(errno));
|
||||
} else {
|
||||
if (log_output >= lINFO || log_stream >= lINFO || log_decode >= lINFO || log_slimproto >= lINFO) {
|
||||
fprintf(stderr, "\n%s\n", cmdline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if LINUX || FREEBSD || SUN
|
||||
if (pidfile) {
|
||||
if (!(pidfp = fopen(pidfile, "w")) ) {
|
||||
fprintf(stderr, "Error opening pidfile %s: %s\n", pidfile, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
pidfile = realpath(pidfile, NULL); // daemonize will change cwd
|
||||
}
|
||||
|
||||
if (daemonize) {
|
||||
if (daemon(0, logfile ? 1 : 0)) {
|
||||
fprintf(stderr, "error daemonizing: %s\n", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
if (pidfp) {
|
||||
fprintf(pidfp, "%d\n", (int) getpid());
|
||||
fclose(pidfp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if WIN
|
||||
winsock_init();
|
||||
#endif
|
||||
|
||||
stream_init(log_stream, stream_buf_size);
|
||||
|
||||
#if EMBEDDED
|
||||
output_init_embedded(log_output, output_device, output_buf_size, output_params, rates, rate_delay, idle);
|
||||
#else
|
||||
if (!strcmp(output_device, "-")) {
|
||||
output_init_stdout(log_output, output_buf_size, output_params, rates, rate_delay);
|
||||
} else {
|
||||
#if ALSA
|
||||
output_init_alsa(log_output, output_device, output_buf_size, output_params, rates, rate_delay, rt_priority, idle, mixer_device, output_mixer,
|
||||
output_mixer_unmute, linear_volume);
|
||||
#endif
|
||||
#if PORTAUDIO
|
||||
output_init_pa(log_output, output_device, output_buf_size, output_params, rates, rate_delay, idle);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if DSD
|
||||
dsd_init(dsd_outfmt, dsd_delay);
|
||||
#endif
|
||||
|
||||
#if VISEXPORT
|
||||
if (visexport) {
|
||||
output_vis_init(log_output, mac);
|
||||
}
|
||||
#endif
|
||||
|
||||
decode_init(log_decode, include_codecs, exclude_codecs);
|
||||
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
if (resample) {
|
||||
process_init(resample);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if IR
|
||||
if (lircrc) {
|
||||
ir_init(log_ir, lircrc);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (name && namefile) {
|
||||
fprintf(stderr, "-n and -N option should not be used at same time\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
slimproto(log_slimproto, server, mac, name, namefile, modelname, maxSampleRate);
|
||||
|
||||
decode_close();
|
||||
stream_close();
|
||||
|
||||
#if EMBEDDED
|
||||
output_close_embedded();
|
||||
#else
|
||||
if (!strcmp(output_device, "-")) {
|
||||
output_close_stdout();
|
||||
} else {
|
||||
#if ALSA
|
||||
output_close_alsa();
|
||||
#endif
|
||||
#if PORTAUDIO
|
||||
output_close_pa();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if IR
|
||||
ir_close();
|
||||
#endif
|
||||
|
||||
#if WIN
|
||||
winsock_close();
|
||||
#endif
|
||||
|
||||
#if LINUX || FREEBSD || SUN
|
||||
if (pidfile) {
|
||||
unlink(pidfile);
|
||||
free(pidfile);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_SSL && !LINKALL
|
||||
free_ssl_symbols();
|
||||
#endif
|
||||
|
||||
exit(0);
|
||||
}
|
||||
8
components/squeezelite/mpg.c
Normal file
8
components/squeezelite/mpg.c
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "squeezelite.h"
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
struct codec *register_mpg(void) {
|
||||
LOG_INFO("mpg unavailable");
|
||||
return NULL;
|
||||
}
|
||||
448
components/squeezelite/output.c
Normal file
448
components/squeezelite/output.c
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Common output function
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
static log_level loglevel;
|
||||
|
||||
struct outputstate output;
|
||||
|
||||
static struct buffer buf;
|
||||
|
||||
struct buffer *outputbuf = &buf;
|
||||
|
||||
u8_t *silencebuf;
|
||||
#if DSD
|
||||
u8_t *silencebuf_dsd;
|
||||
#endif
|
||||
|
||||
bool user_rates = false;
|
||||
|
||||
#define LOCK mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK mutex_unlock(outputbuf->mutex)
|
||||
|
||||
// functions starting _* are called with mutex locked
|
||||
|
||||
frames_t _output_frames(frames_t avail) {
|
||||
|
||||
frames_t frames, size;
|
||||
bool silence;
|
||||
|
||||
s32_t cross_gain_in = 0, cross_gain_out = 0; ISAMPLE_T *cross_ptr = NULL;
|
||||
|
||||
s32_t gainL = output.current_replay_gain ? gain(output.gainL, output.current_replay_gain) : output.gainL;
|
||||
s32_t gainR = output.current_replay_gain ? gain(output.gainR, output.current_replay_gain) : output.gainR;
|
||||
|
||||
if (output.invert) { gainL = -gainL; gainR = -gainR; }
|
||||
|
||||
frames = _buf_used(outputbuf) / BYTES_PER_FRAME;
|
||||
silence = false;
|
||||
|
||||
// start when threshold met
|
||||
if (output.state == OUTPUT_BUFFER && frames > output.threshold * output.next_sample_rate / 10 && frames > output.start_frames) {
|
||||
output.state = OUTPUT_RUNNING;
|
||||
LOG_INFO("start buffer frames: %u", frames);
|
||||
wake_controller();
|
||||
}
|
||||
|
||||
// skip ahead - consume outputbuf but play nothing
|
||||
if (output.state == OUTPUT_SKIP_FRAMES) {
|
||||
if (frames > 0) {
|
||||
frames_t skip = min(frames, output.skip_frames);
|
||||
LOG_INFO("skip %u of %u frames", skip, output.skip_frames);
|
||||
frames -= skip;
|
||||
output.frames_played += skip;
|
||||
while (skip > 0) {
|
||||
frames_t cont_frames = min(skip, _buf_cont_read(outputbuf) / BYTES_PER_FRAME);
|
||||
skip -= cont_frames;
|
||||
_buf_inc_readp(outputbuf, cont_frames * BYTES_PER_FRAME);
|
||||
}
|
||||
}
|
||||
output.state = OUTPUT_RUNNING;
|
||||
}
|
||||
|
||||
// pause frames - play silence for required frames
|
||||
if (output.state == OUTPUT_PAUSE_FRAMES) {
|
||||
LOG_INFO("pause %u frames", output.pause_frames);
|
||||
if (output.pause_frames == 0) {
|
||||
output.state = OUTPUT_RUNNING;
|
||||
} else {
|
||||
silence = true;
|
||||
frames = min(avail, output.pause_frames);
|
||||
frames = min(frames, MAX_SILENCE_FRAMES);
|
||||
output.pause_frames -= frames;
|
||||
}
|
||||
}
|
||||
|
||||
// start at - play silence until jiffies reached
|
||||
if (output.state == OUTPUT_START_AT) {
|
||||
u32_t now = gettime_ms();
|
||||
if (now >= output.start_at || output.start_at > now + 10000) {
|
||||
output.state = OUTPUT_RUNNING;
|
||||
} else {
|
||||
u32_t delta_frames = (output.start_at - now) * output.current_sample_rate / 1000;
|
||||
silence = true;
|
||||
frames = min(avail, delta_frames);
|
||||
frames = min(frames, MAX_SILENCE_FRAMES);
|
||||
}
|
||||
}
|
||||
|
||||
// play silence if buffering or no frames
|
||||
if (output.state <= OUTPUT_BUFFER || frames == 0) {
|
||||
silence = true;
|
||||
frames = min(avail, MAX_SILENCE_FRAMES);
|
||||
}
|
||||
|
||||
LOG_SDEBUG("avail: %d frames: %d silence: %d", avail, frames, silence);
|
||||
frames = min(frames, avail);
|
||||
size = frames;
|
||||
|
||||
while (size > 0) {
|
||||
frames_t out_frames;
|
||||
frames_t cont_frames = _buf_cont_read(outputbuf) / BYTES_PER_FRAME;
|
||||
int wrote;
|
||||
|
||||
if (output.track_start && !silence) {
|
||||
if (output.track_start == outputbuf->readp) {
|
||||
unsigned delay = 0;
|
||||
if (output.current_sample_rate != output.next_sample_rate) {
|
||||
delay = output.rate_delay;
|
||||
}
|
||||
IF_DSD(
|
||||
if (output.outfmt != output.next_fmt) {
|
||||
delay = output.dsd_delay;
|
||||
}
|
||||
)
|
||||
frames -= size;
|
||||
// add silence delay in two halves, before and after track start on rate or pcm-dop change
|
||||
if (delay) {
|
||||
output.state = OUTPUT_PAUSE_FRAMES;
|
||||
if (!output.delay_active) {
|
||||
output.pause_frames = output.current_sample_rate * delay / 2000;
|
||||
output.delay_active = true; // first delay - don't process track start
|
||||
break;
|
||||
} else {
|
||||
output.pause_frames = output.next_sample_rate * delay / 2000;
|
||||
output.delay_active = false; // second delay - process track start
|
||||
}
|
||||
}
|
||||
LOG_INFO("track start sample rate: %u replay_gain: %u", output.next_sample_rate, output.next_replay_gain);
|
||||
output.frames_played = 0;
|
||||
output.track_started = true;
|
||||
output.track_start_time = gettime_ms();
|
||||
output.current_sample_rate = output.next_sample_rate;
|
||||
IF_DSD(
|
||||
output.outfmt = output.next_fmt;
|
||||
)
|
||||
if (output.fade == FADE_INACTIVE || output.fade_mode != FADE_CROSSFADE) {
|
||||
output.current_replay_gain = output.next_replay_gain;
|
||||
}
|
||||
output.track_start = NULL;
|
||||
break;
|
||||
} else if (output.track_start > outputbuf->readp) {
|
||||
// reduce cont_frames so we find the next track start at beginning of next chunk
|
||||
cont_frames = min(cont_frames, (output.track_start - outputbuf->readp) / BYTES_PER_FRAME);
|
||||
}
|
||||
}
|
||||
|
||||
IF_DSD(
|
||||
if (output.outfmt != PCM) {
|
||||
gainL = gainR = FIXED_ONE;
|
||||
}
|
||||
)
|
||||
|
||||
if (output.fade && !silence) {
|
||||
if (output.fade == FADE_DUE) {
|
||||
if (output.fade_start == outputbuf->readp) {
|
||||
LOG_INFO("fade start reached");
|
||||
output.fade = FADE_ACTIVE;
|
||||
} else if (output.fade_start > outputbuf->readp) {
|
||||
cont_frames = min(cont_frames, (output.fade_start - outputbuf->readp) / BYTES_PER_FRAME);
|
||||
}
|
||||
}
|
||||
if (output.fade == FADE_ACTIVE) {
|
||||
// find position within fade
|
||||
frames_t cur_f = outputbuf->readp >= output.fade_start ? (outputbuf->readp - output.fade_start) / BYTES_PER_FRAME :
|
||||
(outputbuf->readp + outputbuf->size - output.fade_start) / BYTES_PER_FRAME;
|
||||
frames_t dur_f = output.fade_end >= output.fade_start ? (output.fade_end - output.fade_start) / BYTES_PER_FRAME :
|
||||
(output.fade_end + outputbuf->size - output.fade_start) / BYTES_PER_FRAME;
|
||||
if (cur_f >= dur_f) {
|
||||
if (output.fade_mode == FADE_INOUT && output.fade_dir == FADE_DOWN) {
|
||||
LOG_INFO("fade down complete, starting fade up");
|
||||
output.fade_dir = FADE_UP;
|
||||
output.fade_start = outputbuf->readp;
|
||||
output.fade_end = outputbuf->readp + dur_f * BYTES_PER_FRAME;
|
||||
if (output.fade_end >= outputbuf->wrap) {
|
||||
output.fade_end -= outputbuf->size;
|
||||
}
|
||||
cur_f = 0;
|
||||
} else if (output.fade_mode == FADE_CROSSFADE) {
|
||||
LOG_INFO("crossfade complete");
|
||||
if (_buf_used(outputbuf) >= dur_f * BYTES_PER_FRAME) {
|
||||
_buf_inc_readp(outputbuf, dur_f * BYTES_PER_FRAME);
|
||||
LOG_INFO("skipped crossfaded start");
|
||||
} else {
|
||||
LOG_WARN("unable to skip crossfaded start");
|
||||
}
|
||||
output.fade = FADE_INACTIVE;
|
||||
output.current_replay_gain = output.next_replay_gain;
|
||||
} else {
|
||||
LOG_INFO("fade complete");
|
||||
output.fade = FADE_INACTIVE;
|
||||
}
|
||||
}
|
||||
// if fade in progress set fade gain, ensure cont_frames reduced so we get to end of fade at start of chunk
|
||||
if (output.fade) {
|
||||
if (output.fade_end > outputbuf->readp) {
|
||||
cont_frames = min(cont_frames, (output.fade_end - outputbuf->readp) / BYTES_PER_FRAME);
|
||||
}
|
||||
if (output.fade_dir == FADE_UP || output.fade_dir == FADE_DOWN) {
|
||||
// fade in, in-out, out handled via altering standard gain
|
||||
s32_t fade_gain;
|
||||
if (output.fade_dir == FADE_DOWN) {
|
||||
cur_f = dur_f - cur_f;
|
||||
}
|
||||
fade_gain = to_gain((float)cur_f / (float)dur_f);
|
||||
gainL = gain(gainL, fade_gain);
|
||||
gainR = gain(gainR, fade_gain);
|
||||
if (output.invert) { gainL = -gainL; gainR = -gainR; }
|
||||
}
|
||||
if (output.fade_dir == FADE_CROSS) {
|
||||
// cross fade requires special treatment - performed later based on these values
|
||||
// support different replay gain for old and new track by retaining old value until crossfade completes
|
||||
if (_buf_used(outputbuf) / BYTES_PER_FRAME > dur_f + size) {
|
||||
cross_gain_in = to_gain((float)cur_f / (float)dur_f);
|
||||
cross_gain_out = FIXED_ONE - cross_gain_in;
|
||||
if (output.current_replay_gain) {
|
||||
cross_gain_out = gain(cross_gain_out, output.current_replay_gain);
|
||||
}
|
||||
if (output.next_replay_gain) {
|
||||
cross_gain_in = gain(cross_gain_in, output.next_replay_gain);
|
||||
}
|
||||
gainL = output.gainL;
|
||||
gainR = output.gainR;
|
||||
if (output.invert) { gainL = -gainL; gainR = -gainR; }
|
||||
cross_ptr = (ISAMPLE_T *)(output.fade_end + cur_f * BYTES_PER_FRAME);
|
||||
} else {
|
||||
LOG_INFO("unable to continue crossfade - too few samples");
|
||||
output.fade = FADE_INACTIVE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out_frames = !silence ? min(size, cont_frames) : size;
|
||||
|
||||
wrote = output.write_cb(out_frames, silence, gainL, gainR, cross_gain_in, cross_gain_out, &cross_ptr);
|
||||
|
||||
if (wrote <= 0) {
|
||||
frames -= size;
|
||||
break;
|
||||
} else {
|
||||
out_frames = (frames_t)wrote;
|
||||
}
|
||||
|
||||
size -= out_frames;
|
||||
|
||||
_vis_export(outputbuf, &output, out_frames, silence);
|
||||
|
||||
if (!silence) {
|
||||
_buf_inc_readp(outputbuf, out_frames * BYTES_PER_FRAME);
|
||||
output.frames_played += out_frames;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_SDEBUG("wrote %u frames", frames);
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
void _checkfade(bool start) {
|
||||
frames_t bytes;
|
||||
|
||||
LOG_INFO("fade mode: %u duration: %u %s", output.fade_mode, output.fade_secs, start ? "track-start" : "track-end");
|
||||
|
||||
bytes = output.next_sample_rate * BYTES_PER_FRAME * output.fade_secs;
|
||||
if (output.fade_mode == FADE_INOUT) {
|
||||
/* align on a frame boundary */
|
||||
bytes = ((bytes / 2) / BYTES_PER_FRAME) * BYTES_PER_FRAME;
|
||||
}
|
||||
|
||||
if (start && (output.fade_mode == FADE_IN || (output.fade_mode == FADE_INOUT && _buf_used(outputbuf) == 0))) {
|
||||
bytes = min(bytes, outputbuf->size - BYTES_PER_FRAME); // shorter than full buffer otherwise start and end align
|
||||
LOG_INFO("fade IN: %u frames", bytes / BYTES_PER_FRAME);
|
||||
output.fade = FADE_DUE;
|
||||
output.fade_dir = FADE_UP;
|
||||
output.fade_start = outputbuf->writep;
|
||||
output.fade_end = output.fade_start + bytes;
|
||||
if (output.fade_end >= outputbuf->wrap) {
|
||||
output.fade_end -= outputbuf->size;
|
||||
}
|
||||
}
|
||||
|
||||
if (!start && (output.fade_mode == FADE_OUT || output.fade_mode == FADE_INOUT)) {
|
||||
bytes = min(_buf_used(outputbuf), bytes);
|
||||
LOG_INFO("fade %s: %u frames", output.fade_mode == FADE_INOUT ? "IN-OUT" : "OUT", bytes / BYTES_PER_FRAME);
|
||||
output.fade = FADE_DUE;
|
||||
output.fade_dir = FADE_DOWN;
|
||||
output.fade_start = outputbuf->writep - bytes;
|
||||
if (output.fade_start < outputbuf->buf) {
|
||||
output.fade_start += outputbuf->size;
|
||||
}
|
||||
output.fade_end = outputbuf->writep;
|
||||
}
|
||||
|
||||
if (start && output.fade_mode == FADE_CROSSFADE) {
|
||||
if (_buf_used(outputbuf) != 0) {
|
||||
if (output.next_sample_rate != output.current_sample_rate) {
|
||||
LOG_INFO("crossfade disabled as sample rates differ");
|
||||
return;
|
||||
}
|
||||
bytes = min(bytes, _buf_used(outputbuf)); // max of current remaining samples from previous track
|
||||
bytes = min(bytes, (frames_t)(outputbuf->size * 0.9)); // max of 90% of outputbuf as we consume additional buffer during crossfade
|
||||
LOG_INFO("CROSSFADE: %u frames", bytes / BYTES_PER_FRAME);
|
||||
output.fade = FADE_DUE;
|
||||
output.fade_dir = FADE_CROSS;
|
||||
output.fade_start = outputbuf->writep - bytes;
|
||||
if (output.fade_start < outputbuf->buf) {
|
||||
output.fade_start += outputbuf->size;
|
||||
}
|
||||
output.fade_end = outputbuf->writep;
|
||||
output.track_start = output.fade_start;
|
||||
} else if (outputbuf->size == OUTPUTBUF_SIZE && outputbuf->readp == outputbuf->buf) {
|
||||
// if default setting used and nothing in buffer attempt to resize to provide full crossfade support
|
||||
LOG_INFO("resize outputbuf for crossfade");
|
||||
_buf_resize(outputbuf, OUTPUTBUF_SIZE_CROSSFADE);
|
||||
#if LINUX || FREEBSD
|
||||
touch_memory(outputbuf->buf, outputbuf->size);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[], unsigned idle) {
|
||||
unsigned i;
|
||||
|
||||
loglevel = level;
|
||||
|
||||
output_buf_size = output_buf_size - (output_buf_size % BYTES_PER_FRAME);
|
||||
LOG_DEBUG("outputbuf size: %u", output_buf_size);
|
||||
|
||||
buf_init(outputbuf, output_buf_size);
|
||||
if (!outputbuf->buf) {
|
||||
LOG_ERROR("unable to malloc output buffer");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
silencebuf = malloc(MAX_SILENCE_FRAMES * BYTES_PER_FRAME);
|
||||
if (!silencebuf) {
|
||||
LOG_ERROR("unable to malloc silence buffer");
|
||||
exit(0);
|
||||
}
|
||||
memset(silencebuf, 0, MAX_SILENCE_FRAMES * BYTES_PER_FRAME);
|
||||
|
||||
IF_DSD(
|
||||
silencebuf_dsd = malloc(MAX_SILENCE_FRAMES * BYTES_PER_FRAME);
|
||||
if (!silencebuf_dsd) {
|
||||
LOG_ERROR("unable to malloc silence dsd buffer");
|
||||
exit(0);
|
||||
}
|
||||
dsd_silence_frames((u32_t *)silencebuf_dsd, MAX_SILENCE_FRAMES);
|
||||
)
|
||||
|
||||
LOG_DEBUG("idle timeout: %u", idle);
|
||||
|
||||
output.state = idle ? OUTPUT_OFF: OUTPUT_STOPPED;
|
||||
output.device = device;
|
||||
output.fade = FADE_INACTIVE;
|
||||
output.invert = false;
|
||||
output.error_opening = false;
|
||||
output.idle_to = (u32_t) idle;
|
||||
|
||||
/* Skip test_open for stdout, set default sample rates */
|
||||
if ( output.device[0] == '-' ) {
|
||||
for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) {
|
||||
output.supported_rates[i] = rates[i];
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!test_open(output.device, output.supported_rates, user_rates)) {
|
||||
LOG_ERROR("unable to open output device: %s", output.device);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (user_rates) {
|
||||
for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) {
|
||||
output.supported_rates[i] = rates[i];
|
||||
}
|
||||
}
|
||||
|
||||
// set initial sample rate, preferring 44100
|
||||
for (i = 0; i < MAX_SUPPORTED_SAMPLERATES; ++i) {
|
||||
if (output.supported_rates[i] == 44100) {
|
||||
output.default_sample_rate = 44100;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!output.default_sample_rate) {
|
||||
output.default_sample_rate = output.supported_rates[0];
|
||||
}
|
||||
|
||||
output.current_sample_rate = output.default_sample_rate;
|
||||
|
||||
if (loglevel >= lINFO) {
|
||||
char rates_buf[10 * MAX_SUPPORTED_SAMPLERATES] = "";
|
||||
for (i = 0; output.supported_rates[i]; ++i) {
|
||||
char s[10];
|
||||
sprintf(s, "%d ", output.supported_rates[i]);
|
||||
strcat(rates_buf, s);
|
||||
}
|
||||
LOG_INFO("supported rates: %s", rates_buf);
|
||||
}
|
||||
}
|
||||
|
||||
void output_close_common(void) {
|
||||
buf_destroy(outputbuf);
|
||||
free(silencebuf);
|
||||
IF_DSD(
|
||||
free(silencebuf_dsd);
|
||||
)
|
||||
}
|
||||
|
||||
void output_flush(void) {
|
||||
LOG_INFO("flush output buffer");
|
||||
buf_flush(outputbuf);
|
||||
LOCK;
|
||||
output.fade = FADE_INACTIVE;
|
||||
if (output.state != OUTPUT_OFF) {
|
||||
output.state = OUTPUT_STOPPED;
|
||||
if (output.error_opening) {
|
||||
output.current_sample_rate = output.default_sample_rate;
|
||||
}
|
||||
output.delay_active = false;
|
||||
}
|
||||
output.frames_played = 0;
|
||||
UNLOCK;
|
||||
}
|
||||
175
components/squeezelite/output_bt.c
Normal file
175
components/squeezelite/output_bt.c
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Squeezelite for esp32
|
||||
*
|
||||
* (c) Sebastien 2019
|
||||
* Philippe G. 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
#include "perf_trace.h"
|
||||
|
||||
extern struct outputstate output;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct buffer *streambuf;
|
||||
extern u8_t *silencebuf;
|
||||
|
||||
#define LOCK mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
|
||||
#define FRAME_BLOCK MAX_SILENCE_FRAMES
|
||||
|
||||
#define STATS_REPORT_DELAY_MS 15000
|
||||
|
||||
extern void hal_bluetooth_init(const char * options);
|
||||
|
||||
static log_level loglevel;
|
||||
uint8_t * btout;
|
||||
|
||||
static int _write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR,
|
||||
s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr);
|
||||
|
||||
#define DECLARE_ALL_MIN_MAX \
|
||||
DECLARE_MIN_MAX(req);\
|
||||
DECLARE_MIN_MAX(rec);\
|
||||
DECLARE_MIN_MAX(bt);\
|
||||
DECLARE_MIN_MAX(under);\
|
||||
DECLARE_MIN_MAX(stream_buf);\
|
||||
DECLARE_MIN_MAX_DURATION(lock_out_time)
|
||||
|
||||
#define RESET_ALL_MIN_MAX \
|
||||
RESET_MIN_MAX(bt); \
|
||||
RESET_MIN_MAX(req); \
|
||||
RESET_MIN_MAX(rec); \
|
||||
RESET_MIN_MAX(under); \
|
||||
RESET_MIN_MAX(stream_buf); \
|
||||
RESET_MIN_MAX_DURATION(lock_out_time)
|
||||
|
||||
DECLARE_ALL_MIN_MAX;
|
||||
|
||||
void output_init_bt(log_level level, char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle) {
|
||||
loglevel = level;
|
||||
hal_bluetooth_init(device);
|
||||
output.write_cb = &_write_frames;
|
||||
}
|
||||
|
||||
static int _write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR,
|
||||
s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr) {
|
||||
|
||||
assert(btout != NULL);
|
||||
|
||||
if (!silence ) {
|
||||
|
||||
if (output.fade == FADE_ACTIVE && output.fade_dir == FADE_CROSS && *cross_ptr) {
|
||||
_apply_cross(outputbuf, out_frames, cross_gain_in, cross_gain_out, cross_ptr);
|
||||
}
|
||||
|
||||
if (gainL != FIXED_ONE || gainR!= FIXED_ONE) {
|
||||
_apply_gain(outputbuf, out_frames, gainL, gainR);
|
||||
}
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
memcpy(btout, outputbuf->readp, out_frames * BYTES_PER_FRAME);
|
||||
#else
|
||||
{
|
||||
frames_t count = out_frames;
|
||||
s32_t *_iptr = (s32_t*) outputbuf->readp;
|
||||
s16_t *_optr = (s16_t*) bt_optr;
|
||||
while (count--) {
|
||||
*_optr++ = *_iptr++ >> 16;
|
||||
*_optr++ = *_iptr++ >> 16;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} else {
|
||||
|
||||
u8_t *buf = silencebuf;
|
||||
memcpy(btout, buf, out_frames * BYTES_PER_FRAME);
|
||||
}
|
||||
|
||||
return (int)out_frames;
|
||||
}
|
||||
|
||||
int32_t output_bt_data(uint8_t *data, int32_t len) {
|
||||
int32_t avail_data = 0, wanted_len = 0, start_timer = 0;
|
||||
|
||||
if (len < 0 || data == NULL ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
btout = data;
|
||||
|
||||
// This is how the BTC layer calculates the number of bytes to
|
||||
// for us to send. (BTC_SBC_DEC_PCM_DATA_LEN * sizeof(OI_INT16) - availPcmBytes
|
||||
wanted_len=len;
|
||||
SET_MIN_MAX(len,req);
|
||||
TIME_MEASUREMENT_START(start_timer);
|
||||
LOCK;
|
||||
output.device_frames = 0; // todo: check if this is the right way do to this.
|
||||
output.updated = gettime_ms();
|
||||
output.frames_played_dmp = output.frames_played;
|
||||
SET_MIN_MAX_SIZED(_buf_used(outputbuf),bt,outputbuf->size);
|
||||
do {
|
||||
avail_data = _output_frames( wanted_len/BYTES_PER_FRAME )*BYTES_PER_FRAME; // Keep the transfer buffer full
|
||||
wanted_len-=avail_data;
|
||||
} while (wanted_len > 0 && avail_data != 0);
|
||||
|
||||
if (wanted_len > 0) {
|
||||
SET_MIN_MAX(wanted_len, under);
|
||||
}
|
||||
|
||||
UNLOCK;
|
||||
SET_MIN_MAX(TIME_MEASUREMENT_GET(start_timer),lock_out_time);
|
||||
SET_MIN_MAX((len-wanted_len), rec);
|
||||
TIME_MEASUREMENT_START(start_timer);
|
||||
|
||||
return len-wanted_len;
|
||||
}
|
||||
|
||||
void output_bt_tick(void) {
|
||||
static time_t lastTime=0;
|
||||
|
||||
LOCK_S;
|
||||
SET_MIN_MAX_SIZED(_buf_used(streambuf), stream_buf, streambuf->size);
|
||||
UNLOCK_S;
|
||||
|
||||
if (lastTime <= gettime_ms() )
|
||||
{
|
||||
lastTime = gettime_ms() + STATS_REPORT_DELAY_MS;
|
||||
LOG_INFO("Statistics over %u secs. " , STATS_REPORT_DELAY_MS/1000);
|
||||
LOG_INFO(" +==========+==========+================+=====+================+");
|
||||
LOG_INFO(" | max | min | average | avg | count |");
|
||||
LOG_INFO(" | (bytes) | (bytes) | (bytes) | pct | |");
|
||||
LOG_INFO(" +==========+==========+================+=====+================+");
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("stream avl",stream_buf));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("output avl",bt));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("requested",req));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("received",rec));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("underrun",under));
|
||||
LOG_INFO( " +==========+==========+================+=====+================+");
|
||||
LOG_INFO("\n");
|
||||
LOG_INFO(" ==========+==========+===========+===========+ ");
|
||||
LOG_INFO(" max (us) | min (us) | avg(us) | count | ");
|
||||
LOG_INFO(" ==========+==========+===========+===========+ ");
|
||||
LOG_INFO(LINE_MIN_MAX_DURATION_FORMAT,LINE_MIN_MAX_DURATION("Out Buf Lock",lock_out_time));
|
||||
LOG_INFO(" ==========+==========+===========+===========+");
|
||||
RESET_ALL_MIN_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
118
components/squeezelite/output_embedded.c
Normal file
118
components/squeezelite/output_embedded.c
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Squeezelite for esp32
|
||||
*
|
||||
* (c) Sebastien 2019
|
||||
* Philippe G. 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
#include "squeezelite.h"
|
||||
|
||||
extern struct outputstate output;
|
||||
extern struct buffer *outputbuf;
|
||||
|
||||
#define FRAME_BLOCK MAX_SILENCE_FRAMES
|
||||
|
||||
#define LOCK mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK mutex_unlock(outputbuf->mutex)
|
||||
|
||||
extern void output_init_bt(log_level level, char *device, unsigned output_buf_size, char *params,
|
||||
unsigned rates[], unsigned rate_delay, unsigned idle);
|
||||
extern void output_init_i2s(log_level level, char *device, unsigned output_buf_size, char *params,
|
||||
unsigned rates[], unsigned rate_delay, unsigned idle);
|
||||
extern void output_close_i2s(void);
|
||||
|
||||
static log_level loglevel;
|
||||
|
||||
static void (*volume_cb)(unsigned left, unsigned right);
|
||||
static void (*close_cb)(void);
|
||||
|
||||
void output_init_embedded(log_level level, char *device, unsigned output_buf_size, char *params,
|
||||
unsigned rates[], unsigned rate_delay, unsigned idle) {
|
||||
loglevel = level;
|
||||
LOG_INFO("init device: %s", device);
|
||||
|
||||
memset(&output, 0, sizeof(output));
|
||||
output_init_common(level, device, output_buf_size, rates, idle);
|
||||
output.start_frames = FRAME_BLOCK;
|
||||
output.rate_delay = rate_delay;
|
||||
|
||||
if (strstr(device, "BT ")) {
|
||||
LOG_INFO("init Bluetooth");
|
||||
output_init_bt(level, device, output_buf_size, params, rates, rate_delay, idle);
|
||||
} else {
|
||||
LOG_INFO("init I2S");
|
||||
close_cb = output_close_i2s;
|
||||
output_init_i2s(level, device, output_buf_size, params, rates, rate_delay, idle);
|
||||
}
|
||||
|
||||
LOG_INFO("init completed.");
|
||||
}
|
||||
|
||||
void output_close_embedded(void) {
|
||||
LOG_INFO("close output");
|
||||
output_close_common();
|
||||
if (close_cb) (*close_cb)();
|
||||
}
|
||||
|
||||
void set_volume(unsigned left, unsigned right) {
|
||||
LOG_DEBUG("setting internal gain left: %u right: %u", left, right);
|
||||
if (!volume_cb) {
|
||||
LOCK;
|
||||
output.gainL = left;
|
||||
output.gainR = right;
|
||||
UNLOCK;
|
||||
} else (*volume_cb)(left, right);
|
||||
}
|
||||
|
||||
bool test_open(const char *device, unsigned rates[], bool userdef_rates) {
|
||||
memset(rates, 0, MAX_SUPPORTED_SAMPLERATES * sizeof(unsigned));
|
||||
if (!strcmp(device, "BT")) {
|
||||
rates[0] = 44100;
|
||||
} else {
|
||||
unsigned _rates[] = { 96000, 88200, 48000, 44100, 32000, 0 };
|
||||
memcpy(rates, _rates, sizeof(_rates));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
char* output_state_str(void){
|
||||
output_state state;
|
||||
LOCK;
|
||||
state = output.state;
|
||||
UNLOCK;
|
||||
switch (state) {
|
||||
case OUTPUT_OFF: return STR(OUTPUT_OFF);
|
||||
case OUTPUT_STOPPED: return STR(OUTPUT_STOPPED);
|
||||
case OUTPUT_BUFFER: return STR(OUTPUT_BUFFER);
|
||||
case OUTPUT_RUNNING: return STR(OUTPUT_RUNNING);
|
||||
case OUTPUT_PAUSE_FRAMES: return STR(OUTPUT_PAUSE_FRAMES);
|
||||
case OUTPUT_SKIP_FRAMES: return STR(OUTPUT_SKIP_FRAMES);
|
||||
case OUTPUT_START_AT: return STR(OUTPUT_START_AT);
|
||||
default: return "OUTPUT_UNKNOWN_STATE";
|
||||
}
|
||||
}
|
||||
|
||||
bool output_stopped(void) {
|
||||
output_state state;
|
||||
LOCK;
|
||||
state = output.state;
|
||||
UNLOCK;
|
||||
return state <= OUTPUT_STOPPED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
380
components/squeezelite/output_i2s.c
Normal file
380
components/squeezelite/output_i2s.c
Normal file
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Squeezelite for esp32
|
||||
*
|
||||
* (c) Sebastien 2019
|
||||
* Philippe G. 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
#include "driver/i2s.h"
|
||||
#include "perf_trace.h"
|
||||
#include <signal.h>
|
||||
#include "time.h"
|
||||
|
||||
|
||||
#define DECLARE_ALL_MIN_MAX \
|
||||
DECLARE_MIN_MAX(o); \
|
||||
DECLARE_MIN_MAX(s); \
|
||||
DECLARE_MIN_MAX(loci2sbuf); \
|
||||
DECLARE_MIN_MAX(req); \
|
||||
DECLARE_MIN_MAX(rec); \
|
||||
DECLARE_MIN_MAX(over); \
|
||||
DECLARE_MIN_MAX(i2savailable);\
|
||||
DECLARE_MIN_MAX(i2s_time); \
|
||||
DECLARE_MIN_MAX(buffering);
|
||||
|
||||
|
||||
#define RESET_ALL_MIN_MAX \
|
||||
RESET_MIN_MAX(o); \
|
||||
RESET_MIN_MAX(s); \
|
||||
RESET_MIN_MAX(loci2sbuf); \
|
||||
RESET_MIN_MAX(req); \
|
||||
RESET_MIN_MAX(rec); \
|
||||
RESET_MIN_MAX(over); \
|
||||
RESET_MIN_MAX(i2savailable);\
|
||||
RESET_MIN_MAX(i2s_time);\
|
||||
RESET_MIN_MAX(buffering);
|
||||
#define STATS_PERIOD_MS 5000
|
||||
|
||||
// Prevent compile errors if dac output is
|
||||
// included in the build and not actually activated in menuconfig
|
||||
#ifndef CONFIG_I2S_BCK_IO
|
||||
#define CONFIG_I2S_BCK_IO -1
|
||||
#endif
|
||||
#ifndef CONFIG_I2S_WS_IO
|
||||
#define CONFIG_I2S_WS_IO -1
|
||||
#endif
|
||||
#ifndef CONFIG_I2S_DO_IO
|
||||
#define CONFIG_I2S_DO_IO -1
|
||||
#endif
|
||||
#ifndef CONFIG_I2S_NUM
|
||||
#define CONFIG_I2S_NUM -1
|
||||
#endif
|
||||
|
||||
#if REPACK && BYTES_PER_FRAMES == 4
|
||||
#error "REPACK is not compatible with BYTES_PER_FRAME=4"
|
||||
#endif
|
||||
|
||||
#define LOCK mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK mutex_unlock(outputbuf->mutex)
|
||||
|
||||
#define FRAME_BLOCK MAX_SILENCE_FRAMES
|
||||
|
||||
extern struct outputstate output;
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern u8_t *silencebuf;
|
||||
|
||||
static log_level loglevel;
|
||||
static size_t i2s_buffer_size = 0;
|
||||
static bool running = true;
|
||||
static bool isI2SStarted=false;
|
||||
static struct buffer _i2s_buffer_structure;
|
||||
static struct buffer *i2sbuffer=&_i2s_buffer_structure;
|
||||
static i2s_config_t i2s_config;
|
||||
static int bytes_per_frame;
|
||||
static thread_type thread;
|
||||
static pthread_t stats_thread;
|
||||
|
||||
DECLARE_ALL_MIN_MAX;
|
||||
|
||||
static int _i2s_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR,
|
||||
s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr);
|
||||
static void *output_thread_i2s();
|
||||
static void *output_thread_i2s_stats();
|
||||
|
||||
/****************************************************************************************
|
||||
* Initialize the DAC output
|
||||
*/
|
||||
void output_init_i2s(log_level level, char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle) {
|
||||
loglevel = level;
|
||||
|
||||
#ifdef CONFIG_I2S_BITS_PER_CHANNEL
|
||||
switch (CONFIG_I2S_BITS_PER_CHANNEL) {
|
||||
case 24:
|
||||
output.format = S24_BE;
|
||||
bytes_per_frame = 2*3;
|
||||
break;
|
||||
case 16:
|
||||
output.format = S16_BE;
|
||||
bytes_per_frame = 2*2;
|
||||
break;
|
||||
case 8:
|
||||
output.format = S8_BE;
|
||||
bytes_per_frame = 2*4;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Unsupported bit depth %d",CONFIG_I2S_BITS_PER_CHANNEL);
|
||||
break;
|
||||
}
|
||||
#else
|
||||
output.format = S16_LE;
|
||||
bytes_per_frame = 2*2;
|
||||
#endif
|
||||
|
||||
output.write_cb = &_i2s_write_frames;
|
||||
|
||||
running=true;
|
||||
|
||||
// todo: move this to a hardware abstraction layer
|
||||
//hal_dac_init(device);
|
||||
|
||||
i2s_config.mode = I2S_MODE_MASTER | I2S_MODE_TX; // Only TX
|
||||
i2s_config.sample_rate = output.current_sample_rate;
|
||||
i2s_config.bits_per_sample = bytes_per_frame * 8 / 2;
|
||||
i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT; //2-channels
|
||||
i2s_config.communication_format = I2S_COMM_FORMAT_I2S| I2S_COMM_FORMAT_I2S_MSB;
|
||||
// todo: tune this parameter. Expressed in number of samples. Byte size depends on bit depth.
|
||||
i2s_config.dma_buf_count = 10;
|
||||
// From the I2S driver source, the DMA buffer size is 4092 bytes.
|
||||
// so buf_len * 2 channels * 2 bytes/sample should be < 4092 or else it will be resized.
|
||||
i2s_config.dma_buf_len = FRAME_BLOCK/2;
|
||||
i2s_config.use_apll = false;
|
||||
i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; //Interrupt level 1
|
||||
|
||||
i2s_pin_config_t pin_config = { .bck_io_num = CONFIG_I2S_BCK_IO, .ws_io_num =
|
||||
CONFIG_I2S_WS_IO, .data_out_num = CONFIG_I2S_DO_IO, .data_in_num = -1 //Not used
|
||||
};
|
||||
LOG_INFO("Initializing I2S with rate: %d, bits per sample: %d, buffer len: %d, number of buffers: %d ",
|
||||
i2s_config.sample_rate, i2s_config.bits_per_sample, i2s_config.dma_buf_len, i2s_config.dma_buf_count);
|
||||
|
||||
i2s_driver_install(CONFIG_I2S_NUM, &i2s_config, 0, NULL);
|
||||
i2s_set_pin(CONFIG_I2S_NUM, &pin_config);
|
||||
i2s_set_clk(CONFIG_I2S_NUM, output.current_sample_rate, i2s_config.bits_per_sample, 2);
|
||||
isI2SStarted=false;
|
||||
i2s_stop(CONFIG_I2S_NUM);
|
||||
|
||||
i2s_buffer_size = 5*FRAME_BLOCK*bytes_per_frame;
|
||||
LOG_INFO("Allocating local DAC transfer buffer of %u bytes.",i2s_buffer_size);
|
||||
|
||||
buf_init(i2sbuffer,i2s_buffer_size);
|
||||
if (!i2sbuffer->buf) {
|
||||
LOG_ERROR("unable to malloc i2s buffer");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + OUTPUT_THREAD_STACK_SIZE);
|
||||
pthread_create(&thread, &attr, output_thread_i2s, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
#if HAS_PTHREAD_SETNAME_NP
|
||||
pthread_setname_np(thread, "output_i2s");
|
||||
#endif
|
||||
|
||||
// leave stack size to default
|
||||
pthread_create(&stats_thread, NULL, output_thread_i2s_stats, NULL);
|
||||
#if HAS_PTHREAD_SETNAME_NP
|
||||
pthread_setname_np(stats_thread, "output_i2s_sts");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/****************************************************************************************
|
||||
* Terminate DAC output
|
||||
*/
|
||||
void output_close_i2s(void) {
|
||||
i2s_driver_uninstall(CONFIG_I2S_NUM);
|
||||
buf_destroy(i2sbuffer);
|
||||
}
|
||||
|
||||
/****************************************************************************************
|
||||
* Write frames to the output buffer
|
||||
*/
|
||||
static int _i2s_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR,
|
||||
s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr) {
|
||||
|
||||
size_t bytes = out_frames * bytes_per_frame;
|
||||
assert(bytes > 0);
|
||||
|
||||
if (!silence) {
|
||||
|
||||
if (output.fade == FADE_ACTIVE && output.fade_dir == FADE_CROSS && *cross_ptr) {
|
||||
_apply_cross(outputbuf, out_frames, cross_gain_in, cross_gain_out, cross_ptr);
|
||||
}
|
||||
|
||||
#if !REPACK
|
||||
if (gainL != FIXED_ONE || gainR!= FIXED_ONE) {
|
||||
_apply_gain(outputbuf, out_frames, gainL, gainR);
|
||||
}
|
||||
|
||||
memcpy(i2sbuffer->writep, outputbuf->readp, bytes);
|
||||
#else
|
||||
obuf = outputbuf->readp;
|
||||
#endif
|
||||
|
||||
} else {
|
||||
|
||||
#if !REPACK
|
||||
memcpy(i2sbuffer->writep, silencebuf, bytes);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#if REPACK
|
||||
_scale_and_pack_frames(optr, (s32_t *)(void *)obuf, out_frames, gainL, gainR, output.format);
|
||||
#endif
|
||||
_buf_inc_writep(i2sbuffer, bytes);
|
||||
|
||||
return bytes / bytes_per_frame;
|
||||
}
|
||||
|
||||
|
||||
/****************************************************************************************
|
||||
* Main output thread
|
||||
*/
|
||||
static void *output_thread_i2s() {
|
||||
frames_t frames=0;
|
||||
frames_t available_frames_space=0;
|
||||
size_t bytes_to_send_i2s=0, // Contiguous buffer which can be addressed
|
||||
i2s_bytes_written = 0,
|
||||
i2s_total_bytes_written=0; //actual size that the i2s port was able to write
|
||||
uint32_t timer_start=0;
|
||||
static int count = 0;
|
||||
output_state state;
|
||||
|
||||
while (running) {
|
||||
i2s_bytes_written=0;
|
||||
frames=0;
|
||||
available_frames_space=0;
|
||||
bytes_to_send_i2s=0, // Contiguous buffer which can be addressed
|
||||
i2s_bytes_written = 0; //actual size that the i2s port was able to write
|
||||
TIME_MEASUREMENT_START(timer_start);
|
||||
LOCK;
|
||||
state =output.state;
|
||||
if (output.state == OUTPUT_OFF) {
|
||||
UNLOCK;
|
||||
LOG_INFO("Output state is off.");
|
||||
LOG_SDEBUG("Current buffer free: %10d, cont read: %10d",_buf_space(i2sbuffer),_buf_cont_read(i2sbuffer));
|
||||
if(isI2SStarted) {
|
||||
isI2SStarted=false;
|
||||
i2s_stop(CONFIG_I2S_NUM);
|
||||
}
|
||||
usleep(200000);
|
||||
continue;
|
||||
}
|
||||
LOG_SDEBUG("Current buffer free: %10d, cont read: %10d",_buf_space(i2sbuffer),_buf_cont_read(i2sbuffer));
|
||||
|
||||
output.device_frames =0;
|
||||
output.updated = gettime_ms();
|
||||
output.frames_played_dmp = output.frames_played;
|
||||
|
||||
do{
|
||||
// fill our buffer
|
||||
available_frames_space = min(_buf_space(i2sbuffer), _buf_cont_write(i2sbuffer)) / bytes_per_frame;
|
||||
if(available_frames_space)
|
||||
{
|
||||
frames = _output_frames( available_frames_space ); // Keep the transfer buffer full
|
||||
SET_MIN_MAX( available_frames_space,req);
|
||||
SET_MIN_MAX(frames,rec);
|
||||
}
|
||||
}while(available_frames_space>0 && frames>0);
|
||||
|
||||
SET_MIN_MAX_SIZED(_buf_used(outputbuf),o,outputbuf->size);
|
||||
SET_MIN_MAX_SIZED(_buf_used(streambuf),s,streambuf->size);
|
||||
UNLOCK;
|
||||
LOG_SDEBUG("Current buffer free: %10d, cont read: %10d",_buf_space(i2sbuffer),_buf_cont_read(i2sbuffer));
|
||||
|
||||
SET_MIN_MAX( TIME_MEASUREMENT_GET(timer_start),buffering);
|
||||
|
||||
SET_MIN_MAX_SIZED(_buf_used(i2sbuffer),loci2sbuf,i2sbuffer->size);
|
||||
bytes_to_send_i2s = _buf_cont_read(i2sbuffer);
|
||||
SET_MIN_MAX(bytes_to_send_i2s,i2savailable);
|
||||
i2s_total_bytes_written=0;
|
||||
|
||||
while (bytes_to_send_i2s>0 )
|
||||
{
|
||||
// now send all the data
|
||||
TIME_MEASUREMENT_START(timer_start);
|
||||
if(!isI2SStarted)
|
||||
{
|
||||
isI2SStarted=true;
|
||||
LOG_INFO("Restarting I2S.");
|
||||
i2s_start(CONFIG_I2S_NUM);
|
||||
if( i2s_config.sample_rate != output.current_sample_rate)
|
||||
{
|
||||
i2s_config.sample_rate = output.current_sample_rate;
|
||||
i2s_set_sample_rates(CONFIG_I2S_NUM, i2s_config.sample_rate);
|
||||
}
|
||||
}
|
||||
count++;
|
||||
LOG_SDEBUG("Outputting to I2S");
|
||||
LOG_SDEBUG("Current buffer free: %10d, cont read: %10d",_buf_space(i2sbuffer),_buf_cont_read(i2sbuffer));
|
||||
|
||||
i2s_write(CONFIG_I2S_NUM, i2sbuffer->readp,bytes_to_send_i2s, &i2s_bytes_written, portMAX_DELAY);
|
||||
_buf_inc_readp(i2sbuffer,i2s_bytes_written);
|
||||
if(i2s_bytes_written!=bytes_to_send_i2s)
|
||||
{
|
||||
LOG_WARN("I2S DMA Overflow! available bytes: %d, I2S wrote %d bytes", bytes_to_send_i2s,i2s_bytes_written);
|
||||
|
||||
}
|
||||
LOG_SDEBUG("DONE Outputting to I2S. Wrote: %d bytes out of %d", i2s_bytes_written,bytes_to_send_i2s);
|
||||
LOG_SDEBUG("Current buffer free: %10d, cont read: %10d",_buf_space(i2sbuffer),_buf_cont_read(i2sbuffer));
|
||||
i2s_total_bytes_written+=i2s_bytes_written;
|
||||
SET_MIN_MAX( TIME_MEASUREMENT_GET(timer_start),i2s_time);
|
||||
if(bytes_to_send_i2s>0) {
|
||||
SET_MIN_MAX(bytes_to_send_i2s-i2s_bytes_written,over);
|
||||
}
|
||||
bytes_to_send_i2s = _buf_cont_read(i2sbuffer);
|
||||
SET_MIN_MAX(bytes_to_send_i2s,i2savailable);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/****************************************************************************************
|
||||
* Stats output thread
|
||||
*/
|
||||
static void *output_thread_i2s_stats() {
|
||||
while (running) {
|
||||
LOCK;
|
||||
output_state state = output.state;
|
||||
UNLOCK;
|
||||
if(state>OUTPUT_STOPPED){
|
||||
LOG_INFO( "Output State: %d, current sample rate: %d, bytes per frame: %d",state,output.current_sample_rate, bytes_per_frame);
|
||||
LOG_INFO( LINE_MIN_MAX_FORMAT_HEAD1);
|
||||
LOG_INFO( LINE_MIN_MAX_FORMAT_HEAD2);
|
||||
LOG_INFO( LINE_MIN_MAX_FORMAT_HEAD3);
|
||||
LOG_INFO( LINE_MIN_MAX_FORMAT_HEAD4);
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT_STREAM, LINE_MIN_MAX_STREAM("stream",s));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("output",o));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("dac buf used",loci2sbuf));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT_FOOTER);
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("i2swrite",i2savailable));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("requested",req));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("received",rec));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT,LINE_MIN_MAX("overflow",over));
|
||||
LOG_INFO(LINE_MIN_MAX_FORMAT_FOOTER);
|
||||
LOG_INFO("");
|
||||
LOG_INFO(" ----------+----------+-----------+-----------+ ");
|
||||
LOG_INFO(" max (us) | min (us) | avg(us) | count | ");
|
||||
LOG_INFO(" ----------+----------+-----------+-----------+ ");
|
||||
LOG_INFO(LINE_MIN_MAX_DURATION_FORMAT,LINE_MIN_MAX_DURATION("Buffering(us)",buffering));
|
||||
LOG_INFO(LINE_MIN_MAX_DURATION_FORMAT,LINE_MIN_MAX_DURATION("i2s tfr(us)",i2s_time));
|
||||
LOG_INFO(" ----------+----------+-----------+-----------+");
|
||||
RESET_ALL_MIN_MAX;
|
||||
}
|
||||
usleep(STATS_PERIOD_MS *1000);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
372
components/squeezelite/output_pack.c
Normal file
372
components/squeezelite/output_pack.c
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Scale and pack functions
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#if BYTES_PER_FRAM == 4
|
||||
#define MAX_VAL16 0x7fffffffLL
|
||||
#define MAX_SCALESAMPLE 0x7fffffffffffLL
|
||||
#define MIN_SCALESAMPLE -MAX_SCALESAMPLE
|
||||
#else
|
||||
#define MAX_SCALESAMPLE 0x7fffffffffffLL
|
||||
#define MIN_SCALESAMPLE -MAX_SCALESAMPLE
|
||||
#endif
|
||||
|
||||
// inlining these on windows prevents them being linkable...
|
||||
#if !WIN
|
||||
inline
|
||||
#endif
|
||||
s32_t gain(s32_t gain, s32_t sample) {
|
||||
s64_t res = (s64_t)gain * (s64_t)sample;
|
||||
if (res > MAX_SCALESAMPLE) res = MAX_SCALESAMPLE;
|
||||
if (res < MIN_SCALESAMPLE) res = MIN_SCALESAMPLE;
|
||||
return (s32_t) (res >> 16);
|
||||
}
|
||||
#if !WIN
|
||||
inline
|
||||
#endif
|
||||
s32_t to_gain(float f) {
|
||||
return (s32_t)(f * 65536.0F);
|
||||
}
|
||||
|
||||
void _scale_and_pack_frames(void *outputptr, s32_t *inputptr, frames_t cnt, s32_t gainL, s32_t gainR, output_format format) {
|
||||
switch(format) {
|
||||
#if DSD
|
||||
case U32_LE:
|
||||
{
|
||||
#if SL_LITTLE_ENDIAN
|
||||
memcpy(outputptr, inputptr, cnt * BYTES_PER_FRAME);
|
||||
#else
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 |
|
||||
(lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24;
|
||||
*(optr++) =
|
||||
(rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 |
|
||||
(rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case U32_BE:
|
||||
{
|
||||
#if SL_LITTLE_ENDIAN
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 |
|
||||
(lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24;
|
||||
*(optr++) =
|
||||
(rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 |
|
||||
(rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24;
|
||||
}
|
||||
#else
|
||||
memcpy(outputptr, inputptr, cnt * BYTES_PER_FRAME);
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case U16_LE:
|
||||
{
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
#if SL_LITTLE_ENDIAN
|
||||
while (cnt--) {
|
||||
*(optr++) = (*(inputptr) >> 16 & 0x0000ffff) | (*(inputptr + 1) & 0xffff0000);
|
||||
inputptr += 2;
|
||||
}
|
||||
#else
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0x00ff0000) << 8 | (lsample & 0xff000000) >> 8 |
|
||||
(rsample & 0x00ff0000) >> 8 | (rsample & 0xff000000) >> 24;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case U16_BE:
|
||||
{
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
#if SL_LITTLE_ENDIAN
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 |
|
||||
(rsample & 0xff000000) >> 8 | (rsample & 0x00ff0000) << 8;
|
||||
}
|
||||
#else
|
||||
while (cnt--) {
|
||||
*(optr++) = (*(inputptr) & 0xffff0000) | (*(inputptr + 1) >> 16 & 0x0000ffff);
|
||||
inputptr += 2;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case U8:
|
||||
{
|
||||
u16_t *optr = (u16_t *)(void *)outputptr;
|
||||
#if SL_LITTLE_ENDIAN
|
||||
while (cnt--) {
|
||||
*(optr++) = (u16_t)((*(inputptr) >> 24 & 0x000000ff) | (*(inputptr + 1) >> 16 & 0x0000ff00));
|
||||
inputptr += 2;
|
||||
}
|
||||
#else
|
||||
while (cnt--) {
|
||||
*(optr++) = (u16_t)((*(inputptr) >> 16 & 0x0000ff00) | (*(inputptr + 1) >> 24 & 0x000000ff));
|
||||
inputptr += 2;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case S16_LE:
|
||||
{
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
#if SL_LITTLE_ENDIAN
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
while (cnt--) {
|
||||
*(optr++) = (*(inputptr) >> 16 & 0x0000ffff) | (*(inputptr + 1) & 0xffff0000);
|
||||
inputptr += 2;
|
||||
}
|
||||
} else {
|
||||
while (cnt--) {
|
||||
*(optr++) = (gain(gainL, *(inputptr)) >> 16 & 0x0000ffff) | (gain(gainR, *(inputptr+1)) & 0xffff0000);
|
||||
inputptr += 2;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0x00ff0000) << 8 | (lsample & 0xff000000) >> 8 |
|
||||
(rsample & 0x00ff0000) >> 8 | (rsample & 0xff000000) >> 24;
|
||||
}
|
||||
} else {
|
||||
while (cnt--) {
|
||||
s32_t lsample = gain(gainL, *(inputptr++));
|
||||
s32_t rsample = gain(gainR, *(inputptr++));
|
||||
*(optr++) =
|
||||
(lsample & 0x00ff0000) << 8 | (lsample & 0xff000000) >> 8 |
|
||||
(rsample & 0x00ff0000) >> 8 | (rsample & 0xff000000) >> 24;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case S24_LE:
|
||||
{
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
#if SL_LITTLE_ENDIAN
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
while (cnt--) {
|
||||
*(optr++) = *(inputptr++) >> 8;
|
||||
*(optr++) = *(inputptr++) >> 8;
|
||||
}
|
||||
} else {
|
||||
while (cnt--) {
|
||||
*(optr++) = gain(gainL, *(inputptr++)) >> 8;
|
||||
*(optr++) = gain(gainR, *(inputptr++)) >> 8;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 16 | (lsample & 0x00ff0000) | (lsample & 0x0000ff00 << 16);
|
||||
*(optr++) =
|
||||
(rsample & 0xff000000) >> 16 | (rsample & 0x00ff0000) | (rsample & 0x0000ff00 << 16);
|
||||
}
|
||||
} else {
|
||||
while (cnt--) {
|
||||
s32_t lsample = gain(gainL, *(inputptr++));
|
||||
s32_t rsample = gain(gainR, *(inputptr++));
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 16 | (lsample & 0x00ff0000) | (lsample & 0x0000ff00 << 16);
|
||||
*(optr++) =
|
||||
(rsample & 0xff000000) >> 16 | (rsample & 0x00ff0000) | (rsample & 0x0000ff00 << 16);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case S24_3LE:
|
||||
{
|
||||
u8_t *optr = (u8_t *)(void *)outputptr;
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
while (cnt) {
|
||||
// attempt to do 32 bit memory accesses - move 2 frames at once: 16 bytes -> 12 bytes
|
||||
// falls through to exception case when not aligned or if less than 2 frames to move
|
||||
if (((uintptr_t)optr & 0x3) == 0 && cnt >= 2) {
|
||||
u32_t *o_ptr = (u32_t *)(void *)optr;
|
||||
while (cnt >= 2) {
|
||||
s32_t l1 = *(inputptr++); s32_t r1 = *(inputptr++);
|
||||
s32_t l2 = *(inputptr++); s32_t r2 = *(inputptr++);
|
||||
#if SL_LITTLE_ENDIAN
|
||||
*(o_ptr++) = (l1 & 0xffffff00) >> 8 | (r1 & 0x0000ff00) << 16;
|
||||
*(o_ptr++) = (r1 & 0xffff0000) >> 16 | (l2 & 0x00ffff00) << 8;
|
||||
*(o_ptr++) = (l2 & 0xff000000) >> 24 | (r2 & 0xffffff00);
|
||||
#else
|
||||
*(o_ptr++) = (l1 & 0x0000ff00) << 16 | (l1 & 0x00ff0000) | (l1 & 0xff000000) >> 16 |
|
||||
(r1 & 0x0000ff00) >> 8;
|
||||
*(o_ptr++) = (r1 & 0x00ff0000) << 8 | (r1 & 0xff000000) >> 8 | (l2 & 0x0000ff00) |
|
||||
(l2 & 0x00ff0000) >> 16;
|
||||
*(o_ptr++) = (l2 & 0xff000000) | (r2 & 0x0000ff00) << 8 | (r2 & 0x00ff0000) >> 8 |
|
||||
(r2 & 0xff000000) >> 24;
|
||||
#endif
|
||||
optr += 12;
|
||||
cnt -= 2;
|
||||
}
|
||||
} else {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) = (lsample & 0x0000ff00) >> 8;
|
||||
*(optr++) = (lsample & 0x00ff0000) >> 16;
|
||||
*(optr++) = (lsample & 0xff000000) >> 24;
|
||||
*(optr++) = (rsample & 0x0000ff00) >> 8;
|
||||
*(optr++) = (rsample & 0x00ff0000) >> 16;
|
||||
*(optr++) = (rsample & 0xff000000) >> 24;
|
||||
cnt--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (cnt) {
|
||||
// attempt to do 32 bit memory accesses - move 2 frames at once: 16 bytes -> 12 bytes
|
||||
// falls through to exception case when not aligned or if less than 2 frames to move
|
||||
if (((uintptr_t)optr & 0x3) == 0 && cnt >= 2) {
|
||||
u32_t *o_ptr = (u32_t *)(void *)optr;
|
||||
while (cnt >= 2) {
|
||||
s32_t l1 = gain(gainL, *(inputptr++)); s32_t r1 = gain(gainR, *(inputptr++));
|
||||
s32_t l2 = gain(gainL, *(inputptr++)); s32_t r2 = gain(gainR, *(inputptr++));
|
||||
#if SL_LITTLE_ENDIAN
|
||||
*(o_ptr++) = (l1 & 0xffffff00) >> 8 | (r1 & 0x0000ff00) << 16;
|
||||
*(o_ptr++) = (r1 & 0xffff0000) >> 16 | (l2 & 0x00ffff00) << 8;
|
||||
*(o_ptr++) = (l2 & 0xff000000) >> 24 | (r2 & 0xffffff00);
|
||||
#else
|
||||
*(o_ptr++) = (l1 & 0x0000ff00) << 16 | (l1 & 0x00ff0000) | (l1 & 0xff000000) >> 16 |
|
||||
(r1 & 0x0000ff00) >> 8;
|
||||
*(o_ptr++) = (r1 & 0x00ff0000) << 8 | (r1 & 0xff000000) >> 8 | (l2 & 0x0000ff00) |
|
||||
(l2 & 0x00ff0000) >> 16;
|
||||
*(o_ptr++) = (l2 & 0xff000000) | (r2 & 0x0000ff00) << 8 | (r2 & 0x00ff0000) >> 8 |
|
||||
(r2 & 0xff000000) >> 24;
|
||||
#endif
|
||||
optr += 12;
|
||||
cnt -= 2;
|
||||
}
|
||||
} else {
|
||||
s32_t lsample = gain(gainL, *(inputptr++));
|
||||
s32_t rsample = gain(gainR, *(inputptr++));
|
||||
*(optr++) = (lsample & 0x0000ff00) >> 8;
|
||||
*(optr++) = (lsample & 0x00ff0000) >> 16;
|
||||
*(optr++) = (lsample & 0xff000000) >> 24;
|
||||
*(optr++) = (rsample & 0x0000ff00) >> 8;
|
||||
*(optr++) = (rsample & 0x00ff0000) >> 16;
|
||||
*(optr++) = (rsample & 0xff000000) >> 24;
|
||||
cnt--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case S32_LE:
|
||||
{
|
||||
u32_t *optr = (u32_t *)(void *)outputptr;
|
||||
#if SL_LITTLE_ENDIAN
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
memcpy(outputptr, inputptr, cnt * BYTES_PER_FRAME);
|
||||
} else {
|
||||
while (cnt--) {
|
||||
*(optr++) = gain(gainL, *(inputptr++));
|
||||
*(optr++) = gain(gainR, *(inputptr++));
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (gainL == FIXED_ONE && gainR == FIXED_ONE) {
|
||||
while (cnt--) {
|
||||
s32_t lsample = *(inputptr++);
|
||||
s32_t rsample = *(inputptr++);
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 |
|
||||
(lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24;
|
||||
*(optr++) =
|
||||
(rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 |
|
||||
(rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24;
|
||||
}
|
||||
} else {
|
||||
while (cnt--) {
|
||||
s32_t lsample = gain(gainL, *(inputptr++));
|
||||
s32_t rsample = gain(gainR, *(inputptr++));
|
||||
*(optr++) =
|
||||
(lsample & 0xff000000) >> 24 | (lsample & 0x00ff0000) >> 8 |
|
||||
(lsample & 0x0000ff00) << 8 | (lsample & 0x000000ff) << 24;
|
||||
*(optr++) =
|
||||
(rsample & 0xff000000) >> 24 | (rsample & 0x00ff0000) >> 8 |
|
||||
(rsample & 0x0000ff00) << 8 | (rsample & 0x000000ff) << 24;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if !WIN
|
||||
inline
|
||||
#endif
|
||||
void _apply_cross(struct buffer *outputbuf, frames_t out_frames, s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr) {
|
||||
ISAMPLE_T *ptr = (ISAMPLE_T *)(void *)outputbuf->readp;
|
||||
frames_t count = out_frames * 2;
|
||||
while (count--) {
|
||||
if (*cross_ptr > (ISAMPLE_T *)outputbuf->wrap) {
|
||||
*cross_ptr -= outputbuf->size / BYTES_PER_FRAME * 2;
|
||||
}
|
||||
*ptr = gain(cross_gain_out, *ptr) + gain(cross_gain_in, **cross_ptr);
|
||||
ptr++; (*cross_ptr)++;
|
||||
}
|
||||
}
|
||||
|
||||
#if !WIN
|
||||
inline
|
||||
#endif
|
||||
void _apply_gain(struct buffer *outputbuf, frames_t count, s32_t gainL, s32_t gainR) {
|
||||
ISAMPLE_T *ptrL = (ISAMPLE_T *)(void *)outputbuf->readp;
|
||||
ISAMPLE_T *ptrR = (ISAMPLE_T *)(void *)outputbuf->readp + 1;
|
||||
while (count--) {
|
||||
*ptrL = gain(gainL, *ptrL);
|
||||
*ptrR = gain(gainR, *ptrR);
|
||||
ptrL += 2;
|
||||
ptrR += 2;
|
||||
}
|
||||
}
|
||||
|
||||
488
components/squeezelite/pcm.c
Normal file
488
components/squeezelite/pcm.c
Normal file
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
#define SHIFT 16
|
||||
#define OPTR_T u16_t
|
||||
#else
|
||||
#define OPTR_T u32_t
|
||||
#define SHIFT 0
|
||||
#endif
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
bool pcm_check_header = false;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_O_not_direct if (!decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_not_direct if (!decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_O_not_direct
|
||||
#define UNLOCK_O_not_direct
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
#define MAX_DECODE_FRAMES 4096
|
||||
|
||||
static u32_t sample_rates[] = {
|
||||
11025, 22050, 32000, 44100, 48000, 8000, 12000, 16000, 24000, 96000, 88200, 176400, 192000, 352800, 384000, 705600, 768000
|
||||
};
|
||||
|
||||
static u32_t sample_rate;
|
||||
static u32_t sample_size;
|
||||
static u32_t channels;
|
||||
static bool bigendian;
|
||||
static bool limit;
|
||||
static u32_t audio_left;
|
||||
static u32_t bytes_per_frame;
|
||||
|
||||
typedef enum { UNKNOWN = 0, WAVE, AIFF } header_format;
|
||||
|
||||
static void _check_header(void) {
|
||||
u8_t *ptr = streambuf->readp;
|
||||
unsigned bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
header_format format = UNKNOWN;
|
||||
|
||||
// simple parsing of wav and aiff headers and get to samples
|
||||
|
||||
if (bytes > 12) {
|
||||
if (!memcmp(ptr, "RIFF", 4) && !memcmp(ptr+8, "WAVE", 4)) {
|
||||
LOG_INFO("WAVE");
|
||||
format = WAVE;
|
||||
} else if (!memcmp(ptr, "FORM", 4) && (!memcmp(ptr+8, "AIFF", 4) || !memcmp(ptr+8, "AIFC", 4))) {
|
||||
LOG_INFO("AIFF");
|
||||
format = AIFF;
|
||||
}
|
||||
}
|
||||
|
||||
if (format != UNKNOWN) {
|
||||
ptr += 12;
|
||||
bytes -= 12;
|
||||
|
||||
while (bytes >= 8) {
|
||||
char id[5];
|
||||
unsigned len;
|
||||
memcpy(id, ptr, 4);
|
||||
id[4] = '\0';
|
||||
|
||||
if (format == WAVE) {
|
||||
len = *(ptr+4) | *(ptr+5) << 8 | *(ptr+6) << 16| *(ptr+7) << 24;
|
||||
} else {
|
||||
len = *(ptr+4) << 24 | *(ptr+5) << 16 | *(ptr+6) << 8 | *(ptr+7);
|
||||
}
|
||||
|
||||
LOG_INFO("header: %s len: %d", id, len);
|
||||
|
||||
if (format == WAVE && !memcmp(ptr, "data", 4)) {
|
||||
ptr += 8;
|
||||
_buf_inc_readp(streambuf, ptr - streambuf->readp);
|
||||
audio_left = len;
|
||||
|
||||
if ((audio_left == 0xFFFFFFFF) || (audio_left == 0x7FFFEFFC)) {
|
||||
LOG_INFO("wav audio size unknown: %u", audio_left);
|
||||
limit = false;
|
||||
} else {
|
||||
LOG_INFO("wav audio size: %u", audio_left);
|
||||
limit = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == AIFF && !memcmp(ptr, "SSND", 4) && bytes >= 16) {
|
||||
unsigned offset = *(ptr+8) << 24 | *(ptr+9) << 16 | *(ptr+10) << 8 | *(ptr+11);
|
||||
// following 4 bytes is blocksize - ignored
|
||||
ptr += 8 + 8;
|
||||
_buf_inc_readp(streambuf, ptr + offset - streambuf->readp);
|
||||
|
||||
// Reading from an upsampled stream, length could be wrong.
|
||||
// Only use length in header for files.
|
||||
if (stream.state == STREAMING_FILE) {
|
||||
audio_left = len - 8 - offset;
|
||||
LOG_INFO("aif audio size: %u", audio_left);
|
||||
limit = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == WAVE && !memcmp(ptr, "fmt ", 4) && bytes >= 24) {
|
||||
// override the server parsed values with our own
|
||||
channels = *(ptr+10) | *(ptr+11) << 8;
|
||||
sample_rate = *(ptr+12) | *(ptr+13) << 8 | *(ptr+14) << 16 | *(ptr+15) << 24;
|
||||
sample_size = (*(ptr+22) | *(ptr+23) << 8) / 8;
|
||||
bigendian = 0;
|
||||
LOG_INFO("pcm size: %u rate: %u chan: %u bigendian: %u", sample_size, sample_rate, channels, bigendian);
|
||||
}
|
||||
|
||||
if (format == AIFF && !memcmp(ptr, "COMM", 4) && bytes >= 26) {
|
||||
int exponent;
|
||||
// override the server parsed values with our own
|
||||
channels = *(ptr+8) << 8 | *(ptr+9);
|
||||
sample_size = (*(ptr+14) << 8 | *(ptr+15)) / 8;
|
||||
bigendian = 1;
|
||||
// sample rate is encoded as IEEE 80 bit extended format
|
||||
// make some assumptions to simplify processing - only use first 32 bits of mantissa
|
||||
exponent = ((*(ptr+16) & 0x7f) << 8 | *(ptr+17)) - 16383 - 31;
|
||||
sample_rate = *(ptr+18) << 24 | *(ptr+19) << 16 | *(ptr+20) << 8 | *(ptr+21);
|
||||
while (exponent < 0) { sample_rate >>= 1; ++exponent; }
|
||||
while (exponent > 0) { sample_rate <<= 1; --exponent; }
|
||||
LOG_INFO("pcm size: %u rate: %u chan: %u bigendian: %u", sample_size, sample_rate, channels, bigendian);
|
||||
}
|
||||
|
||||
if (bytes >= len + 8) {
|
||||
ptr += len + 8;
|
||||
bytes -= (len + 8);
|
||||
} else {
|
||||
LOG_WARN("run out of data");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_WARN("unknown format - can't parse header");
|
||||
}
|
||||
}
|
||||
|
||||
static decode_state pcm_decode(void) {
|
||||
unsigned bytes, in, out;
|
||||
frames_t frames, count;
|
||||
OPTR_T *optr;
|
||||
u8_t *iptr;
|
||||
u8_t tmp[3*8];
|
||||
|
||||
LOCK_S;
|
||||
|
||||
if ( decode.new_stream && ( ( stream.state == STREAMING_FILE ) || pcm_check_header ) ) {
|
||||
_check_header();
|
||||
}
|
||||
|
||||
LOCK_O_direct;
|
||||
|
||||
bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
|
||||
IF_DIRECT(
|
||||
out = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
|
||||
);
|
||||
IF_PROCESS(
|
||||
out = process.max_in_frames;
|
||||
);
|
||||
|
||||
if ((stream.state <= DISCONNECT && bytes == 0) || (limit && audio_left == 0)) {
|
||||
UNLOCK_O_direct;
|
||||
UNLOCK_S;
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
if (decode.new_stream) {
|
||||
LOG_INFO("setting track_start");
|
||||
LOCK_O_not_direct;
|
||||
output.track_start = outputbuf->writep;
|
||||
decode.new_stream = false;
|
||||
#if DSD
|
||||
if (sample_size == 3 &&
|
||||
is_stream_dop(((u8_t *)streambuf->readp) + (bigendian?0:2),
|
||||
((u8_t *)streambuf->readp) + (bigendian?0:2) + sample_size,
|
||||
sample_size * channels, bytes / (sample_size * channels))) {
|
||||
LOG_INFO("file contains DOP");
|
||||
if (output.dsdfmt == DOP_S24_LE || output.dsdfmt == DOP_S24_3LE)
|
||||
output.next_fmt = output.dsdfmt;
|
||||
else
|
||||
output.next_fmt = DOP;
|
||||
output.next_sample_rate = sample_rate;
|
||||
output.fade = FADE_INACTIVE;
|
||||
} else {
|
||||
output.next_sample_rate = decode_newstream(sample_rate, output.supported_rates);
|
||||
output.next_fmt = PCM;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
}
|
||||
#else
|
||||
output.next_sample_rate = decode_newstream(sample_rate, output.supported_rates);
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
#endif
|
||||
UNLOCK_O_not_direct;
|
||||
IF_PROCESS(
|
||||
out = process.max_in_frames;
|
||||
);
|
||||
bytes_per_frame = channels * sample_size;
|
||||
}
|
||||
|
||||
IF_DIRECT(
|
||||
optr = (OPTR_T *)outputbuf->writep;
|
||||
);
|
||||
IF_PROCESS(
|
||||
optr = (OPTR_T *)process.inbuf;
|
||||
);
|
||||
iptr = (u8_t *)streambuf->readp;
|
||||
|
||||
in = bytes / bytes_per_frame;
|
||||
|
||||
// handle frame wrapping round end of streambuf
|
||||
// - only need if resizing of streambuf does not avoid this, could occur in localfile case
|
||||
if (in == 0 && bytes > 0 && _buf_used(streambuf) >= bytes_per_frame) {
|
||||
memcpy(tmp, iptr, bytes);
|
||||
memcpy(tmp + bytes, streambuf->buf, bytes_per_frame - bytes);
|
||||
iptr = tmp;
|
||||
in = 1;
|
||||
}
|
||||
|
||||
frames = min(in, out);
|
||||
frames = min(frames, MAX_DECODE_FRAMES);
|
||||
|
||||
if (limit && frames * bytes_per_frame > audio_left) {
|
||||
LOG_INFO("reached end of audio");
|
||||
frames = audio_left / bytes_per_frame;
|
||||
}
|
||||
|
||||
count = frames * channels;
|
||||
|
||||
if (channels == 2) {
|
||||
if (sample_size == 1) {
|
||||
while (count--) {
|
||||
*optr++ = *iptr++ << (24-SHIFT);
|
||||
}
|
||||
} else if (sample_size == 2) {
|
||||
if (bigendian) {
|
||||
#if BYTES_PER_FRAME == 4 && !SL_LITTLE_ENDIAN
|
||||
// while loop below works as is, but memcpy is a win for that 16/16 typical case
|
||||
memcpy(optr, iptr, count * BYTES_PER_FRAME / 2);
|
||||
#else
|
||||
while (count--) {
|
||||
*optr++ = *(iptr) << (24-SHIFT) | *(iptr+1) << (16-SHIFT);
|
||||
iptr += 2;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#if BYTES_PER_FRAME == 4 && SL_LITTLE_ENDIAN
|
||||
// while loop below works as is, but memcpy is a win for that 16/16 typical case
|
||||
memcpy(optr, iptr, count * BYTES_PER_FRAME / 2);
|
||||
#else
|
||||
while (count--) {
|
||||
*optr++ = *(iptr) << (16-SHIFT) | *(iptr+1) << (24-SHIFT);
|
||||
iptr += 2;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (sample_size == 3) {
|
||||
if (bigendian) {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr) << 8 | *(iptr+1);
|
||||
#else
|
||||
*optr++ = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8;
|
||||
#endif
|
||||
iptr += 3;
|
||||
}
|
||||
} else {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr+1) | *(iptr+2) << 8;
|
||||
#else
|
||||
*optr++ = *(iptr) << 8 | *(iptr+1) << 16 | *(iptr+2) << 24;
|
||||
#endif
|
||||
iptr += 3;
|
||||
}
|
||||
}
|
||||
} else if (sample_size == 4) {
|
||||
if (bigendian) {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr) << 8 | *(iptr+1);
|
||||
#else
|
||||
*optr++ = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8 | *(iptr+3);
|
||||
#endif
|
||||
iptr += 4;
|
||||
}
|
||||
} else {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr+2) | *(iptr+3) << 8;
|
||||
#else
|
||||
*optr++ = *(iptr) | *(iptr+1) << 8 | *(iptr+2) << 16 | *(iptr+3) << 24;
|
||||
#endif
|
||||
iptr += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (channels == 1) {
|
||||
if (sample_size == 1) {
|
||||
while (count--) {
|
||||
*optr = *iptr++ << (24-SHIFT);
|
||||
*(optr+1) = *optr;
|
||||
optr += 2;
|
||||
}
|
||||
} else if (sample_size == 2) {
|
||||
if (bigendian) {
|
||||
while (count--) {
|
||||
*optr = *(iptr) << (24-SHIFT) | *(iptr+1) << (16-SHIFT);
|
||||
*(optr+1) = *optr;
|
||||
iptr += 2;
|
||||
optr += 2;
|
||||
}
|
||||
} else {
|
||||
while (count--) {
|
||||
*optr = *(iptr) << (16-SHIFT) | *(iptr+1) << (24-SHIFT);
|
||||
*(optr+1) = *optr;
|
||||
iptr += 2;
|
||||
optr += 2;
|
||||
}
|
||||
}
|
||||
} else if (sample_size == 3) {
|
||||
if (bigendian) {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr) << 8 | *(iptr+1);
|
||||
#else
|
||||
*optr = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8;
|
||||
#endif
|
||||
*(optr+1) = *optr;
|
||||
iptr += 3;
|
||||
optr += 2;
|
||||
}
|
||||
} else {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr+1) | *(iptr+2) << 8;
|
||||
#else
|
||||
*optr = *(iptr) << 8 | *(iptr+1) << 16 | *(iptr+2) << 24;
|
||||
#endif
|
||||
*(optr+1) = *optr;
|
||||
iptr += 3;
|
||||
optr += 2;
|
||||
}
|
||||
}
|
||||
} else if (sample_size == 4) {
|
||||
if (bigendian) {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr) << 8 | *(iptr+1);
|
||||
#else
|
||||
*optr++ = *(iptr) << 24 | *(iptr+1) << 16 | *(iptr+2) << 8 | *(iptr+3);
|
||||
#endif
|
||||
*(optr+1) = *optr;
|
||||
iptr += 4;
|
||||
optr += 2;
|
||||
}
|
||||
} else {
|
||||
while (count--) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
*optr++ = *(iptr+2) | *(iptr+3) << 8;
|
||||
#else
|
||||
*optr++ = *(iptr) | *(iptr+1) << 8 | *(iptr+2) << 16 | *(iptr+3) << 24;
|
||||
#endif
|
||||
*(optr+1) = *optr;
|
||||
iptr += 4;
|
||||
optr += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("unsupported channels");
|
||||
}
|
||||
|
||||
LOG_SDEBUG("decoded %u frames", frames);
|
||||
|
||||
_buf_inc_readp(streambuf, frames * bytes_per_frame);
|
||||
|
||||
if (limit) {
|
||||
audio_left -= frames * bytes_per_frame;
|
||||
}
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames = frames;
|
||||
);
|
||||
|
||||
UNLOCK_O_direct;
|
||||
UNLOCK_S;
|
||||
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
static void pcm_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
|
||||
sample_size = size - '0' + 1;
|
||||
sample_rate = sample_rates[rate - '0'];
|
||||
channels = chan - '0';
|
||||
bigendian = (endianness == '0');
|
||||
limit = false;
|
||||
|
||||
LOG_INFO("pcm size: %u rate: %u chan: %u bigendian: %u", sample_size, sample_rate, channels, bigendian);
|
||||
buf_adjust(streambuf, sample_size * channels);
|
||||
}
|
||||
|
||||
static void pcm_close(void) {
|
||||
buf_adjust(streambuf, 1);
|
||||
}
|
||||
|
||||
struct codec *register_pcm(void) {
|
||||
if ( pcm_check_header )
|
||||
{
|
||||
static struct codec ret = {
|
||||
'p', // id
|
||||
"wav,aif,pcm", // types
|
||||
4096, // min read
|
||||
102400, // min space
|
||||
pcm_open, // open
|
||||
pcm_close, // close
|
||||
pcm_decode, // decode
|
||||
};
|
||||
|
||||
LOG_INFO("using pcm to decode wav,aif,pcm");
|
||||
return &ret;
|
||||
}
|
||||
else
|
||||
{
|
||||
static struct codec ret = {
|
||||
'p', // id
|
||||
"aif,pcm", // types
|
||||
4096, // min read
|
||||
102400, // min space
|
||||
pcm_open, // open
|
||||
pcm_close, // close
|
||||
pcm_decode, // decode
|
||||
};
|
||||
|
||||
LOG_INFO("using pcm to decode aif,pcm");
|
||||
return &ret;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
194
components/squeezelite/process.c
Normal file
194
components/squeezelite/process.c
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// sample processing - only included when building with PROCESS set
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#if PROCESS
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct decodestate decode;
|
||||
struct processstate process;
|
||||
extern struct codec *codec;
|
||||
|
||||
#define LOCK_D mutex_lock(decode.mutex);
|
||||
#define UNLOCK_D mutex_unlock(decode.mutex);
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
|
||||
// macros to map to processing functions - currently only resample.c
|
||||
// this can be made more generic when multiple processing mechanisms get added
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
#define SAMPLES_FUNC resample_samples
|
||||
#define DRAIN_FUNC resample_drain
|
||||
#define NEWSTREAM_FUNC resample_newstream
|
||||
#define FLUSH_FUNC resample_flush
|
||||
#define INIT_FUNC resample_init
|
||||
#endif
|
||||
|
||||
|
||||
// transfer all processed frames to the output buf
|
||||
static void _write_samples(void) {
|
||||
frames_t frames = process.out_frames;
|
||||
ISAMPLE_T *iptr = (ISAMPLE_T *) process.outbuf;
|
||||
unsigned cnt = 10;
|
||||
|
||||
LOCK_O;
|
||||
|
||||
while (frames > 0) {
|
||||
|
||||
frames_t f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
|
||||
ISAMPLE_T *optr = (ISAMPLE_T*) outputbuf->writep;
|
||||
|
||||
if (f > 0) {
|
||||
|
||||
f = min(f, frames);
|
||||
|
||||
memcpy(optr, iptr, f * BYTES_PER_FRAME);
|
||||
|
||||
frames -= f;
|
||||
|
||||
_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
|
||||
iptr += f * BYTES_PER_FRAME / sizeof(*iptr);
|
||||
|
||||
} else if (cnt--) {
|
||||
|
||||
// there should normally be space in the output buffer, but may need to wait during drain phase
|
||||
UNLOCK_O;
|
||||
usleep(10000);
|
||||
LOCK_O;
|
||||
|
||||
} else {
|
||||
|
||||
// bail out if no space found after 100ms to avoid locking
|
||||
LOG_ERROR("unable to get space in output buffer");
|
||||
UNLOCK_O;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UNLOCK_O;
|
||||
}
|
||||
|
||||
// process samples - called with decode mutex set
|
||||
void process_samples(void) {
|
||||
|
||||
SAMPLES_FUNC(&process);
|
||||
|
||||
_write_samples();
|
||||
|
||||
process.in_frames = 0;
|
||||
}
|
||||
|
||||
// drain at end of track - called with decode mutex set
|
||||
void process_drain(void) {
|
||||
bool done;
|
||||
|
||||
do {
|
||||
|
||||
done = DRAIN_FUNC(&process);
|
||||
|
||||
_write_samples();
|
||||
|
||||
} while (!done);
|
||||
|
||||
LOG_DEBUG("processing track complete - frames in: %lu out: %lu", process.total_in, process.total_out);
|
||||
}
|
||||
|
||||
// new stream - called with decode mutex set
|
||||
unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]) {
|
||||
|
||||
bool active = NEWSTREAM_FUNC(&process, raw_sample_rate, supported_rates);
|
||||
|
||||
LOG_INFO("processing: %s", active ? "active" : "inactive");
|
||||
|
||||
*direct = !active;
|
||||
|
||||
if (active) {
|
||||
|
||||
unsigned max_in_frames, max_out_frames;
|
||||
|
||||
process.in_frames = process.out_frames = 0;
|
||||
process.total_in = process.total_out = 0;
|
||||
|
||||
max_in_frames = codec->min_space / BYTES_PER_FRAME ;
|
||||
|
||||
// increase size of output buffer by 10% as output rate is not an exact multiple of input rate
|
||||
if (process.out_sample_rate % process.in_sample_rate == 0) {
|
||||
max_out_frames = max_in_frames * (process.out_sample_rate / process.in_sample_rate);
|
||||
} else {
|
||||
max_out_frames = (int)(1.1 * (float)max_in_frames * (float)process.out_sample_rate / (float)process.in_sample_rate);
|
||||
}
|
||||
|
||||
if (process.max_in_frames != max_in_frames) {
|
||||
LOG_DEBUG("creating process buf in frames: %u", max_in_frames);
|
||||
if (process.inbuf) free(process.inbuf);
|
||||
process.inbuf = malloc(max_in_frames * BYTES_PER_FRAME);
|
||||
process.max_in_frames = max_in_frames;
|
||||
}
|
||||
|
||||
if (process.max_out_frames != max_out_frames) {
|
||||
LOG_DEBUG("creating process buf out frames: %u", max_out_frames);
|
||||
if (process.outbuf) free(process.outbuf);
|
||||
process.outbuf = malloc(max_out_frames * BYTES_PER_FRAME);
|
||||
process.max_out_frames = max_out_frames;
|
||||
}
|
||||
|
||||
if (!process.inbuf || !process.outbuf) {
|
||||
LOG_ERROR("malloc fail creating process buffers");
|
||||
*direct = true;
|
||||
return raw_sample_rate;
|
||||
}
|
||||
|
||||
return process.out_sample_rate;
|
||||
}
|
||||
|
||||
return raw_sample_rate;
|
||||
}
|
||||
|
||||
// process flush - called with decode mutex set
|
||||
void process_flush(void) {
|
||||
|
||||
LOG_INFO("process flush");
|
||||
|
||||
FLUSH_FUNC();
|
||||
|
||||
process.in_frames = 0;
|
||||
}
|
||||
|
||||
// init - called with no mutex
|
||||
void process_init(char *opt) {
|
||||
|
||||
bool enabled = INIT_FUNC(opt);
|
||||
|
||||
memset(&process, 0, sizeof(process));
|
||||
|
||||
if (enabled) {
|
||||
LOCK_D;
|
||||
decode.process = true;
|
||||
UNLOCK_D;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #if PROCESS
|
||||
379
components/squeezelite/resample.c
Normal file
379
components/squeezelite/resample.c
Normal file
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// upsampling using libsoxr - only included if RESAMPLE set
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#if RESAMPLE
|
||||
|
||||
#include <math.h>
|
||||
#include <soxr.h>
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
struct soxr {
|
||||
soxr_t resampler;
|
||||
size_t old_clips;
|
||||
unsigned long q_recipe;
|
||||
unsigned long q_flags;
|
||||
double q_precision; /* Conversion precision (in bits). 20 */
|
||||
double q_phase_response; /* 0=minimum, ... 50=linear, ... 100=maximum 50 */
|
||||
double q_passband_end; /* 0dB pt. bandwidth to preserve; nyquist=1 0.913 */
|
||||
double q_stopband_begin; /* Aliasing/imaging control; > passband_end 1 */
|
||||
double scale;
|
||||
bool max_rate;
|
||||
bool exception;
|
||||
#if !LINKALL
|
||||
// soxr symbols to be dynamically loaded
|
||||
soxr_io_spec_t (* soxr_io_spec)(soxr_datatype_t itype, soxr_datatype_t otype);
|
||||
soxr_quality_spec_t (* soxr_quality_spec)(unsigned long recipe, unsigned long flags);
|
||||
soxr_t (* soxr_create)(double, double, unsigned, soxr_error_t *,
|
||||
soxr_io_spec_t const *, soxr_quality_spec_t const *, soxr_runtime_spec_t const *);
|
||||
void (* soxr_delete)(soxr_t);
|
||||
soxr_error_t (* soxr_process)(soxr_t, soxr_in_t, size_t, size_t *, soxr_out_t, size_t olen, size_t *);
|
||||
size_t *(* soxr_num_clips)(soxr_t);
|
||||
#if RESAMPLE_MP
|
||||
soxr_runtime_spec_t (* soxr_runtime_spec)(unsigned num_threads);
|
||||
#endif
|
||||
// soxr_strerror is a macro so not included here
|
||||
#endif
|
||||
};
|
||||
|
||||
static struct soxr *r;
|
||||
|
||||
#if LINKALL
|
||||
#define SOXR(h, fn, ...) (soxr_ ## fn)(__VA_ARGS__)
|
||||
#else
|
||||
#define SOXR(h, fn, ...) (h)->soxr_##fn(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
|
||||
void resample_samples(struct processstate *process) {
|
||||
size_t idone, odone;
|
||||
size_t clip_cnt;
|
||||
|
||||
soxr_error_t error =
|
||||
SOXR(r, process, r->resampler, process->inbuf, process->in_frames, &idone, process->outbuf, process->max_out_frames, &odone);
|
||||
if (error) {
|
||||
LOG_INFO("soxr_process error: %s", soxr_strerror(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (idone != process->in_frames) {
|
||||
// should not get here if buffers are big enough...
|
||||
LOG_ERROR("should not get here - partial sox process: %u of %u processed %u of %u out",
|
||||
(unsigned)idone, process->in_frames, (unsigned)odone, process->max_out_frames);
|
||||
}
|
||||
|
||||
process->out_frames = odone;
|
||||
process->total_in += idone;
|
||||
process->total_out += odone;
|
||||
|
||||
clip_cnt = *(SOXR(r, num_clips, r->resampler));
|
||||
if (clip_cnt - r->old_clips) {
|
||||
LOG_SDEBUG("resampling clips: %u", (unsigned)(clip_cnt - r->old_clips));
|
||||
r->old_clips = clip_cnt;
|
||||
}
|
||||
}
|
||||
|
||||
bool resample_drain(struct processstate *process) {
|
||||
size_t odone;
|
||||
size_t clip_cnt;
|
||||
|
||||
soxr_error_t error = SOXR(r, process, r->resampler, NULL, 0, NULL, process->outbuf, process->max_out_frames, &odone);
|
||||
if (error) {
|
||||
LOG_INFO("soxr_process error: %s", soxr_strerror(error));
|
||||
return true;
|
||||
}
|
||||
|
||||
process->out_frames = odone;
|
||||
process->total_out += odone;
|
||||
|
||||
clip_cnt = *(SOXR(r, num_clips, r->resampler));
|
||||
if (clip_cnt - r->old_clips) {
|
||||
LOG_DEBUG("resampling clips: %u", (unsigned)(clip_cnt - r->old_clips));
|
||||
r->old_clips = clip_cnt;
|
||||
}
|
||||
|
||||
if (odone == 0) {
|
||||
|
||||
LOG_INFO("resample track complete - total track clips: %u", r->old_clips);
|
||||
|
||||
SOXR(r, delete, r->resampler);
|
||||
r->resampler = NULL;
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned supported_rates[]) {
|
||||
unsigned outrate = 0;
|
||||
int i;
|
||||
|
||||
if (r->exception) {
|
||||
// find direct match - avoid resampling
|
||||
for (i = 0; supported_rates[i]; i++) {
|
||||
if (raw_sample_rate == supported_rates[i]) {
|
||||
outrate = raw_sample_rate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// else find next highest sync sample rate
|
||||
while (!outrate && i >= 0) {
|
||||
if (supported_rates[i] > raw_sample_rate && supported_rates[i] % raw_sample_rate == 0) {
|
||||
outrate = supported_rates[i];
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (!outrate) {
|
||||
if (r->max_rate) {
|
||||
// resample to max rate for device
|
||||
outrate = supported_rates[0];
|
||||
} else {
|
||||
// resample to max sync sample rate
|
||||
for (i = 0; supported_rates[i]; i++) {
|
||||
if (supported_rates[i] % raw_sample_rate == 0 || raw_sample_rate % supported_rates[i] == 0) {
|
||||
outrate = supported_rates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!outrate) {
|
||||
outrate = supported_rates[0];
|
||||
}
|
||||
}
|
||||
|
||||
process->in_sample_rate = raw_sample_rate;
|
||||
process->out_sample_rate = outrate;
|
||||
|
||||
if (r->resampler) {
|
||||
SOXR(r, delete, r->resampler);
|
||||
r->resampler = NULL;
|
||||
}
|
||||
|
||||
if (raw_sample_rate != outrate) {
|
||||
|
||||
soxr_io_spec_t io_spec;
|
||||
soxr_quality_spec_t q_spec;
|
||||
soxr_error_t error;
|
||||
#if RESAMPLE_MP
|
||||
soxr_runtime_spec_t r_spec;
|
||||
#endif
|
||||
|
||||
LOG_INFO("resampling from %u -> %u", raw_sample_rate, outrate);
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
io_spec = SOXR(r, io_spec, SOXR_INT16_I, SOXR_INT16_I);
|
||||
io_spec.flags = SOXR_NO_DITHER;
|
||||
#else
|
||||
io_spec = SOXR(r, io_spec, SOXR_INT32_I, SOXR_INT32_I);
|
||||
#endif
|
||||
io_spec.scale = r->scale;
|
||||
|
||||
q_spec = SOXR(r, quality_spec, r->q_recipe, r->q_flags);
|
||||
if (r->q_precision > 0) {
|
||||
q_spec.precision = r->q_precision;
|
||||
}
|
||||
if (r->q_passband_end > 0) {
|
||||
q_spec.passband_end = r->q_passband_end;
|
||||
}
|
||||
if (r->q_stopband_begin > 0) {
|
||||
q_spec.stopband_begin = r->q_stopband_begin;
|
||||
}
|
||||
if (r->q_phase_response > -1) {
|
||||
q_spec.phase_response = r->q_phase_response;
|
||||
}
|
||||
|
||||
#if RESAMPLE_MP
|
||||
r_spec = SOXR(r, runtime_spec, 0); // make use of libsoxr OpenMP support allowing parallel execution if multiple cores
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("resampling with soxr_quality_spec_t[precision: %03.1f, passband_end: %03.6f, stopband_begin: %03.6f, "
|
||||
"phase_response: %03.1f, flags: 0x%02x], soxr_io_spec_t[scale: %03.2f]", q_spec.precision,
|
||||
q_spec.passband_end, q_spec.stopband_begin, q_spec.phase_response, q_spec.flags, io_spec.scale);
|
||||
|
||||
#if RESAMPLE_MP
|
||||
r->resampler = SOXR(r, create, raw_sample_rate, outrate, 2, &error, &io_spec, &q_spec, &r_spec);
|
||||
#else
|
||||
r->resampler = SOXR(r, create, raw_sample_rate, outrate, 2, &error, &io_spec, &q_spec, NULL);
|
||||
#endif
|
||||
|
||||
if (error) {
|
||||
LOG_INFO("soxr_create error: %s", soxr_strerror(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
r->old_clips = 0;
|
||||
return true;
|
||||
|
||||
} else {
|
||||
|
||||
LOG_INFO("disable resampling - rates match");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void resample_flush(void) {
|
||||
if (r->resampler) {
|
||||
SOXR(r, delete, r->resampler);
|
||||
r->resampler = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_soxr(void) {
|
||||
#if !LINKALL
|
||||
void *handle = dlopen(LIBSOXR, RTLD_NOW);
|
||||
char *err;
|
||||
|
||||
if (!handle) {
|
||||
LOG_INFO("dlerror: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
r->soxr_io_spec = dlsym(handle, "soxr_io_spec");
|
||||
r->soxr_quality_spec = dlsym(handle, "soxr_quality_spec");
|
||||
r->soxr_create = dlsym(handle, "soxr_create");
|
||||
r->soxr_delete = dlsym(handle, "soxr_delete");
|
||||
r->soxr_process = dlsym(handle, "soxr_process");
|
||||
r->soxr_num_clips = dlsym(handle, "soxr_num_clips");
|
||||
#if RESAMPLE_MP
|
||||
r->soxr_runtime_spec = dlsym(handle, "soxr_runtime_spec");
|
||||
#endif
|
||||
|
||||
if ((err = dlerror()) != NULL) {
|
||||
LOG_INFO("dlerror: %s", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("loaded "LIBSOXR);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resample_init(char *opt) {
|
||||
char *recipe = NULL, *flags = NULL;
|
||||
char *atten = NULL;
|
||||
char *precision = NULL, *passband_end = NULL, *stopband_begin = NULL, *phase_response = NULL;
|
||||
|
||||
r = malloc(sizeof(struct soxr));
|
||||
if (!r) {
|
||||
LOG_WARN("resampling disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
r->resampler = NULL;
|
||||
r->old_clips = 0;
|
||||
r->max_rate = false;
|
||||
r->exception = false;
|
||||
|
||||
if (!load_soxr()) {
|
||||
LOG_WARN("resampling disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opt) {
|
||||
recipe = next_param(opt, ':');
|
||||
flags = next_param(NULL, ':');
|
||||
atten = next_param(NULL, ':');
|
||||
precision = next_param(NULL, ':');
|
||||
passband_end = next_param(NULL, ':');
|
||||
stopband_begin = next_param(NULL, ':');
|
||||
phase_response = next_param(NULL, ':');
|
||||
}
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
// default to LQ (16 bits) if not user specified
|
||||
r->q_recipe = SOXR_LQ | SOXR_MINIMUM_PHASE;
|
||||
r->q_flags = SOXR_ROLLOFF_NONE;
|
||||
r->q_phase_response = 0;
|
||||
#else
|
||||
// default to HQ (20 bits) if not user specified
|
||||
r->q_recipe = SOXR_HQ;
|
||||
r->q_flags = 0;
|
||||
r->q_phase_response = -1;
|
||||
#endif
|
||||
// default to 1db of attenuation if not user specified
|
||||
r->scale = pow(10, -1.0 / 20);
|
||||
// override recipe derived values with user specified values
|
||||
r->q_precision = 0;
|
||||
r->q_passband_end = 0.75;
|
||||
r->q_stopband_begin = 1.25;
|
||||
|
||||
if (recipe && recipe[0] != '\0') {
|
||||
if (strchr(recipe, 'v')) r->q_recipe = SOXR_VHQ;
|
||||
if (strchr(recipe, 'h')) r->q_recipe = SOXR_HQ;
|
||||
if (strchr(recipe, 'm')) r->q_recipe = SOXR_MQ;
|
||||
if (strchr(recipe, 'l')) r->q_recipe = SOXR_LQ;
|
||||
if (strchr(recipe, 'q')) r->q_recipe = SOXR_QQ;
|
||||
if (strchr(recipe, 'L')) r->q_recipe |= SOXR_LINEAR_PHASE;
|
||||
if (strchr(recipe, 'I')) r->q_recipe |= SOXR_INTERMEDIATE_PHASE;
|
||||
if (strchr(recipe, 'M')) r->q_recipe |= SOXR_MINIMUM_PHASE;
|
||||
if (strchr(recipe, 's')) r->q_recipe |= SOXR_STEEP_FILTER;
|
||||
// X = async resampling to max_rate
|
||||
if (strchr(recipe, 'X')) r->max_rate = true;
|
||||
// E = exception, only resample if native rate is not supported
|
||||
if (strchr(recipe, 'E')) r->exception = true;
|
||||
}
|
||||
|
||||
if (flags) {
|
||||
r->q_flags = strtoul(flags, 0, 16);
|
||||
}
|
||||
|
||||
if (atten) {
|
||||
double scale = pow(10, -atof(atten) / 20);
|
||||
if (scale > 0 && scale <= 1.0) {
|
||||
r->scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
if (precision) {
|
||||
r->q_precision = atof(precision);
|
||||
}
|
||||
|
||||
if (passband_end) {
|
||||
r->q_passband_end = atof(passband_end) / 100;
|
||||
}
|
||||
|
||||
if (stopband_begin) {
|
||||
r->q_stopband_begin = atof(stopband_begin) / 100;
|
||||
}
|
||||
|
||||
if (phase_response) {
|
||||
r->q_phase_response = atof(phase_response);
|
||||
}
|
||||
|
||||
LOG_INFO("resampling %s recipe: 0x%02x, flags: 0x%02x, scale: %03.2f, precision: %03.1f, passband_end: %03.5f, stopband_begin: %03.5f, phase_response: %03.1f",
|
||||
r->max_rate ? "async" : "sync",
|
||||
r->q_recipe, r->q_flags, r->scale, r->q_precision, r->q_passband_end, r->q_stopband_begin, r->q_phase_response);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // #if RESAMPLE
|
||||
164
components/squeezelite/resample16.c
Normal file
164
components/squeezelite/resample16.c
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// upsampling using libsoxr - only included if RESAMPLE set
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#if RESAMPLE16
|
||||
|
||||
#include <resample16.h>
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
struct resample16 {
|
||||
struct resample16_s *resampler;
|
||||
bool max_rate;
|
||||
bool exception;
|
||||
bool interp;
|
||||
resample16_filter_e filter;
|
||||
};
|
||||
|
||||
static struct resample16 r;
|
||||
|
||||
void resample_samples(struct processstate *process) {
|
||||
ssize_t odone;
|
||||
|
||||
odone = resample16(r.resampler, (HWORD*) process->inbuf, process->in_frames, (HWORD*) process->outbuf);
|
||||
|
||||
if (odone < 0) {
|
||||
LOG_INFO("resample16 error");
|
||||
return;
|
||||
}
|
||||
|
||||
process->out_frames = odone;
|
||||
process->total_in += process->in_frames;
|
||||
process->total_out += odone;
|
||||
}
|
||||
|
||||
bool resample_drain(struct processstate *process) {
|
||||
process->out_frames = 0;
|
||||
|
||||
LOG_INFO("resample track complete");
|
||||
|
||||
resample16_delete(r.resampler);
|
||||
r.resampler = NULL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned supported_rates[]) {
|
||||
unsigned outrate = 0;
|
||||
int i;
|
||||
|
||||
if (r.exception) {
|
||||
// find direct match - avoid resampling
|
||||
for (i = 0; supported_rates[i]; i++) {
|
||||
if (raw_sample_rate == supported_rates[i]) {
|
||||
outrate = raw_sample_rate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// else find next highest sync sample rate
|
||||
while (!outrate && i >= 0) {
|
||||
if (supported_rates[i] > raw_sample_rate && supported_rates[i] % raw_sample_rate == 0) {
|
||||
outrate = supported_rates[i];
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (!outrate) {
|
||||
if (r.max_rate) {
|
||||
// resample to max rate for device
|
||||
outrate = supported_rates[0];
|
||||
} else {
|
||||
// resample to max sync sample rate
|
||||
for (i = 0; supported_rates[i]; i++) {
|
||||
if (supported_rates[i] % raw_sample_rate == 0 || raw_sample_rate % supported_rates[i] == 0) {
|
||||
outrate = supported_rates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!outrate) {
|
||||
outrate = supported_rates[0];
|
||||
}
|
||||
}
|
||||
|
||||
process->in_sample_rate = raw_sample_rate;
|
||||
process->out_sample_rate = outrate;
|
||||
|
||||
if (r.resampler) {
|
||||
resample16_delete(r.resampler);
|
||||
r.resampler = NULL;
|
||||
}
|
||||
|
||||
if (raw_sample_rate != outrate) {
|
||||
|
||||
LOG_INFO("resampling from %u -> %u", raw_sample_rate, outrate);
|
||||
r.resampler = resample16_create((float) outrate / raw_sample_rate, r.filter, NULL, false);
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
|
||||
LOG_INFO("disable resampling - rates match");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void resample_flush(void) {
|
||||
if (r.resampler) {
|
||||
resample16_delete(r.resampler);
|
||||
r.resampler = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool resample_init(char *opt) {
|
||||
char *filter = NULL, *interp = NULL;
|
||||
|
||||
r.resampler = NULL;
|
||||
r.max_rate = false;
|
||||
r.exception = false;
|
||||
|
||||
if (opt) {
|
||||
filter = next_param(opt, ':');
|
||||
interp = next_param(NULL, ':');
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
if (*filter == 'm') r.filter = RESAMPLE16_MED;
|
||||
else if (*filter == 'l') r.filter = RESAMPLE16_LOW;
|
||||
else r.filter = RESAMPLE16_BASIC;
|
||||
}
|
||||
|
||||
if (interp && *interp == 'i') {
|
||||
r.interp = true;
|
||||
}
|
||||
|
||||
LOG_INFO("Resampling with filter %d %s", r.filter, r.interp ? "(interpolated)" : "");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // #if RESAMPLE16
|
||||
976
components/squeezelite/slimproto.c
Normal file
976
components/squeezelite/slimproto.c
Normal file
@@ -0,0 +1,976 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additions (c) Paul Hermann, 2015-2017 under the same license terms
|
||||
* -Control of Raspberry pi GPIO for amplifier power
|
||||
* -Launch script on power status change from LMS
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
#include "slimproto.h"
|
||||
|
||||
static log_level loglevel;
|
||||
|
||||
#define SQUEEZENETWORK "mysqueezebox.com:3483"
|
||||
|
||||
#define PORT 3483
|
||||
|
||||
#define MAXBUF 4096
|
||||
|
||||
#if SL_LITTLE_ENDIAN
|
||||
#define LOCAL_PLAYER_IP 0x0100007f // 127.0.0.1
|
||||
#define LOCAL_PLAYER_PORT 0x9b0d // 3483
|
||||
#else
|
||||
#define LOCAL_PLAYER_IP 0x7f000001 // 127.0.0.1
|
||||
#define LOCAL_PLAYER_PORT 0x0d9b // 3483
|
||||
#endif
|
||||
|
||||
static sockfd sock = -1;
|
||||
static in_addr_t slimproto_ip = 0;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
|
||||
extern struct codec *codecs[];
|
||||
#if IR
|
||||
extern struct irstate ir;
|
||||
#endif
|
||||
|
||||
event_event wake_e;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_D mutex_lock(decode.mutex)
|
||||
#define UNLOCK_D mutex_unlock(decode.mutex)
|
||||
#if IR
|
||||
#define LOCK_I mutex_lock(ir.mutex)
|
||||
#define UNLOCK_I mutex_unlock(ir.mutex)
|
||||
#endif
|
||||
|
||||
static struct {
|
||||
u32_t updated;
|
||||
u32_t stream_start;
|
||||
u32_t stream_full;
|
||||
u32_t stream_size;
|
||||
u64_t stream_bytes;
|
||||
u32_t output_full;
|
||||
u32_t output_size;
|
||||
u32_t frames_played;
|
||||
u32_t device_frames;
|
||||
u32_t current_sample_rate;
|
||||
u32_t last;
|
||||
stream_state stream_state;
|
||||
} status;
|
||||
|
||||
int autostart;
|
||||
bool sentSTMu, sentSTMo, sentSTMl;
|
||||
u32_t new_server;
|
||||
char *new_server_cap;
|
||||
#define PLAYER_NAME_LEN 64
|
||||
char player_name[PLAYER_NAME_LEN + 1] = "";
|
||||
const char *name_file = NULL;
|
||||
|
||||
void send_packet(u8_t *packet, size_t len) {
|
||||
u8_t *ptr = packet;
|
||||
unsigned try = 0;
|
||||
ssize_t n;
|
||||
|
||||
while (len) {
|
||||
n = send(sock, ptr, len, MSG_NOSIGNAL);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && last_error() == ERROR_WOULDBLOCK && try < 10) {
|
||||
LOG_DEBUG("retrying (%d) writing to socket", ++try);
|
||||
usleep(1000);
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("failed writing to socket: %s", strerror(last_error()));
|
||||
return;
|
||||
}
|
||||
ptr += n;
|
||||
len -= n;
|
||||
}
|
||||
}
|
||||
|
||||
static void sendHELO(bool reconnect, const char *fixed_cap, const char *var_cap, u8_t mac[6]) {
|
||||
#define BASE_CAP "Model=squeezelite,AccuratePlayPoints=1,HasDigitalOut=1,HasPolarityInversion=1,Firmware=" VERSION
|
||||
#define SSL_CAP "CanHTTPS=1"
|
||||
const char *base_cap;
|
||||
struct HELO_packet pkt;
|
||||
|
||||
#if USE_SSL
|
||||
#if !LINKALL
|
||||
if (ssl_loaded) base_cap = SSL_CAP "," BASE_CAP;
|
||||
else base_cap = BASE_CAP;
|
||||
#endif
|
||||
base_cap = SSL_CAP "," BASE_CAP;
|
||||
#else
|
||||
base_cap = BASE_CAP;
|
||||
#endif
|
||||
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
memcpy(&pkt.opcode, "HELO", 4);
|
||||
pkt.length = htonl(sizeof(struct HELO_packet) - 8 + strlen(base_cap) + strlen(fixed_cap) + strlen(var_cap));
|
||||
pkt.deviceid = 12; // squeezeplay
|
||||
pkt.revision = 0;
|
||||
packn(&pkt.wlan_channellist, reconnect ? 0x4000 : 0x0000);
|
||||
packN(&pkt.bytes_received_H, (u64_t)status.stream_bytes >> 32);
|
||||
packN(&pkt.bytes_received_L, (u64_t)status.stream_bytes & 0xffffffff);
|
||||
memcpy(pkt.mac, mac, 6);
|
||||
|
||||
LOG_INFO("mac: %02x:%02x:%02x:%02x:%02x:%02x", pkt.mac[0], pkt.mac[1], pkt.mac[2], pkt.mac[3], pkt.mac[4], pkt.mac[5]);
|
||||
|
||||
LOG_INFO("cap: %s%s%s", base_cap, fixed_cap, var_cap);
|
||||
|
||||
send_packet((u8_t *)&pkt, sizeof(pkt));
|
||||
send_packet((u8_t *)base_cap, strlen(base_cap));
|
||||
send_packet((u8_t *)fixed_cap, strlen(fixed_cap));
|
||||
send_packet((u8_t *)var_cap, strlen(var_cap));
|
||||
}
|
||||
|
||||
static void sendSTAT(const char *event, u32_t server_timestamp) {
|
||||
struct STAT_packet pkt;
|
||||
u32_t now = gettime_ms();
|
||||
u32_t ms_played;
|
||||
|
||||
if (status.current_sample_rate && status.frames_played && status.frames_played > status.device_frames) {
|
||||
ms_played = (u32_t)(((u64_t)(status.frames_played - status.device_frames) * (u64_t)1000) / (u64_t)status.current_sample_rate);
|
||||
#ifndef STATUSHACK
|
||||
if (now > status.updated) ms_played += (now - status.updated);
|
||||
#endif
|
||||
LOG_SDEBUG("ms_played: %u (frames_played: %u device_frames: %u)", ms_played, status.frames_played, status.device_frames);
|
||||
} else if (status.frames_played && now > status.stream_start) {
|
||||
ms_played = now - status.stream_start;
|
||||
LOG_SDEBUG("ms_played: %u using elapsed time (frames_played: %u device_frames: %u)", ms_played, status.frames_played, status.device_frames);
|
||||
} else {
|
||||
LOG_SDEBUG("ms_played: 0");
|
||||
ms_played = 0;
|
||||
}
|
||||
|
||||
memset(&pkt, 0, sizeof(struct STAT_packet));
|
||||
memcpy(&pkt.opcode, "STAT", 4);
|
||||
pkt.length = htonl(sizeof(struct STAT_packet) - 8);
|
||||
memcpy(&pkt.event, event, 4);
|
||||
// num_crlf
|
||||
// mas_initialized; mas_mode;
|
||||
packN(&pkt.stream_buffer_fullness, status.stream_full);
|
||||
packN(&pkt.stream_buffer_size, status.stream_size);
|
||||
packN(&pkt.bytes_received_H, (u64_t)status.stream_bytes >> 32);
|
||||
packN(&pkt.bytes_received_L, (u64_t)status.stream_bytes & 0xffffffff);
|
||||
pkt.signal_strength = 0xffff;
|
||||
packN(&pkt.jiffies, now);
|
||||
packN(&pkt.output_buffer_size, status.output_size);
|
||||
packN(&pkt.output_buffer_fullness, status.output_full);
|
||||
packN(&pkt.elapsed_seconds, ms_played / 1000);
|
||||
// voltage;
|
||||
packN(&pkt.elapsed_milliseconds, ms_played);
|
||||
pkt.server_timestamp = server_timestamp; // keep this is server format - don't unpack/pack
|
||||
// error_code;
|
||||
|
||||
LOG_DEBUG("STAT: %s", event);
|
||||
|
||||
if (loglevel == lSDEBUG) {
|
||||
LOG_SDEBUG("received bytesL: %u streambuf: %u outputbuf: %u calc elapsed: %u real elapsed: %u (diff: %d) device: %u delay: %d",
|
||||
(u32_t)status.stream_bytes, status.stream_full, status.output_full, ms_played, now - status.stream_start,
|
||||
ms_played - now + status.stream_start, status.device_frames * 1000 / status.current_sample_rate, now - status.updated);
|
||||
}
|
||||
|
||||
send_packet((u8_t *)&pkt, sizeof(pkt));
|
||||
}
|
||||
|
||||
static void sendDSCO(disconnect_code disconnect) {
|
||||
struct DSCO_packet pkt;
|
||||
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
memcpy(&pkt.opcode, "DSCO", 4);
|
||||
pkt.length = htonl(sizeof(pkt) - 8);
|
||||
pkt.reason = disconnect & 0xFF;
|
||||
|
||||
LOG_DEBUG("DSCO: %d", disconnect);
|
||||
|
||||
send_packet((u8_t *)&pkt, sizeof(pkt));
|
||||
}
|
||||
|
||||
static void sendRESP(const char *header, size_t len) {
|
||||
struct RESP_header pkt_header;
|
||||
|
||||
memset(&pkt_header, 0, sizeof(pkt_header));
|
||||
memcpy(&pkt_header.opcode, "RESP", 4);
|
||||
pkt_header.length = htonl(sizeof(pkt_header) + len - 8);
|
||||
|
||||
LOG_DEBUG("RESP");
|
||||
|
||||
send_packet((u8_t *)&pkt_header, sizeof(pkt_header));
|
||||
send_packet((u8_t *)header, len);
|
||||
}
|
||||
|
||||
static void sendMETA(const char *meta, size_t len) {
|
||||
struct META_header pkt_header;
|
||||
|
||||
memset(&pkt_header, 0, sizeof(pkt_header));
|
||||
memcpy(&pkt_header.opcode, "META", 4);
|
||||
pkt_header.length = htonl(sizeof(pkt_header) + len - 8);
|
||||
|
||||
LOG_DEBUG("META");
|
||||
|
||||
send_packet((u8_t *)&pkt_header, sizeof(pkt_header));
|
||||
send_packet((u8_t *)meta, len);
|
||||
}
|
||||
|
||||
static void sendSETDName(const char *name) {
|
||||
struct SETD_header pkt_header;
|
||||
|
||||
memset(&pkt_header, 0, sizeof(pkt_header));
|
||||
memcpy(&pkt_header.opcode, "SETD", 4);
|
||||
|
||||
pkt_header.id = 0; // id 0 is playername S:P:Squeezebox2
|
||||
pkt_header.length = htonl(sizeof(pkt_header) + strlen(name) + 1 - 8);
|
||||
|
||||
LOG_DEBUG("set playername: %s", name);
|
||||
|
||||
send_packet((u8_t *)&pkt_header, sizeof(pkt_header));
|
||||
send_packet((u8_t *)name, strlen(name) + 1);
|
||||
}
|
||||
|
||||
#if IR
|
||||
void sendIR(u32_t code, u32_t ts) {
|
||||
struct IR_packet pkt;
|
||||
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
memcpy(&pkt.opcode, "IR ", 4);
|
||||
pkt.length = htonl(sizeof(pkt) - 8);
|
||||
|
||||
packN(&pkt.jiffies, ts);
|
||||
pkt.ir_code = htonl(code);
|
||||
|
||||
LOG_DEBUG("IR: ir code: 0x%x ts: %u", code, ts);
|
||||
|
||||
send_packet((u8_t *)&pkt, sizeof(pkt));
|
||||
}
|
||||
#endif
|
||||
|
||||
static void process_strm(u8_t *pkt, int len) {
|
||||
struct strm_packet *strm = (struct strm_packet *)pkt;
|
||||
|
||||
LOG_DEBUG("strm command %c", strm->command);
|
||||
|
||||
switch(strm->command) {
|
||||
case 't':
|
||||
sendSTAT("STMt", strm->replay_gain); // STMt replay_gain is no longer used to track latency, but support it
|
||||
break;
|
||||
case 'q':
|
||||
decode_flush();
|
||||
output_flush();
|
||||
status.frames_played = 0;
|
||||
stream_disconnect();
|
||||
sendSTAT("STMf", 0);
|
||||
buf_flush(streambuf);
|
||||
break;
|
||||
case 'f':
|
||||
decode_flush();
|
||||
output_flush();
|
||||
status.frames_played = 0;
|
||||
if (stream_disconnect()) {
|
||||
sendSTAT("STMf", 0);
|
||||
}
|
||||
buf_flush(streambuf);
|
||||
break;
|
||||
case 'p':
|
||||
{
|
||||
unsigned interval = unpackN(&strm->replay_gain);
|
||||
LOCK_O;
|
||||
output.pause_frames = interval * status.current_sample_rate / 1000;
|
||||
if (interval) {
|
||||
output.state = OUTPUT_PAUSE_FRAMES;
|
||||
} else {
|
||||
output.state = OUTPUT_STOPPED;
|
||||
output.stop_time = gettime_ms();
|
||||
}
|
||||
UNLOCK_O;
|
||||
if (!interval) sendSTAT("STMp", 0);
|
||||
LOG_DEBUG("pause interval: %u", interval);
|
||||
}
|
||||
break;
|
||||
case 'a':
|
||||
{
|
||||
unsigned interval = unpackN(&strm->replay_gain);
|
||||
LOCK_O;
|
||||
output.skip_frames = interval * status.current_sample_rate / 1000;
|
||||
output.state = OUTPUT_SKIP_FRAMES;
|
||||
UNLOCK_O;
|
||||
LOG_DEBUG("skip ahead interval: %u", interval);
|
||||
}
|
||||
break;
|
||||
case 'u':
|
||||
{
|
||||
unsigned jiffies = unpackN(&strm->replay_gain);
|
||||
LOCK_O;
|
||||
output.state = jiffies ? OUTPUT_START_AT : OUTPUT_RUNNING;
|
||||
output.start_at = jiffies;
|
||||
#ifdef STATUSHACK
|
||||
status.frames_played = output.frames_played;
|
||||
#endif
|
||||
UNLOCK_O;
|
||||
|
||||
LOG_DEBUG("unpause at: %u now: %u", jiffies, gettime_ms());
|
||||
sendSTAT("STMr", 0);
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
{
|
||||
unsigned header_len = len - sizeof(struct strm_packet);
|
||||
char *header = (char *)(pkt + sizeof(struct strm_packet));
|
||||
in_addr_t ip = (in_addr_t)strm->server_ip; // keep in network byte order
|
||||
u16_t port = strm->server_port; // keep in network byte order
|
||||
if (ip == 0) ip = slimproto_ip;
|
||||
|
||||
LOG_DEBUG("strm s autostart: %c transition period: %u transition type: %u codec: %c",
|
||||
strm->autostart, strm->transition_period, strm->transition_type - '0', strm->format);
|
||||
|
||||
autostart = strm->autostart - '0';
|
||||
|
||||
sendSTAT("STMf", 0);
|
||||
if (header_len > MAX_HEADER -1) {
|
||||
LOG_WARN("header too long: %u", header_len);
|
||||
break;
|
||||
}
|
||||
if (strm->format != '?') {
|
||||
codec_open(strm->format, strm->pcm_sample_size, strm->pcm_sample_rate, strm->pcm_channels, strm->pcm_endianness);
|
||||
} else if (autostart >= 2) {
|
||||
// extension to slimproto to allow server to detect codec from response header and send back in codc message
|
||||
LOG_DEBUG("streaming unknown codec");
|
||||
} else {
|
||||
LOG_WARN("unknown codec requires autostart >= 2");
|
||||
break;
|
||||
}
|
||||
if (ip == LOCAL_PLAYER_IP && port == LOCAL_PLAYER_PORT) {
|
||||
// extension to slimproto for LocalPlayer - header is filename not http header, don't expect cont
|
||||
stream_file(header, header_len, strm->threshold * 1024);
|
||||
autostart -= 2;
|
||||
} else {
|
||||
stream_sock(ip, port, header, header_len, strm->threshold * 1024, autostart >= 2);
|
||||
}
|
||||
sendSTAT("STMc", 0);
|
||||
sentSTMu = sentSTMo = sentSTMl = false;
|
||||
LOCK_O;
|
||||
output.threshold = strm->output_threshold;
|
||||
output.next_replay_gain = unpackN(&strm->replay_gain);
|
||||
output.fade_mode = strm->transition_type - '0';
|
||||
output.fade_secs = strm->transition_period;
|
||||
output.invert = (strm->flags & 0x03) == 0x03;
|
||||
LOG_DEBUG("set fade mode: %u", output.fade_mode);
|
||||
UNLOCK_O;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG_WARN("unhandled strm %c", strm->command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void process_cont(u8_t *pkt, int len) {
|
||||
struct cont_packet *cont = (struct cont_packet *)pkt;
|
||||
cont->metaint = unpackN(&cont->metaint);
|
||||
|
||||
LOG_DEBUG("cont metaint: %u loop: %u", cont->metaint, cont->loop);
|
||||
|
||||
if (autostart > 1) {
|
||||
autostart -= 2;
|
||||
LOCK_S;
|
||||
if (stream.state == STREAMING_WAIT) {
|
||||
stream.state = STREAMING_BUFFERING;
|
||||
stream.meta_interval = stream.meta_next = cont->metaint;
|
||||
}
|
||||
UNLOCK_S;
|
||||
wake_controller();
|
||||
}
|
||||
}
|
||||
|
||||
static void process_codc(u8_t *pkt, int len) {
|
||||
struct codc_packet *codc = (struct codc_packet *)pkt;
|
||||
|
||||
LOG_DEBUG("codc: %c", codc->format);
|
||||
codec_open(codc->format, codc->pcm_sample_size, codc->pcm_sample_rate, codc->pcm_channels, codc->pcm_endianness);
|
||||
}
|
||||
|
||||
static void process_aude(u8_t *pkt, int len) {
|
||||
struct aude_packet *aude = (struct aude_packet *)pkt;
|
||||
|
||||
LOG_DEBUG("enable spdif: %d dac: %d", aude->enable_spdif, aude->enable_dac);
|
||||
|
||||
LOCK_O;
|
||||
if (!aude->enable_spdif && output.state != OUTPUT_OFF) {
|
||||
output.state = OUTPUT_OFF;
|
||||
}
|
||||
if (aude->enable_spdif && output.state == OUTPUT_OFF && !output.idle_to) {
|
||||
output.state = OUTPUT_STOPPED;
|
||||
output.stop_time = gettime_ms();
|
||||
}
|
||||
UNLOCK_O;
|
||||
}
|
||||
|
||||
static void process_audg(u8_t *pkt, int len) {
|
||||
struct audg_packet *audg = (struct audg_packet *)pkt;
|
||||
audg->gainL = unpackN(&audg->gainL);
|
||||
audg->gainR = unpackN(&audg->gainR);
|
||||
|
||||
LOG_DEBUG("audg gainL: %u gainR: %u adjust: %u", audg->gainL, audg->gainR, audg->adjust);
|
||||
|
||||
set_volume(audg->adjust ? audg->gainL : FIXED_ONE, audg->adjust ? audg->gainR : FIXED_ONE);
|
||||
}
|
||||
|
||||
static void process_setd(u8_t *pkt, int len) {
|
||||
struct setd_packet *setd = (struct setd_packet *)pkt;
|
||||
|
||||
// handle player name query and change
|
||||
if (setd->id == 0) {
|
||||
if (len == 5) {
|
||||
if (strlen(player_name)) {
|
||||
sendSETDName(player_name);
|
||||
}
|
||||
} else if (len > 5) {
|
||||
strncpy(player_name, setd->data, PLAYER_NAME_LEN);
|
||||
player_name[PLAYER_NAME_LEN] = '\0';
|
||||
LOG_INFO("set name: %s", setd->data);
|
||||
// confirm change to server
|
||||
sendSETDName(setd->data);
|
||||
// write name to name_file if -N option set
|
||||
if (name_file) {
|
||||
FILE *fp = fopen(name_file, "w");
|
||||
if (fp) {
|
||||
LOG_INFO("storing name in %s", name_file);
|
||||
fputs(player_name, fp);
|
||||
fclose(fp);
|
||||
} else {
|
||||
LOG_WARN("unable to store new name in %s", name_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define SYNC_CAP ",SyncgroupID="
|
||||
#define SYNC_CAP_LEN 13
|
||||
|
||||
static void process_serv(u8_t *pkt, int len) {
|
||||
struct serv_packet *serv = (struct serv_packet *)pkt;
|
||||
|
||||
unsigned slimproto_port = 0;
|
||||
char squeezeserver[] = SQUEEZENETWORK;
|
||||
|
||||
if(pkt[4] == 0 && pkt[5] == 0 && pkt[6] == 0 && pkt[7] == 1) {
|
||||
server_addr(squeezeserver, &new_server, &slimproto_port);
|
||||
} else {
|
||||
new_server = serv->server_ip;
|
||||
}
|
||||
|
||||
LOG_INFO("switch server");
|
||||
|
||||
if (len - sizeof(struct serv_packet) == 10) {
|
||||
if (!new_server_cap) {
|
||||
new_server_cap = malloc(SYNC_CAP_LEN + 10 + 1);
|
||||
}
|
||||
new_server_cap[0] = '\0';
|
||||
strcat(new_server_cap, SYNC_CAP);
|
||||
strncat(new_server_cap, (const char *)(pkt + sizeof(struct serv_packet)), 10);
|
||||
} else {
|
||||
if (new_server_cap) {
|
||||
free(new_server_cap);
|
||||
new_server_cap = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct handler {
|
||||
char opcode[5];
|
||||
void (*handler)(u8_t *, int);
|
||||
};
|
||||
|
||||
static struct handler handlers[] = {
|
||||
{ "strm", process_strm },
|
||||
{ "cont", process_cont },
|
||||
{ "codc", process_codc },
|
||||
{ "aude", process_aude },
|
||||
{ "audg", process_audg },
|
||||
{ "setd", process_setd },
|
||||
{ "serv", process_serv },
|
||||
{ "", NULL },
|
||||
};
|
||||
|
||||
static void process(u8_t *pack, int len) {
|
||||
struct handler *h = handlers;
|
||||
while (h->handler && strncmp((char *)pack, h->opcode, 4)) { h++; }
|
||||
|
||||
if (h->handler) {
|
||||
LOG_DEBUG("%s", h->opcode);
|
||||
h->handler(pack, len);
|
||||
} else {
|
||||
pack[4] = '\0';
|
||||
LOG_WARN("unhandled %s", (char *)pack);
|
||||
}
|
||||
}
|
||||
|
||||
static bool running;
|
||||
|
||||
static void slimproto_run() {
|
||||
static u8_t buffer[MAXBUF];
|
||||
int expect = 0;
|
||||
int got = 0;
|
||||
u32_t now;
|
||||
static u32_t last = 0;
|
||||
event_handle ehandles[2];
|
||||
int timeouts = 0;
|
||||
|
||||
set_readwake_handles(ehandles, sock, wake_e);
|
||||
|
||||
while (running && !new_server) {
|
||||
|
||||
bool wake = false;
|
||||
event_type ev;
|
||||
|
||||
if ((ev = wait_readwake(ehandles, 1000)) != EVENT_TIMEOUT) {
|
||||
|
||||
if (ev == EVENT_READ) {
|
||||
|
||||
if (expect > 0) {
|
||||
int n = recv(sock, buffer + got, expect, 0);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && last_error() == ERROR_WOULDBLOCK) {
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("error reading from socket: %s", n ? strerror(last_error()) : "closed");
|
||||
return;
|
||||
}
|
||||
expect -= n;
|
||||
got += n;
|
||||
if (expect == 0) {
|
||||
process(buffer, got);
|
||||
got = 0;
|
||||
}
|
||||
} else if (expect == 0) {
|
||||
int n = recv(sock, buffer + got, 2 - got, 0);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && last_error() == ERROR_WOULDBLOCK) {
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("error reading from socket: %s", n ? strerror(last_error()) : "closed");
|
||||
return;
|
||||
}
|
||||
got += n;
|
||||
if (got == 2) {
|
||||
expect = buffer[0] << 8 | buffer[1]; // length pack 'n'
|
||||
got = 0;
|
||||
if (expect > MAXBUF) {
|
||||
LOG_ERROR("FATAL: slimproto packet too big: %d > %d", expect, MAXBUF);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("FATAL: negative expect");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ev == EVENT_WAKE) {
|
||||
wake = true;
|
||||
}
|
||||
|
||||
timeouts = 0;
|
||||
|
||||
} else if (++timeouts > 35) {
|
||||
|
||||
// expect message from server every 5 seconds, but 30 seconds on mysb.com so timeout after 35 seconds
|
||||
LOG_INFO("No messages from server - connection dead");
|
||||
return;
|
||||
}
|
||||
|
||||
// update playback state when woken or every 100ms
|
||||
now = gettime_ms();
|
||||
|
||||
if (wake || now - last > 100 || last > now) {
|
||||
bool _sendSTMs = false;
|
||||
bool _sendDSCO = false;
|
||||
bool _sendRESP = false;
|
||||
bool _sendMETA = false;
|
||||
bool _sendSTMd = false;
|
||||
bool _sendSTMt = false;
|
||||
bool _sendSTMl = false;
|
||||
bool _sendSTMu = false;
|
||||
bool _sendSTMo = false;
|
||||
bool _sendSTMn = false;
|
||||
bool _stream_disconnect = false;
|
||||
bool _start_output = false;
|
||||
decode_state _decode_state;
|
||||
disconnect_code disconnect_code;
|
||||
static char header[MAX_HEADER];
|
||||
size_t header_len = 0;
|
||||
#if IR
|
||||
bool _sendIR = false;
|
||||
u32_t ir_code, ir_ts;
|
||||
#endif
|
||||
last = now;
|
||||
|
||||
|
||||
LOCK_S;
|
||||
status.stream_full = _buf_used(streambuf);
|
||||
status.stream_size = streambuf->size;
|
||||
status.stream_bytes = stream.bytes;
|
||||
status.stream_state = stream.state;
|
||||
|
||||
if (stream.state == DISCONNECT) {
|
||||
disconnect_code = stream.disconnect;
|
||||
stream.state = STOPPED;
|
||||
_sendDSCO = true;
|
||||
}
|
||||
if (!stream.sent_headers &&
|
||||
(stream.state == STREAMING_HTTP || stream.state == STREAMING_WAIT || stream.state == STREAMING_BUFFERING)) {
|
||||
header_len = stream.header_len;
|
||||
memcpy(header, stream.header, header_len);
|
||||
_sendRESP = true;
|
||||
stream.sent_headers = true;
|
||||
}
|
||||
if (stream.meta_send) {
|
||||
header_len = stream.header_len;
|
||||
memcpy(header, stream.header, header_len);
|
||||
_sendMETA = true;
|
||||
stream.meta_send = false;
|
||||
}
|
||||
UNLOCK_S;
|
||||
|
||||
LOCK_D;
|
||||
if ((status.stream_state == STREAMING_HTTP || status.stream_state == STREAMING_FILE ||
|
||||
(status.stream_state == DISCONNECT && stream.disconnect == DISCONNECT_OK)) &&
|
||||
!sentSTMl && decode.state == DECODE_READY) {
|
||||
if (autostart == 0) {
|
||||
decode.state = DECODE_RUNNING;
|
||||
_sendSTMl = true;
|
||||
sentSTMl = true;
|
||||
} else if (autostart == 1) {
|
||||
decode.state = DECODE_RUNNING;
|
||||
_start_output = true;
|
||||
}
|
||||
// autostart 2 and 3 require cont to be received first
|
||||
}
|
||||
if (decode.state == DECODE_COMPLETE || decode.state == DECODE_ERROR) {
|
||||
if (decode.state == DECODE_COMPLETE) _sendSTMd = true;
|
||||
if (decode.state == DECODE_ERROR) _sendSTMn = true;
|
||||
decode.state = DECODE_STOPPED;
|
||||
if (status.stream_state == STREAMING_HTTP || status.stream_state == STREAMING_FILE) {
|
||||
_stream_disconnect = true;
|
||||
}
|
||||
}
|
||||
_decode_state = decode.state;
|
||||
UNLOCK_D;
|
||||
|
||||
LOCK_O;
|
||||
status.output_full = _buf_used(outputbuf);
|
||||
status.output_size = outputbuf->size;
|
||||
status.frames_played = output.frames_played_dmp;
|
||||
status.current_sample_rate = output.current_sample_rate;
|
||||
status.updated = output.updated;
|
||||
status.device_frames = output.device_frames;
|
||||
|
||||
if (output.track_started) {
|
||||
_sendSTMs = true;
|
||||
output.track_started = false;
|
||||
status.stream_start = output.track_start_time;
|
||||
#ifdef STATUSHACK
|
||||
status.frames_played = output.frames_played;
|
||||
#endif
|
||||
}
|
||||
#if PORTAUDIO
|
||||
if (output.pa_reopen) {
|
||||
_pa_open();
|
||||
output.pa_reopen = false;
|
||||
}
|
||||
#endif
|
||||
if (_start_output && (output.state == OUTPUT_STOPPED || output.state == OUTPUT_OFF)) {
|
||||
output.state = OUTPUT_BUFFER;
|
||||
}
|
||||
if (output.state == OUTPUT_RUNNING && !sentSTMu && status.output_full == 0 && status.stream_state <= DISCONNECT &&
|
||||
_decode_state == DECODE_STOPPED) {
|
||||
|
||||
_sendSTMu = true;
|
||||
sentSTMu = true;
|
||||
LOG_DEBUG("output underrun");
|
||||
output.state = OUTPUT_STOPPED;
|
||||
output.stop_time = now;
|
||||
}
|
||||
if (output.state == OUTPUT_RUNNING && !sentSTMo && status.output_full == 0 && status.stream_state == STREAMING_HTTP) {
|
||||
|
||||
_sendSTMo = true;
|
||||
sentSTMo = true;
|
||||
}
|
||||
if (output.state == OUTPUT_STOPPED && output.idle_to && (now - output.stop_time > output.idle_to)) {
|
||||
output.state = OUTPUT_OFF;
|
||||
LOG_DEBUG("output timeout");
|
||||
}
|
||||
if (output.state == OUTPUT_RUNNING && now - status.last > 1000) {
|
||||
_sendSTMt = true;
|
||||
status.last = now;
|
||||
}
|
||||
UNLOCK_O;
|
||||
|
||||
#if IR
|
||||
LOCK_I;
|
||||
if (ir.code) {
|
||||
_sendIR = true;
|
||||
ir_code = ir.code;
|
||||
ir_ts = ir.ts;
|
||||
ir.code = 0;
|
||||
}
|
||||
UNLOCK_I;
|
||||
#endif
|
||||
|
||||
if (_stream_disconnect) stream_disconnect();
|
||||
|
||||
// send packets once locks released as packet sending can block
|
||||
if (_sendDSCO) sendDSCO(disconnect_code);
|
||||
if (_sendSTMs) sendSTAT("STMs", 0);
|
||||
if (_sendSTMd) sendSTAT("STMd", 0);
|
||||
if (_sendSTMt) sendSTAT("STMt", 0);
|
||||
if (_sendSTMl) sendSTAT("STMl", 0);
|
||||
if (_sendSTMu) sendSTAT("STMu", 0);
|
||||
if (_sendSTMo) sendSTAT("STMo", 0);
|
||||
if (_sendSTMn) sendSTAT("STMn", 0);
|
||||
if (_sendRESP) sendRESP(header, header_len);
|
||||
if (_sendMETA) sendMETA(header, header_len);
|
||||
#if IR
|
||||
if (_sendIR) sendIR(ir_code, ir_ts);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// called from other threads to wake state machine above
|
||||
void wake_controller(void) {
|
||||
wake_signal(wake_e);
|
||||
}
|
||||
|
||||
in_addr_t discover_server(char *default_server) {
|
||||
struct sockaddr_in d;
|
||||
struct sockaddr_in s;
|
||||
char *buf;
|
||||
struct pollfd pollinfo;
|
||||
unsigned port;
|
||||
|
||||
int disc_sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
|
||||
socklen_t enable = 1;
|
||||
setsockopt(disc_sock, SOL_SOCKET, SO_BROADCAST, (const void *)&enable, sizeof(enable));
|
||||
|
||||
buf = "e";
|
||||
|
||||
memset(&d, 0, sizeof(d));
|
||||
d.sin_family = AF_INET;
|
||||
d.sin_port = htons(PORT);
|
||||
d.sin_addr.s_addr = htonl(INADDR_BROADCAST);
|
||||
|
||||
pollinfo.fd = disc_sock;
|
||||
pollinfo.events = POLLIN;
|
||||
|
||||
do {
|
||||
|
||||
LOG_INFO("sending discovery");
|
||||
memset(&s, 0, sizeof(s));
|
||||
|
||||
if (sendto(disc_sock, buf, 1, 0, (struct sockaddr *)&d, sizeof(d)) < 0) {
|
||||
LOG_INFO("error sending disovery");
|
||||
}
|
||||
|
||||
if (poll(&pollinfo, 1, 5000) == 1) {
|
||||
char readbuf[10];
|
||||
socklen_t slen = sizeof(s);
|
||||
recvfrom(disc_sock, readbuf, 10, 0, (struct sockaddr *)&s, &slen);
|
||||
LOG_INFO("got response from: %s:%d", inet_ntoa(s.sin_addr), ntohs(s.sin_port));
|
||||
}
|
||||
|
||||
if (default_server) {
|
||||
server_addr(default_server, &s.sin_addr.s_addr, &port);
|
||||
}
|
||||
|
||||
} while (s.sin_addr.s_addr == 0 && running);
|
||||
|
||||
closesocket(disc_sock);
|
||||
|
||||
return s.sin_addr.s_addr;
|
||||
}
|
||||
|
||||
#define FIXED_CAP_LEN 256
|
||||
#define VAR_CAP_LEN 128
|
||||
|
||||
void slimproto(log_level level, char *server, u8_t mac[6], const char *name, const char *namefile, const char *modelname, int maxSampleRate) {
|
||||
struct sockaddr_in serv_addr;
|
||||
static char fixed_cap[FIXED_CAP_LEN], var_cap[VAR_CAP_LEN] = "";
|
||||
bool reconnect = false;
|
||||
unsigned failed_connect = 0;
|
||||
unsigned slimproto_port = 0;
|
||||
in_addr_t previous_server = 0;
|
||||
int i;
|
||||
|
||||
memset(&status, 0, sizeof(status));
|
||||
|
||||
wake_create(wake_e);
|
||||
|
||||
loglevel = level;
|
||||
running = true;
|
||||
|
||||
if (server) {
|
||||
server_addr(server, &slimproto_ip, &slimproto_port);
|
||||
}
|
||||
|
||||
if (!slimproto_ip) {
|
||||
slimproto_ip = discover_server(server);
|
||||
}
|
||||
|
||||
if (!slimproto_port) {
|
||||
slimproto_port = PORT;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
strncpy(player_name, name, PLAYER_NAME_LEN);
|
||||
player_name[PLAYER_NAME_LEN] = '\0';
|
||||
}
|
||||
|
||||
if (namefile) {
|
||||
FILE *fp;
|
||||
name_file = namefile;
|
||||
fp = fopen(namefile, "r");
|
||||
if (fp) {
|
||||
if (!fgets(player_name, PLAYER_NAME_LEN, fp)) {
|
||||
player_name[PLAYER_NAME_LEN] = '\0';
|
||||
} else {
|
||||
// strip any \n from fgets response
|
||||
int len = strlen(player_name);
|
||||
if (len > 0 && player_name[len - 1] == '\n') {
|
||||
player_name[len - 1] = '\0';
|
||||
}
|
||||
LOG_INFO("retrieved name %s from %s", player_name, name_file);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
if (!running) return;
|
||||
|
||||
LOCK_O;
|
||||
snprintf(fixed_cap, FIXED_CAP_LEN, ",ModelName=%s,MaxSampleRate=%u", modelname ? modelname : MODEL_NAME_STRING,
|
||||
((maxSampleRate > 0) ? maxSampleRate : output.supported_rates[0]));
|
||||
|
||||
for (i = 0; i < MAX_CODECS; i++) {
|
||||
if (codecs[i] && codecs[i]->id && strlen(fixed_cap) < FIXED_CAP_LEN - 10) {
|
||||
strcat(fixed_cap, ",");
|
||||
strcat(fixed_cap, codecs[i]->types);
|
||||
}
|
||||
}
|
||||
UNLOCK_O;
|
||||
|
||||
memset(&serv_addr, 0, sizeof(serv_addr));
|
||||
serv_addr.sin_family = AF_INET;
|
||||
serv_addr.sin_addr.s_addr = slimproto_ip;
|
||||
serv_addr.sin_port = htons(slimproto_port);
|
||||
|
||||
LOG_INFO("connecting to %s:%d", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port));
|
||||
|
||||
new_server = 0;
|
||||
|
||||
while (running) {
|
||||
|
||||
if (new_server) {
|
||||
previous_server = slimproto_ip;
|
||||
slimproto_ip = serv_addr.sin_addr.s_addr = new_server;
|
||||
LOG_INFO("switching server to %s:%d", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port));
|
||||
new_server = 0;
|
||||
reconnect = false;
|
||||
}
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
|
||||
set_nonblock(sock);
|
||||
set_nosigpipe(sock);
|
||||
|
||||
if (connect_timeout(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr), 5) != 0) {
|
||||
|
||||
if (previous_server) {
|
||||
slimproto_ip = serv_addr.sin_addr.s_addr = previous_server;
|
||||
LOG_INFO("new server not reachable, reverting to previous server %s:%d", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port));
|
||||
} else {
|
||||
LOG_INFO("unable to connect to server %u", failed_connect);
|
||||
sleep(5);
|
||||
}
|
||||
|
||||
// rediscover server if it was not set at startup
|
||||
if (!server && ++failed_connect > 5) {
|
||||
slimproto_ip = serv_addr.sin_addr.s_addr = discover_server(NULL);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
struct sockaddr_in our_addr;
|
||||
socklen_t len;
|
||||
|
||||
LOG_INFO("connected");
|
||||
|
||||
var_cap[0] = '\0';
|
||||
failed_connect = 0;
|
||||
|
||||
// check if this is a local player now we are connected & signal to server via 'loc' format
|
||||
// this requires LocalPlayer server plugin to enable direct file access
|
||||
len = sizeof(our_addr);
|
||||
getsockname(sock, (struct sockaddr *) &our_addr, &len);
|
||||
|
||||
if (our_addr.sin_addr.s_addr == serv_addr.sin_addr.s_addr) {
|
||||
LOG_INFO("local player");
|
||||
strcat(var_cap, ",loc");
|
||||
}
|
||||
|
||||
// add on any capablity to be sent to the new server
|
||||
if (new_server_cap) {
|
||||
strcat(var_cap, new_server_cap);
|
||||
free(new_server_cap);
|
||||
new_server_cap = NULL;
|
||||
}
|
||||
|
||||
sendHELO(reconnect, fixed_cap, var_cap, mac);
|
||||
|
||||
slimproto_run();
|
||||
|
||||
if (!reconnect) {
|
||||
reconnect = true;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
previous_server = 0;
|
||||
|
||||
closesocket(sock);
|
||||
}
|
||||
}
|
||||
|
||||
void slimproto_stop(void) {
|
||||
LOG_INFO("slimproto stop");
|
||||
running = false;
|
||||
}
|
||||
185
components/squeezelite/slimproto.h
Normal file
185
components/squeezelite/slimproto.h
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// packet formats for slimproto
|
||||
|
||||
#ifndef SUN
|
||||
#pragma pack(push, 1)
|
||||
#else
|
||||
#pragma pack(1)
|
||||
#endif
|
||||
|
||||
// from S:N:Slimproto _hello_handler
|
||||
struct HELO_packet {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
u8_t deviceid;
|
||||
u8_t revision;
|
||||
u8_t mac[6];
|
||||
u8_t uuid[16];
|
||||
u16_t wlan_channellist;
|
||||
u32_t bytes_received_H, bytes_received_L;
|
||||
char lang[2];
|
||||
// u8_t capabilities[];
|
||||
};
|
||||
|
||||
// S:N:Slimproto _stat_handler
|
||||
struct STAT_packet {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
u32_t event;
|
||||
u8_t num_crlf;
|
||||
u8_t mas_initialized;
|
||||
u8_t mas_mode;
|
||||
u32_t stream_buffer_size;
|
||||
u32_t stream_buffer_fullness;
|
||||
u32_t bytes_received_H;
|
||||
u32_t bytes_received_L;
|
||||
u16_t signal_strength;
|
||||
u32_t jiffies;
|
||||
u32_t output_buffer_size;
|
||||
u32_t output_buffer_fullness;
|
||||
u32_t elapsed_seconds;
|
||||
u16_t voltage;
|
||||
u32_t elapsed_milliseconds;
|
||||
u32_t server_timestamp;
|
||||
u16_t error_code;
|
||||
};
|
||||
|
||||
// S:N:Slimproto _disco_handler
|
||||
struct DSCO_packet {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
u8_t reason;
|
||||
};
|
||||
|
||||
// S:N:Slimproto _http_response_handler
|
||||
struct RESP_header {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
// char header[] - added in sendRESP
|
||||
};
|
||||
|
||||
// S:N:Slimproto _http_metadata_handler
|
||||
struct META_header {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
// char metadata[]
|
||||
};
|
||||
|
||||
// S:N:Slimproto _http_setting_handler
|
||||
struct SETD_header {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
u8_t id;
|
||||
// data
|
||||
};
|
||||
|
||||
#if IR
|
||||
struct IR_packet {
|
||||
char opcode[4];
|
||||
u32_t length;
|
||||
u32_t jiffies;
|
||||
u8_t format; // ignored by server
|
||||
u8_t bits; // ignored by server
|
||||
u32_t ir_code;
|
||||
};
|
||||
#endif
|
||||
|
||||
// from S:P:Squeezebox stream_s
|
||||
struct strm_packet {
|
||||
char opcode[4];
|
||||
char command;
|
||||
u8_t autostart;
|
||||
u8_t format;
|
||||
u8_t pcm_sample_size;
|
||||
u8_t pcm_sample_rate;
|
||||
u8_t pcm_channels;
|
||||
u8_t pcm_endianness;
|
||||
u8_t threshold;
|
||||
u8_t spdif_enable;
|
||||
u8_t transition_period;
|
||||
u8_t transition_type;
|
||||
u8_t flags;
|
||||
u8_t output_threshold;
|
||||
u8_t slaves;
|
||||
u32_t replay_gain;
|
||||
u16_t server_port;
|
||||
u32_t server_ip;
|
||||
//char request_string[];
|
||||
};
|
||||
|
||||
// S:P:Squeezebox2
|
||||
struct aude_packet {
|
||||
char opcode[4];
|
||||
u8_t enable_spdif;
|
||||
u8_t enable_dac;
|
||||
};
|
||||
|
||||
// S:P:Squeezebox2
|
||||
struct audg_packet {
|
||||
char opcode[4];
|
||||
u32_t old_gainL; // unused
|
||||
u32_t old_gainR; // unused
|
||||
u8_t adjust;
|
||||
u8_t preamp; // unused
|
||||
u32_t gainL;
|
||||
u32_t gainR;
|
||||
// squence ids - unused
|
||||
};
|
||||
|
||||
// S:P:Squeezebox2
|
||||
struct cont_packet {
|
||||
char opcode[4];
|
||||
u32_t metaint;
|
||||
u8_t loop;
|
||||
// guids we don't use
|
||||
};
|
||||
|
||||
// S:C:Commands
|
||||
struct serv_packet {
|
||||
char opcode[4];
|
||||
u32_t server_ip;
|
||||
// possible sync group
|
||||
};
|
||||
|
||||
// S:P:Squeezebox2
|
||||
struct setd_packet {
|
||||
char opcode[4];
|
||||
u8_t id;
|
||||
char data[];
|
||||
};
|
||||
|
||||
// codec open - this is an extension to slimproto to allow the server to read the header and then return decode params
|
||||
struct codc_packet {
|
||||
char opcode[4];
|
||||
u8_t format;
|
||||
u8_t pcm_sample_size;
|
||||
u8_t pcm_sample_rate;
|
||||
u8_t pcm_channels;
|
||||
u8_t pcm_endianness;
|
||||
};
|
||||
|
||||
#ifndef SUN
|
||||
#pragma pack(pop)
|
||||
#else
|
||||
#pragma pack()
|
||||
#endif
|
||||
803
components/squeezelite/squeezelite.h
Normal file
803
components/squeezelite/squeezelite.h
Normal file
@@ -0,0 +1,803 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additions (c) Paul Hermann, 2015-2017 under the same license terms
|
||||
* -Control of Raspberry pi GPIO for amplifier power
|
||||
* -Launch script on power status change from LMS
|
||||
*/
|
||||
|
||||
// make may define: PORTAUDIO, SELFPIPE, RESAMPLE, RESAMPLE_MP, VISEXPORT, GPIO, IR, DSD, LINKALL to influence build
|
||||
|
||||
#define MAJOR_VERSION "1.9"
|
||||
#define MINOR_VERSION "2"
|
||||
#define MICRO_VERSION "1145"
|
||||
|
||||
#if defined(CUSTOM_VERSION)
|
||||
#define VERSION "v" MAJOR_VERSION "." MINOR_VERSION "-" MICRO_VERSION STR(CUSTOM_VERSION)
|
||||
#else
|
||||
#define VERSION "v" MAJOR_VERSION "." MINOR_VERSION "-" MICRO_VERSION
|
||||
#endif
|
||||
|
||||
|
||||
#if !defined(MODEL_NAME)
|
||||
#define MODEL_NAME SqueezeLite
|
||||
#endif
|
||||
|
||||
#define QUOTE(name) #name
|
||||
#define STR(macro) QUOTE(macro)
|
||||
#define MODEL_NAME_STRING STR(MODEL_NAME)
|
||||
|
||||
// build detection
|
||||
#if defined (EMBEDDED)
|
||||
#undef EMBEDDED
|
||||
#define EMBEDDED 1
|
||||
#elif defined(linux)
|
||||
#define LINUX 1
|
||||
#define OSX 0
|
||||
#define WIN 0
|
||||
#define FREEBSD 0
|
||||
#elif defined (__APPLE__)
|
||||
#define LINUX 0
|
||||
#define OSX 1
|
||||
#define WIN 0
|
||||
#define FREEBSD 0
|
||||
#elif defined (_MSC_VER)
|
||||
#define LINUX 0
|
||||
#define OSX 0
|
||||
#define WIN 1
|
||||
#define FREEBSD 0
|
||||
#elif defined(__FreeBSD__)
|
||||
#define LINUX 0
|
||||
#define OSX 0
|
||||
#define WIN 0
|
||||
#define FREEBSD 1
|
||||
#elif defined (__sun)
|
||||
#define SUN 1
|
||||
#define LINUX 1
|
||||
#define PORTAUDIO 1
|
||||
#define PA18API 1
|
||||
#define OSX 0
|
||||
#define WIN 0
|
||||
#else
|
||||
#error unknown target
|
||||
#endif
|
||||
|
||||
#if !EMBEDDED
|
||||
#if LINUX && !defined(PORTAUDIO)
|
||||
#define ALSA 1
|
||||
#define PORTAUDIO 0
|
||||
#else
|
||||
#define ALSA 0
|
||||
#define PORTAUDIO 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(LOOPBACK)
|
||||
#if SUN
|
||||
#define EVENTFD 0
|
||||
#define WINEVENT 0
|
||||
#define SELFPIPE 1
|
||||
#elif LINUX && !defined(SELFPIPE)
|
||||
#define EVENTFD 1
|
||||
#define SELFPIPE 0
|
||||
#define WINEVENT 0
|
||||
#endif
|
||||
#if (LINUX && !EVENTFD) || OSX || FREEBSD
|
||||
#define EVENTFD 0
|
||||
#define SELFPIPE 1
|
||||
#define WINEVENT 0
|
||||
#endif
|
||||
#if WIN
|
||||
#define EVENTFD 0
|
||||
#define SELFPIPE 0
|
||||
#define WINEVENT 1
|
||||
#endif
|
||||
#else
|
||||
#define EVENTFD 0
|
||||
#define SELFPIPE 0
|
||||
#define WINEVENT 0
|
||||
#undef LOOPBACK
|
||||
#define LOOPBACK 1
|
||||
#endif
|
||||
|
||||
#if defined(RESAMPLE) || defined(RESAMPLE_MP)
|
||||
#undef RESAMPLE
|
||||
#define RESAMPLE 1 // resampling
|
||||
#define PROCESS 1 // any sample processing (only resampling at present)
|
||||
#elif defined(RESAMPLE16)
|
||||
#undef RESAMPLE16
|
||||
#define RESAMPLE16 1
|
||||
#define PROCESS 1
|
||||
#else
|
||||
#define RESAMPLE 0
|
||||
#define PROCESS 0
|
||||
#endif
|
||||
#if defined(RESAMPLE_MP)
|
||||
#undef RESAMPLE_MP
|
||||
#define RESAMPLE_MP 1
|
||||
#else
|
||||
#define RESAMPLE_MP 0
|
||||
#endif
|
||||
|
||||
#if defined(FFMPEG)
|
||||
#undef FFMPEG
|
||||
#define FFMPEG 1
|
||||
#else
|
||||
#define FFMPEG 0
|
||||
#endif
|
||||
|
||||
#if (LINUX || OSX) && defined(VISEXPORT)
|
||||
#undef VISEXPORT
|
||||
#define VISEXPORT 1 // visulizer export support uses linux shared memory
|
||||
#else
|
||||
#define VISEXPORT 0
|
||||
#endif
|
||||
|
||||
#if LINUX && defined(IR)
|
||||
#undef IR
|
||||
#define IR 1
|
||||
#else
|
||||
#define IR 0
|
||||
#endif
|
||||
|
||||
#if defined(DSD)
|
||||
#undef DSD
|
||||
#define DSD 1
|
||||
#define IF_DSD(x) { x }
|
||||
#else
|
||||
#undef DSD
|
||||
#define DSD 0
|
||||
#define IF_DSD(x)
|
||||
#endif
|
||||
|
||||
#if defined(LINKALL)
|
||||
#undef LINKALL
|
||||
#define LINKALL 1 // link all libraries at build time - requires all to be available at run time
|
||||
#else
|
||||
#define LINKALL 0
|
||||
#endif
|
||||
|
||||
#if defined (USE_SSL)
|
||||
#undef USE_SSL
|
||||
#define USE_SSL 1
|
||||
#else
|
||||
#define USE_SSL 0
|
||||
#endif
|
||||
|
||||
|
||||
#if !LINKALL
|
||||
|
||||
// dynamically loaded libraries at run time
|
||||
|
||||
#if LINUX
|
||||
#define LIBFLAC "libFLAC.so.8"
|
||||
#define LIBMAD "libmad.so.0"
|
||||
#define LIBMPG "libmpg123.so.0"
|
||||
#define LIBVORBIS "libvorbisfile.so.3"
|
||||
#define LIBTREMOR "libvorbisidec.so.1"
|
||||
#define LIBFAAD "libfaad.so.2"
|
||||
#define LIBAVUTIL "libavutil.so.%d"
|
||||
#define LIBAVCODEC "libavcodec.so.%d"
|
||||
#define LIBAVFORMAT "libavformat.so.%d"
|
||||
#define LIBSOXR "libsoxr.so.0"
|
||||
#define LIBLIRC "liblirc_client.so.0"
|
||||
#endif
|
||||
|
||||
#if OSX
|
||||
#define LIBFLAC "libFLAC.8.dylib"
|
||||
#define LIBMAD "libmad.0.dylib"
|
||||
#define LIBMPG "libmpg123.0.dylib"
|
||||
#define LIBVORBIS "libvorbisfile.3.dylib"
|
||||
#define LIBTREMOR "libvorbisidec.1.dylib"
|
||||
#define LIBFAAD "libfaad.2.dylib"
|
||||
#define LIBAVUTIL "libavutil.%d.dylib"
|
||||
#define LIBAVCODEC "libavcodec.%d.dylib"
|
||||
#define LIBAVFORMAT "libavformat.%d.dylib"
|
||||
#define LIBSOXR "libsoxr.0.dylib"
|
||||
#endif
|
||||
|
||||
#if WIN
|
||||
#define LIBFLAC "libFLAC.dll"
|
||||
#define LIBMAD "libmad-0.dll"
|
||||
#define LIBMPG "libmpg123-0.dll"
|
||||
#define LIBVORBIS "libvorbisfile.dll"
|
||||
#define LIBTREMOR "libvorbisidec.dll"
|
||||
#define LIBFAAD "libfaad2.dll"
|
||||
#define LIBAVUTIL "avutil-%d.dll"
|
||||
#define LIBAVCODEC "avcodec-%d.dll"
|
||||
#define LIBAVFORMAT "avformat-%d.dll"
|
||||
#define LIBSOXR "libsoxr.dll"
|
||||
#endif
|
||||
|
||||
#if FREEBSD
|
||||
#define LIBFLAC "libFLAC.so.11"
|
||||
#define LIBMAD "libmad.so.2"
|
||||
#define LIBMPG "libmpg123.so.0"
|
||||
#define LIBVORBIS "libvorbisfile.so.6"
|
||||
#define LIBTREMOR "libvorbisidec.so.1"
|
||||
#define LIBFAAD "libfaad.so.2"
|
||||
#define LIBAVUTIL "libavutil.so.%d"
|
||||
#define LIBAVCODEC "libavcodec.so.%d"
|
||||
#define LIBAVFORMAT "libavformat.so.%d"
|
||||
#endif
|
||||
|
||||
#endif // !LINKALL
|
||||
|
||||
// config options
|
||||
#define STREAMBUF_SIZE (2 * 1024 * 1024)
|
||||
#define OUTPUTBUF_SIZE (44100 * 8 * 10)
|
||||
#define OUTPUTBUF_SIZE_CROSSFADE (OUTPUTBUF_SIZE * 12 / 10)
|
||||
|
||||
#define MAX_HEADER 4096 // do not reduce as icy-meta max is 4080
|
||||
|
||||
#if ALSA
|
||||
#define ALSA_BUFFER_TIME 40
|
||||
#define ALSA_PERIOD_COUNT 4
|
||||
#define OUTPUT_RT_PRIORITY 45
|
||||
#endif
|
||||
|
||||
#define SL_LITTLE_ENDIAN (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
||||
|
||||
#if SUN || OSXPPC
|
||||
#undef SL_LITTLE_ENDIAN
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#if EMBEDDED
|
||||
#include "embedded.h"
|
||||
#endif
|
||||
|
||||
#if LINUX || OSX || FREEBSD || EMBEDDED
|
||||
#include <unistd.h>
|
||||
#include <stdbool.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/poll.h>
|
||||
#if !LINKALL
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#if SUN
|
||||
#include <sys/types.h>
|
||||
#endif /* SUN */
|
||||
|
||||
#define STREAM_THREAD_STACK_SIZE 8 * 1024
|
||||
#define DECODE_THREAD_STACK_SIZE 20 * 1024
|
||||
#define OUTPUT_THREAD_STACK_SIZE 8 * 1024
|
||||
#define IR_THREAD_STACK_SIZE 8 * 1024
|
||||
#if !OSX
|
||||
#define thread_t pthread_t;
|
||||
#endif
|
||||
#define closesocket(s) close(s)
|
||||
#define last_error() errno
|
||||
#define ERROR_WOULDBLOCK EWOULDBLOCK
|
||||
|
||||
#if !EMBEDDED
|
||||
#ifdef SUN
|
||||
typedef uint8_t u8_t;
|
||||
typedef uint16_t u16_t;
|
||||
typedef uint32_t u32_t;
|
||||
typedef uint64_t u64_t;
|
||||
#else
|
||||
typedef u_int8_t u8_t;
|
||||
typedef u_int16_t u16_t;
|
||||
typedef u_int32_t u32_t;
|
||||
typedef u_int64_t u64_t;
|
||||
#endif /* SUN */
|
||||
typedef int16_t s16_t;
|
||||
typedef int32_t s32_t;
|
||||
typedef int64_t s64_t;
|
||||
#endif
|
||||
|
||||
#define mutex_type pthread_mutex_t
|
||||
#define mutex_create(m) pthread_mutex_init(&m, NULL)
|
||||
#if HAS_MUTEX_CREATE_P
|
||||
#define mutex_create_p(m) pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(&m, &attr); pthread_mutexattr_destroy(&attr)
|
||||
#else
|
||||
#define mutex_create_p(m) mutex_create(m)
|
||||
#endif
|
||||
#define mutex_lock(m) pthread_mutex_lock(&m)
|
||||
#define mutex_unlock(m) pthread_mutex_unlock(&m)
|
||||
#define mutex_destroy(m) pthread_mutex_destroy(&m)
|
||||
#define thread_type pthread_t
|
||||
#endif
|
||||
|
||||
#if WIN
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <io.h>
|
||||
|
||||
#define STREAM_THREAD_STACK_SIZE (1024 * 64)
|
||||
#define DECODE_THREAD_STACK_SIZE (1024 * 128)
|
||||
#define OUTPUT_THREAD_STACK_SIZE (1024 * 64)
|
||||
|
||||
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;
|
||||
|
||||
typedef BOOL bool;
|
||||
#define true TRUE
|
||||
#define false FALSE
|
||||
|
||||
#define inline __inline
|
||||
|
||||
#define mutex_type HANDLE
|
||||
#define mutex_create(m) m = CreateMutex(NULL, FALSE, NULL)
|
||||
#define mutex_create_p mutex_create
|
||||
#define mutex_lock(m) WaitForSingleObject(m, INFINITE)
|
||||
#define mutex_unlock(m) ReleaseMutex(m)
|
||||
#define mutex_destroy(m) CloseHandle(m)
|
||||
#define thread_type HANDLE
|
||||
|
||||
#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 snprintf _snprintf
|
||||
|
||||
#define in_addr_t u32_t
|
||||
#define socklen_t int
|
||||
#define ssize_t int
|
||||
|
||||
#define RTLD_NOW 0
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined(MSG_NOSIGNAL)
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
|
||||
typedef u32_t frames_t;
|
||||
typedef int sockfd;
|
||||
|
||||
#if EVENTFD
|
||||
#include <sys/eventfd.h>
|
||||
#define event_event int
|
||||
#define event_handle struct pollfd
|
||||
#define wake_create(e) e = eventfd(0, 0)
|
||||
#define wake_signal(e) eventfd_write(e, 1)
|
||||
#define wake_clear(e) eventfd_t val; eventfd_read(e, &val)
|
||||
#define wake_close(e) close(e)
|
||||
#endif
|
||||
|
||||
#if SELFPIPE
|
||||
#define event_handle struct pollfd
|
||||
#define event_event struct wake
|
||||
#define wake_create(e) pipe(e.fds); set_nonblock(e.fds[0]); set_nonblock(e.fds[1])
|
||||
#define wake_signal(e) write(e.fds[1], ".", 1)
|
||||
#define wake_clear(e) char c[10]; read(e, &c, 10)
|
||||
#define wake_close(e) close(e.fds[0]); close(e.fds[1])
|
||||
struct wake {
|
||||
int fds[2];
|
||||
};
|
||||
#endif
|
||||
|
||||
#if LOOPBACK
|
||||
#define event_handle struct pollfd
|
||||
#define event_event struct wake
|
||||
#define wake_create(e) _wake_create(&e)
|
||||
#define wake_signal(e) send(e.fds[1], ".", 1, 0)
|
||||
#define wake_clear(e) char c; recv(e, &c, 1, 0)
|
||||
#define wake_close(e) closesocket(e.mfds); closesocket(e.fds[0]); closesocket(e.fds[1])
|
||||
struct wake {
|
||||
int mfds;
|
||||
int fds[2];
|
||||
};
|
||||
void _wake_create(event_event*);
|
||||
#endif
|
||||
|
||||
#if WINEVENT
|
||||
#define event_event HANDLE
|
||||
#define event_handle HANDLE
|
||||
#define wake_create(e) e = CreateEvent(NULL, FALSE, FALSE, NULL)
|
||||
#define wake_signal(e) SetEvent(e)
|
||||
#define wake_close(e) CloseHandle(e)
|
||||
#endif
|
||||
|
||||
// printf/scanf formats for u64_t
|
||||
#if (LINUX && __WORDSIZE == 64) || (FREEBSD && __LP64__)
|
||||
#define FMT_u64 "%lu"
|
||||
#define FMT_x64 "%lx"
|
||||
#elif __GLIBC_HAVE_LONG_LONG || defined __GNUC__ || WIN || SUN
|
||||
#define FMT_u64 "%llu"
|
||||
#define FMT_x64 "%llx"
|
||||
#else
|
||||
#error can not support u64_t
|
||||
#endif
|
||||
|
||||
#define MAX_SILENCE_FRAMES 2048
|
||||
|
||||
#define FIXED_ONE 0x10000
|
||||
|
||||
#ifndef BYTES_PER_FRAME
|
||||
#define BYTES_PER_FRAME 8
|
||||
#endif
|
||||
|
||||
#if BYTES_PER_FRAME == 8
|
||||
#define ISAMPLE_T s32_t
|
||||
#else
|
||||
#define ISAMPLE_T s16_t
|
||||
#endif
|
||||
|
||||
#define min(a,b) (((a) < (b)) ? (a) : (b))
|
||||
|
||||
// logging
|
||||
typedef enum { lERROR = 0, lWARN, lINFO, lDEBUG, lSDEBUG } log_level;
|
||||
|
||||
const char *logtime(void);
|
||||
void logprint(const char *fmt, ...);
|
||||
|
||||
#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__)
|
||||
|
||||
// utils.c (non logging)
|
||||
typedef enum { EVENT_TIMEOUT = 0, EVENT_READ, EVENT_WAKE } event_type;
|
||||
#if WIN && USE_SSL
|
||||
char* strcasestr(const char *haystack, const char *needle);
|
||||
#endif
|
||||
|
||||
char *next_param(char *src, char c);
|
||||
u32_t gettime_ms(void);
|
||||
void get_mac(u8_t *mac);
|
||||
void set_nonblock(sockfd s);
|
||||
int connect_timeout(sockfd sock, const struct sockaddr *addr, socklen_t addrlen, int timeout);
|
||||
void server_addr(char *server, in_addr_t *ip_ptr, unsigned *port_ptr);
|
||||
void set_readwake_handles(event_handle handles[], sockfd s, event_event e);
|
||||
event_type wait_readwake(event_handle handles[], int timeout);
|
||||
void packN(u32_t *dest, u32_t val);
|
||||
void packn(u16_t *dest, u16_t val);
|
||||
u32_t unpackN(u32_t *src);
|
||||
u16_t unpackn(u16_t *src);
|
||||
#if OSX
|
||||
void set_nosigpipe(sockfd s);
|
||||
#else
|
||||
#define set_nosigpipe(s)
|
||||
#endif
|
||||
#if SUN
|
||||
void init_daemonize(void);
|
||||
int daemon(int,int);
|
||||
#endif
|
||||
#if WIN
|
||||
void winsock_init(void);
|
||||
void winsock_close(void);
|
||||
void *dlopen(const char *filename, int flag);
|
||||
void *dlsym(void *handle, const char *symbol);
|
||||
char *dlerror(void);
|
||||
int poll(struct pollfd *fds, unsigned long numfds, int timeout);
|
||||
#endif
|
||||
#if LINUX || FREEBSD
|
||||
void touch_memory(u8_t *buf, size_t size);
|
||||
#endif
|
||||
|
||||
// buffer.c
|
||||
struct buffer {
|
||||
u8_t *buf;
|
||||
u8_t *readp;
|
||||
u8_t *writep;
|
||||
u8_t *wrap;
|
||||
size_t size;
|
||||
size_t base_size;
|
||||
mutex_type mutex;
|
||||
};
|
||||
|
||||
// _* called with mutex locked
|
||||
unsigned _buf_used(struct buffer *buf);
|
||||
unsigned _buf_space(struct buffer *buf);
|
||||
unsigned _buf_cont_read(struct buffer *buf);
|
||||
unsigned _buf_cont_write(struct buffer *buf);
|
||||
void _buf_inc_readp(struct buffer *buf, unsigned by);
|
||||
void _buf_inc_writep(struct buffer *buf, unsigned by);
|
||||
void buf_flush(struct buffer *buf);
|
||||
void buf_adjust(struct buffer *buf, size_t mod);
|
||||
void _buf_resize(struct buffer *buf, size_t size);
|
||||
void buf_init(struct buffer *buf, size_t size);
|
||||
void buf_destroy(struct buffer *buf);
|
||||
|
||||
// slimproto.c
|
||||
void slimproto(log_level level, char *server, u8_t mac[6], const char *name, const char *namefile, const char *modelname, int maxSampleRate);
|
||||
void slimproto_stop(void);
|
||||
void wake_controller(void);
|
||||
|
||||
// stream.c
|
||||
typedef enum { STOPPED = 0, DISCONNECT, STREAMING_WAIT,
|
||||
STREAMING_BUFFERING, STREAMING_FILE, STREAMING_HTTP, SEND_HEADERS, RECV_HEADERS } stream_state;
|
||||
typedef enum { DISCONNECT_OK = 0, LOCAL_DISCONNECT = 1, REMOTE_DISCONNECT = 2, UNREACHABLE = 3, TIMEOUT = 4 } disconnect_code;
|
||||
|
||||
struct streamstate {
|
||||
stream_state state;
|
||||
disconnect_code disconnect;
|
||||
char *header;
|
||||
size_t header_len;
|
||||
bool sent_headers;
|
||||
bool cont_wait;
|
||||
u64_t bytes;
|
||||
unsigned threshold;
|
||||
u32_t meta_interval;
|
||||
u32_t meta_next;
|
||||
u32_t meta_left;
|
||||
bool meta_send;
|
||||
};
|
||||
|
||||
void stream_init(log_level level, unsigned stream_buf_size);
|
||||
void stream_close(void);
|
||||
void stream_file(const char *header, size_t header_len, unsigned threshold);
|
||||
void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait);
|
||||
bool stream_disconnect(void);
|
||||
|
||||
// decode.c
|
||||
typedef enum { DECODE_STOPPED = 0, DECODE_READY, DECODE_RUNNING, DECODE_COMPLETE, DECODE_ERROR } decode_state;
|
||||
|
||||
struct decodestate {
|
||||
decode_state state;
|
||||
bool new_stream;
|
||||
mutex_type mutex;
|
||||
#if PROCESS
|
||||
bool direct;
|
||||
bool process;
|
||||
#endif
|
||||
};
|
||||
|
||||
#if PROCESS
|
||||
struct processstate {
|
||||
u8_t *inbuf, *outbuf;
|
||||
unsigned max_in_frames, max_out_frames;
|
||||
unsigned in_frames, out_frames;
|
||||
unsigned in_sample_rate, out_sample_rate;
|
||||
unsigned long total_in, total_out;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct codec {
|
||||
char id;
|
||||
char *types;
|
||||
unsigned min_read_bytes;
|
||||
unsigned min_space;
|
||||
void (*open)(u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness);
|
||||
void (*close)(void);
|
||||
decode_state (*decode)(void);
|
||||
};
|
||||
|
||||
void decode_init(log_level level, const char *include_codecs, const char *exclude_codecs);
|
||||
void decode_close(void);
|
||||
void decode_flush(void);
|
||||
unsigned decode_newstream(unsigned sample_rate, unsigned supported_rates[]);
|
||||
void codec_open(u8_t format, u8_t sample_size, u8_t sample_rate, u8_t channels, u8_t endianness);
|
||||
|
||||
#if PROCESS
|
||||
// process.c
|
||||
void process_samples(void);
|
||||
void process_drain(void);
|
||||
void process_flush(void);
|
||||
unsigned process_newstream(bool *direct, unsigned raw_sample_rate, unsigned supported_rates[]);
|
||||
void process_init(char *opt);
|
||||
#endif
|
||||
|
||||
#if RESAMPLE || RESAMPLE16
|
||||
// resample.c
|
||||
void resample_samples(struct processstate *process);
|
||||
bool resample_drain(struct processstate *process);
|
||||
bool resample_newstream(struct processstate *process, unsigned raw_sample_rate, unsigned supported_rates[]);
|
||||
void resample_flush(void);
|
||||
bool resample_init(char *opt);
|
||||
#endif
|
||||
|
||||
// output.c output_alsa.c output_pa.c output_pack.c
|
||||
typedef enum { OUTPUT_OFF = -1, OUTPUT_STOPPED = 0, OUTPUT_BUFFER, OUTPUT_RUNNING,
|
||||
OUTPUT_PAUSE_FRAMES, OUTPUT_SKIP_FRAMES, OUTPUT_START_AT } output_state;
|
||||
|
||||
#if DSD
|
||||
typedef enum { PCM, DOP, DSD_U8, DSD_U16_LE, DSD_U32_LE, DSD_U16_BE, DSD_U32_BE, DOP_S24_LE, DOP_S24_3LE } dsd_format;
|
||||
typedef enum { S32_LE, S24_LE, S24_3LE, S16_LE, U8, U16_LE, U16_BE, U32_LE, U32_BE } output_format;
|
||||
#else
|
||||
typedef enum { S32_LE, S24_LE, S24_3LE, S16_LE, S24_BE, S24_3BE, S16_BE, S8_BE } output_format;
|
||||
#endif
|
||||
|
||||
typedef enum { FADE_INACTIVE = 0, FADE_DUE, FADE_ACTIVE } fade_state;
|
||||
typedef enum { FADE_UP = 1, FADE_DOWN, FADE_CROSS } fade_dir;
|
||||
typedef enum { FADE_NONE = 0, FADE_CROSSFADE, FADE_IN, FADE_OUT, FADE_INOUT } fade_mode;
|
||||
|
||||
#define MAX_SUPPORTED_SAMPLERATES 18
|
||||
#define TEST_RATES = { 768000, 705600, 384000, 352800, 192000, 176400, 96000, 88200, 48000, 44100, 32000, 24000, 22500, 16000, 12000, 11025, 8000, 0 }
|
||||
|
||||
struct outputstate {
|
||||
output_state state;
|
||||
output_format format;
|
||||
const char *device;
|
||||
#if ALSA
|
||||
unsigned buffer;
|
||||
unsigned period;
|
||||
#endif
|
||||
bool track_started;
|
||||
#if PORTAUDIO
|
||||
bool pa_reopen;
|
||||
unsigned latency;
|
||||
int pa_hostapi_option;
|
||||
#endif
|
||||
int (* write_cb)(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR, s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr);
|
||||
unsigned start_frames;
|
||||
unsigned frames_played;
|
||||
unsigned frames_played_dmp;// frames played at the point delay is measured
|
||||
unsigned current_sample_rate;
|
||||
unsigned supported_rates[MAX_SUPPORTED_SAMPLERATES]; // ordered largest first so [0] is max_rate
|
||||
unsigned default_sample_rate;
|
||||
bool error_opening;
|
||||
unsigned device_frames;
|
||||
u32_t updated;
|
||||
u32_t track_start_time;
|
||||
u32_t current_replay_gain;
|
||||
union {
|
||||
u32_t pause_frames;
|
||||
u32_t skip_frames;
|
||||
u32_t start_at;
|
||||
};
|
||||
unsigned next_sample_rate; // set in decode thread
|
||||
u8_t *track_start; // set in decode thread
|
||||
u32_t gainL; // set by slimproto
|
||||
u32_t gainR; // set by slimproto
|
||||
bool invert; // set by slimproto
|
||||
u32_t next_replay_gain; // set by slimproto
|
||||
unsigned threshold; // set by slimproto
|
||||
fade_state fade;
|
||||
u8_t *fade_start;
|
||||
u8_t *fade_end;
|
||||
fade_dir fade_dir;
|
||||
fade_mode fade_mode; // set by slimproto
|
||||
unsigned fade_secs; // set by slimproto
|
||||
unsigned rate_delay;
|
||||
bool delay_active;
|
||||
u32_t stop_time;
|
||||
u32_t idle_to;
|
||||
#if DSD
|
||||
dsd_format next_fmt; // set in decode thread
|
||||
dsd_format outfmt;
|
||||
dsd_format dsdfmt; // set in dsd_init - output for DSD: DOP, DSD_U8, ...
|
||||
unsigned dsd_delay; // set in dsd_init - delay in ms switching to/from dop
|
||||
#endif
|
||||
};
|
||||
|
||||
void output_init_common(log_level level, const char *device, unsigned output_buf_size, unsigned rates[], unsigned idle);
|
||||
void output_close_common(void);
|
||||
void output_flush(void);
|
||||
// _* called with mutex locked
|
||||
frames_t _output_frames(frames_t avail);
|
||||
void _checkfade(bool);
|
||||
|
||||
// output_alsa.c
|
||||
#if ALSA
|
||||
void list_devices(void);
|
||||
void list_mixers(const char *output_device);
|
||||
void set_volume(unsigned left, unsigned right);
|
||||
bool test_open(const char *device, unsigned rates[], bool userdef_rates);
|
||||
void output_init_alsa(log_level level, const char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned rt_priority, unsigned idle, char *mixer_device, char *volume_mixer, bool mixer_unmute, bool mixer_linear);
|
||||
void output_close_alsa(void);
|
||||
#endif
|
||||
|
||||
// output_pa.c
|
||||
#if PORTAUDIO
|
||||
void list_devices(void);
|
||||
void set_volume(unsigned left, unsigned right);
|
||||
bool test_open(const char *device, unsigned rates[], bool userdef_rates);
|
||||
void output_init_pa(log_level level, const char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle);
|
||||
void output_close_pa(void);
|
||||
void _pa_open(void);
|
||||
#endif
|
||||
|
||||
// output_embedded.c
|
||||
#if EMBEDDED
|
||||
void set_volume(unsigned left, unsigned right);
|
||||
bool test_open(const char *device, unsigned rates[], bool userdef_rates);
|
||||
void output_init_embedded(log_level level, char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle);
|
||||
void output_close_embedded(void);
|
||||
#else
|
||||
// output_stdout.c
|
||||
void output_init_stdout(log_level level, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay);
|
||||
void output_close_stdout(void);
|
||||
#endif
|
||||
|
||||
// output_pack.c
|
||||
void _scale_and_pack_frames(void *outputptr, s32_t *inputptr, frames_t cnt, s32_t gainL, s32_t gainR, output_format format);
|
||||
void _apply_cross(struct buffer *outputbuf, frames_t out_frames, s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr);
|
||||
void _apply_gain(struct buffer *outputbuf, frames_t count, s32_t gainL, s32_t gainR);
|
||||
s32_t gain(s32_t gain, s32_t sample);
|
||||
s32_t to_gain(float f);
|
||||
|
||||
// output_vis.c
|
||||
#if VISEXPORT
|
||||
void _vis_export(struct buffer *outputbuf, struct outputstate *output, frames_t out_frames, bool silence);
|
||||
void output_vis_init(log_level level, u8_t *mac);
|
||||
void vis_stop(void);
|
||||
#else
|
||||
#define _vis_export(...)
|
||||
#define vis_stop()
|
||||
#endif
|
||||
|
||||
// dop.c
|
||||
#if DSD
|
||||
bool is_stream_dop(u8_t *lptr, u8_t *rptr, int step, frames_t frames);
|
||||
void update_dop(u32_t *ptr, frames_t frames, bool invert);
|
||||
void dsd_silence_frames(u32_t *ptr, frames_t frames);
|
||||
void dsd_invert(u32_t *ptr, frames_t frames);
|
||||
void dsd_init(dsd_format format, unsigned delay);
|
||||
#endif
|
||||
|
||||
// codecs
|
||||
#define MAX_CODECS 9
|
||||
|
||||
struct codec *register_flac(void);
|
||||
struct codec *register_pcm(void);
|
||||
struct codec *register_mad(void);
|
||||
struct codec *register_mpg(void);
|
||||
struct codec *register_vorbis(void);
|
||||
struct codec *register_faad(void);
|
||||
struct codec *register_helixaac(void);
|
||||
struct codec *register_dsd(void);
|
||||
struct codec *register_alac(void);
|
||||
struct codec *register_ff(const char *codec);
|
||||
|
||||
//gpio.c
|
||||
#if GPIO
|
||||
void relay( int state);
|
||||
void relay_script(int state);
|
||||
int gpio_pin;
|
||||
bool gpio_active_low;
|
||||
bool gpio_active;
|
||||
char *power_script;
|
||||
// my amp state
|
||||
int ampstate;
|
||||
#endif
|
||||
|
||||
// ir.c
|
||||
#if IR
|
||||
struct irstate {
|
||||
mutex_type mutex;
|
||||
u32_t code;
|
||||
u32_t ts;
|
||||
};
|
||||
|
||||
void ir_init(log_level level, char *lircrc);
|
||||
void ir_close(void);
|
||||
#endif
|
||||
|
||||
// sslsym.c
|
||||
#if USE_SSL && !LINKALL
|
||||
bool load_ssl_symbols(void);
|
||||
void free_ssl_symbols(void);
|
||||
bool ssl_loaded;
|
||||
#endif
|
||||
|
||||
593
components/squeezelite/stream.c
Normal file
593
components/squeezelite/stream.c
Normal file
@@ -0,0 +1,593 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// stream thread
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
#if USE_SSL
|
||||
#include "openssl/ssl.h"
|
||||
#include "openssl/err.h"
|
||||
#endif
|
||||
|
||||
#if SUN
|
||||
#include <signal.h>
|
||||
#endif
|
||||
static log_level loglevel;
|
||||
|
||||
static struct buffer buf;
|
||||
struct buffer *streambuf = &buf;
|
||||
|
||||
#define LOCK mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK mutex_unlock(streambuf->mutex)
|
||||
|
||||
static sockfd fd;
|
||||
|
||||
struct streamstate stream;
|
||||
|
||||
#if USE_SSL
|
||||
static SSL_CTX *SSLctx;
|
||||
SSL *ssl;
|
||||
#endif
|
||||
|
||||
#if !USE_SSL
|
||||
#define _recv(ssl, fc, buf, n, opt) recv(fd, buf, n, opt)
|
||||
#define _send(ssl, fd, buf, n, opt) send(fd, buf, n, opt)
|
||||
#define _poll(ssl, pollinfo, timeout) poll(pollinfo, 1, timeout)
|
||||
#define _last_error() last_error()
|
||||
#else
|
||||
#define _last_error() ERROR_WOULDBLOCK
|
||||
|
||||
static int _recv(SSL *ssl, int fd, void *buffer, size_t bytes, int options) {
|
||||
int n;
|
||||
if (!ssl) return recv(fd, buffer, bytes, options);
|
||||
n = SSL_read(ssl, (u8_t*) buffer, bytes);
|
||||
if (n <= 0 && SSL_get_error(ssl, n) == SSL_ERROR_ZERO_RETURN) return 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
static int _send(SSL *ssl, int fd, void *buffer, size_t bytes, int options) {
|
||||
int n;
|
||||
if (!ssl) return send(fd, buffer, bytes, options);
|
||||
while (1) {
|
||||
int err;
|
||||
ERR_clear_error();
|
||||
if ((n = SSL_write(ssl, (u8_t*) buffer, bytes)) >= 0) return n;
|
||||
err = SSL_get_error(ssl, n);
|
||||
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue;
|
||||
LOG_INFO("SSL write error %d", err );
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
can't mimic exactly poll as SSL is a real pain. Even if SSL_pending returns
|
||||
0, there might be bytes to read but when select (poll) return > 0, there might
|
||||
be no frame available. As well select (poll) < 0 does not mean that there is
|
||||
no data pending
|
||||
*/
|
||||
static int _poll(SSL *ssl, struct pollfd *pollinfo, int timeout) {
|
||||
if (!ssl) return poll(pollinfo, 1, timeout);
|
||||
if (pollinfo->events & POLLIN && SSL_pending(ssl)) {
|
||||
if (pollinfo->events & POLLOUT) poll(pollinfo, 1, 0);
|
||||
pollinfo->revents = POLLIN;
|
||||
return 1;
|
||||
}
|
||||
return poll(pollinfo, 1, timeout);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
static bool send_header(void) {
|
||||
char *ptr = stream.header;
|
||||
int len = stream.header_len;
|
||||
|
||||
unsigned try = 0;
|
||||
ssize_t n;
|
||||
|
||||
while (len) {
|
||||
n = _send(ssl, fd, ptr, len, MSG_NOSIGNAL);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && _last_error() == ERROR_WOULDBLOCK && try < 10) {
|
||||
LOG_SDEBUG("retrying (%d) writing to socket", ++try);
|
||||
usleep(1000);
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("failed writing to socket: %s", strerror(last_error()));
|
||||
stream.disconnect = LOCAL_DISCONNECT;
|
||||
stream.state = DISCONNECT;
|
||||
wake_controller();
|
||||
return false;
|
||||
}
|
||||
LOG_SDEBUG("wrote %d bytes to socket", n);
|
||||
ptr += n;
|
||||
len -= n;
|
||||
}
|
||||
LOG_SDEBUG("wrote header");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool running = true;
|
||||
|
||||
static void _disconnect(stream_state state, disconnect_code disconnect) {
|
||||
stream.state = state;
|
||||
stream.disconnect = disconnect;
|
||||
#if USE_SSL
|
||||
if (ssl) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
ssl = NULL;
|
||||
}
|
||||
#endif
|
||||
closesocket(fd);
|
||||
fd = -1;
|
||||
wake_controller();
|
||||
}
|
||||
|
||||
static void *stream_thread() {
|
||||
|
||||
while (running) {
|
||||
|
||||
struct pollfd pollinfo;
|
||||
size_t space;
|
||||
|
||||
LOCK;
|
||||
|
||||
space = min(_buf_space(streambuf), _buf_cont_write(streambuf));
|
||||
|
||||
if (fd < 0 || !space || stream.state <= STREAMING_WAIT) {
|
||||
UNLOCK;
|
||||
usleep(space ? 100000 : 25000);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stream.state == STREAMING_FILE) {
|
||||
|
||||
int n = read(fd, streambuf->writep, space);
|
||||
if (n == 0) {
|
||||
LOG_INFO("end of stream");
|
||||
_disconnect(DISCONNECT, DISCONNECT_OK);
|
||||
}
|
||||
if (n > 0) {
|
||||
_buf_inc_writep(streambuf, n);
|
||||
stream.bytes += n;
|
||||
LOG_SDEBUG("streambuf read %d bytes", n);
|
||||
}
|
||||
if (n < 0) {
|
||||
LOG_WARN("error reading: %s", strerror(last_error()));
|
||||
_disconnect(DISCONNECT, REMOTE_DISCONNECT);
|
||||
}
|
||||
|
||||
UNLOCK;
|
||||
continue;
|
||||
|
||||
} else {
|
||||
|
||||
pollinfo.fd = fd;
|
||||
pollinfo.events = POLLIN;
|
||||
if (stream.state == SEND_HEADERS) {
|
||||
pollinfo.events |= POLLOUT;
|
||||
}
|
||||
}
|
||||
|
||||
UNLOCK;
|
||||
|
||||
if (_poll(ssl, &pollinfo, 100)) {
|
||||
|
||||
LOCK;
|
||||
|
||||
// check socket has not been closed while in poll
|
||||
if (fd < 0) {
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((pollinfo.revents & POLLOUT) && stream.state == SEND_HEADERS) {
|
||||
if (send_header()) stream.state = RECV_HEADERS;
|
||||
stream.header_len = 0;
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pollinfo.revents & (POLLIN | POLLHUP)) {
|
||||
|
||||
// get response headers
|
||||
if (stream.state == RECV_HEADERS) {
|
||||
|
||||
// read one byte at a time to catch end of header
|
||||
char c;
|
||||
static int endtok;
|
||||
|
||||
int n = _recv(ssl, fd, &c, 1, 0);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && _last_error() == ERROR_WOULDBLOCK) {
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("error reading headers: %s", n ? strerror(last_error()) : "closed");
|
||||
_disconnect(STOPPED, LOCAL_DISCONNECT);
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
|
||||
*(stream.header + stream.header_len) = c;
|
||||
stream.header_len++;
|
||||
|
||||
if (stream.header_len > MAX_HEADER - 1) {
|
||||
LOG_ERROR("received headers too long: %u", stream.header_len);
|
||||
_disconnect(DISCONNECT, LOCAL_DISCONNECT);
|
||||
}
|
||||
|
||||
if (stream.header_len > 1 && (c == '\r' || c == '\n')) {
|
||||
endtok++;
|
||||
if (endtok == 4) {
|
||||
*(stream.header + stream.header_len) = '\0';
|
||||
LOG_INFO("headers: len: %d\n%s", stream.header_len, stream.header);
|
||||
stream.state = stream.cont_wait ? STREAMING_WAIT : STREAMING_BUFFERING;
|
||||
wake_controller();
|
||||
}
|
||||
} else {
|
||||
endtok = 0;
|
||||
}
|
||||
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
|
||||
// receive icy meta data
|
||||
if (stream.meta_interval && stream.meta_next == 0) {
|
||||
|
||||
if (stream.meta_left == 0) {
|
||||
// read meta length
|
||||
u8_t c;
|
||||
int n = _recv(ssl, fd, &c, 1, 0);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && _last_error() == ERROR_WOULDBLOCK) {
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("error reading icy meta: %s", n ? strerror(last_error()) : "closed");
|
||||
_disconnect(STOPPED, LOCAL_DISCONNECT);
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
stream.meta_left = 16 * c;
|
||||
stream.header_len = 0; // amount of received meta data
|
||||
// MAX_HEADER must be more than meta max of 16 * 255
|
||||
}
|
||||
|
||||
if (stream.meta_left) {
|
||||
int n = _recv(ssl, fd, stream.header + stream.header_len, stream.meta_left, 0);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && _last_error() == ERROR_WOULDBLOCK) {
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("error reading icy meta: %s", n ? strerror(last_error()) : "closed");
|
||||
_disconnect(STOPPED, LOCAL_DISCONNECT);
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
stream.meta_left -= n;
|
||||
stream.header_len += n;
|
||||
}
|
||||
|
||||
if (stream.meta_left == 0) {
|
||||
if (stream.header_len) {
|
||||
*(stream.header + stream.header_len) = '\0';
|
||||
LOG_INFO("icy meta: len: %u\n%s", stream.header_len, stream.header);
|
||||
stream.meta_send = true;
|
||||
wake_controller();
|
||||
}
|
||||
stream.meta_next = stream.meta_interval;
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
|
||||
// stream body into streambuf
|
||||
} else {
|
||||
int n;
|
||||
|
||||
space = min(_buf_space(streambuf), _buf_cont_write(streambuf));
|
||||
if (stream.meta_interval) {
|
||||
space = min(space, stream.meta_next);
|
||||
}
|
||||
|
||||
n = _recv(ssl, fd, streambuf->writep, space, 0);
|
||||
if (n == 0) {
|
||||
LOG_INFO("end of stream");
|
||||
_disconnect(DISCONNECT, DISCONNECT_OK);
|
||||
}
|
||||
if (n < 0 && _last_error() != ERROR_WOULDBLOCK) {
|
||||
LOG_INFO("error reading: %s", strerror(last_error()));
|
||||
_disconnect(DISCONNECT, REMOTE_DISCONNECT);
|
||||
}
|
||||
|
||||
if (n > 0) {
|
||||
_buf_inc_writep(streambuf, n);
|
||||
stream.bytes += n;
|
||||
if (stream.meta_interval) {
|
||||
stream.meta_next -= n;
|
||||
}
|
||||
} else {
|
||||
UNLOCK;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stream.state == STREAMING_BUFFERING && stream.bytes > stream.threshold) {
|
||||
stream.state = STREAMING_HTTP;
|
||||
wake_controller();
|
||||
}
|
||||
|
||||
LOG_SDEBUG("streambuf read %d bytes", n);
|
||||
}
|
||||
}
|
||||
|
||||
UNLOCK;
|
||||
|
||||
} else {
|
||||
|
||||
LOG_SDEBUG("poll timeout");
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_SSL
|
||||
if (SSLctx) {
|
||||
SSL_CTX_free(SSLctx);
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static thread_type thread;
|
||||
|
||||
void stream_init(log_level level, unsigned stream_buf_size) {
|
||||
loglevel = level;
|
||||
|
||||
LOG_INFO("init stream");
|
||||
LOG_DEBUG("streambuf size: %u", stream_buf_size);
|
||||
|
||||
buf_init(streambuf, stream_buf_size);
|
||||
if (streambuf->buf == NULL) {
|
||||
LOG_ERROR("unable to malloc buffer");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
#if USE_SSL
|
||||
#if !LINKALL
|
||||
if (ssl_loaded) {
|
||||
#endif
|
||||
SSL_library_init();
|
||||
SSLctx = SSL_CTX_new(SSLv23_client_method());
|
||||
if (SSLctx == NULL) {
|
||||
LOG_ERROR("unable to allocate SSL context");
|
||||
exit(0);
|
||||
}
|
||||
SSL_CTX_set_options(SSLctx, SSL_OP_NO_SSLv2);
|
||||
#if !LINKALL
|
||||
}
|
||||
#endif
|
||||
ssl = NULL;
|
||||
#endif
|
||||
|
||||
#if SUN
|
||||
signal(SIGPIPE, SIG_IGN); /* Force sockets to return -1 with EPIPE on pipe signal */
|
||||
#endif
|
||||
stream.state = STOPPED;
|
||||
stream.header = malloc(MAX_HEADER);
|
||||
*stream.header = '\0';
|
||||
|
||||
fd = -1;
|
||||
|
||||
#if LINUX || FREEBSD
|
||||
touch_memory(streambuf->buf, streambuf->size);
|
||||
#endif
|
||||
|
||||
#if LINUX || OSX || FREEBSD || EMBEDDED
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
#ifdef PTHREAD_STACK_MIN
|
||||
pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + STREAM_THREAD_STACK_SIZE);
|
||||
#endif
|
||||
pthread_create(&thread, &attr, stream_thread, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
#if HAS_PTHREAD_SETNAME_NP
|
||||
pthread_setname_np(thread, "stream");
|
||||
#endif
|
||||
#endif
|
||||
#if WIN
|
||||
thread = CreateThread(NULL, STREAM_THREAD_STACK_SIZE, (LPTHREAD_START_ROUTINE)&stream_thread, NULL, 0, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
void stream_close(void) {
|
||||
LOG_INFO("close stream");
|
||||
LOCK;
|
||||
running = false;
|
||||
UNLOCK;
|
||||
#if LINUX || OSX || FREEBSD || POSIX
|
||||
pthread_join(thread, NULL);
|
||||
#endif
|
||||
free(stream.header);
|
||||
buf_destroy(streambuf);
|
||||
}
|
||||
|
||||
void stream_file(const char *header, size_t header_len, unsigned threshold) {
|
||||
buf_flush(streambuf);
|
||||
|
||||
LOCK;
|
||||
|
||||
stream.header_len = header_len;
|
||||
memcpy(stream.header, header, header_len);
|
||||
*(stream.header+header_len) = '\0';
|
||||
|
||||
LOG_INFO("opening local file: %s", stream.header);
|
||||
|
||||
#if WIN
|
||||
fd = open(stream.header, O_RDONLY | O_BINARY);
|
||||
#else
|
||||
fd = open(stream.header, O_RDONLY);
|
||||
#endif
|
||||
|
||||
stream.state = STREAMING_FILE;
|
||||
if (fd < 0) {
|
||||
LOG_INFO("can't open file: %s", stream.header);
|
||||
stream.state = DISCONNECT;
|
||||
}
|
||||
wake_controller();
|
||||
|
||||
stream.cont_wait = false;
|
||||
stream.meta_interval = 0;
|
||||
stream.meta_next = 0;
|
||||
stream.meta_left = 0;
|
||||
stream.meta_send = false;
|
||||
stream.sent_headers = false;
|
||||
stream.bytes = 0;
|
||||
stream.threshold = threshold;
|
||||
|
||||
UNLOCK;
|
||||
}
|
||||
|
||||
void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait) {
|
||||
struct sockaddr_in addr;
|
||||
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
|
||||
if (sock < 0) {
|
||||
LOG_ERROR("failed to create socket");
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = ip;
|
||||
addr.sin_port = port;
|
||||
|
||||
LOG_INFO("connecting to %s:%d", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
|
||||
|
||||
set_nonblock(sock);
|
||||
set_nosigpipe(sock);
|
||||
|
||||
if (connect_timeout(sock, (struct sockaddr *) &addr, sizeof(addr), 10) < 0) {
|
||||
LOG_INFO("unable to connect to server");
|
||||
LOCK;
|
||||
stream.state = DISCONNECT;
|
||||
stream.disconnect = UNREACHABLE;
|
||||
UNLOCK;
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_SSL
|
||||
if (ntohs(port) == 443) {
|
||||
char *server = strcasestr(header, "Host:");
|
||||
|
||||
ssl = SSL_new(SSLctx);
|
||||
SSL_set_fd(ssl, sock);
|
||||
|
||||
// add SNI
|
||||
if (server) {
|
||||
char *p, *servername = malloc(1024);
|
||||
|
||||
sscanf(server, "Host:%255[^:]s", servername);
|
||||
for (p = servername; *p == ' '; p++);
|
||||
SSL_set_tlsext_host_name(ssl, p);
|
||||
free(servername);
|
||||
}
|
||||
|
||||
while (1) {
|
||||
int status, err = 0;
|
||||
|
||||
ERR_clear_error();
|
||||
status = SSL_connect(ssl);
|
||||
|
||||
// successful negotiation
|
||||
if (status == 1) break;
|
||||
|
||||
// error or non-blocking requires more time
|
||||
if (status < 0) {
|
||||
err = SSL_get_error(ssl, status);
|
||||
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue;
|
||||
}
|
||||
|
||||
LOG_WARN("unable to open SSL socket %d (%d)", status, err);
|
||||
closesocket(sock);
|
||||
SSL_free(ssl);
|
||||
ssl = NULL;
|
||||
LOCK;
|
||||
stream.state = DISCONNECT;
|
||||
stream.disconnect = UNREACHABLE;
|
||||
UNLOCK;
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
ssl = NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
buf_flush(streambuf);
|
||||
|
||||
LOCK;
|
||||
|
||||
fd = sock;
|
||||
stream.state = SEND_HEADERS;
|
||||
stream.cont_wait = cont_wait;
|
||||
stream.meta_interval = 0;
|
||||
stream.meta_next = 0;
|
||||
stream.meta_left = 0;
|
||||
stream.meta_send = false;
|
||||
stream.header_len = header_len;
|
||||
memcpy(stream.header, header, header_len);
|
||||
*(stream.header+header_len) = '\0';
|
||||
|
||||
LOG_INFO("header: %s", stream.header);
|
||||
|
||||
stream.sent_headers = false;
|
||||
stream.bytes = 0;
|
||||
stream.threshold = threshold;
|
||||
|
||||
UNLOCK;
|
||||
}
|
||||
|
||||
bool stream_disconnect(void) {
|
||||
bool disc = false;
|
||||
LOCK;
|
||||
#if USE_SSL
|
||||
if (ssl) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
ssl = NULL;
|
||||
}
|
||||
#endif
|
||||
if (fd != -1) {
|
||||
closesocket(fd);
|
||||
fd = -1;
|
||||
disc = true;
|
||||
}
|
||||
stream.state = STOPPED;
|
||||
UNLOCK;
|
||||
return disc;
|
||||
}
|
||||
563
components/squeezelite/utils.c
Normal file
563
components/squeezelite/utils.c
Normal file
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
#if LINUX || OSX || FREEBSD || EMBEDDED
|
||||
#include <sys/ioctl.h>
|
||||
#include <net/if.h>
|
||||
#include <netdb.h>
|
||||
#if FREEBSD
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if_dl.h>
|
||||
#include <net/if_types.h>
|
||||
#endif
|
||||
#endif
|
||||
#if SUN
|
||||
#include <sys/socket.h>
|
||||
#include <sys/sockio.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <net/if.h>
|
||||
#include <net/if_arp.h>
|
||||
#include <net/if_dl.h>
|
||||
#include <net/if_types.h>
|
||||
#endif
|
||||
#if WIN
|
||||
#include <iphlpapi.h>
|
||||
#if USE_SSL
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#endif
|
||||
#endif
|
||||
#if OSX
|
||||
#include <net/if_dl.h>
|
||||
#include <net/if_types.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
// logging functions
|
||||
const char *logtime(void) {
|
||||
static char buf[100];
|
||||
#if WIN
|
||||
SYSTEMTIME lt;
|
||||
GetLocalTime(<);
|
||||
sprintf(buf, "[%02d:%02d:%02d.%03d]", lt.wHour, lt.wMinute, lt.wSecond, lt.wMilliseconds);
|
||||
#else
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
strftime(buf, sizeof(buf), "[%T.", localtime(&tv.tv_sec));
|
||||
sprintf(buf+strlen(buf), "%06ld]", (long)tv.tv_usec);
|
||||
#endif
|
||||
return buf;
|
||||
}
|
||||
|
||||
void logprint(const char *fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vfprintf(stderr, fmt, args);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
// cmdline parsing
|
||||
char *next_param(char *src, char c) {
|
||||
static char *str = NULL;
|
||||
char *ptr, *ret;
|
||||
if (src) str = src;
|
||||
if (str && (ptr = strchr(str, c))) {
|
||||
ret = str;
|
||||
*ptr = '\0';
|
||||
str = ptr + 1;
|
||||
} else {
|
||||
ret = str;
|
||||
str = NULL;
|
||||
}
|
||||
|
||||
return ret && ret[0] ? ret : NULL;
|
||||
}
|
||||
|
||||
// clock
|
||||
u32_t gettime_ms(void) {
|
||||
#if WIN
|
||||
return GetTickCount();
|
||||
#else
|
||||
#if LINUX || FREEBSD || EMBEDDED
|
||||
struct timespec ts;
|
||||
#ifdef CLOCK_MONOTONIC
|
||||
if (!clock_gettime(CLOCK_MONOTONIC, &ts)) {
|
||||
#else
|
||||
if (!clock_gettime(CLOCK_REALTIME, &ts)) {
|
||||
#endif
|
||||
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
#endif
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
|
||||
#endif
|
||||
}
|
||||
|
||||
// mac address
|
||||
#if LINUX && !defined(SUN)
|
||||
// search first 4 interfaces returned by IFCONF
|
||||
void get_mac(u8_t mac[]) {
|
||||
char *utmac;
|
||||
struct ifconf ifc;
|
||||
struct ifreq *ifr, *ifend;
|
||||
struct ifreq ifreq;
|
||||
struct ifreq ifs[4];
|
||||
|
||||
utmac = getenv("UTMAC");
|
||||
if (utmac)
|
||||
{
|
||||
if ( strlen(utmac) == 17 )
|
||||
{
|
||||
if (sscanf(utmac,"%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx",
|
||||
&mac[0],&mac[1],&mac[2],&mac[3],&mac[4],&mac[5]) == 6)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0;
|
||||
|
||||
int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
|
||||
ifc.ifc_len = sizeof(ifs);
|
||||
ifc.ifc_req = ifs;
|
||||
|
||||
if (ioctl(s, SIOCGIFCONF, &ifc) == 0) {
|
||||
ifend = ifs + (ifc.ifc_len / sizeof(struct ifreq));
|
||||
|
||||
for (ifr = ifc.ifc_req; ifr < ifend; ifr++) {
|
||||
if (ifr->ifr_addr.sa_family == AF_INET) {
|
||||
|
||||
strncpy(ifreq.ifr_name, ifr->ifr_name, sizeof(ifreq.ifr_name));
|
||||
if (ioctl (s, SIOCGIFHWADDR, &ifreq) == 0) {
|
||||
memcpy(mac, ifreq.ifr_hwaddr.sa_data, 6);
|
||||
if (mac[0]+mac[1]+mac[2] != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SUN
|
||||
void get_mac(u8_t mac[]) {
|
||||
struct arpreq parpreq;
|
||||
struct sockaddr_in *psa;
|
||||
struct in_addr inaddr;
|
||||
struct hostent *phost;
|
||||
char hostname[MAXHOSTNAMELEN];
|
||||
char **paddrs;
|
||||
char *utmac;
|
||||
int sock;
|
||||
int status=0;
|
||||
|
||||
utmac = getenv("UTMAC");
|
||||
if (utmac)
|
||||
{
|
||||
if ( strlen(utmac) == 17 )
|
||||
{
|
||||
if (sscanf(utmac,"%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx",
|
||||
&mac[0],&mac[1],&mac[2],&mac[3],&mac[4],&mac[5]) == 6)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0;
|
||||
|
||||
gethostname(hostname, MAXHOSTNAMELEN);
|
||||
|
||||
phost = gethostbyname(hostname);
|
||||
|
||||
paddrs = phost->h_addr_list;
|
||||
memcpy(&inaddr.s_addr, *paddrs, sizeof(inaddr.s_addr));
|
||||
|
||||
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
|
||||
if(sock == -1)
|
||||
{
|
||||
mac[5] = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&parpreq, 0, sizeof(struct arpreq));
|
||||
psa = (struct sockaddr_in *) &parpreq.arp_pa;
|
||||
memset(psa, 0, sizeof(struct sockaddr_in));
|
||||
psa->sin_family = AF_INET;
|
||||
memcpy(&psa->sin_addr, *paddrs, sizeof(struct in_addr));
|
||||
|
||||
status = ioctl(sock, SIOCGARP, &parpreq);
|
||||
|
||||
if(status == -1)
|
||||
{
|
||||
mac[5] = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
mac[0] = (unsigned char) parpreq.arp_ha.sa_data[0];
|
||||
mac[1] = (unsigned char) parpreq.arp_ha.sa_data[1];
|
||||
mac[2] = (unsigned char) parpreq.arp_ha.sa_data[2];
|
||||
mac[3] = (unsigned char) parpreq.arp_ha.sa_data[3];
|
||||
mac[4] = (unsigned char) parpreq.arp_ha.sa_data[4];
|
||||
mac[5] = (unsigned char) parpreq.arp_ha.sa_data[5];
|
||||
}
|
||||
#endif
|
||||
|
||||
#if OSX || FREEBSD
|
||||
void get_mac(u8_t mac[]) {
|
||||
struct ifaddrs *addrs, *ptr;
|
||||
const struct sockaddr_dl *dlAddr;
|
||||
const unsigned char *base;
|
||||
|
||||
mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0;
|
||||
|
||||
if (getifaddrs(&addrs) == 0) {
|
||||
ptr = addrs;
|
||||
while (ptr) {
|
||||
if (ptr->ifa_addr->sa_family == AF_LINK && ((const struct sockaddr_dl *) ptr->ifa_addr)->sdl_type == IFT_ETHER) {
|
||||
dlAddr = (const struct sockaddr_dl *)ptr->ifa_addr;
|
||||
base = (const unsigned char*) &dlAddr->sdl_data[dlAddr->sdl_nlen];
|
||||
memcpy(mac, base, min(dlAddr->sdl_alen, 6));
|
||||
break;
|
||||
}
|
||||
ptr = ptr->ifa_next;
|
||||
}
|
||||
freeifaddrs(addrs);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if WIN
|
||||
#pragma comment(lib, "IPHLPAPI.lib")
|
||||
void get_mac(u8_t mac[]) {
|
||||
IP_ADAPTER_INFO AdapterInfo[16];
|
||||
DWORD dwBufLen = sizeof(AdapterInfo);
|
||||
DWORD dwStatus = GetAdaptersInfo(AdapterInfo, &dwBufLen);
|
||||
|
||||
mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0;
|
||||
|
||||
if (GetAdaptersInfo(AdapterInfo, &dwBufLen) == ERROR_SUCCESS) {
|
||||
memcpy(mac, AdapterInfo[0].Address, 6);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void set_nonblock(sockfd s) {
|
||||
#if WIN
|
||||
u_long iMode = 1;
|
||||
ioctlsocket(s, FIONBIO, &iMode);
|
||||
#else
|
||||
int flags = fcntl(s, F_GETFL,0);
|
||||
fcntl(s, F_SETFL, flags | O_NONBLOCK);
|
||||
#endif
|
||||
}
|
||||
|
||||
// connect for socket already set to non blocking with timeout in seconds
|
||||
int connect_timeout(sockfd sock, const struct sockaddr *addr, socklen_t addrlen, int timeout) {
|
||||
fd_set w, e;
|
||||
struct timeval tval;
|
||||
|
||||
if (connect(sock, addr, addrlen) < 0) {
|
||||
#if !WIN
|
||||
if (last_error() != EINPROGRESS) {
|
||||
#else
|
||||
if (last_error() != WSAEWOULDBLOCK) {
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
FD_ZERO(&w);
|
||||
FD_SET(sock, &w);
|
||||
e = w;
|
||||
tval.tv_sec = timeout;
|
||||
tval.tv_usec = 0;
|
||||
|
||||
// only return 0 if w set and sock error is zero, otherwise return error code
|
||||
if (select(sock + 1, NULL, &w, &e, timeout ? &tval : NULL) == 1 && FD_ISSET(sock, &w)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
getsockopt(sock, SOL_SOCKET, SO_ERROR, (void *)&error, &len);
|
||||
return error;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void server_addr(char *server, in_addr_t *ip_ptr, unsigned *port_ptr) {
|
||||
struct addrinfo *res = NULL;
|
||||
struct addrinfo hints;
|
||||
const char *port = NULL;
|
||||
|
||||
if (strtok(server, ":")) {
|
||||
port = strtok(NULL, ":");
|
||||
if (port) {
|
||||
*port_ptr = atoi(port);
|
||||
}
|
||||
}
|
||||
|
||||
memset(&hints, 0, sizeof(struct addrinfo));
|
||||
hints.ai_family = AF_INET;
|
||||
|
||||
getaddrinfo(server, NULL, &hints, &res);
|
||||
|
||||
if (res && res->ai_addr) {
|
||||
*ip_ptr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr;
|
||||
}
|
||||
|
||||
if (res) {
|
||||
freeaddrinfo(res);
|
||||
}
|
||||
}
|
||||
|
||||
void set_readwake_handles(event_handle handles[], sockfd s, event_event e) {
|
||||
#if WINEVENT
|
||||
handles[0] = WSACreateEvent();
|
||||
handles[1] = e;
|
||||
WSAEventSelect(s, handles[0], FD_READ | FD_CLOSE);
|
||||
#elif SELFPIPE || LOOPBACK
|
||||
handles[0].fd = s;
|
||||
handles[1].fd = e.fds[0];
|
||||
handles[0].events = POLLIN;
|
||||
handles[1].events = POLLIN;
|
||||
#else
|
||||
handles[0].fd = s;
|
||||
handles[1].fd = e;
|
||||
handles[0].events = POLLIN;
|
||||
handles[1].events = POLLIN;
|
||||
#endif
|
||||
}
|
||||
|
||||
event_type wait_readwake(event_handle handles[], int timeout) {
|
||||
#if WINEVENT
|
||||
int wait = WSAWaitForMultipleEvents(2, handles, FALSE, timeout, FALSE);
|
||||
if (wait == WSA_WAIT_EVENT_0) {
|
||||
WSAResetEvent(handles[0]);
|
||||
return EVENT_READ;
|
||||
} else if (wait == WSA_WAIT_EVENT_0 + 1) {
|
||||
return EVENT_WAKE;
|
||||
} else {
|
||||
return EVENT_TIMEOUT;
|
||||
}
|
||||
#else
|
||||
if (poll(handles, 2, timeout) > 0) {
|
||||
if (handles[0].revents) {
|
||||
return EVENT_READ;
|
||||
}
|
||||
if (handles[1].revents) {
|
||||
wake_clear(handles[1].fd);
|
||||
return EVENT_WAKE;
|
||||
}
|
||||
}
|
||||
return EVENT_TIMEOUT;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if LOOPBACK
|
||||
void _wake_create(event_event* e) {
|
||||
struct sockaddr_in addr;
|
||||
short port;
|
||||
socklen_t len;
|
||||
|
||||
e->mfds = e->fds[0] = e->fds[1] = -1;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
// create sending socket - will wait for connections
|
||||
addr.sin_port = 0;
|
||||
e->mfds = socket(AF_INET, SOCK_STREAM, 0);
|
||||
bind(e->mfds, (struct sockaddr*) &addr, sizeof(addr));
|
||||
len = sizeof(struct sockaddr);
|
||||
|
||||
// get assigned port & listen
|
||||
getsockname(e->mfds, (struct sockaddr *) &addr, &len);
|
||||
port = addr.sin_port;
|
||||
listen(e->mfds, 1);
|
||||
|
||||
// create receiving socket
|
||||
addr.sin_port = 0;
|
||||
e->fds[0] = socket(AF_INET, SOCK_STREAM, 0);
|
||||
bind(e->fds[0], (struct sockaddr*) &addr, sizeof(addr));
|
||||
|
||||
// connect to sender (we listen so it can be blocking)
|
||||
addr.sin_port = port;
|
||||
connect(e->fds[0], (struct sockaddr*) &addr, sizeof(addr));
|
||||
|
||||
// this one will work or fail, but not block
|
||||
len = sizeof(struct sockaddr);
|
||||
e->fds[1] = accept(e->mfds, (struct sockaddr*) &addr, &len);
|
||||
}
|
||||
#endif
|
||||
|
||||
// pack/unpack to network byte order
|
||||
void packN(u32_t *dest, u32_t val) {
|
||||
u8_t *ptr = (u8_t *)dest;
|
||||
*(ptr) = (val >> 24) & 0xFF; *(ptr+1) = (val >> 16) & 0xFF; *(ptr+2) = (val >> 8) & 0xFF; *(ptr+3) = val & 0xFF;
|
||||
}
|
||||
|
||||
void packn(u16_t *dest, u16_t val) {
|
||||
u8_t *ptr = (u8_t *)dest;
|
||||
*(ptr) = (val >> 8) & 0xFF; *(ptr+1) = val & 0xFF;
|
||||
}
|
||||
|
||||
u32_t unpackN(u32_t *src) {
|
||||
u8_t *ptr = (u8_t *)src;
|
||||
return *(ptr) << 24 | *(ptr+1) << 16 | *(ptr+2) << 8 | *(ptr+3);
|
||||
}
|
||||
|
||||
u16_t unpackn(u16_t *src) {
|
||||
u8_t *ptr = (u8_t *)src;
|
||||
return *(ptr) << 8 | *(ptr+1);
|
||||
}
|
||||
|
||||
#if OSX
|
||||
void set_nosigpipe(sockfd s) {
|
||||
int set = 1;
|
||||
setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if WIN
|
||||
void winsock_init(void) {
|
||||
WSADATA wsaData;
|
||||
WORD wVersionRequested = MAKEWORD(2, 2);
|
||||
int WSerr = WSAStartup(wVersionRequested, &wsaData);
|
||||
if (WSerr != 0) {
|
||||
LOG_ERROR("Bad winsock version");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void winsock_close(void) {
|
||||
WSACleanup();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int poll(struct pollfd *fds, unsigned long numfds, int timeout) {
|
||||
fd_set r, w;
|
||||
struct timeval tv;
|
||||
int ret, i, max_fds = fds[0].fd;
|
||||
|
||||
FD_ZERO(&r);
|
||||
FD_ZERO(&w);
|
||||
|
||||
for (i = 0; i < numfds; i++) {
|
||||
if (fds[i].events & POLLIN) FD_SET(fds[i].fd, &r);
|
||||
if (fds[i].events & POLLOUT) FD_SET(fds[i].fd, &w);
|
||||
if (max_fds < fds[i].fd) max_fds = fds[i].fd;
|
||||
}
|
||||
|
||||
tv.tv_sec = timeout / 1000;
|
||||
tv.tv_usec = 1000 * (timeout % 1000);
|
||||
|
||||
ret = select(max_fds + 1, &r, &w, NULL, &tv);
|
||||
|
||||
if (ret < 0) return ret;
|
||||
|
||||
for (i = 0; i < numfds; i++) {
|
||||
fds[i].revents = 0;
|
||||
if (FD_ISSET(fds[i].fd, &r)) fds[i].revents |= POLLIN;
|
||||
if (FD_ISSET(fds[i].fd, &w)) fds[i].revents |= POLLOUT;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if LINUX || FREEBSD
|
||||
void touch_memory(u8_t *buf, size_t size) {
|
||||
u8_t *ptr;
|
||||
for (ptr = buf; ptr < buf + size; ptr += sysconf(_SC_PAGESIZE)) {
|
||||
*ptr = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if WIN && USE_SSL
|
||||
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) - 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;
|
||||
}
|
||||
#endif
|
||||
372
components/squeezelite/vorbis.c
Normal file
372
components/squeezelite/vorbis.c
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Squeezelite - lightweight headless squeezebox emulator
|
||||
*
|
||||
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
|
||||
* Ralph Irving 2015-2017, ralph_irving@hotmail.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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "squeezelite.h"
|
||||
|
||||
/*
|
||||
* with some low-end CPU, the decode call takes a fair bit of time and if the outputbuf is locked during that
|
||||
* period, the output_thread (or equivalent) will be locked although there is plenty of samples available.
|
||||
* Normally, with PRIO_INHERIT, that thread should increase decoder priority and get the lock quickly but it
|
||||
* seems that when the streambuf has plenty of data, the decode thread grabs the CPU to much, even it the output
|
||||
* thread has a higher priority. Using an interim buffer where vorbis decoder writes the output is not great from
|
||||
* an efficiency (one extra memory copy) point of view, but it allows the lock to not be kept for too long
|
||||
*/
|
||||
#define FRAME_BUF 2048
|
||||
|
||||
#if BYTES_PER_FRAME == 4
|
||||
#define ALIGN(n) (n)
|
||||
#else
|
||||
#define ALIGN(n) (n << 16)
|
||||
#endif
|
||||
|
||||
// automatically select between floating point (preferred) and fixed point libraries:
|
||||
// NOTE: works with Tremor version here: http://svn.xiph.org/trunk/Tremor, not vorbisidec.1.0.2 currently in ubuntu
|
||||
|
||||
// we take common definations from <vorbis/vorbisfile.h> even though we can use tremor at run time
|
||||
// tremor's OggVorbis_File struct is normally smaller so this is ok, but padding added to malloc in case it is bigger
|
||||
#define OV_EXCLUDE_STATIC_CALLBACKS
|
||||
|
||||
#ifdef TREMOR_ONLY
|
||||
#include <ivorbisfile.h>
|
||||
#else
|
||||
#include <vorbis/vorbisfile.h>
|
||||
#endif
|
||||
|
||||
struct vorbis {
|
||||
OggVorbis_File *vf;
|
||||
bool opened;
|
||||
#if FRAME_BUF
|
||||
u8_t *write_buf;
|
||||
#endif
|
||||
#if !LINKALL
|
||||
// vorbis symbols to be dynamically loaded - from either vorbisfile or vorbisidec (tremor) version of library
|
||||
vorbis_info *(* ov_info)(OggVorbis_File *vf, int link);
|
||||
int (* ov_clear)(OggVorbis_File *vf);
|
||||
long (* ov_read)(OggVorbis_File *vf, char *buffer, int length, int bigendianp, int word, int sgned, int *bitstream);
|
||||
long (* ov_read_tremor)(OggVorbis_File *vf, char *buffer, int length, int *bitstream);
|
||||
int (* ov_open_callbacks)(void *datasource, OggVorbis_File *vf, const char *initial, long ibytes, ov_callbacks callbacks);
|
||||
#endif
|
||||
};
|
||||
|
||||
static struct vorbis *v;
|
||||
|
||||
extern log_level loglevel;
|
||||
|
||||
extern struct buffer *streambuf;
|
||||
extern struct buffer *outputbuf;
|
||||
extern struct streamstate stream;
|
||||
extern struct outputstate output;
|
||||
extern struct decodestate decode;
|
||||
extern struct processstate process;
|
||||
|
||||
#define LOCK_S mutex_lock(streambuf->mutex)
|
||||
#define UNLOCK_S mutex_unlock(streambuf->mutex)
|
||||
#define LOCK_O mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
|
||||
#if PROCESS
|
||||
#define LOCK_O_direct if (decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_O_not_direct if (!decode.direct) mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_not_direct if (!decode.direct) mutex_unlock(outputbuf->mutex)
|
||||
#define IF_DIRECT(x) if (decode.direct) { x }
|
||||
#define IF_PROCESS(x) if (!decode.direct) { x }
|
||||
#else
|
||||
#define LOCK_O_direct mutex_lock(outputbuf->mutex)
|
||||
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
|
||||
#define LOCK_O_not_direct
|
||||
#define UNLOCK_O_not_direct
|
||||
#define IF_DIRECT(x) { x }
|
||||
#define IF_PROCESS(x)
|
||||
#endif
|
||||
|
||||
#if LINKALL
|
||||
#define OV(h, fn, ...) (ov_ ## fn)(__VA_ARGS__)
|
||||
#define TREMOR(h) 0
|
||||
#if !WIN
|
||||
extern int ov_read_tremor(); // needed to enable compilation, not linked
|
||||
#endif
|
||||
#else
|
||||
#define OV(h, fn, ...) (h)->ov_##fn(__VA_ARGS__)
|
||||
#define TREMOR(h) (h)->ov_read_tremor
|
||||
#endif
|
||||
|
||||
// called with mutex locked within vorbis_decode to avoid locking O before S
|
||||
static size_t _read_cb(void *ptr, size_t size, size_t nmemb, void *datasource) {
|
||||
size_t bytes;
|
||||
|
||||
LOCK_S;
|
||||
|
||||
bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
|
||||
bytes = min(bytes, size * nmemb);
|
||||
|
||||
memcpy(ptr, streambuf->readp, bytes);
|
||||
_buf_inc_readp(streambuf, bytes);
|
||||
|
||||
UNLOCK_S;
|
||||
|
||||
return bytes / size;
|
||||
}
|
||||
|
||||
// these are needed for older versions of tremor, later versions and libvorbis allow NULL to be used
|
||||
static int _seek_cb(void *datasource, ogg_int64_t offset, int whence) { return -1; }
|
||||
static int _close_cb(void *datasource) { return 0; }
|
||||
static long _tell_cb(void *datasource) { return 0; }
|
||||
|
||||
static decode_state vorbis_decode(void) {
|
||||
static int channels;
|
||||
frames_t frames;
|
||||
int bytes, s, n;
|
||||
u8_t *write_buf;
|
||||
|
||||
LOCK_S;
|
||||
|
||||
if (stream.state <= DISCONNECT && !_buf_used(streambuf)) {
|
||||
UNLOCK_S;
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
UNLOCK_S;
|
||||
|
||||
if (decode.new_stream) {
|
||||
ov_callbacks cbs;
|
||||
int err;
|
||||
struct vorbis_info *info;
|
||||
|
||||
cbs.read_func = _read_cb;
|
||||
|
||||
if (TREMOR(v)) {
|
||||
cbs.seek_func = _seek_cb; cbs.close_func = _close_cb; cbs.tell_func = _tell_cb;
|
||||
} else {
|
||||
cbs.seek_func = NULL; cbs.close_func = NULL; cbs.tell_func = NULL;
|
||||
}
|
||||
|
||||
if ((err = OV(v, open_callbacks, streambuf, v->vf, NULL, 0, cbs)) < 0) {
|
||||
LOG_WARN("open_callbacks error: %d", err);
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
v->opened = true;
|
||||
info = OV(v, info, v->vf, -1);
|
||||
|
||||
LOG_INFO("setting track_start");
|
||||
LOCK_O;
|
||||
output.next_sample_rate = decode_newstream(info->rate, output.supported_rates);
|
||||
IF_DSD( output.next_fmt = PCM; )
|
||||
output.track_start = outputbuf->writep;
|
||||
if (output.fade_mode) _checkfade(true);
|
||||
decode.new_stream = false;
|
||||
UNLOCK_O;
|
||||
|
||||
channels = info->channels;
|
||||
|
||||
if (channels > 2) {
|
||||
LOG_WARN("too many channels: %d", channels);
|
||||
return DECODE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#if !FRAME_BUF
|
||||
LOCK_O_direct;
|
||||
#endif
|
||||
|
||||
IF_DIRECT(
|
||||
frames = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
|
||||
#if FRAME_BUF
|
||||
write_buf = v->write_buf;
|
||||
#else
|
||||
write_buf = outputbuf->writep;
|
||||
#endif
|
||||
);
|
||||
IF_PROCESS(
|
||||
frames = process.max_in_frames;
|
||||
write_buf = process.inbuf;
|
||||
);
|
||||
|
||||
#if FRAME_BUF
|
||||
frames = min(frames, FRAME_BUF);
|
||||
#endif
|
||||
bytes = frames * 2 * channels; // samples returned are 16 bits
|
||||
|
||||
// write the decoded frames into outputbuf even though they are 16 bits per sample, then unpack them
|
||||
#ifdef TREMOR_ONLY
|
||||
n = OV(v, read, v->vf, (char *)write_buf, bytes, &s);
|
||||
#else
|
||||
if (!TREMOR(v)) {
|
||||
#if SL_LITTLE_ENDIAN
|
||||
n = OV(v, read, v->vf, (char *)write_buf, bytes, 0, 2, 1, &s);
|
||||
#else
|
||||
n = OV(v, read, v->vf, (char *)write_buf, bytes, 1, 2, 1, &s);
|
||||
#endif
|
||||
#if !WIN
|
||||
} else {
|
||||
n = OV(v, read_tremor, v->vf, (char *)write_buf, bytes, &s);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FRAME_BUF
|
||||
LOCK_O_direct;
|
||||
#endif
|
||||
|
||||
if (n > 0) {
|
||||
frames_t count;
|
||||
s16_t *iptr;
|
||||
ISAMPLE_T *optr;
|
||||
|
||||
frames = n / 2 / channels;
|
||||
count = frames * channels;
|
||||
|
||||
iptr = (s16_t *)write_buf + count;
|
||||
optr = (ISAMPLE_T *)write_buf + frames * 2;
|
||||
|
||||
if (channels == 2) {
|
||||
#if BYTES_PER_FRAME == 4
|
||||
memcpy(outputbuf->writep, write_buf, frames * BYTES_PER_FRAME);
|
||||
#else
|
||||
while (count--) {
|
||||
*--optr = *--iptr << 16;
|
||||
}
|
||||
#endif
|
||||
} else if (channels == 1) {
|
||||
while (count--) {
|
||||
*--optr = ALIGN(*--iptr);
|
||||
*--optr = ALIGN(*iptr);
|
||||
}
|
||||
}
|
||||
|
||||
IF_DIRECT(
|
||||
_buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME);
|
||||
);
|
||||
IF_PROCESS(
|
||||
process.in_frames = frames;
|
||||
);
|
||||
|
||||
LOG_SDEBUG("wrote %u frames", frames);
|
||||
|
||||
} else if (n == 0) {
|
||||
|
||||
LOG_INFO("end of stream");
|
||||
UNLOCK_O_direct;
|
||||
return DECODE_COMPLETE;
|
||||
|
||||
} else if (n == OV_HOLE) {
|
||||
|
||||
// recoverable hole in stream, seen when skipping
|
||||
LOG_DEBUG("hole in stream");
|
||||
|
||||
} else {
|
||||
|
||||
LOG_INFO("ov_read error: %d", n);
|
||||
UNLOCK_O_direct;
|
||||
return DECODE_COMPLETE;
|
||||
}
|
||||
|
||||
UNLOCK_O_direct;
|
||||
return DECODE_RUNNING;
|
||||
}
|
||||
|
||||
static void vorbis_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
|
||||
if (!v->vf) {
|
||||
v->vf = malloc(sizeof(OggVorbis_File) + 128); // add some padding as struct size may be larger
|
||||
memset(v->vf, 0, sizeof(OggVorbis_File) + 128);
|
||||
#if FRAME_BUF
|
||||
v->write_buf = malloc(FRAME_BUF * BYTES_PER_FRAME);
|
||||
#endif
|
||||
} else {
|
||||
if (v->opened) {
|
||||
OV(v, clear, v->vf);
|
||||
v->opened = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void vorbis_close(void) {
|
||||
if (v->opened) {
|
||||
OV(v, clear, v->vf);
|
||||
v->opened = false;
|
||||
}
|
||||
free(v->vf);
|
||||
#if FRAME_BUF
|
||||
free(v->write_buf);
|
||||
v->write_buf = NULL;
|
||||
#endif
|
||||
v->vf = NULL;
|
||||
}
|
||||
|
||||
static bool load_vorbis() {
|
||||
#if !LINKALL
|
||||
void *handle = dlopen(LIBVORBIS, RTLD_NOW);
|
||||
char *err;
|
||||
bool tremor = false;
|
||||
|
||||
if (!handle) {
|
||||
handle = dlopen(LIBTREMOR, RTLD_NOW);
|
||||
if (handle) {
|
||||
tremor = true;
|
||||
} else {
|
||||
LOG_INFO("dlerror: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
v->ov_read = tremor ? NULL : dlsym(handle, "ov_read");
|
||||
v->ov_read_tremor = tremor ? dlsym(handle, "ov_read") : NULL;
|
||||
v->ov_info = dlsym(handle, "ov_info");
|
||||
v->ov_clear = dlsym(handle, "ov_clear");
|
||||
v->ov_open_callbacks = dlsym(handle, "ov_open_callbacks");
|
||||
|
||||
if ((err = dlerror()) != NULL) {
|
||||
LOG_INFO("dlerror: %s", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("loaded %s", tremor ? LIBTREMOR : LIBVORBIS);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct codec *register_vorbis(void) {
|
||||
static struct codec ret = {
|
||||
'o', // id
|
||||
"ogg", // types
|
||||
4096, // min read
|
||||
20480, // min space
|
||||
vorbis_open, // open
|
||||
vorbis_close, // close
|
||||
vorbis_decode,// decode
|
||||
};
|
||||
|
||||
v = malloc(sizeof(struct vorbis));
|
||||
if (!v) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->vf = NULL;
|
||||
v->opened = false;
|
||||
|
||||
if (!load_vorbis()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_INFO("using vorbis to decode ogg");
|
||||
return &ret;
|
||||
}
|
||||
@@ -19,9 +19,12 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include "platform_esp32.h"
|
||||
#pragma once
|
||||
|
||||
#ifndef QUOTE
|
||||
#define QUOTE(name) #name
|
||||
#endif
|
||||
|
||||
#define ESP_LOG_DEBUG_EVENT(tag,e) ESP_LOGD(tag,"evt: " e)
|
||||
|
||||
|
||||
void app_main()
|
||||
{
|
||||
console_start();
|
||||
}
|
||||
Reference in New Issue
Block a user