(2)プログラムの詳細と局選択

前回プロジェクトのコンパイルと実行が出来たので、今回はプログラムの詳細を見て行きます。

プログラムは、 “play_radiko.c” と “radiko.c”の2つ

このプロジェクトのプログラムは、mainフォルダーの “play_radiko.c” と radiko-esp32フォルダーの “radiko.c”の2つ。先ずはメインプログラム ”play_radiko.c”。前回オリジナルを変更して下記の様にしています。

play_radiko.c

/* Play M3U HTTP Living stream

   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 <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "audio_common.h"
#include "http_stream.h"
#include "i2s_stream.h"
#include "aac_decoder.h"
#include "esp_tls.h"
#include "esp_http_client.h"
#include "radiko.h"
#include "esp_peripherals.h"
#include "periph_wifi.h"
#include "board.h"

#if __has_include("esp_idf_version.h")
#include "esp_idf_version.h"
#else
#define ESP_IDF_VERSION_VAL(major, minor, patch) 1
#endif

#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0))
#include "esp_netif.h"
#else
#include "tcpip_adapter.h"
#endif
static const char *TAG = "HTTP_LIVINGSTREAM_EXAMPLE";

//#define AAC_STREAM_URI "http://open.ls.qingting.fm/live/274/64k.m3u8?format=aac"
#define MAX_HTTP_RECV_BUFFER 512
#define MAX_HTTP_content 8192

audio_pipeline_handle_t pipeline;
audio_element_handle_t http_stream_reader, i2s_stream_writer, aac_decoder;

int _http_stream_event_handle(http_stream_event_msg_t *msg)
{
    if (msg->event_id == HTTP_STREAM_RESOLVE_ALL_TRACKS) {
        return ESP_OK;
    }

    if (msg->event_id == HTTP_STREAM_FINISH_TRACK) {
        return http_stream_next_track(msg->el);
    }
    if (msg->event_id == HTTP_STREAM_FINISH_PLAYLIST) {
        return http_stream_fetch_again(msg->el);
    }
    return ESP_OK;
}


void app_main(void)
{
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES) 
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0))
    ESP_ERROR_CHECK(esp_netif_init());
#else
    tcpip_adapter_init();
#endif


    esp_log_level_set("*", ESP_LOG_INFO);
    esp_log_level_set(TAG, ESP_LOG_DEBUG);

//    ESP_LOGI(TAG, "[ 1 ] Start audio codec chip");
//    audio_board_handle_t board_handle = audio_board_init();
//    audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START);
//    audio_hal_set_volume(board_handle->audio_hal, 50);
//    int player_volume;
//    int current_station = 0; 
//    audio_hal_get_volume(board_handle->audio_hal, &player_volume);

    ESP_LOGI(TAG, "[2.0] Create audio pipeline for playback");
    audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
    pipeline = audio_pipeline_init(&pipeline_cfg);

    ESP_LOGI(TAG, "[2.1] Create http stream to read data");
    http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
    http_cfg.event_handle = _http_stream_event_handle;
    http_cfg.type = AUDIO_STREAM_READER;
    http_cfg.enable_playlist_parser = true;
    http_stream_reader = http_stream_init(&http_cfg);

    ESP_LOGI(TAG, "[2.2] Create i2s stream to write data to codec chip");
    i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
    i2s_cfg.type = AUDIO_STREAM_WRITER;
    i2s_stream_writer = i2s_stream_init(&i2s_cfg);

    ESP_LOGI(TAG, "[2.3] Create aac decoder to decode aac file");
    aac_decoder_cfg_t aac_cfg = DEFAULT_AAC_DECODER_CONFIG();
    aac_decoder = aac_decoder_init(&aac_cfg);

    ESP_LOGI(TAG, "[2.4] Register all elements to audio pipeline");
    audio_pipeline_register(pipeline, http_stream_reader, "http");
    audio_pipeline_register(pipeline, aac_decoder,        "aac");
    audio_pipeline_register(pipeline, i2s_stream_writer,  "i2s");

    ESP_LOGI(TAG, "[2.5] Link it together http_stream-->aac_decoder-->i2s_stream-->[codec_chip]");
    const char *link_tag[3] = {"http", "aac", "i2s"};
    audio_pipeline_link(pipeline, &link_tag[0], 3);


    ESP_LOGI(TAG, "[ 3 ] Start and wait for Wi-Fi network");
    esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
    esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);

//    audio_board_key_init(set);


    periph_wifi_cfg_t wifi_cfg = 
    {
        .ssid = CONFIG_WIFI_SSID,
        .password = CONFIG_WIFI_PASSWORD,
    };

    esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg);
    esp_periph_start(set, wifi_handle);
    periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY);
    auth();
    get_station_list();
    generate_playlist_url(stations);
    ESP_LOGI(TAG, "[2.6] Set up  uri (http as http_stream, aac as aac decoder, and default output is i2s)");
    audio_element_set_uri(http_stream_reader, _playlist_url);

    ESP_LOGI(TAG, "[ 4 ] Set up  event listener");
    audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
    audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);

    ESP_LOGI(TAG, "[4.1] Listening event from all elements of pipeline");
    audio_pipeline_set_listener(pipeline, evt);

    ESP_LOGI(TAG, "[4.2] Listening event from peripherals");
    audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), evt);

    ESP_LOGI(TAG, "[ 5 ] Start audio_pipeline");
    audio_pipeline_run(pipeline);

    while (1) 
    {
        audio_event_iface_msg_t msg;
        esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY);
        if (ret != ESP_OK) 
        {
            ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret);
            continue;
        }

        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT
            && msg.source == (void *) aac_decoder
            && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) 
        {
            audio_element_info_t music_info = {0};
            audio_element_getinfo(aac_decoder, &music_info);

            ESP_LOGI(TAG, "[ * ] Receive music info from aac decoder, sample_rates=%d, bits=%d, ch=%d",
                    music_info.sample_rates, music_info.bits, music_info.channels);

            audio_element_setinfo(i2s_stream_writer, &music_info);
            i2s_stream_set_clk(i2s_stream_writer, music_info.sample_rates, music_info.bits, music_info.channels);
            continue;
        }

        /* restart stream when the first pipeline element (http_stream_reader in this case) receives stop event (caused by reading errors) */
        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) http_stream_reader
            && msg.cmd == AEL_MSG_CMD_REPORT_STATUS && (int) msg.data == AEL_STATUS_ERROR_OPEN) 
        {
            ESP_LOGW(TAG, "[ * ] Restart stream");
            audio_pipeline_stop(pipeline);
            audio_pipeline_wait_for_stop(pipeline);
            audio_element_reset_state(aac_decoder);
            audio_element_reset_state(i2s_stream_writer);
            audio_pipeline_reset_ringbuffer(pipeline);
            audio_pipeline_reset_items_state(pipeline);
            audio_pipeline_run(pipeline);
            continue;
        }
    }
    ESP_LOGI(TAG, "[ 6 ] Stop audio_pipeline");
    audio_pipeline_stop(pipeline);
    audio_pipeline_wait_for_stop(pipeline);
    audio_pipeline_terminate(pipeline);

    audio_pipeline_unregister(pipeline, http_stream_reader);
    audio_pipeline_unregister(pipeline, i2s_stream_writer);
    audio_pipeline_unregister(pipeline, aac_decoder);

    /* Terminate the pipeline before removing the listener */
    audio_pipeline_remove_listener(pipeline);

    /* Stop all peripherals before removing the listener */
    esp_periph_set_stop_all(set);
    audio_event_iface_remove_listener(esp_periph_set_get_event_iface(set), evt);

    /* Make sure audio_pipeline_remove_listener & audio_event_iface_remove_listener are called before destroying event_iface */
    audio_event_iface_destroy(evt);

    /* Release all resources */
    audio_pipeline_deinit(pipeline);
    audio_element_deinit(http_stream_reader);
    audio_element_deinit(i2s_stream_writer);
    audio_element_deinit(aac_decoder);
    esp_periph_set_destroy(set);
}

