Radikoを聞く(Arduino編)ーAACファイルの再生ー

前回は AACファイルのリンク先が示されたファイル(仮にChunk_List_Fileとします)の取得まで出来ました。今回はAACファイルを再生しRadikoを聞きたいと思います。

取り敢えずAACファイルをダウンロードしてみる

先ずはChunk_List_Fileに書かれているAACファイルが本当に音声データかを確認します。確認は下記の方法で行います。

  1.  Chunk_List_Fileに書かれているAACファイルをメモリーにダウンロード
  2.  メモリーに保存したデータをSPIFFSを使用してESP32のFLASHに保存
  3.  ESP32でHTTPサーバを上げてPCでブラウザを使ってそのサーバにアクセス
  4.  サーバのHPか保存したファイルをPCにダンロード
  5.  PCに保存したファイルをメディアプレーヤーで再生。

今回は、サーバ本体のプログラムとサーバHP用HTMLファイルを使用します。

本体プログラム

radiko_01_temp.ino

#include <Arduino.h>
#include <WiFi.h>
#include <string.h>
#include "mbedtls/base64.h"
#include <HTTPClient.h>

#include <WebServer.h>
#include "SPIFFS.h"

// Enter your WiFi setup here:
const char *SSID = "XXXXXXXX";
const char *PASSWORD = "YYYYYYYY";

String _auth_token;

WebServer server(80);
byte _buff[1024*32];

void setup()
{
  WiFiClient * stream;
  HTTPClient http;
  File fp;
  int a,b;
  String str;
    
   //-----------------  part0 ------------------------------------------------------------------------------
    Serial.begin(115200);
    delay(500);
    
    WiFi.disconnect();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    WiFi.begin(SSID, PASSWORD);
    while (WiFi.status() != WL_CONNECTED) 
    {
      Serial.println("...Connecting to WiFi");
      delay(1000);
    }
    Serial.println("Connected");
    Serial.println(SSID);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.println();
        
    //-----------------  HTTP Server & SPIFFS ----------------------------------------------------------------------
    server.on("/", handleRoot);
    server.onNotFound(handleWebRequests);
    server.begin();
    Serial.println("HTTP server started");

    SPIFFS.begin();
    Serial.println("SPIFFS started");
    
    //-----------------  Get aac file  ----------------------------------------------------------------------------
    str = radiko();
    a = str.indexOf("https");
    str = &str[a];
    a = str.indexOf(".aac");
    str = str.substring(0, a + 4);

    Serial.println("Chunk URL = " + str);
    http.begin(str);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    Serial.println("Code = " + String(http.GET()));  
    Serial.println("Size = " + String(http.getSize()));
    
    stream = http.getStreamPtr();
    a = stream->available();
    stream->readBytes(_buff, a);
    while(!(b = stream->available())) NOP();
    stream->readBytes(&_buff[a], b);
    http.end(); 

    fp = SPIFFS.open("/data.aac", "w");
    fp.write(_buff, a + b); 
    fp.close();
    
    Serial.println("1st: " + String(a) + " , 2nd: " + String(b) + " , Total: " + String(a + b));
    Serial.println("File Saved");
}

void loop()
{
    server.handleClient();
}

void handleRoot() 
{
  File fp;
  
    fp = SPIFFS.open("/download.html","r");
    server.streamFile(fp,"text/html");
    fp.close();
}

void handleWebRequests()
{
  String path;
  File dataFile;

    path = server.uri();
    dataFile = SPIFFS.open(path.c_str(), "r");
    server.streamFile(dataFile, "audio/aac");
    dataFile.close();
}

