Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7dc66e04b | |||
| 15aad3f585 | |||
| 9a307e522d | |||
| c3abe79c90 | |||
| 05ce4335ff | |||
| ec2a6c0b88 | |||
| 0966cd45a4 |
@@ -1,6 +1,4 @@
|
|||||||
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
|
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS ${SOURCE_FILES}
|
SRCS "source/module.cpp"
|
||||||
REQUIRES TactilityKernel Tactility
|
REQUIRES TactilityKernel
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
general.vendor=LCDWIKI
|
general.vendor=CYD
|
||||||
general.name=ES3C28P
|
general.name=ES3C28P
|
||||||
|
|
||||||
apps.launcherAppId=Launcher
|
apps.launcherAppId=Launcher
|
||||||
@@ -20,5 +20,7 @@ lvgl.colorDepth=16
|
|||||||
|
|
||||||
storage.userDataLocation=SD
|
storage.userDataLocation=SD
|
||||||
|
|
||||||
|
dependencies.useDeprecatedHal=false
|
||||||
|
|
||||||
sdkconfig.CONFIG_LV_CACHE_DEF_SIZE=524288
|
sdkconfig.CONFIG_LV_CACHE_DEF_SIZE=524288
|
||||||
sdkconfig.CONFIG_LV_IMAGE_HEADER_CACHE_DEF_CNT=16
|
sdkconfig.CONFIG_LV_IMAGE_HEADER_CACHE_DEF_CNT=16
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
#include <tactility/module.h>
|
#include <tactility/module.h>
|
||||||
#include <Tactility/hal/Configuration.h>
|
|
||||||
|
|
||||||
// Legacy placeholder (required until legacy HAL is cleaned up everywhere)
|
|
||||||
extern const tt::hal::Configuration hardwareConfiguration = {};
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
|
|||||||
@@ -553,9 +553,14 @@ error_t close_stream(AudioStreamHandle handle_base) {
|
|||||||
Device* codec = is_input ? data->input_codec : data->output_codec;
|
Device* codec = is_input ? data->input_codec : data->output_codec;
|
||||||
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
||||||
|
|
||||||
|
// Determine if underlying codec is shared (BOTH codec used for both directions)
|
||||||
|
// In that case we must NOT close the codec if the other direction is still active.
|
||||||
|
Device* other_codec = is_input ? data->output_codec : data->input_codec;
|
||||||
|
AudioStreamHandleImpl** other_slot = is_input ? &data->open_output : &data->open_input;
|
||||||
|
bool codec_shared = (codec != nullptr && other_codec != nullptr && codec == other_codec);
|
||||||
|
|
||||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||||
if (handle->closing) {
|
if (handle->closing) {
|
||||||
// Already being closed by another caller (e.g. concurrent set_enabled + app close).
|
|
||||||
xSemaphoreGive(data->mutex);
|
xSemaphoreGive(data->mutex);
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
@@ -563,14 +568,16 @@ error_t close_stream(AudioStreamHandle handle_base) {
|
|||||||
if (*slot == handle) {
|
if (*slot == handle) {
|
||||||
*slot = nullptr;
|
*slot = nullptr;
|
||||||
}
|
}
|
||||||
|
bool other_still_open = (other_slot != nullptr && *other_slot != nullptr && *other_slot != reinterpret_cast<AudioStreamHandleImpl*>(1));
|
||||||
bool must_drain = (handle->busy_count > 0);
|
bool must_drain = (handle->busy_count > 0);
|
||||||
|
bool should_close_codec = !codec_shared || !other_still_open;
|
||||||
xSemaphoreGive(data->mutex);
|
xSemaphoreGive(data->mutex);
|
||||||
|
|
||||||
if (must_drain && handle->drain_semaphore != nullptr) {
|
if (must_drain && handle->drain_semaphore != nullptr) {
|
||||||
xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY);
|
xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codec != nullptr) {
|
if (should_close_codec && codec != nullptr) {
|
||||||
audio_codec_close(codec);
|
audio_codec_close(codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,16 +53,36 @@ error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data->is_open) {
|
if (data->is_open) {
|
||||||
// open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the
|
// ES8311 is configured for WORK_MODE_BOTH, so an already-open device
|
||||||
// same sample settings -- only an exact direction mismatch (e.g. requesting BOTH
|
// can serve the opposite direction without reopening, provided sample
|
||||||
// while opened for INPUT only) needs a reopen.
|
// settings match. Promote open_direction to BOTH when we see a
|
||||||
bool direction_compatible = data->open_direction == config->direction
|
// complementary request.
|
||||||
|| data->open_direction == AUDIO_CODEC_DIR_BOTH;
|
bool is_complementary = (data->open_direction == AUDIO_CODEC_DIR_OUTPUT && config->direction == AUDIO_CODEC_DIR_INPUT)
|
||||||
|
|| (data->open_direction == AUDIO_CODEC_DIR_INPUT && config->direction == AUDIO_CODEC_DIR_OUTPUT);
|
||||||
|
bool direction_compatible = (data->open_direction == config->direction)
|
||||||
|
|| (data->open_direction == AUDIO_CODEC_DIR_BOTH)
|
||||||
|
|| (config->direction == AUDIO_CODEC_DIR_BOTH)
|
||||||
|
|| is_complementary;
|
||||||
bool same_config = direction_compatible
|
bool same_config = direction_compatible
|
||||||
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||||
&& data->open_sample_info.channel == sample_info.channel
|
&& data->open_sample_info.channel == sample_info.channel
|
||||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
if (same_config) {
|
||||||
|
// If we opened OUTPUT then INPUT (or vice versa), mark as BOTH
|
||||||
|
if (is_complementary) {
|
||||||
|
data->open_direction = AUDIO_CODEC_DIR_BOTH;
|
||||||
|
}
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
// Different sample config for opposite direction - ES8311 can only have one
|
||||||
|
// sample rate at a time (native 44100 resampled via audio-stream), so if
|
||||||
|
// codec rates differ we must fail. But if both sides use native 44100 (audio-stream
|
||||||
|
// always opens codec with native rate), we allow it.
|
||||||
|
if (direction_compatible) {
|
||||||
|
// Allow if both use same native rate path (audio-stream opens with codec's native)
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
return ERROR_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
namespace tt::app::files { bool isSupportedAppFile(const std::string& filename); bool isSupportedImageFile(const std::string& filename); bool isSupportedTextFile(const std::string& filename); bool isSupportedAudioFile(const std::string& filename); } // namespace
|
||||||
namespace tt::app::files {
|
|
||||||
|
|
||||||
bool isSupportedAppFile(const std::string& filename);
|
|
||||||
bool isSupportedImageFile(const std::string& filename);
|
|
||||||
bool isSupportedTextFile(const std::string& filename);
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/audio_stream.h>
|
||||||
|
|
||||||
namespace tt::mcp {
|
namespace tt::mcp {
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ struct McpSystemState {
|
|||||||
|
|
||||||
// Audio device status
|
// Audio device status
|
||||||
Device* i2sDevice = nullptr;
|
Device* i2sDevice = nullptr;
|
||||||
|
Device* audioStreamDevice = nullptr;
|
||||||
|
AudioStreamHandle audioHandle = nullptr;
|
||||||
volatile bool audioBusy = false;
|
volatile bool audioBusy = false;
|
||||||
volatile bool audioRunning = false; // Used to abort play/record loop
|
volatile bool audioRunning = false; // Used to abort play/record loop
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class DisplayIdleService final : public Service {
|
|||||||
bool backlightOff = false;
|
bool backlightOff = false;
|
||||||
|
|
||||||
static void stopScreensaverCb(lv_event_t* e);
|
static void stopScreensaverCb(lv_event_t* e);
|
||||||
|
void stopScreensaverLocked();
|
||||||
|
|
||||||
/** @pre Caller must hold LVGL lock */
|
/** @pre Caller must hold LVGL lock */
|
||||||
void activateScreensaver();
|
void activateScreensaver();
|
||||||
@@ -63,6 +64,7 @@ public:
|
|||||||
* arbitrary threads while the timer is running.
|
* arbitrary threads while the timer is running.
|
||||||
*/
|
*/
|
||||||
void stopScreensaver();
|
void stopScreensaver();
|
||||||
|
void startMcpScreensaver();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the screensaver is currently active.
|
* Check if the screensaver is currently active.
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ namespace app {
|
|||||||
namespace apwebserver { extern const AppManifest manifest; }
|
namespace apwebserver { extern const AppManifest manifest; }
|
||||||
namespace crashdiagnostics { extern const AppManifest manifest; }
|
namespace crashdiagnostics { extern const AppManifest manifest; }
|
||||||
namespace webserversettings { extern const AppManifest manifest; }
|
namespace webserversettings { extern const AppManifest manifest; }
|
||||||
|
namespace mcpsettings { extern const AppManifest manifest; }
|
||||||
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
||||||
namespace keyboardsettings { extern const AppManifest manifest; } // T-Deck only for now
|
namespace keyboardsettings { extern const AppManifest manifest; } // T-Deck only for now
|
||||||
namespace trackballsettings { extern const AppManifest manifest; } // T-Deck only for now
|
namespace trackballsettings { extern const AppManifest manifest; } // T-Deck only for now
|
||||||
@@ -213,6 +214,8 @@ static void registerInternalApps() {
|
|||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
addAppManifest(app::apwebserver::manifest);
|
addAppManifest(app::apwebserver::manifest);
|
||||||
addAppManifest(app::webserversettings::manifest);
|
addAppManifest(app::webserversettings::manifest);
|
||||||
|
addAppManifest(app::mcpsettings::manifest);
|
||||||
|
// mcpoverride internal only via McpScreensaver, not shown in launcher
|
||||||
addAppManifest(app::crashdiagnostics::manifest);
|
addAppManifest(app::crashdiagnostics::manifest);
|
||||||
addAppManifest(app::development::manifest);
|
addAppManifest(app::development::manifest);
|
||||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||||
|
|||||||
@@ -1,29 +1,3 @@
|
|||||||
#include <Tactility/StringUtils.h>
|
#include <Tactility/StringUtils.h>
|
||||||
#include <Tactility/TactilityCore.h>
|
#include <Tactility/TactilityCore.h>
|
||||||
|
namespace tt::app::files { constexpr auto* TAG = "Files"; bool isSupportedAppFile(const std::string& filename) { return filename.ends_with(".app"); } bool isSupportedImageFile(const std::string& filename) { return string::lowercase(filename).ends_with(".png"); } bool isSupportedTextFile(const std::string& filename) { std::string l=string::lowercase(filename); return l.ends_with(".txt")||l.ends_with(".ini")||l.ends_with(".json")||l.ends_with(".yaml")||l.ends_with(".yml")||l.ends_with(".lua")||l.ends_with(".js")||l.ends_with(".properties"); } bool isSupportedAudioFile(const std::string& filename) { std::string l=string::lowercase(filename); return l.ends_with(".mp3")||l.ends_with(".wav")||l.ends_with(".ogg")||l.ends_with(".flac"); } } // namespace
|
||||||
namespace tt::app::files {
|
|
||||||
|
|
||||||
constexpr auto* TAG = "Files";
|
|
||||||
|
|
||||||
bool isSupportedAppFile(const std::string& filename) {
|
|
||||||
return filename.ends_with(".app");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSupportedImageFile(const std::string& filename) {
|
|
||||||
// Currently only the PNG library is built into Tactility
|
|
||||||
return string::lowercase(filename).ends_with(".png");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSupportedTextFile(const std::string& filename) {
|
|
||||||
std::string filename_lower = string::lowercase(filename);
|
|
||||||
return filename_lower.ends_with(".txt") ||
|
|
||||||
filename_lower.ends_with(".ini") ||
|
|
||||||
filename_lower.ends_with(".json") ||
|
|
||||||
filename_lower.ends_with(".yaml") ||
|
|
||||||
filename_lower.ends_with(".yml") ||
|
|
||||||
filename_lower.ends_with(".lua") ||
|
|
||||||
filename_lower.ends_with(".js") ||
|
|
||||||
filename_lower.ends_with(".properties");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace tt::app::filebrowser
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <Tactility/app/files/SupportedFiles.h>
|
#include <Tactility/app/files/SupportedFiles.h>
|
||||||
|
#include <Tactility/Bundle.h>
|
||||||
#include <Tactility/app/files/View.h>
|
#include <Tactility/app/files/View.h>
|
||||||
|
|
||||||
#include <Tactility/Platform.h>
|
#include <Tactility/Platform.h>
|
||||||
@@ -228,9 +229,17 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
|||||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||||
notes::start(processed_filepath);
|
notes::start(processed_filepath);
|
||||||
} else {
|
} else {
|
||||||
// Remove forward slash, because we need a relative path
|
|
||||||
notes::start(processed_filepath.substr(1));
|
notes::start(processed_filepath.substr(1));
|
||||||
}
|
}
|
||||||
|
} else if (isSupportedAudioFile(filename)) {
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
auto bundle = std::make_shared<Bundle>();
|
||||||
|
bundle->putString("file", processed_filepath);
|
||||||
|
auto loader = service::loader::findLoaderService();
|
||||||
|
if (loader) {
|
||||||
|
loader->start("one.tactility.mp3player", bundle);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
LOG_W(TAG, "Opening files of this type is not supported");
|
LOG_W(TAG, "Opening files of this type is not supported");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ constexpr auto* TAG = "McpSystem";
|
|||||||
#include <esp_log.h>
|
#include <esp_log.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <tactility/drivers/i2s_controller.h>
|
#include <tactility/drivers/i2s_controller.h>
|
||||||
|
#include <tactility/drivers/audio_stream.h>
|
||||||
#include <mbedtls/base64.h>
|
#include <mbedtls/base64.h>
|
||||||
#include <Tactility/hal/power/PowerDevice.h>
|
#include <Tactility/hal/power/PowerDevice.h>
|
||||||
#include <tactility/hal/Device.h>
|
#include <tactility/hal/Device.h>
|
||||||
@@ -427,19 +428,35 @@ std::string getScreenshotPbmBase64() {
|
|||||||
return encoded;
|
return encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Audio Porting Helper
|
// Audio Porting Helper
|
||||||
static void set_error(std::string& error, const std::string& message) {
|
static void set_error(std::string& error, const std::string& message) {
|
||||||
error = message;
|
error = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool get_audio_device(McpSystemState& state, std::string& error) {
|
static bool get_audio_device(McpSystemState& state, std::string& error) {
|
||||||
|
// Keep i2sDevice lookup for backward compat but also resolve audio-stream device
|
||||||
if (state.i2sDevice == nullptr) {
|
if (state.i2sDevice == nullptr) {
|
||||||
state.i2sDevice = device_find_by_name("i2s0");
|
state.i2sDevice = device_find_by_name("i2s0");
|
||||||
}
|
}
|
||||||
|
if (state.audioStreamDevice == nullptr) {
|
||||||
|
device_for_each_of_type(&AUDIO_STREAM_TYPE, &state, [](Device* dev, void* ctx)->bool {
|
||||||
|
auto* out = static_cast<McpSystemState*>(ctx);
|
||||||
|
out->audioStreamDevice = dev;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (state.audioStreamDevice == nullptr) {
|
||||||
|
state.audioStreamDevice = device_find_by_name("audio-stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.audioStreamDevice == nullptr) {
|
||||||
|
set_error(error, "Audio stream device not found");
|
||||||
|
// Fall back to i2s check to keep old path for recording without audio-stream? will fail later
|
||||||
if (state.i2sDevice == nullptr) {
|
if (state.i2sDevice == nullptr) {
|
||||||
set_error(error, "I2S device 'i2s0' was not found");
|
set_error(error, "I2S device 'i2s0' was not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,12 +470,18 @@ static bool begin_audio(McpSystemState& state, std::string& error) {
|
|||||||
}
|
}
|
||||||
state.audioBusy = true;
|
state.audioBusy = true;
|
||||||
state.audioRunning = true;
|
state.audioRunning = true;
|
||||||
|
state.audioHandle = nullptr;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void end_audio(McpSystemState& state) {
|
static void end_audio(McpSystemState& state) {
|
||||||
state.audioRunning = false;
|
state.audioRunning = false;
|
||||||
|
if (state.audioHandle != nullptr) {
|
||||||
|
audio_stream_close(state.audioHandle);
|
||||||
|
state.audioHandle = nullptr;
|
||||||
|
}
|
||||||
state.audioBusy = false;
|
state.audioBusy = false;
|
||||||
|
// legacy reset
|
||||||
if (state.i2sDevice != nullptr) {
|
if (state.i2sDevice != nullptr) {
|
||||||
device_lock(state.i2sDevice);
|
device_lock(state.i2sDevice);
|
||||||
i2s_controller_reset(state.i2sDevice);
|
i2s_controller_reset(state.i2sDevice);
|
||||||
@@ -467,6 +490,12 @@ static void end_audio(McpSystemState& state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool configure_i2s(McpSystemState& state, int sample_rate, int channels, std::string& error) {
|
static bool configure_i2s(McpSystemState& state, int sample_rate, int channels, std::string& error) {
|
||||||
|
if (state.audioHandle != nullptr) {
|
||||||
|
audio_stream_close(state.audioHandle);
|
||||||
|
state.audioHandle = nullptr;
|
||||||
|
}
|
||||||
|
if (state.audioStreamDevice == nullptr) {
|
||||||
|
// Fallback to direct I2S if audio-stream not found (very old boards)
|
||||||
I2sConfig config = {
|
I2sConfig config = {
|
||||||
.communication_format = I2S_FORMAT_STAND_I2S,
|
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||||
.sample_rate = static_cast<uint32_t>(sample_rate),
|
.sample_rate = static_cast<uint32_t>(sample_rate),
|
||||||
@@ -474,7 +503,6 @@ static bool configure_i2s(McpSystemState& state, int sample_rate, int channels,
|
|||||||
.channel_left = 0,
|
.channel_left = 0,
|
||||||
.channel_right = static_cast<int8_t>(channels == 2 ? 1 : I2S_CHANNEL_NONE)
|
.channel_right = static_cast<int8_t>(channels == 2 ? 1 : I2S_CHANNEL_NONE)
|
||||||
};
|
};
|
||||||
|
|
||||||
device_lock(state.i2sDevice);
|
device_lock(state.i2sDevice);
|
||||||
error_t result = i2s_controller_set_config(state.i2sDevice, &config);
|
error_t result = i2s_controller_set_config(state.i2sDevice, &config);
|
||||||
device_unlock(state.i2sDevice);
|
device_unlock(state.i2sDevice);
|
||||||
@@ -485,6 +513,19 @@ static bool configure_i2s(McpSystemState& state, int sample_rate, int channels,
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
AudioStreamConfig cfg = {
|
||||||
|
.sample_rate = static_cast<uint32_t>(sample_rate),
|
||||||
|
.bits_per_sample = AUDIO_BITS_PER_SAMPLE,
|
||||||
|
.channels = static_cast<uint8_t>(channels),
|
||||||
|
};
|
||||||
|
error_t res = audio_stream_open_output(state.audioStreamDevice, &cfg, &state.audioHandle);
|
||||||
|
if (res != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "audio_stream_open_output failed: %d rate=%d ch=%d", res, sample_rate, channels);
|
||||||
|
set_error(error, "Failed to open audio stream");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void apply_volume(uint8_t* data, size_t data_size, int volume) {
|
static void apply_volume(uint8_t* data, size_t data_size, int volume) {
|
||||||
int16_t* samples = (int16_t*)data;
|
int16_t* samples = (int16_t*)data;
|
||||||
@@ -498,6 +539,32 @@ static void apply_volume(uint8_t* data, size_t data_size, int volume) {
|
|||||||
static bool write_audio(McpSystemState& state, uint8_t* data, size_t data_size, int volume, std::string& error) {
|
static bool write_audio(McpSystemState& state, uint8_t* data, size_t data_size, int volume, std::string& error) {
|
||||||
apply_volume(data, data_size, volume);
|
apply_volume(data, data_size, volume);
|
||||||
|
|
||||||
|
if (state.audioStreamDevice != nullptr && state.audioHandle != nullptr) {
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < data_size && state.audioRunning) {
|
||||||
|
size_t bytes_written = 0;
|
||||||
|
error_t result = audio_stream_write(
|
||||||
|
state.audioHandle,
|
||||||
|
data + offset,
|
||||||
|
data_size - offset,
|
||||||
|
&bytes_written,
|
||||||
|
pdMS_TO_TICKS(500)
|
||||||
|
);
|
||||||
|
if (result != ERROR_NONE || bytes_written == 0) {
|
||||||
|
LOG_E(TAG, "audio_stream_write failed: result=%d written=%u", result, (unsigned)bytes_written);
|
||||||
|
set_error(error, "Audio playback failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offset += bytes_written;
|
||||||
|
}
|
||||||
|
if (!state.audioRunning) {
|
||||||
|
set_error(error, "Audio playback was cancelled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback direct I2S
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
while (offset < data_size && state.audioRunning) {
|
while (offset < data_size && state.audioRunning) {
|
||||||
size_t bytes_written = 0;
|
size_t bytes_written = 0;
|
||||||
@@ -523,6 +590,7 @@ static bool write_audio(McpSystemState& state, uint8_t* data, size_t data_size,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool resolve_file(const std::string& filename, char* path, size_t path_size, std::string& error) {
|
static bool resolve_file(const std::string& filename, char* path, size_t path_size, std::string& error) {
|
||||||
if (filename.empty() || filename.length() > 96 ||
|
if (filename.empty() || filename.length() > 96 ||
|
||||||
filename.find("..") != std::string::npos ||
|
filename.find("..") != std::string::npos ||
|
||||||
@@ -673,17 +741,17 @@ bool playTone(int frequency, int durationMs, int volume, std::string& error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply volume already in samples amplitude, reuse write_audio path via audio_stream
|
||||||
size_t bytes_written = 0;
|
size_t bytes_written = 0;
|
||||||
error_t result = i2s_controller_write(
|
error_t result = ERROR_NONE;
|
||||||
state.i2sDevice,
|
if (state.audioHandle != nullptr) {
|
||||||
samples,
|
result = audio_stream_write(state.audioHandle, samples, count * sizeof(int16_t), &bytes_written, pdMS_TO_TICKS(500));
|
||||||
count * sizeof(int16_t),
|
} else {
|
||||||
&bytes_written,
|
result = i2s_controller_write(state.i2sDevice, samples, count * sizeof(int16_t), &bytes_written, pdMS_TO_TICKS(250));
|
||||||
pdMS_TO_TICKS(250)
|
}
|
||||||
);
|
|
||||||
if (result != ERROR_NONE || bytes_written != count * sizeof(int16_t)) {
|
if (result != ERROR_NONE || bytes_written != count * sizeof(int16_t)) {
|
||||||
LOG_E(TAG, "Tone write failed: result=%d written=%u", result, (unsigned)bytes_written);
|
LOG_E(TAG, "Tone write failed: result=%d written=%u", result, (unsigned)bytes_written);
|
||||||
set_error(error, "I2S tone playback failed");
|
set_error(error, "Audio tone playback failed");
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
generated += count;
|
generated += count;
|
||||||
@@ -710,10 +778,32 @@ bool recordVoice(int durationSec, const std::string& filename, size_t& recordedB
|
|||||||
bool success = false;
|
bool success = false;
|
||||||
FILE* file = NULL;
|
FILE* file = NULL;
|
||||||
char path[256];
|
char path[256];
|
||||||
if (!resolve_file(filename, path, sizeof(path), error) ||
|
if (!resolve_file(filename, path, sizeof(path), error)) {
|
||||||
!configure_i2s(state, AUDIO_SAMPLE_RATE, 1, error)) {
|
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
// For recording, open input stream
|
||||||
|
if (state.audioStreamDevice != nullptr) {
|
||||||
|
if (state.audioHandle != nullptr) {
|
||||||
|
audio_stream_close(state.audioHandle);
|
||||||
|
state.audioHandle = nullptr;
|
||||||
|
}
|
||||||
|
AudioStreamConfig in_cfg = {
|
||||||
|
.sample_rate = AUDIO_SAMPLE_RATE,
|
||||||
|
.bits_per_sample = AUDIO_BITS_PER_SAMPLE,
|
||||||
|
.channels = 1,
|
||||||
|
};
|
||||||
|
if (audio_stream_open_input(state.audioStreamDevice, &in_cfg, &state.audioHandle) != ERROR_NONE) {
|
||||||
|
set_error(error, "Failed to open audio input stream");
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
audio_stream_set_mute(state.audioStreamDevice, AUDIO_CODEC_DIR_INPUT, false);
|
||||||
|
audio_stream_set_volume(state.audioStreamDevice, AUDIO_CODEC_DIR_INPUT, 100.0f);
|
||||||
|
LOG_I(TAG, "Mic unmuted gain 100%% for %s", filename.c_str());
|
||||||
|
} else {
|
||||||
|
if (!configure_i2s(state, AUDIO_SAMPLE_RATE, 1, error)) {
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file = fopen(path, "wb");
|
file = fopen(path, "wb");
|
||||||
if (file == NULL) {
|
if (file == NULL) {
|
||||||
@@ -729,13 +819,12 @@ bool recordVoice(int durationSec, const std::string& filename, size_t& recordedB
|
|||||||
size_t remaining = target - total;
|
size_t remaining = target - total;
|
||||||
size_t requested = remaining < sizeof(buffer) ? remaining : sizeof(buffer);
|
size_t requested = remaining < sizeof(buffer) ? remaining : sizeof(buffer);
|
||||||
size_t bytes_read = 0;
|
size_t bytes_read = 0;
|
||||||
error_t result = i2s_controller_read(
|
error_t result = ERROR_NONE;
|
||||||
state.i2sDevice,
|
if (state.audioHandle != nullptr) {
|
||||||
buffer,
|
result = audio_stream_read(state.audioHandle, buffer, requested, &bytes_read, pdMS_TO_TICKS(500));
|
||||||
requested,
|
} else {
|
||||||
&bytes_read,
|
result = i2s_controller_read(state.i2sDevice, buffer, requested, &bytes_read, pdMS_TO_TICKS(250));
|
||||||
pdMS_TO_TICKS(250)
|
}
|
||||||
);
|
|
||||||
if (result != ERROR_NONE || bytes_read == 0) {
|
if (result != ERROR_NONE || bytes_read == 0) {
|
||||||
LOG_E(TAG, "I2S read failed: result=%d read=%u", result, (unsigned)bytes_read);
|
LOG_E(TAG, "I2S read failed: result=%d read=%u", result, (unsigned)bytes_read);
|
||||||
set_error(error, "I2S recording failed");
|
set_error(error, "I2S recording failed");
|
||||||
|
|||||||
Reference in New Issue
Block a user