プログラムの流れは以前製作した 簡単なWebRadio(MP3編)とほとんど同じです。違いは、

  • 140行:auth();
  • 141行:get_station_list();
  • 142行:generate_playlist_url(stations);

の3行で、ここでRadiko関係の設定を行っている様です。そしてそれらの関数はradiko-esp32フォルダーの “radiko.c”に記述されています。

“radiko.c”の説明

“radiko.c”は以下の通り。

radiko.c

#include "esp_http_client.h"
#include "radiko.h"
#include "mbedtls/base64.h"
#include "expat.h"
#include "string.h"
#include "esp_log.h"
#include "freertos/event_groups.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "esp_tls.h"

static const char *TAG = "RADIKO";
extern const char radiko_jp_root_cert_pem_start[] asm("_binary_radiko_jp_root_cert_pem_start");
extern const char radiko_jp_root_cert_pem_end[]   asm("_binary_radiko_jp_root_cert_pem_end");

char * content;
int _length;
int _offset;

typedef enum 
{
  DOCUMENT,
  STATIONS,
  STATION, 
  ID, 
  NAME, 
  ASCII_NAME, 
  HREF,
  LOGO_XSMALL,
  LOGO_SMALL,
  LOGO_MEDIUM,
  LOGO_LARGE,
  LOGO,
  FEED,
  BANNER
} element;

element current = DOCUMENT;


esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    const char * TAG = "EVT_HTTP";
    static char *output_buffer;  // Buffer to store response of http request from event handler
    static int output_len;       // Stores number of bytes read
    switch(evt->event_id) 
    {
        case HTTP_EVENT_ERROR:
            ESP_LOGE(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            if(strstr((char *)evt->header_key, "X-Radiko-AuthToken") == (char *)evt->header_key)
            {
                ESP_LOGI(TAG, "GOT TOKEN");
                strcpy(_auth_token, evt->header_value);
            }
            else if(strstr((char *)evt->header_key, "X-Radiko-Authtoken") == (char *)evt->header_key)
            {
                ESP_LOGI(TAG, "GOT TOKEN");
                strcpy(_auth_token, evt->header_value);
            }
	    else if(strstr((char *)evt->header_key, "X-RADIKO-AUTHTOKEN") == (char *)evt->header_key)
            {
                ESP_LOGI(TAG, "GOT TOKEN");
                strcpy(_auth_token, evt->header_value);
            }

            else if(strstr((char *)evt->header_key, "X-Radiko-KeyOffset") == (char *)evt->header_key)
            {
                ESP_LOGI(TAG, "GOT OFFSET");
                char offset_c[8];
                strcpy(offset_c, evt->header_value);
                _offset = atoi(offset_c);
            }
            else if(strstr((char *)evt->header_key, "X-Radiko-KeyLength") == (char *)evt->header_key)
            {
                ESP_LOGI(TAG, "GOT LENGTH");
                char length_c[8];
                strcpy(length_c, evt->header_value);
                _length = atoi(length_c);
            }
            break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
	    char * endptr = evt->data + evt->data_len;
	    *endptr = '\0';
	    ESP_LOGI(TAG, "Received data: %s", (char *)evt->data);
            /*
             *  Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data.
             *  However, event handler can also be used in case chunked encoding is used.
             */
            if (!esp_http_client_is_chunked_response(evt->client)) 
            {
                // If user_data buffer is configured, copy the response into the buffer
                if (evt->user_data) 
                {
                    memcpy(evt->user_data + output_len, evt->data, evt->data_len);
                } 
                else 
                {
                    if (output_buffer == NULL) 
                    {
                        output_buffer = (char *) malloc(esp_http_client_get_content_length(evt->client));
                        output_len = 0;
                        if (output_buffer == NULL) 
                        {
                            ESP_LOGE(TAG, "Failed to allocate memory for output buffer");
                            return ESP_FAIL;
                        }
                    }
                    memcpy(output_buffer + output_len, evt->data, evt->data_len);
                }
                output_len += evt->data_len;
            }

            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
            if (output_buffer != NULL) 
            {
                // Response is accumulated in output_buffer. Uncomment the below line to print the accumulated response
                // ESP_LOG_BUFFER_HEX(TAG, output_buffer, output_len);
                if(content != NULL)
                {
                    memcpy(content, output_buffer, output_len);
                }
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            int mbedtls_err = 0;
            esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL);
            if (err != 0) 
            {
                ESP_LOGW(TAG, "Last esp error code: 0x%x", err);
                ESP_LOGW(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
            }
            else
            {
                if (output_buffer != NULL)
                {
                    if(content != NULL)
                    {
                        memcpy(content, output_buffer, output_len);
                    }
                    free(output_buffer);
                    output_buffer = NULL;
                }
                output_len = 0;
            }
            break;
    }
    return ESP_OK;
}

esp_err_t _http_event_handler_2(esp_http_client_event_t *evt)
{
    const char * TAG = "EVT_HTTP";
    static int output_len;       // Stores number of bytes read
    switch(evt->event_id) 
    {
        case HTTP_EVENT_ERROR:
            ESP_LOGE(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            //ESP_LOGI(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            break;
        case HTTP_EVENT_ON_DATA:
            //ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
                memcpy(content + output_len, evt->data, evt->data_len);
                output_len += evt->data_len;
                *(content + output_len) = '\0';
            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
            output_len = 0;
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            int mbedtls_err = 0;
            esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL);
            if (err != 0) 
            {
                ESP_LOGW(TAG, "Last esp error code: 0x%x", err);
                ESP_LOGW(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
            }
            else
            {
                output_len = 0;
            }
            break;
    }
    return ESP_OK;
}


static void XMLCALL
elementStart(void *user_data, const XML_Char *el, const XML_Char *attr[]) 
{
  //printf("element: [%s]\n", el);
  switch (current) {
    case DOCUMENT:
      if (strcmp(el,"stations") == 0)
        current = STATIONS;
      break;
    case STATIONS:
      if (strcmp(el,"station") == 0)
        current = STATION;
      break;
    case STATION:
      if (strcmp(el,"id") == 0)
        current = ID;
      else if (strcmp(el,"name") == 0)
        current = NAME;
      else if (strcmp(el,"ascii_name") == 0)
        current = ASCII_NAME;
      else if (strcmp(el,"href") == 0)
        current = HREF;
      else if (strcmp(el,"logo_xsmall") == 0)
        current = LOGO_XSMALL;
      else if (strcmp(el,"logo_small") == 0)
        current = LOGO_SMALL;
      else if (strcmp(el,"logo_medium") == 0)
        current = LOGO_MEDIUM;
      else if (strcmp(el,"logo_large") == 0)
        current = LOGO_LARGE;
      else if (strcmp(el,"logo") == 0)
        current = LOGO;
      else if (strcmp(el,"feed") == 0)
        current = FEED;
      else if (strcmp(el,"banner") == 0)
        current = BANNER;
      break;
    case ID:
    case NAME:
    case ASCII_NAME:
    case HREF:
      break;
    default:
      current = DOCUMENT;
      break;
  }
}

static void XMLCALL
elementEnd(void *user_data, const XML_Char *el) 
{
  switch (current) {
    case DOCUMENT:
      break;
    case STATIONS:
      current = DOCUMENT;
      break;
    case STATION:
      current = STATIONS;
      printf("\n");
      break;
    case ID:
      current = STATION;
      break;
    case NAME:
      current = STATION;
      break;
    case ASCII_NAME:
      current = STATION;
      break;
    case HREF:
      current = STATION;
      break;
    case LOGO_XSMALL:
      current = STATION;
      break;
    case LOGO_SMALL:
      current = STATION;
      break;
    case LOGO_MEDIUM:
      current = STATION;
      break;
    case LOGO_LARGE:
      current = STATION;
      break;
    case LOGO:
      current = STATION;
      break;
    case FEED:
      current = STATION;
      break;
    case BANNER:
      current = STATION;
      break;
    default:
      current = STATIONS;
      break;
  }
}

static void XMLCALL elementData(void *user_data, const XML_Char *data, int data_size) 
{
    bool use_el = true;
    char type_str[16];
    switch (current) 
    {
        case ID:
            strlcpy(&(stations -> id), data, data_size + 1);
            strcpy(type_str, "ID");
            break;
        case NAME:
            strlcpy(&(stations -> name), data, data_size + 1);
            strcpy(type_str, "NAME");
            break;
        case ASCII_NAME:
            strlcpy(&(stations -> ascii_name), data, data_size + 1);
            strcpy(type_str, "ASCII_NAME");
            break;
        case LOGO_XSMALL:
            strlcpy(&(stations -> logo_xsmall), data, data_size + 1);
            strcpy(type_str, "LOGO_XSMALL");
            stations ++;
            station_count ++;
            break;
        default:
            use_el = false;
            break;
    }
    if(use_el)
    {
        char tmp_str[data_size + 1];
        strlcpy(tmp_str, data, data_size + 1);
        ESP_LOGI("ELEMDATA", "[%s]: %s", type_str, tmp_str);
    }
}

void parse_xml(char * input)
{
    stations = malloc(sizeof(station_t) * 16);
    if(stations != NULL)
    {
        station_t * st_p = stations;
        XML_Parser parser = XML_ParserCreate(NULL);
        XML_SetElementHandler(parser, elementStart, elementEnd);
        XML_SetCharacterDataHandler(parser, elementData);

        if (XML_Parse(parser, input, strlen(input), 1) == XML_STATUS_ERROR) 
        {
            ESP_LOGE("PARSE XML", "Parsing error");
        }    
        stations = st_p;
        for(int i = 0; i < station_count; i ++)
        {
            ESP_LOGI("RESULTS", "[%d]: %s", i, (st_p -> id));
            st_p ++;
        }
        XML_ParserFree(parser);
    }
}

void get_station_list()
{
    const char * TAG = "GET ST LIST";
    esp_http_client_config_t config = 
    {
        .url = "",
        .transport_type = HTTP_TRANSPORT_OVER_SSL,
        .event_handler = _http_event_handler_2,
        .cert_pem = radiko_jp_root_cert_pem_start
    };
    char tmp_url[128];
    sprintf(tmp_url, "https://radiko.jp/v2/station/list/%s.xml", _region_id);
    config.url = tmp_url;
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    ESP_LOGI(TAG, "Allocating xml buffer");
    content = malloc(38400 * sizeof(char));
    ESP_LOGI(TAG, "Starting https request");
    esp_err_t err = esp_http_client_perform(client);
    if (err == ESP_OK)
    {
        int status_code = esp_http_client_get_status_code(client);
        int len = esp_http_client_get_content_length(client);
        ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
            status_code, len);
        ESP_LOGD(TAG, "XML buffer string length = %d", strlen(content));
        ESP_LOGD(TAG, "Received body: %s", content);
        parse_xml(content);
    }
    else 
    {
        ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
    }
    if(content != NULL)
    {
        ESP_LOGI(TAG, "Removing buffer");
        free(content);
        content = NULL;
    }
    esp_http_client_cleanup(client);    
}

void generate_playlist_url(station_t * st)
{
    esp_http_client_config_t config = 
    {
        .url = "",
        .transport_type = HTTP_TRANSPORT_OVER_SSL,
        .event_handler = _http_event_handler,
        .cert_pem = radiko_jp_root_cert_pem_start
    };
    config.transport_type = HTTP_TRANSPORT_OVER_TCP;
    config.event_handler = _http_event_handler_2;
    char tmp_url[256];
    sprintf(tmp_url, "http://f-radiko.smartstream.ne.jp/%s/_definst_/simul-stream.stream/playlist.m3u8", (st -> id));
    ESP_LOGI(TAG, "TMP_URL: %s", tmp_url);
    config.url = tmp_url;
    ESP_LOGW(TAG, "URL: %s", config.url);
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_http_client_set_header(client, "X-Radiko-AuthToken", _auth_token);
    ESP_LOGI(TAG, "Starting get playlist");

    content = malloc(8192 * sizeof(char));
    esp_err_t err = esp_http_client_perform(client);
    int status_code = esp_http_client_get_status_code(client);
    if (err == ESP_OK) 
    {
        int len = esp_http_client_get_content_length(client);
        ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
            status_code, len);
        ESP_LOGI(TAG, "Received body: %s", content);
        char * url_start = strstr(content, "http://");
        char * url_end = strstr(content, ".m3u8") + 5;
        int url_len = (url_end - url_start);
        strncpy(_playlist_url, url_start, url_len);

        *(_playlist_url + url_len) = '\0';
        ESP_LOGI(TAG, "Stream url: %s", _playlist_url);
    }
    else 
    {
        ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
    }
    if(content != NULL)
    {
        free(content);
        content = NULL;
    }
    esp_http_client_cleanup(client);

}