String radiko()
{ 
  char * auth_key = "bcd151073c03b352e1ef2fd66c32209da9ca0afa" ;
  HTTPClient http;
  String chunk_url;
  int _length, _offset; 
  char base64_encoded[32],token_p[32];
  size_t base64_len;
  const char *headerKeys[] = {"X-Radiko-AuthToken", "X-Radiko-KeyLength", "X-Radiko-KeyOffset"};
  int a,b;
  String str;
    
    //-----------------  part1 ------------------------------------------------------------------------------
    Serial.println("====================  Part 1 ======================================");
    http.begin("https://radiko.jp/v2/api/auth1"); 
    http.addHeader("User-Agent", "esp32/4");
    http.addHeader("Accept", "*/*");
    http.addHeader("X-Radiko-App", "pc_html5");
    http.addHeader("X-Radiko-App-Version", "0.0.1");
    http.addHeader("X-Radiko-User", "dummy_user");
    http.addHeader("X-Radiko-Device", "pc");
    http.collectHeaders(headerKeys, 3);
    a = http.GET();
    Serial.println("part1 = " + String(a));
    _offset = String(http.header(headerKeys[2])).toInt();
    _length = String(http.header(headerKeys[1])).toInt();
    _auth_token = http.header(headerKeys[0]);
/*
    Serial.println("_auth_token = " + _auth_token);
    Serial.println("_offset = " + String(_offset));
    Serial.println("_length = " + String(_length));
*/
    http.end(); 

    token_p[0] = '\0';
    strncpy(token_p, &auth_key[_offset], _length);
    token_p[_length] = '\0';
    mbedtls_base64_encode((unsigned char *) base64_encoded, sizeof(base64_encoded),&base64_len, (unsigned char *) token_p, strlen(token_p));
/*    
    Serial.print("auth_key = ");
    Serial.println(auth_key);
    Serial.print("token_p = ");
    Serial.println(token_p);
    Serial.print("base64_encoded = ");
    Serial.println(base64_encoded);
*/
    //-----------------  part2 ------------------------------------------------------------------------------
    Serial.println("====================  Part 2 ======================================");
    http.begin("https://radiko.jp/v2/api/auth2"); 
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    http.addHeader("X-Radiko-Partialkey", base64_encoded);
    http.addHeader("X-Radiko-User", "dummy_user");
    http.addHeader("X-Radiko-Device", "pc");
    a = http.GET();
    Serial.println("part2 = " + String(a));
    str = http.getString();
//    Serial.print("Area data = " + str);
    str = str.substring(0,str.indexOf(","));
//    Serial.println("Area code = " + str);
    http.end(); 
       
    //-----------------  part3 ------------------------------------------------------------------------------
    //    http.begin("https://radiko.jp/v2/station/list/[ID].xml"); 
    Serial.println("====================  Part 3 ======================================");
    str = "https://radiko.jp/v2/station/list/" + str + ".xml";
//    Serial.println(str);
    http.begin(str); 
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    a = http.GET();
    Serial.println("part3 = " + String(a));
    str = http.getString();
//    Serial.println(str);

    //-----------------  part4 ------------------------------------------------------------------------------
    Serial.println("====================  Part 4 ======================================");
    str = "https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/playlist.m3u8";
    http.begin(str); 
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    a = http.GET();
    Serial.println("part4 = " + String(a));
    str = http.getString();
//    Serial.println(str);
    chunk_url = &str[str.indexOf("https")];
//    Serial.println(chunk_url);
    http.end(); 
    
    //-----------------  part5 ------------------------------------------------------------------------------
    Serial.println("====================  Part 5 ======================================");
    http.begin(chunk_url);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    a = http.GET();
    Serial.println("part5 = " + String(a));
    str = http.getString();
    Serial.println(str);
    
    return str;     //  <--- Added  
}

サーバ用HTMLのScript

download.html

<!doctype html>
<html>
    <head>
        <meta charset='utf-8'>
        <title>Download</title>
    </head>
    <body>
        <center>
            <a style='font-size: 30px;' href='/data.aac' download='data.aac'>Download</a>
        </center>
    </body>
</html>

