/* Host-only RTOS/codec boundary stubs. Production functions are inserted below. * Tests use synthetic byte markers, no hardware, network, or captured audio. */ #include #include #include #include #include #include #include #include #include #include #include #define ESP_PLATFORM 1 #define pdTRUE 1 #define pdPASS 1 #define pdMS_TO_TICKS(n) (n) #define portMAX_DELAY UINT32_MAX #define tskNO_AFFINITY (-1) #define MALLOC_CAP_SPIRAM 1 #define MALLOC_CAP_8BIT 2 #define ESP_AUDIO_ERR_OK 0 #define ESP_OPUS_DEC_FRAME_DURATION_120_MS 120 #define ESP_OPUS_DEC_CONFIG_DEFAULT() {0} #define VOLC_DATA_TYPE_AUDIO 1 #define VOLC_AUDIO_DATA_TYPE_PCM 1 #define AUDIO_DATA_TYPE_OPUS 2 #define VOLC_DATA_TYPE_MESSAGE 3 #define VOLC_MSG_CONV_STATUS 4 #define VOLC_CONV_STATUS_INTERRUPTED 5 #define MAGIC_CONV "conv" #define MAGIC_OFFSET 8 #define LOGI(...) ((void)0) #define LOGE(...) ((void)0) #define HAL_SAFE_FREE(p) do {free(p); (p)=NULL;} while(0) typedef int audio_data_type_e; typedef int BaseType_t; static void __rtc_diagnostic_priority_apply(void) {} /* separately tested in voice-diagnostic-host.c */ typedef int volc_audio_data_type_e; typedef void *byte_rtc_engine_t; typedef void *volc_opt_t; typedef struct {int unused;} volc_room_info_t; typedef struct {int unused;} byte_rtc_event_handler_t; typedef void (*volc_msg_cb)(void *, void *); typedef struct {int code;struct {int conv_status;} data;} volc_msg_t; typedef struct {int type; struct {struct {int data_type;} audio;struct {bool is_binary;} message;} info;} volc_data_info_t; typedef void (*volc_data_cb)(void *, const void *, int, volc_data_info_t *); typedef struct {char *p_channel_name,*p_uid,*p_token;} volc_rtc_option_t; typedef struct {bool auto_publish_audio,auto_publish_video,auto_subscribe_audio,auto_subscribe_video;} byte_rtc_room_options_t; static uint64_t esp_timer_get_time(void) { struct timespec now; clock_gettime(CLOCK_MONOTONIC,&now); return (uint64_t)now.tv_sec*1000000+now.tv_nsec/1000; } static uint64_t volc_osal_get_time_ms(void) {return esp_timer_get_time()/1000;} static void vTaskDelay(unsigned ms) { struct timespec t={.tv_sec=ms/1000,.tv_nsec=(long)(ms%1000)*1000000}; nanosleep(&t,NULL); } static struct timespec deadline(unsigned ms) { struct timespec t; clock_gettime(CLOCK_REALTIME,&t); t.tv_sec+=ms/1000; t.tv_nsec+=(long)(ms%1000)*1000000; if(t.tv_nsec>=1000000000){t.tv_sec++;t.tv_nsec-=1000000000;} return t; } static atomic_int fail_alloc_after=-1, live_allocations, live_tasks; static void *host_alloc(size_t n) { int value=atomic_load(&fail_alloc_after); if(value>=0 && atomic_fetch_sub(&fail_alloc_after,1)==0) return NULL; void *p=calloc(1,n); assert(p); atomic_fetch_add(&live_allocations,1); return p; } static void host_free(void *p) {if(p){free(p);atomic_fetch_sub(&live_allocations,1);}} static void *heap_caps_calloc(size_t n,size_t size,int caps) { assert(caps==(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT));return host_alloc(n*size); } static void heap_caps_free(void *p) {host_free(p);} typedef struct { pthread_mutex_t mutex; pthread_cond_t readable; unsigned *items, capacity, head, count; } *QueueHandle_t; static QueueHandle_t xQueueCreate(unsigned n,unsigned item_size) { assert(item_size==sizeof(unsigned)); QueueHandle_t q=host_alloc(sizeof(*q)+n*sizeof(unsigned)); if(!q)return NULL; q->items=(unsigned *)(q+1);q->capacity=n; pthread_mutex_init(&q->mutex,NULL);pthread_cond_init(&q->readable,NULL);return q; } static int xQueueSend(QueueHandle_t q,const unsigned *item,unsigned wait) { assert(wait==0);assert(q);pthread_mutex_lock(&q->mutex); int ok=q->countcapacity; if(ok){q->items[(q->head+q->count)%q->capacity]=*item;q->count++;pthread_cond_signal(&q->readable);} pthread_mutex_unlock(&q->mutex);return ok; } static int xQueueReceive(QueueHandle_t q,unsigned *item,unsigned wait) { assert(q);pthread_mutex_lock(&q->mutex); struct timespec end=deadline(wait==portMAX_DELAY?5000:wait); while(!q->count && wait){int result=pthread_cond_timedwait(&q->readable,&q->mutex,&end);if(result==ETIMEDOUT)break;assert(result==0);} int ok=q->count!=0; if(ok){*item=q->items[q->head];q->head=(q->head+1)%q->capacity;q->count--;} pthread_mutex_unlock(&q->mutex);return ok; } static unsigned uxQueueMessagesWaiting(QueueHandle_t q) { pthread_mutex_lock(&q->mutex);unsigned count=q->count;pthread_mutex_unlock(&q->mutex);return count; } static void vQueueDelete(QueueHandle_t q) { pthread_mutex_destroy(&q->mutex);pthread_cond_destroy(&q->readable);host_free(q); } typedef struct {pthread_mutex_t mutex;pthread_cond_t ready;unsigned count;} *SemaphoreHandle_t; static SemaphoreHandle_t make_sem(unsigned count) { SemaphoreHandle_t s=host_alloc(sizeof(*s));if(!s)return NULL; pthread_mutex_init(&s->mutex,NULL);pthread_cond_init(&s->ready,NULL);s->count=count;return s; } static SemaphoreHandle_t xSemaphoreCreateMutex(void){return make_sem(1);} static SemaphoreHandle_t xSemaphoreCreateBinary(void){return make_sem(0);} static int xSemaphoreTake(SemaphoreHandle_t s,unsigned wait) { assert(s);pthread_mutex_lock(&s->mutex);struct timespec end=deadline(wait==portMAX_DELAY?5000:wait); while(!s->count && wait){int rc=pthread_cond_timedwait(&s->ready,&s->mutex,&end);if(rc==ETIMEDOUT){assert(wait!=portMAX_DELAY);break;}assert(rc==0);} int ok=s->count!=0;if(ok)s->count--;pthread_mutex_unlock(&s->mutex);return ok; } static int xSemaphoreGive(SemaphoreHandle_t s) { pthread_mutex_lock(&s->mutex);assert(s->count==0);s->count=1; pthread_cond_signal(&s->ready);pthread_mutex_unlock(&s->mutex);return pdTRUE; } static void vSemaphoreDelete(SemaphoreHandle_t s) { pthread_mutex_destroy(&s->mutex);pthread_cond_destroy(&s->ready);host_free(s); } typedef struct {void (*fn)(void *);void *arg;pthread_t thread;} *TaskHandle_t; static atomic_bool worker_allowed=true; static void *task_entry(void *arg) { TaskHandle_t t=arg;void (*fn)(void *)=t->fn;void *context=t->arg; while(!atomic_load(&worker_allowed))vTaskDelay(1); fn(context);assert(!"task should delete itself");return NULL; } static int xTaskCreatePinnedToCoreWithCaps(void (*fn)(void *),const char *name,unsigned stack,void *arg,int priority,TaskHandle_t *out,int core,int caps) { (void)name;assert(stack==16384 && priority==5 && core==tskNO_AFFINITY && caps==(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT)); TaskHandle_t t=host_alloc(sizeof(*t));if(!t)return 0;t->fn=fn;t->arg=arg;*out=t; atomic_fetch_add(&live_tasks,1);assert(!pthread_create(&t->thread,NULL,task_entry,t));return pdPASS; } static void vTaskSuspend(TaskHandle_t task) {assert(!task);for(;;)vTaskDelay(1000);} static void vTaskDeleteWithCaps(TaskHandle_t task) { assert(task);pthread_cancel(task->thread);pthread_join(task->thread,NULL);host_free(task);atomic_fetch_sub(&live_tasks,1); } typedef struct {int sample_rate,channel,frame_duration;} esp_opus_dec_cfg_t; typedef struct {uint8_t *buffer;size_t len,consumed;} esp_audio_dec_in_raw_t; typedef struct {uint8_t *buffer;size_t len,decoded_size;} esp_audio_dec_out_frame_t; typedef struct {int unused;} esp_audio_dec_info_t; static atomic_int decode_calls, decoder_opens, decoder_mode, decoder_open_fail; static atomic_bool decoder_blocked; static pthread_t test_thread; static int esp_opus_dec_open(const esp_opus_dec_cfg_t *cfg,size_t size,void **handle) { (void)size;assert(cfg->sample_rate==16000 && cfg->channel==1 && cfg->frame_duration==120); if(atomic_load(&decoder_open_fail))return -1; *handle=host_alloc(1);atomic_fetch_add(&decoder_opens,1);return *handle?0:-1; } static void esp_opus_dec_close(void *handle){host_free(handle);} static int esp_opus_dec_decode(void *handle,esp_audio_dec_in_raw_t *raw,esp_audio_dec_out_frame_t *frame,esp_audio_dec_info_t *info) { (void)info;assert(handle && raw->len && frame->len>=2);assert(!pthread_equal(pthread_self(),test_thread)); atomic_fetch_add(&decode_calls,1);while(atomic_load(&decoder_blocked))vTaskDelay(1); int mode=atomic_load(&decoder_mode);raw->consumed=1;frame->decoded_size=2; frame->buffer[0]=raw->buffer[0];frame->buffer[1]=0; if(mode==1)return -1; if(mode==2)raw->consumed=0; if(mode==3)raw->consumed=raw->len+1; if(mode==4)frame->decoded_size=frame->len+1; return 0; } static int join_result,leave_result; static void *byte_rtc_get_user_data(byte_rtc_engine_t engine){return engine;} static int byte_rtc_join_room(byte_rtc_engine_t engine,const char *room,const char *uid,const char *token,byte_rtc_room_options_t *options) { (void)engine;(void)room;(void)uid;(void)token;(void)options;return join_result; } static int byte_rtc_leave_room(byte_rtc_engine_t engine,const char *room){(void)engine;(void)room;return leave_result;} static bool _is_target_message(const uint8_t *message,const char *magic){return !memcmp(message,magic,4);} static int _on_conversion_status_message_parsed(uint8_t *message,void *rtc){(void)rtc;return *message;} /* @PRODUCTION@ */ static atomic_uint emitted; static atomic_uint interruptions; static atomic_bool emit_blocked,emit_entered; static uint8_t output[256]; static void on_pcm(void *context,const void *data,int size,volc_data_info_t *info) { (void)context;assert(size>0 && info->type==VOLC_DATA_TYPE_AUDIO); atomic_store(&emit_entered,true);while(atomic_load(&emit_blocked))vTaskDelay(1); unsigned count=atomic_load(&emitted);assert(countcode==VOLC_MSG_CONV_STATUS && msg->data.conv_status==VOLC_CONV_STATUS_INTERRUPTED); /* This models the service clearing queued PCM, not a raw-audio capture. */ atomic_store(&emitted,0);atomic_fetch_add(&interruptions,1); } static void wait_value(atomic_int *value,int expected) { uint64_t end=esp_timer_get_time()+3000000; while(atomic_load(value)=expected); } static void wait_emitted(unsigned count) { uint64_t end=esp_timer_get_time()+3000000; while(atomic_load(&emitted)rx_free)rx_free)==RTC_RX_PACKET_COUNT); } static void reset_stubs(void) { atomic_store(&worker_allowed,true);atomic_store(&decoder_blocked,false); atomic_store(&decode_calls,0);atomic_store(&decoder_opens,0);atomic_store(&emitted,0); atomic_store(&decoder_mode,0);atomic_store(&decoder_open_fail,0);atomic_store(&fail_alloc_after,-1); atomic_store(&interruptions,0);atomic_store(&emit_blocked,false);atomic_store(&emit_entered,false); join_result=leave_result=0;test_thread=pthread_self();memset(output,0,sizeof(output)); } static void fixture_init(rtc_impl_t *rtc,bool allow_worker) { reset_stubs();atomic_store(&worker_allowed,allow_worker);memset(rtc,0,sizeof(*rtc)); rtc->rtc=rtc;rtc->opus_encoder=(void *)1;rtc->data_callback=on_pcm;rtc->message_callback=on_message; assert(__rtc_rx_init(rtc)==0); volc_rtc_option_t opt={.p_channel_name="room-A",.p_uid="device",.p_token="fixture"}; assert(__rtc_start(rtc,&opt)==0); rtc->b_channel_joined=rtc->b_user_joined=true; } static void fixture_close(rtc_impl_t *rtc) { atomic_store(&worker_allowed,true);atomic_store(&decoder_blocked,false);leave_result=0; atomic_store(&emit_blocked,false); __rtc_stop(rtc);__rtc_rx_destroy(rtc); if(rtc->opus_decoder){esp_opus_dec_close(rtc->opus_decoder);rtc->opus_decoder=NULL;} uint64_t end=esp_timer_get_time()+3000000; while(atomic_load(&live_tasks) && esp_timer_get_time()result=__rtc_stop(a->rtc);atomic_store(&a->done,true);return NULL;} static void test_stop_race_and_room(void) { rtc_impl_t rtc;fixture_init(&rtc,true);atomic_store(&decoder_blocked,true);uint8_t old=41; packet(&rtc,"room-A",&old,1);wait_value(&decode_calls,1);packet(&rtc,"room-A",&old,1); stop_arg arg={.rtc=&rtc};pthread_t stopper;assert(!pthread_create(&stopper,NULL,stop_call,&arg)); uint64_t end=esp_timer_get_time()+3000000;while(atomic_load(&rtc.rx_accepting) && esp_timer_get_time()