void auth(void)
{
    station_count = 0;
    _length = 0;
    _offset = 0;
    _auth_token[0] = '\0';
    char token_p[32];
    token_p[0] = '\0';
    const char * TAG = "GET";
    esp_http_client_config_t config = 
    {
        .url = "https://radiko.jp/v2/api/auth1",
        .transport_type = HTTP_TRANSPORT_OVER_SSL,
        .event_handler = _http_event_handler,
        .cert_pem = radiko_jp_root_cert_pem_start
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    esp_http_client_set_header(client, "User-Agent", "esp32/4");
    esp_http_client_set_header(client, "Accept", "*/*");
    esp_http_client_set_header(client, "X-Radiko-App", "pc_html5");
    esp_http_client_set_header(client, "X-Radiko-App-Version", "0.0.1");
    esp_http_client_set_header(client, "X-Radiko-User", "dummy_user");
    esp_http_client_set_header(client, "X-Radiko-Device", "pc");
    ESP_LOGI(TAG, "Starting auth1");
    esp_err_t err = esp_http_client_perform(client);
    int status_code = esp_http_client_get_status_code(client);
    if (err == ESP_OK) 
    {
        ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
                status_code, esp_http_client_get_content_length(client));
        ESP_LOGI(TAG, "Token: %s", _auth_token);        
        const char * auth_key = CONFIG_AUTH_KEY;
        strncpy(token_p, (auth_key + _offset), _length);
        token_p[_length] = '\0';
        ESP_LOGI(TAG, "Partial Key: %s", token_p);
    }
    else 
    {
        ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
    }
    esp_http_client_cleanup(client);

    if(strlen(token_p) != 0)
    {
        char base64_encoded[32];
        size_t base64_len;
        mbedtls_base64_encode((unsigned char *) base64_encoded, sizeof(base64_encoded),
                                &base64_len, (unsigned char *) token_p, strlen(token_p));
        if(strlen(base64_encoded) > 0)
        {
            ESP_LOGI(TAG, "Base64 encoded: %s", base64_encoded);
        }
        else
        {
            ESP_LOGE(TAG, "Base64 encode failed");
        }
        config.event_handler = _http_event_handler_2;
        config.url = "https://radiko.jp/v2/api/auth2";
        client = esp_http_client_init(&config);
        esp_http_client_set_header(client, "X-Radiko-AuthToken", _auth_token);
        esp_http_client_set_header(client, "X-Radiko-Partialkey", base64_encoded);
        esp_http_client_set_header(client, "X-Radiko-User", "dummy_user");
        esp_http_client_set_header(client, "X-Radiko-Device", "pc");
        ESP_LOGI(TAG, "Starting auth2");
        content = malloc(128 * sizeof(char));
        esp_err_t err = esp_http_client_perform(client);
        int status_code = esp_http_client_get_status_code(client);
        if (err == ESP_OK) 
        {
            ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
                    status_code, esp_http_client_get_content_length(client));
            char * endptr = strstr(content, ",");
            *endptr = '\0';
            strcpy(_region_id, content);
            ESP_LOGI(TAG, "Region: %s", content);
            free(content);
        }
        else 
        {
            ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
        }
        esp_http_client_cleanup(client);
    }
}