前回からの変更

  • SPIFFSを使用しています。SPIFFSの詳細はSPIFFSプラグインを参考にして下さい
    • HTTPサーバHPのHTMLを”download.html”としてESP32に保存しています。
    • プログラム実行前にファイルをESP32にUploadして下さい。
  • Part0部は変更していません。
    • Part0部でHTTPサーバのIPアドレスを表示している箇所が有ります。(41行)
    • このアドレスはHTTPサーバにアクセスする時に必要になります。
  • 108行: 関数 String radiko()を宣言して、前回のPart1からPart5までをそこに移動。
    • Part1からPart4までは、リターンコード以外の表示を廃止。Part5は変更無し。
    • Part5でChunkファイルの内容を String str に読み込んでいます。これを戻り値にする。
      • 203行: return str; // <— Added

HTTP ServerとSPIFFSの追加

  • HTTP ServerとSPIFFSを追加するので、以下を追加
    • 7行:  #include <WebServer.h>
    • 8行:  #include <SPIFFS.h>
    • 16行: WebServer server(80); 
  • 47から50行: HTTPサーバーの設定
  • 47行: GETでサーバーにアクセスされた時にハンドルする関数 
  • 52行: SPIFFS開始

AACファイルのRadikoサーバーからの読み込みとESP32への保存 (55から81行)

HTTPサーバのHPからファイルをPCへダウンロード

  • シリアルモニタに、”File Saved”と表示されたらHTTPサーバにアクセスします
  • PCでブラウザを上げ、Part0部で表示されたHTTPサーバのIPアドレスをブラウザに入力します。
  • 今回は 192.168.3.9でした。入力後下記のサーバのHPが表示されます。
  • 画面の”DownLoad”をクリックするとデータがPCにダウンロードされます。
    • ダウンロードされるファイルは”data.aac”と設定したのですが”data.mpga”となってしまいました。
    • でもダウンロードしたファイルをメディアプレーヤーで再生すると確かにJ-WAVWの音声でした。

ラジオの再生はChunk_List_FileのAACを順に再生すれば良い事が分かりました。

AACファイルをどの様に再生するか

次の問題はこのAACファイルをどの様に再生するかです。以前ここでファイルに保存されたAACデータの再生をやっています。まさかサイトからダウンロードしそれをFLAHに焼いて再生する分けには行きません。

そこでVS1053を使う事にしました。ここー>VS1053bを使うでSDカードに保存されたAACファイルの再生が出来る事は確認済です。そこでこのダウンロードしたファイルをSDカードに保存してVS1053で再生出来るか試して見ました。最初の一瞬音飛びの様になるのですが再生しました。

最初の音飛びが気になったのでAACファイルをバイナリーエディターで開くと、最初の74バイトがファイルのヘッダーの様に見えます。これは音声データでは無い。

そこでこの74バイトを読み捨て残りの部分をVS1053に送ると今度は綺麗に再生されました(音飛びが無い)。VS1053で問題無くデータを再生出来るようです。

Chunk_List fileをもう一度見る

再生方法が決まったので、もう一度Chunk_List_Fileを見てみる事にしました。

Part5

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:288554
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288554.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288555.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288556.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288557.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288558.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288559.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288560.aac
#EXTINF:5.12,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288561.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288562.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288563.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288564.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288565.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288566.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288567.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288568.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288569.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288570.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288571.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288572.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288573.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288574.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288575.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288576.aac
#EXTINF:5.12,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288577.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288578.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288579.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288580.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288581.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288582.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288583.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288584.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288585.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288586.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288587.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288588.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288589.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288590.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288591.aac
#EXTINF:4.992,
https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/media-u37e6lseu_w2087160318_288592.aac
  • 最初の5行
    • EXTM3U _____________毎回同じ
    • EXT-X-VERSION:3__________毎回同じ
    • EXT-X-TARGETDURATION:6______変わる。MAXの再生時間か?
    • EXT-X-MEDIA-SEQUENCE:288554___リストの最初のファイルに振られた番号?
    • EXT-X-DISCONTINUITY-SEQUENCE:0__送信の回数か?
  • これら2つはペアー
    • EXTINF:4.992,: 続くACCファイルの再生時間(秒)の模様。約5秒
    • https://f-radiko.smartstream.ne.jp/FMJ/definst/simul-stream.stream/media-u37e6lseu_w2087160318_288554.aac <ー音声ファイル
  • Chunk_List_Fileに入っていた”AAC”ファイルの数は常に39個
  • URL最後の数字部分はシーケンシャルになっている。
  • 各”AAC”ファイルの再生時間は約5秒。容量的には約30kbyte。
  • よってChunk_List_Fileの再生時間は 5 x 39 ÷ 60 =3.25。 約3分強。

