/
singlwolf
/
Radiola-2S3
Обзор
Документация
Войти
/
singlwolf
/
Radiola-2S3
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/audioVS.cpp
1 998 строк
51 KB
SinglWolf
v1.0.6
15 апр 2026, 05:04
15 апр 2026, 05:04
55b96c5
Код
Авторство
О чём код?
#include "audioVS.h" #include "iconvlite.h" #include "prio_.h" #include <utility> #include "soc/gpio_struct.h" #include "vs1053b-patches.h" // #include "esp_log.h" #include "hal/spi_types.h" #include "AsyncSSLClient.h" #include "hardware.h" #include "eeStorage.h" #include "web_server.h" #include "services.h" #include "lv_i18n.h" #include "url_parser.h" #include <sstream> #define VS_HOST SPI1_HOST #define VS_XDCS 6 #define VS_XRES 7 #define VS_XCS 15 #define VS_DREQ 46 #define VS_SCLK 16 #define VS_MISO 18 #define VS_MOSI 17 #define MAXTICK 1024 * 5 #define CONFIG_VS_PLAYER_DBG #ifdef CONFIG_VS_PLAYER_DBG #include "HardwareSerial.h" #include "esp_log_color.h" #define txtPrintInfo(fmt, ...) \ do \ { \ Serial.printf(LOG_COLOR_I); \ Serial.printf("[PLAYER INFO] "); \ Serial.printf(fmt, ##__VA_ARGS__); \ Serial.printf(LOG_RESET_COLOR "\n"); \ } while (0) // #define txtPrintWarn(fmt, ...) \ do \ { \ Serial.printf(LOG_COLOR_W); \ Serial.printf("[PLAYER WARN] "); \ Serial.printf(fmt, ##__VA_ARGS__); \ Serial.printf(LOG_RESET_COLOR "\n"); \ } while (0) // #define txtPrintDebug(fmt, ...) \ do \ { \ Serial.printf(LOG_ANSI_COLOR(LOG_COLOR_BLUE)); \ Serial.printf("[PLAYER DEBUG] "); \ Serial.printf(fmt, ##__VA_ARGS__); \ Serial.printf(LOG_RESET_COLOR "\n"); \ } while (0) // #define txtPrintError(fmt, ...) \ do \ { \ Serial.printf(LOG_COLOR_E); \ Serial.printf("[PLAYER ERROR] "); \ Serial.printf(fmt, ##__VA_ARGS__); \ Serial.printf(LOG_RESET_COLOR "\n"); \ } while (0) #else #define txtPrintInfo(fmt, ...) \ do \ { \ } while (0) #define txtPrintWarn(fmt, ...) \ do \ { \ } while (0) #define txtPrintDebug(fmt, ...) \ do \ { \ } while (0) #define txtPrintError(fmt, ...) \ do \ { \ } while (0) #endif // enum VS_FORMAT : uint8_t { UNSUPPORTED = 0, MP3_V1_L1, MP3_V1_L2, MP3_V1_L3, MP3_V2_L1, MP3_V2_L2, MP3_V2_L3, MP3_V25_L1, MP3_V25_L2, MP3_V25_L3, WAV, AAC_AT, AAC_AD, AAC_M4, WMA, OGG_V, MIDI, }; // enum datamode_t : uint8_t { INIT = 0x01, HEADER = 0x02, DATA = 0x04, METADATA = 0x08, // PLAYLISTINIT = 0x10, // PLAYLISTHEADER = 0x20, // PLAYLISTDATA = 0x40, }; // AsyncSSLClient *mp3client = nullptr; VS_PLAYER player; volatile datamode_t datamode; volatile bool is_Playing = false; volatile bool is_Running = false; volatile bool connectOK = false; volatile bool is_Checked = false; bool chunked = false; size_t chunkcount = 0; size_t chunksize = 0; int metaint = 0; size_t datacount = 0; int metacount; int16_t metalinebfx; uint32_t clength = 0; std::string userAgent = USERAGENT; // std::string userAgent = "VLC/3.0.21 LibVLC/3.0.21"; // std::string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/26.3.0.0 Safari/537.36"; static std::string local_url; static uint32_t totalcount; static bool redirection = false; std::string header_line; // bool mute_on = false; uint8_t mute_volume = 0; #define VOL_STEP 1 static uint8_t tempVolume = 0; // size_t buff_init(); void changeMaxBlockSize(uint16_t mbs); uint16_t getMaxBlockSize(); size_t freeSpace(); size_t writeSpace(); size_t bufferFilled(); void bytesWritten(size_t bw); void bytesWasRead(size_t br); uint8_t *getWritePtr(); uint8_t *getReadPtr(); uint32_t getWritePos(); uint32_t getReadPos(); void resetBuffer(); const size_t _buffSizePSRAM = 256 * 1024; size_t _buffSize = 0; size_t _resBuffSizeRAM = 2048; size_t _resBuffSizePSRAM = 4096; size_t _maxBlockSize = 2048; uint8_t *_buffer = nullptr; uint8_t *_writePtr = nullptr; uint8_t *_readPtr = nullptr; uint8_t *_endPtr = nullptr; bool _flag_start = true; // bool showstreamtitle(void *arg, const char *ml, bool full = false); // std::string decode_spec_chars(std::string str) { size_t inx, inx2; // Переносим данные из str в res без копирования std::string res = std::move(str); // Используем find вместо indexOf while ((inx = res.find("&#")) != std::string::npos) { inx2 = res.find(';', inx); if (inx2 == std::string::npos) { break; } // Считаем число между &# и ; int val = 0; for (size_t i = inx + 2; i < inx2; i++) { if (isdigit((unsigned char)res[i])) { val = val * 10 + (res[i] - '0'); } } // Собираем результат: начало + один символ + конец // substr(inx2 + 1) — берет всё после ';' до конца строки res = res.substr(0, inx) + (char)val + res.substr(inx2 + 1); } return res; } // bool showstreamtitle(void *arg, const char *ml, bool full) { if (!ml || strlen(ml) == 0) return false; auto pp = (VS_PLAYER *)arg; std::string raw(ml); std::string new_title; // 1. Ищем StreamTitle size_t p1 = raw.find("StreamTitle='"); if (p1 != std::string::npos) { p1 += 13; size_t p2 = raw.find("';", p1); if (p2 != std::string::npos) { new_title = raw.substr(p1, p2 - p1); } } else if (full) { // Если тега нет, но взведен флаг full (инфо из плейлиста) new_title = raw; } if (new_title.empty()) return false; // 2. Декодер HTML-сущностей new_title = decode_spec_chars(new_title); if (!is_valid_utf8(new_title.c_str())) { raw = cp2utf(new_title); if (is_valid_utf8(raw.c_str())) { new_title = raw; } else { new_title = _("Кодировка текста не распознана!"); } } if (pp->title[ICY_META] != new_title) { pp->setTitle(ICY_META, new_title); } return true; } // bool VS_PLAYER::connecttohost(const char *url) { resetBuffer(); sdi_send_fillers(64); // Clear ram buffer chip is_Running = false; is_Checked = false; datamode = INIT; bool isSecure = false; bool err = false; url_t *ClientURI = url_parse(url); if (ClientURI->redirect_url != nullptr) { std::string redirect_url = ClientURI->redirect_url; url_free(ClientURI); ClientURI = url_parse(redirect_url.c_str()); } std::string path = ClientURI->path; std::string host = ClientURI->host; std::string scheme = ClientURI->scheme; uint16_t port = ClientURI->port; if (ClientURI->extension != nullptr) { std::string extension = ClientURI->extension; if ((extension.starts_with("m3u")) || (extension == "asx") || (extension == "pls")) { err = true; } } // mp3client->setAckTimeout(2500); if (scheme == "https") { txtPrintWarn("isSecure = true"); // mp3client->setAckTimeout(2700); isSecure = true; } url_free(ClientURI); if (err) { title[ICY_META] = _("Воспроизведение плейлистов не поддерживается"); return false; } bool ret = false; if (mp3client->connect(host.c_str(), port, isSecure)) { uint8_t retrycount = 0; while (mp3client->disconnected()) { if (retrycount++ > 60) // на 1 сек больше таймаута { txtPrintError("Коннекта не дождались"); return ret; } vTaskDelay(100 / portTICK_PERIOD_MS); } if (mp3client->connected()) { char *getreq = nullptr; asprintf(&getreq, "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Icy-MetaData: 2\r\n" "Accept:*/*\r\n" "User-Agent: %s\r\n" "Accept-Encoding: identity;q=1,*;q=0\r\n" "Connection: close\r\n\r\n", path.c_str(), host.c_str(), userAgent.c_str()); // txtPrintInfo("send GET command"); if (mp3client->canSend()) { size_t len = strlen(getreq); size_t result = mp3client->write(getreq, len); ret = result == len; if (!ret) { txtPrintError(" mp3client->write len: %u result: %u!", len, result); } } free(getreq); } } else { txtPrintError("Request %s failed!", host.c_str()); } return ret; } // void on_Connect(void *arg, AsyncSSLClient *client) { txtPrintWarn("Connected to host at %s on port %d", client->remoteIP().toString().c_str(), client->remotePort()); } // void on_DisConnect(void *arg, AsyncSSLClient *client) { txtPrintWarn("Host disconnected, State: %s", client->stateToString()); } // void on_Error(void *arg, AsyncSSLClient *client, err_t err) { auto pp = (VS_PLAYER *)arg; if (err == -22) { if (!connectOK) pp->title[ICY_META] = _("Время ожидания подключения истекло"); else pp->title[ICY_META] = _("Время ожидания данных истекло"); } else { pp->title[ICY_META] = client->errorToString(err); } if (connectOK && is_Running) { pp->stop_error(); } connectOK = false; txtPrintWarn("Host error %s", client->errorToString(err)); txtPrintWarn("Error State: %s", client->stateToString()); } // uint16_t playlistcnt; int LFcount; bool ctseen = false; enum icy_flag : uint8_t { F_BR = 0x01, // F_URL = 0x02, // F_NAME = 0x04, // F_GENRE = 0x08, // F_DESC = 0x10, // F_TYPE = 0x20, // F_LOC = 0x40, // F_LEN = 0x80 // }; void on_Data(void *arg, AsyncSSLClient *client, void *data, size_t len) { auto pp = (VS_PLAYER *)arg; auto *p = (uint8_t *)data; while (len > 0) { uint8_t b = *p; // --- ТРАНСПОРТНЫЙ УРОВЕНЬ (CHUNKED) --- if (chunked && (datamode & (DATA | METADATA))) { if (chunkcount == 0) { if (b == '\r') { goto next_byte; } else if (b == '\n') { chunkcount = chunksize; chunksize = 0; goto next_byte; } uint8_t hex = toupper(b) - '0'; if (hex > 9) hex = hex - 7; chunksize = (chunksize << 4) + hex; goto next_byte; } if (datamode != DATA) chunkcount--; } // --- ИНИЦИАЛИЗАЦИЯ --- if (datamode == INIT) { ctseen = false; redirection = false; metaint = 0; LFcount = 0; txtPrintWarn("Switch to HEADER"); datamode = HEADER; totalcount = 0; metalinebfx = 0; header_line.clear(); } // --- ПАРСЕР ХЕДЕРОВ --- if (datamode == HEADER) { // 1. Ищем границу заголовков во всем пришедшем блоке std::string chunk((char *)p, len); size_t header_end = chunk.find("\r\n\r\n"); if (header_end == std::string::npos) { // Граница не найдена — копим всё и ждем догрузки header_line.append((char *)p, len); p += len; len = 0; continue; } else { // 2. Нашли! Копируем в строку ТОЛЬКО текст до начала \r\n\r\n header_line.append((char *)p, header_end); // Serial.printf("\n--- HEADER START (len: %u) ---\n", (unsigned int)header_end); // Serial.printf("%s", header_line.c_str()); // Serial.printf("\n--- HEADER END ---\n"); // Сдвигаем указатели сразу за границу \r\n\r\n (4 байта) p += header_end + 4; len -= header_end + 4; // 3. ПАРСИМ СТРОКИ (как блок текста) std::stringstream ss(header_line); std::string line; // Обработка первой строки (Status Line) if (std::getline(ss, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); size_t sp = line.find(' '); if (sp != std::string::npos) { int code = std::atoi(line.substr(sp + 1, 3).c_str()); txtPrintWarn("Status: %d", code); std::string Status = ""; if (code >= 300 && code < 400) { Status = "OK"; redirection = true; } else if (code >= 200 && code < 300) { Status = "OK"; } else if (code == 400) { Status = _("Ошибка запроса"); } else if ((code == 402) || (code == 401)) { Status = _("Нужна авторизация"); } else if (code == 403) { Status = _("Доступ запрещен"); } else if (code == 404) { Status = _("Поток не найден"); } else if (code >= 500) { Status = _("Ошибка сервера"); } else { Status = _("Неизвестная ошибка"); } if (Status != "OK") { txtPrintError("Status: %s", Status.c_str()); pp->title[ICY_META] = Status; connectOK = false; client->abort(); return; } } } // Обработка ключей h_key: h_val uint8_t seen_flags = 0; while (std::getline(ss, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); if (line.empty()) continue; size_t sep = line.find(':'); if (sep != std::string::npos) { std::string h_key = line.substr(0, sep); std::string h_val = line.substr(sep + 1); // Trim начала h_val size_t first = h_val.find_first_not_of(' '); if (first != std::string::npos) h_val = h_val.substr(first); // Lowercase h_key for (auto &c : h_key) if (c >= 'A' && c <= 'Z') c += 32; if (h_key == "location" && !(seen_flags & F_LOC)) { seen_flags |= F_LOC; datamode = INIT; client->close(true); // delay(200); pp->play(h_val.c_str(), pp->REDIRECT); connectOK = false; return; } else if (h_key == "icy-metaint") { metaint = std::atoi(h_val.c_str()); } else if (h_key == "content-type" && !(seen_flags & F_TYPE)) { pp->title[ICY_FORMAT] = h_key; seen_flags |= F_TYPE; if (strstr(h_val.c_str(), "audio/") || strstr(h_val.c_str(), "video/mp2t") || strstr(h_val.c_str(), "application/")) { const char *vs_pls[][2] = { {"mpegurl", "M3U"}, {"pls+xml", "PLS"}, {"scpls", "PLS"}, {"asf", "ASX"}, }; for (auto &i : vs_pls) { if (strstr(h_val.c_str(), i[0])) { connectOK = false; pp->title[ICY_META] = _("Воспроизведение плейлистов не поддерживается"); pp->stop_error(); client->close(true); return; } } const char *vs_ext[][2] = { {"mpeg", "MP3"}, {"mp3", "MP3"}, {"aac", "AAC"}, {"mp2t", "AAC"}, {"m4a", "M4A"}, {"mp4", "M4A"}, {"ogg", "OGG"}, {"wav", "WAV"}, }; for (auto &i : vs_ext) { if (strstr(h_val.c_str(), i[0])) { pp->setTitle(ICY_FORMAT, i[1]); ctseen = true; break; } } } } else if (h_key == "transfer-encoding") { if (strcasestr(h_val.c_str(), "chunked")) { txtPrintWarn("chunked = true"); chunked = true; chunkcount = 0; } } else if (h_key == "icy-name" && !(seen_flags & F_NAME)) { seen_flags |= F_NAME; pp->setTitle(ICY_NAME, (h_val)); } else if (h_key == "icy-br" && !(seen_flags & F_BR)) { seen_flags |= F_BR; pp->setTitle(ICY_BITRATE, h_val); } else if (h_key == "icy-genre" && !(seen_flags & F_GENRE)) { seen_flags |= F_GENRE; pp->setTitle(ICY_GENGE, (h_val)); } else if (h_key == "icy-description" && !(seen_flags & F_DESC)) { seen_flags |= F_DESC; pp->setTitle(ICY_DESCR, h_val); } else if (h_key == "icy-url" && !(seen_flags & F_URL)) { seen_flags |= F_URL; if (h_val.find('.') != std::string::npos) { size_t pos = h_val.find("%3a%2f%2f"); if (pos != std::string::npos) h_val.replace(pos, 9, "://"); pos = h_val.find("%3A%2F%2F"); if (pos != std::string::npos) h_val.replace(pos, 9, "://"); if (!h_val.starts_with("http")) h_val.insert(0, "http://"); pp->setTitle(ICY_URI, h_val); } } else if (h_key == "content-length" && !(seen_flags & F_LEN)) { seen_flags |= F_LEN; // pp->local(h_val.c_str(), pp->WEBFILE); } } } header_line.clear(); if (ctseen) { // txtPrintWarn("Switch to DATA"); datamode = DATA; datacount = metaint; pp->clearTitle(false); } else { datamode = INIT; pp->title[ICY_META] = pp->title[ICY_FORMAT]; pp->title[ICY_META] += ": "; pp->title[ICY_META] += _("Неподдерживаемый формат"); client->abort(); connectOK = false; return; } // 4. Сброс и переход в DATA (или INIT при редиректе) continue; } } // --- ДАННЫЕ (DATA) --- if (datamode == DATA) { size_t can_grab = std::min(len, writeSpace()); if (chunked) { can_grab = std::min(can_grab, chunkcount); } if (metaint > 0) { can_grab = std::min(can_grab, (size_t)datacount); } if (can_grab > 0) { memcpy(getWritePtr(), p, can_grab); bytesWritten(can_grab); // ПРОВЕРКА НАКОПЛЕНИЯ ПЕРЕД СТАРТОМ if (!is_Running) { // Накопим, например, 64КБ или 25% от буфера if (bufferFilled() >= (256 * 1024 / 4)) { pp->play_type = pp->STREAM; is_Running = true; // txtPrintInfo("Buffer Ready. Starting VS1053."); } } p += can_grab; len -= can_grab; if (chunked) chunkcount -= can_grab; if (metaint > 0) { datacount -= can_grab; if (datacount == 0) { datamode = METADATA; metalinebfx = -1; } } } else if (freeSpace() == 0) { // Места нет совсем — выходим! // mp3client->ackLater(); pp->title[ICY_META] = _("Аудиобуфер ПЕРЕПОЛНЕН!!!"); txtPrintError("%s", pp->title[ICY_META].c_str()); pp->stop_error(); client->close(true); return; } continue; } // --- МЕТАДАННЫЕ (METADATA) --- if (datamode == METADATA) { if (metalinebfx < 0) { header_line.clear(); metalinebfx = 0; metacount = b * 16; } else { header_line += (char)b; --metacount; } if (metacount == 0) { if (!header_line.empty()) { showstreamtitle(pp, header_line.c_str()); } datacount = metaint; datamode = DATA; } } next_byte: p++; len--; } } // bool VS_PLAYER::isPlaying() { return is_Playing; } // void VS_PLAYER::begin() { if (!buff_init()) { txtPrintError("Не удалось выделить память под Buffer!"); } play_mode = WAIT_COMM; mp3client = &asyncClient; mp3client->onData(&on_Data, this); mp3client->onConnect(&on_Connect, this); mp3client->onDisconnect(&on_DisConnect, this); mp3client->onError(&on_Error, this); clearTitle(); dev.set_mute_ON(); audioSetQueue = xQueueCreate(16, sizeof(Player_cmd)); if (!audioSetQueue) { txtPrintError("audioQueue are not initialized"); while (true) { delay(1); } } init_vs(); sdi_send_fillers(64); // Clear ram buffer chip if (version != -1) { if (Config->firmware != version) { Config->firmware = version; eeprom.SaveConfig(); } } eeprom.GetEqData(&eq); if (version != 4) { eq.spatial_mode = 0xFF; eeprom.SetEqData(&eq); } eeprom.GetEqData(eq.band, eq.set, eq.mode); setAllBands(); setVolumeSteps(eeprom.GetMaxVolume()); dev.set_mute_OFF(); } // void VS_PLAYER::clearTitle(bool content) { serv.evtClearTitle(); if (content) { title[0].clear(); // ICY-NAME title[1].clear(); // ICY-URI title[2].clear(); // ICY-META title[3].clear(); // ICY-DESCR title[4].clear(); // ICY-GENGE title[5].clear(); // ICY-FORMAT title[6].clear(); // ICY-BITRATE } web.requestOnChange(ICY); } // void VS_PLAYER::StopClient() { if (mp3client->connected()) { txtPrintWarn("Stopping client..."); mp3client->stop(); // ДАЕМ КОМАНДУ НА ЗАКРЫТИЕ uint8_t timeout = 100; // 100 мс максимум while (mp3client->connected() && timeout > 0) { vTaskDelay(1 / portTICK_PERIOD_MS); timeout--; } if (timeout == 0) { txtPrintError("Timeout, client is not stopped!"); } } } //--------------------------------------------------------------------------------------------------------------------- // static const size_t AUDIO_STACK_SIZE = 3300; // static StaticTask_t __attribute__((unused)) xAudioTaskBuffer; // static StackType_t __attribute__((unused)) xAudioStack[AUDIO_STACK_SIZE]; void VS_PLAYER::startAudioTask() { if (xTaskCreatePinnedToCore( &VS_PLAYER::taskWrapper, NAME_VS_PLAYER, STACK_VS_PLAYER, this, PRIO_VS_PLAYER, nullptr, CPU_VS_PLAYER) != pdPASS) { txtPrintError("ERROR creating" NAME_VS_PLAYER " task! Out of memory?"); } } void VS_PLAYER::taskWrapper(void *param) { auto *runner = static_cast<VS_PLAYER *>(param); runner->audioTask(); } // void VS_PLAYER::init_vs() { spi_VS1053 = new SPIClass(VS_HOST); spi_VS1053->begin(VS_SCLK, VS_MISO, VS_MOSI, -1); spi_sci = SPISettings(1000000, MSBFIRST, SPI_MODE0); spi_sdi = SPISettings(6700000, MSBFIRST, SPI_MODE0); pinMode(VS_DREQ, INPUT_PULLDOWN); // DREQ is an input pinMode(VS_XCS, OUTPUT); // The SCI and SDI signals pinMode(VS_XDCS, OUTPUT); DCS_HIGH(); CS_HIGH(); pinMode(VS_XRES, OUTPUT); digitalWrite(VS_XRES, LOW); delay(30); digitalWrite(VS_XRES, HIGH); delay(100); if (!digitalRead(VS_DREQ)) vTaskDelay(50); // wait a bit more // Check DREQ uint16_t time_out = 0; while (!digitalRead(VS_DREQ) && time_out++ < MAXTICK) { NOP(); } if (!digitalRead(VS_DREQ)) { version = -1; txtPrintError("NO VS1053 detected!!!"); return; } // softReset(); // Do a soft reset write_register(SCI_MODE, _BV(SM_SDINEW) | _BV(SM_RESET)); delay(30); write_register(SCI_VOL, 0xFEFE); // Mute version = (int8_t)((read_register(SCI_STATUS) >> 4) & 0x000F); // Mask out only the four version bits // 0 for VS1001, 1 for VS1011, 2 for VS1002, 3 for VS1003, 4 for VS1053 and VS8053, // 5 for VS1033, 7 for VS1103, and 6 for VS1063 const char *chipLabel[] = { "VS1001", "VS1011", "VS1002", "VS1003", "VS1053|VS8053", "VS1033", "VS1063", "VS1103", }; txtPrintInfo("VS10xx detection. CHIP: %s", chipLabel[version]); if (version == 4) { write_register(SCI_CLOCKF, 0xB800); delay(100); loadUserCode(); uint32_t start = millis(); while (!digitalRead(VS_DREQ) && (millis() - start) < 100) { yield(); } if (!digitalRead(VS_DREQ)) { txtPrintError("VS1053: DREQ timeout after load User Code"); return; } write_register(SCI_BASS, 0); // wram_write(EQ5_ADDR, 0); // eq5Dummy wram_write(EQ5_BASS_LVL, 0); // Level 1 wram_write(EQ5_BASS_FRQ, BASS); // Freq 1 wram_write(EQ5_HIGBASS_LVL, 0); // Level 2 wram_write(EQ5_HIGBASS_FRQ, HIGBASS); // Freq 2 wram_write(EQ5_MID_LVL, 0); // Level 3 wram_write(EQ5_MID_FRQ, MID); // Freq 3 wram_write(EQ5_LOWTREB_LVL, 0); // Level 4 wram_write(EQ5_LOWTREB_FRQ, LOWTREBLE); // Freq 4 wram_write(EQ5_TREBLE_LVL, 0); // Level 5 wram_write(EQ5_UPDATE, 1); // Флаг готовности // wram_write(EQ5_PLAYMODE, _BV(SM_EQ5)); // write_register(SCI_AIADDR, EQ5_START_ADDR); delay(10); await_data_request(); if (wram_read(EQ5_UPDATE) == 0) { eqPtr = EQ5_ADDR; } else { eqPtr = 0x00; txtPrintError("VS1053: EQ5 not working..."); } uint16_t status = read_register(SCI_STATUS); if (status) { write_register(SCI_STATUS, status | _BV(9)); VUmeter = true; } } else { write_register(SCI_CLOCKF, 0xB000); delay(100); } startAudioTask(); } // Для VS_XDCS inline void VS_PLAYER::DCS_HIGH() { if constexpr (VS_XDCS >= 32) { GPIO.out1_w1ts.val = 1UL << (VS_XDCS - 32); } else { GPIO.out_w1ts = 1UL << VS_XDCS; } } inline void VS_PLAYER::DCS_LOW() { if constexpr (VS_XDCS >= 32) { GPIO.out1_w1tc.val = 1UL << (VS_XDCS - 32); } else { GPIO.out_w1tc = 1UL << VS_XDCS; } } // Для VS_XCS inline void VS_PLAYER::CS_HIGH() { if constexpr (VS_XCS >= 32) { GPIO.out1_w1ts.val = 1UL << (VS_XCS - 32); } else { GPIO.out_w1ts = 1UL << VS_XCS; } } inline void VS_PLAYER::CS_LOW() { if constexpr (VS_XCS >= 32) { GPIO.out1_w1tc.val = 1UL << (VS_XCS - 32); } else { GPIO.out_w1tc = 1UL << VS_XCS; } } inline void VS_PLAYER::await_data_request() { while (!digitalRead(VS_DREQ)) NOP(); } // Very short delay inline bool VS_PLAYER::data_request() { return (digitalRead(VS_DREQ) == HIGH); } // void VS_PLAYER::control_mode_off() { CS_HIGH(); spi_VS1053->endTransaction(); } void VS_PLAYER::control_mode_on() { spi_VS1053->beginTransaction(spi_sci); DCS_HIGH(); CS_LOW(); } void VS_PLAYER::data_mode_on() { spi_VS1053->beginTransaction(spi_sdi); CS_HIGH(); DCS_LOW(); } void VS_PLAYER::data_mode_off() { DCS_HIGH(); spi_VS1053->endTransaction(); } //--------------------------------------------------------------------------------------------------------------------- uint16_t VS_PLAYER::read_register(uint8_t _reg) { if (version == -1) return 0; await_data_request(); uint16_t result = 0; control_mode_on(); spi_VS1053->write(3); // Read operation spi_VS1053->write(_reg); // Register to write (0..0xF) // Note: transfer16 does not seem to work result = (spi_VS1053->transfer(0xFF) << 8) | (spi_VS1053->transfer(0xFF)); // Read 16 bits data control_mode_off(); return result; } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::write_register(uint8_t _reg, uint16_t _value) { if (version == -1) return; await_data_request(); control_mode_on(); spi_VS1053->write(2); // Write operation spi_VS1053->write(_reg); // Register to write (0..0xF) spi_VS1053->write16(_value); // Send 16 bits data control_mode_off(); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::wram_write(uint16_t address, uint16_t data) { write_register(SCI_WRAMADDR, address); write_register(SCI_WRAM, data); } //--------------------------------------------------------------------------------------------------------------------- uint16_t VS_PLAYER::wram_read(uint16_t address) { write_register(SCI_WRAMADDR, address); // Start reading from WRAM return read_register(SCI_WRAM); // Read back result } //--------------------------------------------------------------------------------------------------------------------- uint16_t VS_PLAYER::getVUlevel() { if (!VUmeter) return 0; return read_register(SCI_AICTRL3); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::loadUserCode() { uint16_t i = 0; while (i < sizeof(plugin) / sizeof(plugin[0])) { unsigned short addr, n, val; addr = plugin[i++]; n = plugin[i++]; if (n & 0x8000U) { /* RLE run, replicate n samples */ n &= 0x7FFF; val = plugin[i++]; while (n--) { write_register(addr, val); } } else { /* Copy run, copy n samples */ while (n--) { val = plugin[i++]; write_register(addr, val); } } } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::sdi_send_fillers(size_t numchunks) { bool endFillByte = wram_read(0x1E06) & 0xFF; while (numchunks--) // More to do? { await_data_request(); // Wait for space available data_mode_on(); // Start data-mode transaction for (uint8_t i = 0; i < VS1053_CHUNK_SIZE; i++) { spi_VS1053->write(endFillByte); } data_mode_off(); // End data-mode transaction } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::playAudioData() { // 1. Берем СНИМОК заполненности ОДИН раз size_t filled = bufferFilled(); size_t blockSize = getMaxBlockSize(); if (filled < blockSize) return; // 2. sendBytes работает с указателем, который мы тоже "заморозили" в getReadPtr int bytesDecoded = sendBytes(getReadPtr(), blockSize); if (bytesDecoded < 0) { txtPrintError("err bytesDecoded %i", bytesDecoded); uint8_t next = 200; // Используем наш снимок 'filled', а не вызываем метод заново if (filled < next) next = filled; bytesWasRead(next); } else { if (bytesDecoded > 0) { bytesWasRead(bytesDecoded); return; } } return; } //--------------------------------------------------------------------------------------------------------------------- int VS_PLAYER::sendBytes(uint8_t *data, size_t len) { size_t chunk_length = 0; // Length of chunk 32 byte or shorter int bytesDecoded = 0; data_mode_on(); while (len) { // More to do? if (!digitalRead(VS_DREQ)) break; chunk_length = len; if (len > VS1053_CHUNK_SIZE) { chunk_length = VS1053_CHUNK_SIZE; } spi_VS1053->writeBytes(data, chunk_length); data += chunk_length; len -= chunk_length; bytesDecoded += (int)chunk_length; } data_mode_off(); return bytesDecoded; } //--------------------------------------------------------------------------------------------------------------------- bool VS_PLAYER::stopSong(uint8_t *data, size_t len) { uint16_t modereg = read_register(SCI_MODE); // Read from mode register write_register(SCI_MODE, modereg | _BV(SM_CANCEL)); size_t chunk_length; while (len) { chunk_length = len; if (len > VS1053_CHUNK_SIZE) { chunk_length = VS1053_CHUNK_SIZE; } len -= chunk_length; data_mode_on(); await_data_request(); spi_VS1053->writeBytes(data, chunk_length); data += chunk_length; data_mode_off(); modereg = read_register(SCI_MODE); if ((modereg & _BV(SM_CANCEL)) == 0) { sdi_send_fillers(64); txtPrintWarn("Song stopped correctly."); return true; } } txtPrintError("Song stopped incorrectly!"); return false; } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setAllBands() { setBand(EQ_BASS, eq.band[EQ_BASS], false); setBand(EQ_HIGBASS, eq.band[EQ_HIGBASS], false); setBand(EQ_MID, eq.band[EQ_MID], false); setBand(EQ_LOWTREBLE, eq.band[EQ_LOWTREBLE], false); setBand(EQ_TREBLE, eq.band[EQ_TREBLE]); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setPreset(const int8_t *preset) { eq.band[EQ_BASS] = preset[EQ_BASS]; eq.band[EQ_HIGBASS] = preset[EQ_HIGBASS]; eq.band[EQ_MID] = preset[EQ_MID]; eq.band[EQ_LOWTREBLE] = preset[EQ_LOWTREBLE]; eq.band[EQ_TREBLE] = preset[EQ_TREBLE]; setAllBands(); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setEqMode(uint8_t mode) { if (mode > 5) return; eq.mode = mode; switch (mode) { case EQ_OFF: // OFF setPreset(PRESET_OFF); break; case EQ_MUSIC: // MUSIC setPreset(PRESET_MUSIC); break; case EQ_DYNAMIC: // DYNAMIC setPreset(PRESET_DYNAMIC); break; case EQ_VOCAL: // VOCAL setPreset(PRESET_VOCAL); break; case EQ_CUSTOM: setPreset(eq.set); break; case EQ_DEFAULT: setPreset(eq.band); break; default: break; } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setBand(uint8_t band, int8_t gain_dB, bool upd) { if (eqPtr != EQ5_ADDR) return; if (gain_dB < EQ_GAIN_MIN) gain_dB = EQ_GAIN_MIN; if (gain_dB > EQ_GAIN_MAX) gain_dB = EQ_GAIN_MAX; uint16_t lvl = (int16_t)gain_dB * 2; wram_write(EQ5_BASS_LVL + (band << 1), lvl); delay(1); if (upd) { wram_write(EQ5_UPDATE, 1); } delay(1); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setUserAgent(std::string ua) { userAgent = std::move(ua); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setVolumeSteps(uint8_t steps) { if (!steps) steps = 1; // 0 is nonsense volume_steps = steps; } //--------------------------------------------------------------------------------------------------------------------- uint8_t VS_PLAYER::maxVolume() { return volume_steps; } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setVolume(uint8_t vol) { vs_volume = vol; setBalance(balance); // ← пересчитать с текущим балансом } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setBalance(int8_t val) { // Ограничиваем входное значение balance = constrain(balance, -16, 16); // Сохраняем для setVolume() balance = val; // Базовая громкость (как в setVolume) uint8_t map_v = map(vs_volume, 0x00, 0xFF, 0x01, volume_steps); auto base_attenuation = (uint8_t)(log10(255.0f / (float)map_v) * 105.54571334); uint8_t left = base_attenuation; uint8_t right = base_attenuation; if (balance < 0) { // Левый громче → правый приглушаем // -16..-1 → 1..16 auto delta = (uint8_t)((abs((float)balance) / 16.0f) * 90.0f); // до ~90 дБ приглушения right = min(254, base_attenuation + delta); } else if (balance > 0) { // Правый громче → левый приглушаем auto delta = (uint8_t)(((float)balance / 16.0f) * 90.0f); left = min(254, base_attenuation + delta); } write_register(SCI_VOL, word(left, right)); } //--------------------------------------------------------------------------------------------------------------------- std::string VS_PLAYER::getStrFormat(uint8_t val) { const char *vs_support[] = { _("Неподдерживаемый формат"), "MPEG 1 Layer I", "MPEG 1 Layer II", "MPEG 1 Layer III", "MPEG 2 Layer I", "MPEG 2 Layer II", "MPEG 2 Layer III", "MPEG 2.5 Layer I", "MPEG 2.5 Layer II", "MPEG 2.5 Layer III", "WAV", "AAC ADTS", "AAC ADIF", "AAC MP4/M4A", "WMA", "Ogg Vorbis", "MIDI", }; return vs_support[val]; } //--------------------------------------------------------------------------------------------------------------------- uint8_t VS_PLAYER::checkFormat() { uint16_t h0 = read_register(SCI_HDAT0); uint16_t h1 = read_register(SCI_HDAT1); // 1. Проверка на MP3 (Syncword 11 бит) if ((h1 >> 5) == 0x07FF) { uint8_t id = (h1 >> 3) & 0x03; uint8_t layer = (h1 >> 1) & 0x03; uint8_t sr_idx = (h0 >> 10) & 0x03; // Samplerate bits // Проверка на (Reserved) if (layer == 0 || sr_idx == 3) return UNSUPPORTED; // Определяем тип if (id == 3) { // MPEG 1.0 if (layer == 3) return MP3_V1_L1; if (layer == 2) return MP3_V1_L2; return MP3_V1_L3; } else if (id == 2) { // MPEG 2.0 if (layer == 3) return MP3_V2_L1; if (layer == 2) return MP3_V2_L2; return MP3_V2_L3; } else { // MPEG 2.5 (id 0 или 1) if (layer == 3) return MP3_V25_L1; if (layer == 2) return MP3_V25_L2; return MP3_V25_L3; } } // 2. Остальные форматы switch (h1) { case 0x7665: return WAV; case 0x4154: return AAC_AT; case 0x4144: return AAC_AD; case 0x4D34: return AAC_M4; case 0x574D: return WMA; case 0x4F67: return OGG_V; case 0x4D54: return MIDI; default: return UNSUPPORTED; } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::stop_error() { Player_cmd SetCMD = {}; serv.evtSetSensor(false); SetCMD.cmd = SET_ERROR; if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::stop() { if (play_mode == STOP) return; Player_cmd SetCMD = {}; serv.evtSetSensor(false); SetCMD.cmd = SET_STOP; if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::ToneSet(uint8_t band, int8_t gain_dB) { Player_cmd SetCMD = {}; SetCMD.cmd = SET_TONE; SetCMD.value = (band << 8) | (uint8_t)gain_dB; if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); eeprom.SetEqData(&eq); } // void VS_PLAYER::BalanceSet(int8_t balance) { Player_cmd SetCMD = {}; SetCMD.cmd = SET_BALANSE; SetCMD.value = ((int32_t)balance); if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); eeprom.SetBalanse(balance); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setVol(uint8_t volume, bool req) { Player_cmd SetCMD = {}; serv.evtSetSensor(false); tempVolume = volume; SetCMD.cmd = SET_VOLUME; SetCMD.value = volume; eeprom.SetVolume(volume); if (req) web.requestOnChange(VOLUME); if (mute_on) { mute_volume = volume; return; } if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::mute(bool set) { Player_cmd SetCMD = {}; mute_on = set; if (set) { mute_volume = 0; } else { mute_volume = eeprom.GetVolume(); } SetCMD.cmd = SET_VOLUME; SetCMD.value = mute_volume; if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::play(const char *url, play_type_t p_type) { if (userAgent != ee->ua) setUserAgent(ee->ua); connectOK = false; Player_cmd SetCMD = {}; if (is_Playing) { stop(); delay(1); while (is_Playing) { delay(1); } delay(100); } if (p_type == LOCAL) { serv.evtSetSensor(false); clearTitle(); serv.evtSetName(0); cli_printf("#STATION#: 0: %s", _("БЕЗ ИМЕНИ")); cli_printf("#URLSET#: %s", url); local_url = url; } else if (p_type == STREAM) { serv.evtSetSensor(false); clearTitle(); uint8_t id = eeprom.GetID(); serv.evtSetName(id); cli_printf("#STATION#: %u: %s", id, eeprom.GetStaName(id).c_str()); cli_printf("#URLSET#: %s", eeprom.GetStaUrl(id).c_str()); local_url = eeprom.GetStaUrl(id); } if (p_type != REDIRECT) { serv.evtSetWait(WAIT_CONN); play_mode = WAIT_CONN; } else { serv.evtSetWait(WAIT_REDIRECT); play_mode = WAIT_REDIRECT; local_url = url; } web.requestOnChange(MODE); play_type = p_type; SetCMD.cmd = SET_PLAY; if (audioSetQueue != nullptr) xQueueSend(audioSetQueue, &SetCMD, portMAX_DELAY); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::play(int16_t stationId) { if (eeprom.GetTotalSta() > 0) { if (stationId != eeprom.GetID()) { eeprom.SetID(stationId); } play(); } else { setStation(); } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setStation() { clearTitle(); serv.evtSetSta(); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::prev() { serv.evtSetSensor(false); if (eeprom.GetID() > 1) { eeprom.SetID(eeprom.GetID() - 1); play(eeprom.GetID()); } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::next() { serv.evtSetSensor(false); eeprom.SetID(eeprom.GetID() + 1); if (eeprom.GetID() > eeprom.GetTotalSta()) { eeprom.SetID(1); } play(eeprom.GetID()); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::toggle() { serv.evtSetSensor(false); if (play_mode == PLAY) { stop(); delay(10); } else { play(eeprom.GetID()); } } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setRelVolume(int8_t vol) { int16_t rvol; rvol = (int16_t)(eeprom.GetVolume() + vol); if (rvol < 0) rvol = 0; if (rvol > 255) rvol = 255; setVol(rvol); } //--------------------------------------------------------------------------------------------------------------------- void VS_PLAYER::setTitle(title_number num, std::string text) { title[num] = std::move(text); web.requestOnChange(TITLE, num, 0); } //--------------------------------------------------------------------------------------------------------------------- size_t buff_init() { if (_buffer) free(_buffer); _buffer = nullptr; if (psramInit()) { // PSRAM found, AudioBuffer will be allocated in PSRAM _buffSize = _buffSizePSRAM; if (_buffer == nullptr) { _buffer = (uint8_t *)heap_caps_aligned_alloc(32, _buffSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA); _buffSize = _buffSizePSRAM - _resBuffSizePSRAM; } } else { // no PSRAM available return 0; } if (!_buffer) return 0; resetBuffer(); return _buffSize; } void changeMaxBlockSize(uint16_t mbs) { _maxBlockSize = mbs; return; } uint16_t getMaxBlockSize() { return _maxBlockSize; } size_t freeSpace() { // Делаем снимок, чтобы указатели не "гуляли" во время расчета auto *r = (uint8_t *)*((volatile uint8_t **)&_readPtr); auto *w = (uint8_t *)*((volatile uint8_t **)&_writePtr); size_t res; if (r >= w) { res = (r - w); } else { res = (_endPtr - w) + (r - _buffer); } if (_flag_start) res = _buffSize; return (res > 0) ? (res - 1) : 0; } size_t writeSpace() { // 1. Делаем мгновенный снимок указателей auto *r = (uint8_t *)*((volatile uint8_t **)&_readPtr); auto *w = (uint8_t *)*((volatile uint8_t **)&_writePtr); size_t res; if (r > w) { res = (r - w - 1); } else { // Если чтение в начале буфера, оставляем 1 байт зазора, чтобы не сравняться с записью if (r == _buffer) res = (_endPtr - w - 1); else res = (_endPtr - w); } // Если буфер еще пустой (флаг старта), отдаем почти весь размер if (_flag_start) res = _buffSize - 1; return res; } size_t bufferFilled() { // Копируем указатели в локальные переменные (в стек таска) auto *w = (uint8_t *)*((volatile uint8_t **)&_writePtr); auto *r = (uint8_t *)*((volatile uint8_t **)&_readPtr); if (w >= r) { return (size_t)(w - r); } else { return (size_t)((_endPtr - r) + (w - _buffer)); } } void bytesWritten(size_t bw) { uint8_t *w = _writePtr + bw; // считаем в стеке if (w >= _endPtr) { // проверка на вылет за границы w = _buffer + (w - _endPtr); } *((volatile uint8_t **)&_writePtr) = w; if (bw && _flag_start) _flag_start = false; } void bytesWasRead(size_t br) { uint8_t *r = _readPtr + br; if (r >= _endPtr) r = _buffer + (r - _endPtr); *((volatile uint8_t **)&_readPtr) = r; // Сообщаем другому ядру } uint8_t *getWritePtr() { return _writePtr; } uint8_t *getReadPtr() { auto *r = (uint8_t *)*((volatile uint8_t **)&_readPtr); size_t len = _endPtr - r; if (len < _maxBlockSize) { // _maxBlockSize = 2048 memcpy(_endPtr, _buffer, _maxBlockSize - len); } return r; } void resetBuffer() { _writePtr = _buffer; _readPtr = _buffer; _endPtr = _buffer + _buffSize; _flag_start = true; // memset(_buffer, 0, _buffSize); //Clear Inputbuffer } uint32_t getWritePos() { return _writePtr - _buffer; } uint32_t getReadPos() { return _readPtr - _buffer; } //*************************************// void VS_PLAYER::audioTask() { Player_cmd GetCMD = {}; txtPrintWarn("Starting VS1053 playtask.."); while (true) { if (is_Playing) { if (play_mode == PLAY) { size_t before = bufferFilled(); playAudioData(); if (!is_Checked) { serv.evtCheckFormat(); is_Checked = true; } size_t after = bufferFilled(); if (before > after) { mp3client->ack(before - after); } } else if ((play_mode == STOP) || (play_mode == ERROR)) { stopSong(getReadPtr(), bufferFilled()); StopClient(); if (play_mode == STOP) { serv.evtSetStop(); cli_printf("#STOPPED#"); web.requestOnChange(MODE); } is_Playing = false; resetBuffer(); sdi_send_fillers(64); // Clear ram buffer chip serv.evtVUmeter(false); delay(500); } } if (xQueueReceive(audioSetQueue, &GetCMD, 1) == pdPASS) { switch (GetCMD.cmd) { case SET_VOLUME: { cli_printf("#VOLUME#: %d", GetCMD.value); setVolume(GetCMD.value); serv.evtSetVol(); } break; case SET_BALANSE: { setBalance((int8_t)GetCMD.value); } break; case SET_TONE: { uint8_t band = (GetCMD.value >> 8) & 0xFF; auto gain_dB = (int8_t)(GetCMD.value & 0xFF); setBand(band, gain_dB); } break; case SET_PLAY: { connectOK = connecttohost(local_url.c_str()); if (play_type == REDIRECT) { delay(1000); } if (connectOK) { txtPrintInfo("SET_PLAY"); while (!is_Running) { delay(1); if (!connectOK) break; } } if (connectOK && (play_type != REDIRECT)) { txtPrintInfo("play_mode = PLAY"); is_Playing = true; play_mode = PLAY; if (play_type == LOCAL) { play_mode = PLAY_LOCAL; } serv.evtSetLogo(); // if (!mute_on) { setVolume(eeprom.GetVolume()); } web.requestOnChange(MODE); serv.evtSetPlay(); cli_printf("#PLAYING#"); break; } } case SET_ERROR: { if (!title[ICY_META].empty()) { serv.evtSetStop(); std::string text_msg = _("ОШИБКА"); text_msg += " - "; text_msg += title[ICY_META]; setTitle(ICY_META, text_msg.c_str()); cli_printf("#ERROR STOPPED#"); play_mode = ERROR; web.requestOnChange(MODE); } } break; case SET_STOP: { play_mode = STOP; } break; default: break; } } vTaskDelay(10); } vTaskDelete(nullptr); } //---------------------------------------------------------------------------------------------------------------------