プログラム467行からauth();の定義が始まっています。

  • 先ずRadikoのHPへアクセツ
  • 数字とHPからもらい、認証キーと合わせて数字の列を作成
  • その数字の列を持ってもう一度アクセツすると承認される

これはRajikoからデータをもらう前に必ずやらなければ行けない作業です。

get_station_list();は375行から定義されています。やっていて分かったのですが、このRadikoプログラムはradikoのHPは、https://radiko.jp/ この矢印で指した部分に載っているラジオ局が聞ける様です。この関数でこの局関連のデータを作っている様です。

generate_playlist_url(stations);は417行から定義されています。引数 stations で指定したラジオ局のURLを取得する関数です。

Radiko はアクセツする地域で視聴出来るラジオ局が変わります。上記は東京の場合です。東京では16局のラジオ局が視聴出来ます。これを引数stations を使って指定する様です。ちなみに引数として、”stations+0”を指定するとTBS。”stations+15”がNHK-FMでした。ここをプログラムで指定できればラジオ局の選択が可能になる事が分かりました。選局する方法を考えて見ました。

選局にロータリースイッチを使う

選局にDIPロータリースイッチ 16ポジション 0~Fを使う事にしました。これは0から15の数字に合わせてスイッチの4個の端子が0/1に変化するモジュールです。つまり、

  • スイッチの0をTBS、・・・・ 15をNHKーFMに対応させる
  • スイッチの4ピンをESP32に接続し4ピンの状態が変わる度にイベントを発生させる。