まだ良くわからない点も有りますが取り敢えず先に進みます。

先ずはハード

VS1053bを使うと同じですが、VS1053の5VをESP32からでは無く独立に取っています。また今回はSDカードは使っていません。

次はソフト

プログラムを開発上で生じた問題とその対策

  • 再生の時間が合わない。
    • Chunk_List_Fileに有る”AAC”ファイルを正常に再生すると39個で決まった時間になります。
    • ところが実際にESP32とVS1053で再生すると、再生時間にブレが生じる事が分かりました。
    • 再生が規定時間より速く終わると次の出だしが重なり、遅く終わると音声が飛ぶ。
    • 39個の塊で時間を合わせるのは難しく、そこでAACファイル順番にアクセスする事にしました。
      • AACファイルURLの最後は通しの数字になっています。この番号を自分で増やして新たにURLを作成すればChunk_List_Fileにアクセスする必要がなくなる。
      • 自分のペースで再生しているので音の重なり、飛びは無くなりますが、下記の心配が有ります。
        • 実機の再生が常に遅い場合、時間がどんどん遅れる。速い場合はどんどん進む。
        • AACファイルのリンク先をこちらで勝手に製作しているので無いファイルをアクセスする可能性が有る
      • これらに対しては追々対応して行く予定です。
  • 再生時間遅れが有る。
    • Chunk_List_Fileをもとに再生すると実際の放送に対して約3分強遅れて再生される。
    • 解決策
      • Chunk_List_Fileから得られたAACリンク先の数字の部分に遅れ分を足してアクセスする
  • 時々不定期にノイズ、音飛びが有る。
    • これは原因が分かりません。
    • ダンロード時にデータが破壊されたと思うのですが確かめられません。

プログラムの流れ

  1.  Radikoサーバにアクセスし認証を得る。
  2.  AACファイルを見込みバッファーに保管する。
  3.  VS1053のDREC信号をESP32の割り込みトリガーに使用しデータを送る
  4.  2,3を繰り返す
radiko01.ino



#include <WiFi.h>
#include <string.h>
#include "mbedtls/base64.h"
#include <HTTPClient.h>

#include <WebServer.h>
#include "SPIFFS.h"
#include <SPI.h>

// Enter your WiFi setup here:
const char *SSID = "XXXXXXXX";
const char *PASSWORD = "YYYYYYYY";

WebServer _server(80);

//For VS1053b PIN
#define XCS           17
#define XDCS          22
#define DREQ          39
#define XRESET        21
#define Buf_busy      32
#define Dummy         25

#define CLK           18
#define MISO          19
#define MOSI          23

//VS1053 register
#define VLSI_MODE     0x00
#define VLSI_VOL      0x0B

#define AAC_HEADER    0x49
#define UNIT_SIZE     4096
#define T_SIZE        17
#define TIME_OFFSET   40

int _buff_pt, _play_pt, _buff_MAX, _aac_no;
byte _sound_buff[UNIT_SIZE * (T_SIZE + 1)];
String _auth_token, _chunk_url, _base_aac_url;

void setup()
{
  File fp;
  int a,b;
  String str;

   //-----------------  part0 ------------------------------------------------------------------------------
    Serial.begin(115200);
    delay(500);

    WiFi.disconnect();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    WiFi.begin(SSID, PASSWORD);
    while (WiFi.status() != WL_CONNECTED)
    {
      Serial.println("...Connecting to WiFi");
      delay(1000);
    }
    Serial.println("Connected");
    Serial.println(SSID);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.println();

    //-----------------  HTTP Server & SPIFFS ----------------------------------------------------------------------
    _server.on("/", handleRoot);
    _server.onNotFound(handleWebRequests);
    _server.begin();
    Serial.println("HTTP server started");

    SPIFFS.begin();
    Serial.println("SPIFFS started");
    init_vs1053();
    VLSI_SetVolume(70);
    Serial.print("Init vs1053: ");
    Serial.println(VLSIReadReg(VLSI_MODE),HEX);

    //-----------------  Radiko Player  ----------------------------------------------------------------------------
    radiko();
    get_aac_url();
    _buff_pt = 0;
    ring_buffer();
    _play_pt = _buff_MAX = 0;
    send_vs1053();
    attachInterrupt(DREQ, send_vs1053, RISING);
}

void loop()
{
  int a;

    _server.handleClient();

    if(_buff_pt > _play_pt) a = _buff_pt - _play_pt;
    else a = _buff_MAX - _play_pt + _buff_pt;

    if( a < (UNIT_SIZE * 8)) ring_buffer();
}

void send_vs1053()
{
  digitalWrite(XDCS,LOW);
  while(digitalRead(DREQ))
  {
    SPI.write(_sound_buff[_play_pt]);
    _play_pt ++;
    if(_play_pt == _buff_MAX) _play_pt = 0;
  }
  digitalWrite(XDCS,HIGH);
}

void ring_buffer()
{
  int a, aac_size, avail;
  byte d_buff[0x50];
  String str;
  HTTPClient http;
  WiFiClient * stream;

    str =_base_aac_url + String(_aac_no) + ".aac";
    http.begin(str);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    if(http.GET() != 200)
    {
        http.end();
        get_aac_url();
        str =_base_aac_url + String(_aac_no) + ".aac";
        http.begin(str);
        http.addHeader("X-Radiko-AuthToken", _auth_token);
        http.GET();
    }
    Serial.println(str);
    aac_size =  http.getSize();
    stream = http.getStreamPtr();
    avail = stream->available();
    aac_size -= AAC_HEADER;
    avail -= AAC_HEADER;
    stream->readBytes(d_buff, AAC_HEADER);

    while(aac_size)
    {
      if(avail > UNIT_SIZE) a = UNIT_SIZE;
      else a = avail;
      stream->readBytes(&_sound_buff[_buff_pt], a);
      _buff_pt += a;
      avail -= a;
      aac_size -= a;

      if(_buff_pt > (UNIT_SIZE * T_SIZE - 1))
      {
        _buff_MAX = _buff_pt;
        _buff_pt = 0;
      }

      if(!avail)
      {
        if(aac_size)
          while(!(avail = stream->available())) NOP();
      }
    }

    _aac_no ++;
    http.end();
}

void get_aac_url()
{
  int a,b;
  String str;
  HTTPClient http;

    http.begin(_chunk_url);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    http.GET();
    str = http.getString();
    a = str.indexOf("https");
    str = &str[a];
    a = str.indexOf(".aac");
    _base_aac_url =str.substring(0,a);
    a =_base_aac_url.length();
    while(_base_aac_url[a] != '_') a --;
    str = &_base_aac_url[a + 1];
    _base_aac_url =_base_aac_url.substring(0,a + 1);
    Serial.println(_base_aac_url);
    _aac_no = str.toInt();
    http.end();

    a = TIME_OFFSET;
    b = 1;
    while(b)
    {
      a --;
      str =_base_aac_url + String(_aac_no + a) + ".aac";
      http.begin(str);
      http.addHeader("X-Radiko-AuthToken", _auth_token);
      if(http.GET() == 200) b = 0;
      http.end();
      if(a == 0) b = 0;
    }
    Serial.println("TIME OFFSET: " + String(a));
    _aac_no += a;
}