とすれば、選局が可能と思われます。イベントは、mainフォルダー “play_radiko.c”のaudio_event_iface_listen(evt, &msg, portMAX_DELAY);関数が検知します。この関数に検知させるには、サンプルプログラムにボタンを追加するで行った事と同じ事をやれば良いのです。

先ずはハードの変更

ロータリースイッチを下記の様に配線しました。

ESP32 スイッチ 
 GPIO35   1
 GPIO39  2
 GPIO34  4
 GPIO36  8

次はソフトの変更

今回追加したスイッチによりイベントを発生させるには、

  • /components/my_board/my_board_v1_0フォルダーに、board_def.hファイルを作成しスイッチの定義を行う。
  • /components/my_board/my_board_v1_0フォルダーに、board.cファイルを作成しスイッチのハンドルを作成し起動
  • 最後に/main/play_radiko.cでイベント登録する。

board_def.hファイルの説明。

board_def.h

#ifndef _AUDIO_BOARD_DEFINITION_H_
#define _AUDIO_BOARD_DEFINITION_H_

#define BUTTON_DG_0    GPIO_NUM_35   /* You need to define the GPIO pins of your board */
#define BUTTON_DG_2    GPIO_NUM_39   /* You need to define the GPIO pins of your board */
#define BUTTON_DG_4    GPIO_NUM_34   /* You need to define the GPIO pins of your board */
#define BUTTON_DG_8    GPIO_NUM_36   /* You need to define the GPIO pins of your board */

#define INPUT_KEY_NUM     4             /* You need to define the number of input buttons on your board */

#define INPUT_KEY_DEFAULT_INFO() {                      \
    {                                                   \
        .type = PERIPH_ID_BUTTON,                       \
        .user_id = INPUT_KEY_USER_ID_DG_0,              \
        .act_id = BUTTON_DG_0,                          \
    },                                                  \
    {                                                   \
        .type = PERIPH_ID_BUTTON,                       \
        .user_id = INPUT_KEY_USER_ID_DG_2,              \
        .act_id = BUTTON_DG_2,                          \
    },                                                  \
    {                                                   \
        .type = PERIPH_ID_BUTTON,                       \
        .user_id = INPUT_KEY_USER_ID_DG_4,              \
        .act_id = BUTTON_DG_4,                          \
    },                                                  \
    {                                                   \
        .type = PERIPH_ID_BUTTON,                       \
        .user_id = INPUT_KEY_USER_ID_DG_8,              \
        .act_id = BUTTON_DG_8,                          \
    },                                                  \
}
										
#endif
  • ロータリースイッチのそれぞれのピンをBUTTON_DG_0,2,4,8と定義して
  • 使用するボタンを4個
  • 11行からはそれぞれのボタンの詳細を指定しています。

board.cファイルの説明

board.c

#include "esp_log.h"
#include "board.h"
#include "audio_mem.h"

#include "periph_button.h"

static const char *TAG = "AUDIO_BOARD";

esp_err_t audio_board_key_init(esp_periph_set_handle_t set)
{
    periph_button_cfg_t btn_cfg = {
        .gpio_mask = (0x27ULL << 34),  //Set Buttoms
    };
    esp_periph_handle_t button_handle = periph_button_init(&btn_cfg);
    AUDIO_NULL_CHECK(TAG, button_handle, return ESP_ERR_ADF_MEMORY_LACK);
    esp_err_t ret = ESP_OK;
    ret = esp_periph_start(set, button_handle);

    return ret;
}
  • 使用するポートを開けて
  • ハンドルを作成
  • 作成したハンドルを引数で受け取ったハンドル”set”に登録し動作開始

play_radiko.cの説明

ここでは、使用するポートの設定。スイッチの数字読み出し用関数の追加。ボタンイベントの登録。をしています。

play_radiko.c

/* Play M3U HTTP Living stream

   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 <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "audio_common.h"
#include "http_stream.h"
#include "i2s_stream.h"
#include "aac_decoder.h"
#include "esp_tls.h"
#include "esp_http_client.h"
#include "radiko.h"
#include "esp_peripherals.h"
#include "periph_wifi.h"
#include "board.h"

#if __has_include("esp_idf_version.h")
#include "esp_idf_version.h"
#else
#define ESP_IDF_VERSION_VAL(major, minor, patch) 1
#endif

#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0))
#include "esp_netif.h"
#else
#include "tcpip_adapter.h"
#endif

#include "driver/gpio.h"
#include <stdio.h>
#include <stdlib.h>

static const char *TAG = "HTTP_LIVINGSTREAM_EXAMPLE";

//#define AAC_STREAM_URI "http://open.ls.qingting.fm/live/274/64k.m3u8?format=aac"
#define MAX_HTTP_RECV_BUFFER 512
#define MAX_HTTP_content 8192

audio_pipeline_handle_t pipeline;
audio_element_handle_t http_stream_reader, i2s_stream_writer, aac_decoder;

#define st_no1          35
#define st_no2          39
#define st_no4          34
#define st_no8          36

void init_port(void)
{
    gpio_config_t io_conf = {};
    io_conf.mode = GPIO_MODE_INPUT;	    			//set as input mode
    io_conf.pin_bit_mask = 0x27ULL << 34;			//bit mask of the pins GPIO34,35,36,39	
    io_conf.pull_up_en = 0;						    //enable pull-up mode
    gpio_config(&io_conf);
}

int get_staion_no()
{
  int a;

    a = 0;
    if(gpio_get_level(st_no1)) a += 1;
    if(gpio_get_level(st_no2)) a += 2;
    if(gpio_get_level(st_no4)) a += 4;
    if(gpio_get_level(st_no8)) a += 8;

    return a;
}

int _http_stream_event_handle(http_stream_event_msg_t *msg)
{
    if (msg->event_id == HTTP_STREAM_RESOLVE_ALL_TRACKS) {
        return ESP_OK;
    }

    if (msg->event_id == HTTP_STREAM_FINISH_TRACK) {
        return http_stream_next_track(msg->el);
    }
    if (msg->event_id == HTTP_STREAM_FINISH_PLAYLIST) {
        return http_stream_fetch_again(msg->el);
    }
    return ESP_OK;
}


void app_main(void)
{
	int st_now;

	init_port();
	
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES) 
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0))
    ESP_ERROR_CHECK(esp_netif_init());
#else
    tcpip_adapter_init();
#endif


    esp_log_level_set("*", ESP_LOG_INFO);
    esp_log_level_set(TAG, ESP_LOG_DEBUG);

    ESP_LOGI(TAG, "[2.0] Create audio pipeline for playback");
    audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
    pipeline = audio_pipeline_init(&pipeline_cfg);

    ESP_LOGI(TAG, "[2.1] Create http stream to read data");
    http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
    http_cfg.event_handle = _http_stream_event_handle;
    http_cfg.type = AUDIO_STREAM_READER;
    http_cfg.enable_playlist_parser = true;
    http_stream_reader = http_stream_init(&http_cfg);

    ESP_LOGI(TAG, "[2.2] Create i2s stream to write data to codec chip");
    i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
    i2s_cfg.type = AUDIO_STREAM_WRITER;
    i2s_stream_writer = i2s_stream_init(&i2s_cfg);

    ESP_LOGI(TAG, "[2.3] Create aac decoder to decode aac file");
    aac_decoder_cfg_t aac_cfg = DEFAULT_AAC_DECODER_CONFIG();
    aac_decoder = aac_decoder_init(&aac_cfg);

    ESP_LOGI(TAG, "[2.4] Register all elements to audio pipeline");
    audio_pipeline_register(pipeline, http_stream_reader, "http");
    audio_pipeline_register(pipeline, aac_decoder,        "aac");
    audio_pipeline_register(pipeline, i2s_stream_writer,  "i2s");

    ESP_LOGI(TAG, "[2.5] Link it together http_stream-->aac_decoder-->i2s_stream-->[codec_chip]");
    const char *link_tag[3] = {"http", "aac", "i2s"};
    audio_pipeline_link(pipeline, &link_tag[0], 3);


    ESP_LOGI(TAG, "[ 3 ] Start and wait for Wi-Fi network");
    esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
    esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);

    ESP_LOGI(TAG, "[3.1] Initialize keys on board");
    audio_board_key_init(set);

    periph_wifi_cfg_t wifi_cfg = 
    {
        .ssid = CONFIG_WIFI_SSID,
        .password = CONFIG_WIFI_PASSWORD,
    };

    esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg);
    esp_periph_start(set, wifi_handle);
    periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY);

    auth();
    get_station_list();
	st_now = get_staion_no();
    
    generate_playlist_url(stations + st_now);
    ESP_LOGI(TAG, "[2.6] Set up  uri (http as http_stream, aac as aac decoder, and default output is i2s)");
    audio_element_set_uri(http_stream_reader, _playlist_url);

    ESP_LOGI(TAG, "[ 4 ] Set up  event listener");
    audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
    audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);

    ESP_LOGI(TAG, "[4.1] Listening event from all elements of pipeline");
    audio_pipeline_set_listener(pipeline, evt);

    ESP_LOGI(TAG, "[4.2] Listening event from peripherals");
    audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), evt);

    ESP_LOGI(TAG, "[ 5 ] Start audio_pipeline");
    audio_pipeline_run(pipeline);

	int flg_no;
    while (1) 
    {
        audio_event_iface_msg_t msg;
        esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY);
        if (ret != ESP_OK) 
        {
            ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret);
            continue;
        }

        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT
            && msg.source == (void *) aac_decoder
            && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) 
        {
            audio_element_info_t music_info = {0};
            audio_element_getinfo(aac_decoder, &music_info);

            ESP_LOGI(TAG, "[ * ] Receive music info from aac decoder, sample_rates=%d, bits=%d, ch=%d",
                    music_info.sample_rates, music_info.bits, music_info.channels);

            audio_element_setinfo(i2s_stream_writer, &music_info);
            i2s_stream_set_clk(i2s_stream_writer, music_info.sample_rates, music_info.bits, music_info.channels);
            continue;
        }

        /* restart stream when the first pipeline element (http_stream_reader in this case) receives stop event (caused by reading errors) */
        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) http_stream_reader
            && msg.cmd == AEL_MSG_CMD_REPORT_STATUS && (int) msg.data == AEL_STATUS_ERROR_OPEN) 
        {
            ESP_LOGW(TAG, "[ * ] Restart stream");
            audio_pipeline_stop(pipeline);
            audio_pipeline_wait_for_stop(pipeline);
            audio_element_reset_state(aac_decoder);
            audio_element_reset_state(i2s_stream_writer);
            audio_pipeline_reset_ringbuffer(pipeline);
            audio_pipeline_reset_items_state(pipeline);
            audio_pipeline_run(pipeline);
            continue;
        }
        
        if (msg.source_type == PERIPH_ID_BUTTON)
        {
	        flg_no = get_staion_no();
    	    if( flg_no != st_now)
    	    {
    	        ESP_LOGI(TAG, "Station No:%d",flg_no);
    	    	audio_pipeline_stop(pipeline);
    	        audio_pipeline_wait_for_stop(pipeline);

    	        st_now = flg_no;
    	        generate_playlist_url(stations + st_now);
    	        audio_element_set_uri(http_stream_reader, _playlist_url);

    	        ESP_LOGW(TAG, "[ * ] Restart stream");
    	        audio_element_reset_state(aac_decoder);
    	        audio_element_reset_state(i2s_stream_writer);
    	        audio_pipeline_reset_ringbuffer(pipeline);
    	        audio_pipeline_reset_items_state(pipeline);
    	        audio_pipeline_run(pipeline);
    	    }
       }
       
    }
    ESP_LOGI(TAG, "[ 6 ] Stop audio_pipeline");
    audio_pipeline_stop(pipeline);
    audio_pipeline_wait_for_stop(pipeline);
    audio_pipeline_terminate(pipeline);

    audio_pipeline_unregister(pipeline, http_stream_reader);
    audio_pipeline_unregister(pipeline, i2s_stream_writer);
    audio_pipeline_unregister(pipeline, aac_decoder);

    /* Terminate the pipeline before removing the listener */
    audio_pipeline_remove_listener(pipeline);

    /* Stop all peripherals before removing the listener */
    esp_periph_set_stop_all(set);
    audio_event_iface_remove_listener(esp_periph_set_get_event_iface(set), evt);

    /* Make sure audio_pipeline_remove_listener & audio_event_iface_remove_listener are called before destroying event_iface */
    audio_event_iface_destroy(evt);

    /* Release all resources */
    audio_pipeline_deinit(pipeline);
    audio_element_deinit(http_stream_reader);
    audio_element_deinit(i2s_stream_writer);
    audio_element_deinit(aac_decoder);
    esp_periph_set_destroy(set);
}

  • 62行:void init_port(void)
    • GPIO34,35,36,39のマスクを開放
  • 71行:int get_staion_no()
  • 171行: ロータリースイッチの値を呼んで
  • 173行: generate_playlist_url(stations + st_now); で選局したURLを取得。これでラジオ局が選択出来ます。
  • 231行: ロータリースイッチに変更が有るとこの部分が実行されます。
    • 現在実行している局の再生を停止。
    • 新しい局番号を、st_now変数に保存。
    • generate_playlist_url()関数に新しい局データの引数を渡して局を選択。
    • URLを取得して実行