void init_vs1053()
{
    pinMode(XCS, OUTPUT);
    digitalWrite(XCS,HIGH);

    pinMode(XDCS, OUTPUT);
    digitalWrite(XDCS,HIGH);

    pinMode(Buf_busy, OUTPUT);
    digitalWrite(Buf_busy,HIGH);

    pinMode(XRESET, OUTPUT);
    digitalWrite(XRESET,HIGH);

    pinMode(DREQ,INPUT);

    // Reset VS1053
    digitalWrite(XRESET,LOW);
    delay(500);
    digitalWrite(XRESET,HIGH);
    delay(500);

    // SPI
    SPI.begin(CLK, MISO, MOSI, Dummy);
    VLSIWriteReg(VLSI_MODE, 0x800);
}

void VLSI_SetVolume(byte v_data)
{
  float a;
  uint16_t vol_data;

    a = v_data * 2.5;
    vol_data = a;
    vol_data = 250 - vol_data;
    vol_data |= (vol_data << 8);
    VLSIWriteReg(VLSI_VOL, vol_data);
}

uint16_t VLSIReadReg(byte vAddress)
{
  uint16_t wValue;

    while(!digitalRead(DREQ)) NOP();
    digitalWrite(XCS,LOW);
    SPI.write(0x03);                         // Read
    SPI.write(vAddress);                     // Read
    ((byte*)&wValue)[1] = SPI.transfer(0xFF);   // 16 bit high byte
    ((byte*)&wValue)[0] = SPI.transfer(0xFF);   // 16 bit low byte
    digitalWrite(XCS,HIGH);
    return wValue;
}

void VLSIWriteReg(byte vAddress, uint16_t wValue)
{
    while(!digitalRead(DREQ)) NOP();
    digitalWrite(XCS,LOW);
    SPI.write(0x02);                     // Write
    SPI.write(vAddress);                 // Register address
    SPI.write(((byte*)&wValue)[1]);      // 16 bit write high byte
    SPI.write(((byte*)&wValue)[0]);      // 16 bit write low byte
    digitalWrite(XCS,HIGH);
}

void handleRoot()
{
  File fp;

    fp = SPIFFS.open("/download.html","r");
    _server.streamFile(fp,"text/html");
    fp.close();
}

void handleWebRequests()
{
  String path;
  File dataFile;

    path = _server.uri();
    dataFile = SPIFFS.open(path.c_str(), "r");
    _server.streamFile(dataFile, "audio/aac");
    dataFile.close();
}

void radiko()
{
  char * auth_key = "bcd151073c03b352e1ef2fd66c32209da9ca0afa" ;
  HTTPClient http;
  int key_length, offset;
  char base64_encoded[32],token_p[32];
  size_t base64_len;
  const char *headerKeys[] = {"X-Radiko-AuthToken", "X-Radiko-KeyLength", "X-Radiko-KeyOffset"};
  int a,b;
  String str;

    //-----------------  part1 ------------------------------------------------------------------------------
    http.begin("https://radiko.jp/v2/api/auth1");
    http.addHeader("User-Agent", "esp32/4");
    http.addHeader("Accept", "*/*");
    http.addHeader("X-Radiko-App", "pc_html5");
    http.addHeader("X-Radiko-App-Version", "0.0.1");
    http.addHeader("X-Radiko-User", "dummy_user");
    http.addHeader("X-Radiko-Device", "pc");
    http.collectHeaders(headerKeys, 3);
    a = http.GET();
    Serial.println("part1 = " + String(a));
    offset = String(http.header(headerKeys[2])).toInt();
    key_length = String(http.header(headerKeys[1])).toInt();
    _auth_token = http.header(headerKeys[0]);
    http.end();

    token_p[0] = '\0';
    strncpy(token_p, &auth_key[offset], key_length);
    token_p[key_length] = '\0';
    mbedtls_base64_encode((unsigned char *) base64_encoded, sizeof(base64_encoded),&base64_len, (unsigned char *) token_p, strlen(token_p));

    //-----------------  part2 ------------------------------------------------------------------------------
    http.begin("https://radiko.jp/v2/api/auth2");
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    http.addHeader("X-Radiko-Partialkey", base64_encoded);
    http.addHeader("X-Radiko-User", "dummy_user");
    http.addHeader("X-Radiko-Device", "pc");
    a = http.GET();
    Serial.println("part2 = " + String(a));
    str = http.getString();
    str = str.substring(0,str.indexOf(","));
    http.end();

    //-----------------  part3 ------------------------------------------------------------------------------
    //    http.begin("https://radiko.jp/v2/station/list/[ID].xml");
    str = "https://radiko.jp/v2/station/list/" + str + ".xml";
    http.begin(str);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    a = http.GET();
    Serial.println("part3 = " + String(a));
    str = http.getString();
    http.end();

    //-----------------  part4 ------------------------------------------------------------------------------
//    str = "https://f-radiko.smartstream.ne.jp/FMJ/_definst_/simul-stream.stream/playlist.m3u8";
    str = "https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/playlist.m3u8";
    http.begin(str);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    a = http.GET();
    Serial.println("part4 = " + String(a));
    str = http.getString();
    _chunk_url = &str[str.indexOf("https")];
    http.end();
}
  • 80行: radiko();  
    • ここでRadikoの認証とChunk_List_FileのURLを得ます。
    • 345行:str = “https://f-radiko.smartstream.ne.jp/TBS/definst/simul-stream.stream/playlist.m3u8″;
      • ここでラジオ局を指定しています。今回は TBS です。
      • 前回のPart3Listにラジオ局一覧が有ります。
      • TBSの部分を希望のラジオ局にIDにすればそのラジオ局が聞けます。
  • 81行: get_aac_url();
    • Chunk_List_FileのURLから最初のAACファイルURLを得ます。
    • 184行:_base_aac_url =_base_aac_url.substring(0,a + 1);
      • URLの数字以外の部分の取り出し
    • 186行:_aac_no = str.toInt();
      • URLの数字の部分の取り出し
    • 189から202行
      • ここで時間ラグを調整しています。
      • 上記で得たURLにTIME_OFFSET (=40)を足し新しいURLでアクセス
      • リターンコードが200になるまで数字を減らしてアクセスする。
      • リターンコードが200となった値をオフセット値とする
  • 83行:ring_buffer();
    • バッファーにAACのデータを保存
  • 85行:send_vs1053();
    • VS1053にデータを送信
  • 86行:attachInterrupt(DREQ, send_vs1053, RISING);
    • VS1053のDREC信号をESP32の割り込みに設定
  • 95から98行: ここで次のAACファイルの読み込みタイミングをチェック。
  • プログラムを実行するとシリアルモニタに下記が表示されます
monitor

HTTP server started
SPIFFS started
Init vs1053: 800
part1 = 200
part2 = 200
part3 = 200
part4 = 200
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110794
TIME OFFSET: 38
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110832.aac
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110833.aac
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110834.aac
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110835.aac
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110836.aac
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110837.aac
https://f-radiko.smartstream.ne.jp/TBS/_definst_/simul-stream.stream/media-udx5r2kwo_w92886994_110838.aac
  • 3行:Init vs1053: 800
    • VS1053の初期設定。
    • 800はVS1053に書き込んだ設定値をVS1053から読み込んで表示
  • 4行から7行:Radikoの認証が完了とChunklist URLの取得
  • 8行:Chunklistファイルの最初のAACファイルのURL
  • 9行:これを元に時間のずれを計算
    • オフセットが38ー>AACファイル38個分約190秒
  • 10行以降
    • ダウンロードしているAACファイルのURLを表示。
    • URLの数字部分が38個分増加している事を確認。

最後に

やっとRadikoの再生が出来ました。HTTPサーバ画面を使って、ラジオ局の選択、音声の調整とが出来るプレーヤーを作って行きたいと思います。