プロジェクトの構成

今回の変更でプロジェクトは以下の様になりました。


- radiko-esp32-main_01/
      - components/
                   - radiko-esp32/
                              - CMakeLists.txt
                              - Kconfig.projbuild
                              - radiko.c
                              - radiko.h
                              - radiko_jp_rootcert.pem
                   - my_board/
                              - my_board_v1_0/
                                        - board.c
                                        - board.h
                                        - board_def.h
                                        - board_pins_config.c
                              - CMakeLists.txt
      - main/
                   - CMakeLists.txt
                   - Kconfig.projbuild
                   - play_radiko.c
      - CMakeLists.txt
      - partitions.csv
      - sdkconfig.defaults

コンパイルと実行

  • ESP32を使用することを前提としています。
  • 先ずは、idf.py menuconfig を実行して初期設定を行います。
    • Topー>Audio HALー>Audio board custom audio board を選択
    • Example ConfigurationでWiFi関係の設定を行います。mainフォルダー下のKconfig.projbuildでWiFi関係の設定を行っていれば入力不要。
    • Radiko Configurationで認証キーを入力します。今回は、radiko-esp32フォルダー下のKconfig.projbuildで指定しているので入力不要。
  • この後、idf.py -p [USB Port] flash monitor を実行して下さい。コンパイルして実行出来ます。
  • 実行後しばらくしてラジオの再生が始まります。そうしたらロータリースイッチを回して下さい。ラジオ局が選択されます。

次は

ロータリースイッチを回してラジオ局が選択出来るようになりました。選局出来るようになったのは嬉しいのですが、これでは今どの局が再生されているか分かりません。次回は再生されている局を表示するDisplay を追加したいと思います。