From 61142f6f5f71a2fcb47c7696d5eb8d7423ad33c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=80=9D=E4=BC=9F?= <283569385@qq.com> Date: Sun, 7 Jul 2024 15:11:38 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=80=BB=E4=BD=93=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 10/museum-self-explanation-system | 1 + 1 file changed, 1 insertion(+) create mode 160000 10/museum-self-explanation-system diff --git a/10/museum-self-explanation-system b/10/museum-self-explanation-system new file mode 160000 index 0000000..c61cdac --- /dev/null +++ b/10/museum-self-explanation-system @@ -0,0 +1 @@ +Subproject commit c61cdacc08113456fe2ca1010b07be38c366babd -- Gitee From 5cdec5fd48f08b56e71ce6b96ecdda533694f95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=80=9D=E4=BC=9F?= <283569385@qq.com> Date: Sun, 7 Jul 2024 07:46:13 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=AD=90=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=2010/museum-self-explanation-system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 10/museum-self-explanation-system | 1 - 1 file changed, 1 deletion(-) delete mode 160000 10/museum-self-explanation-system diff --git a/10/museum-self-explanation-system b/10/museum-self-explanation-system deleted file mode 160000 index c61cdac..0000000 --- a/10/museum-self-explanation-system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c61cdacc08113456fe2ca1010b07be38c366babd -- Gitee From 876a7da067a39e6e9e6478c50a6190cfa2ad6905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=80=9D=E4=BC=9F?= <283569385@qq.com> Date: Sun, 7 Jul 2024 07:47:05 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20museum-self-explanatio?= =?UTF-8?q?n-system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 10/museum-self-explanation-system/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 10/museum-self-explanation-system/.keep diff --git a/10/museum-self-explanation-system/.keep b/10/museum-self-explanation-system/.keep new file mode 100644 index 0000000..e69de29 -- Gitee From e7cdc2dd9eb717c0141bb7d1d2f9e22a51e19757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=80=9D=E4=BC=9F?= <283569385@qq.com> Date: Sun, 7 Jul 2024 07:49:36 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=E4=B8=BB=E8=A6=81=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 程思伟 <283569385@qq.com> --- 10/museum-self-explanation-system/chat.c | 177 ++++++++ 10/museum-self-explanation-system/config.c | 26 ++ 10/museum-self-explanation-system/control.c | 434 ++++++++++++++++++++ 10/museum-self-explanation-system/http.c | 110 +++++ 10/museum-self-explanation-system/jrsc.c | 95 +++++ 10/museum-self-explanation-system/key.c | 106 +++++ 10/museum-self-explanation-system/led.c | 31 ++ 10/museum-self-explanation-system/main.c | 5 + 10/museum-self-explanation-system/play.c | 150 +++++++ 10/museum-self-explanation-system/record.c | 135 ++++++ 10/museum-self-explanation-system/stt.c | 153 +++++++ 10/museum-self-explanation-system/token.c | 66 +++ 10/museum-self-explanation-system/tts.c | 248 +++++++++++ 10/museum-self-explanation-system/utils.c | 41 ++ 10/museum-self-explanation-system/wake.c | 236 +++++++++++ 15 files changed, 2013 insertions(+) create mode 100644 10/museum-self-explanation-system/chat.c create mode 100644 10/museum-self-explanation-system/config.c create mode 100644 10/museum-self-explanation-system/control.c create mode 100644 10/museum-self-explanation-system/http.c create mode 100644 10/museum-self-explanation-system/jrsc.c create mode 100644 10/museum-self-explanation-system/key.c create mode 100644 10/museum-self-explanation-system/led.c create mode 100644 10/museum-self-explanation-system/main.c create mode 100644 10/museum-self-explanation-system/play.c create mode 100644 10/museum-self-explanation-system/record.c create mode 100644 10/museum-self-explanation-system/stt.c create mode 100644 10/museum-self-explanation-system/token.c create mode 100644 10/museum-self-explanation-system/tts.c create mode 100644 10/museum-self-explanation-system/utils.c create mode 100644 10/museum-self-explanation-system/wake.c diff --git a/10/museum-self-explanation-system/chat.c b/10/museum-self-explanation-system/chat.c new file mode 100644 index 0000000..99d04be --- /dev/null +++ b/10/museum-self-explanation-system/chat.c @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include "config.h" +#include "http.h" + +char* app_id = "0a9a80f0-2e03-435a-81a3-e2b402cc8f3a"; + +//创建会话 +char* create_conversation(char* authtoken) +{ + char* url = "https://qianfan.baidubce.com/v2/app/conversation"; + + //拼接Authorization字段 + char* auth; + asprintf(&auth, "Authorization: Bearer %s", authtoken); + + //添加请求头部字段 + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/json;charset=utf-8"); + headers = curl_slist_append(headers, auth); + + //准备请求正文 + cJSON* obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "app_id", app_id); + //将JSON对象转换为JSON字符串 + char* json = cJSON_Print(obj); + + //发送请求 + size_t size = strlen(json); + char* response = http_post(url, headers, json, &size); + cJSON_Delete(obj); + free(json); + free(auth); + curl_slist_free_all(headers); + + if (!response) + { + return NULL; + } + + obj = cJSON_ParseWithLength(response, size); + free(response); + + if (!obj) + { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return NULL; + } + + cJSON* conversation_id = cJSON_GetObjectItem(obj, "conversation_id"); + if (!conversation_id) + { + fprintf(stderr, "conversation_id 字段不存在\n"); + cJSON_Delete(obj); + return NULL; + } + + char* retval = strdup(conversation_id->valuestring); + + cJSON_Delete(obj); + + return retval; +} + +//调用对话API +//authtoken: 平台密钥 +//conv_id: 会话ID +//query: 用户输入的文本 +//返回值: 对话结果 +char* chat(char* authtoken, char* conv_id, char* query) +{ + char* url = "https://qianfan.baidubce.com/v2/app/conversation/runs"; + + //拼接Authorization字段 + char* auth; + asprintf(&auth, "Authorization: Bearer %s", authtoken); + + //设置请求头部字段 + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/json;charset=utf-8"); + headers = curl_slist_append(headers, auth); + + //准备请求正文 + cJSON* obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "app_id", app_id); + cJSON_AddStringToObject(obj, "conversation_id", conv_id); + cJSON_AddStringToObject(obj, "query", query); + cJSON_AddBoolToObject(obj, "stream", false); + //将JSON对象转换为JSON字符串 + char* json = cJSON_Print(obj); + + //发送请求 + size_t size = strlen(json); + char* response = http_post(url, headers, json, &size); + cJSON_Delete(obj); + free(json); + curl_slist_free_all(headers); + free(auth); + + if (!response) + { + return NULL; + } + + obj = cJSON_ParseWithLength(response, size); + free(response); + + if (!obj) + { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return NULL; + } + + cJSON* answer = cJSON_GetObjectItem(obj, "answer"); + if (!answer) + { + fprintf(stderr, "answer 字段不存在\n"); + puts(cJSON_Print(obj)); + cJSON_Delete(obj); + return NULL; + } + + char* retval = strdup(answer->valuestring); + + cJSON_Delete(obj); + + return retval; +} + +int main() +{ + //读取配置文件中的平台密钥 + cJSON* config = read_config("config.json"); + if (!config) + { + return EXIT_FAILURE; + } + + cJSON* authtoken = cJSON_GetObjectItem(config, "authtoken"); + if (!authtoken) + { + fprintf(stderr, "配置文件错误: 找不到 'authtoken' 字段\n"); + cJSON_Delete(config); + return EXIT_FAILURE; + } + + printf("%s\n", authtoken->valuestring); + + //创建会话 + char* conv_id = create_conversation(authtoken->valuestring); + + printf("conv_id: %s\n", conv_id); + + //读取用户输入的文本 + char line[512]; + while(1) + { + printf("> "); + if (fgets(line, sizeof(line), stdin) == NULL) + { + break; + } + + //调用百度云千帆AppBuilder API进行对话 + char* answer = chat(authtoken->valuestring, conv_id, line); + if (!answer) + { + continue; + } + + printf("< %s\n", answer); + } + + return EXIT_SUCCESS; +} diff --git a/10/museum-self-explanation-system/config.c b/10/museum-self-explanation-system/config.c new file mode 100644 index 0000000..ab84519 --- /dev/null +++ b/10/museum-self-explanation-system/config.c @@ -0,0 +1,26 @@ +#include +#include +#include "config.h" + +//读取配置信息 +cJSON* read_config(const char* file) +{ + //读取json文件内容 + FILE* fp = fopen(file, "r"); + if (!fp) { + perror(file); + return NULL; + } + //移动文件指针到文件末尾 + fseek(fp, 0, SEEK_END); + //获取文件大小 + long size = ftell(fp); + //移动文件指针到文件开头 + fseek(fp, 0, SEEK_SET); + //将文件内容读取到缓冲区中 + char* buffer = (char*)malloc(size + 1); + fread(buffer, 1, size, fp); + fclose(fp); + //将缓冲区中的内容转换为json对象 + return cJSON_ParseWithLength(buffer, size); +} \ No newline at end of file diff --git a/10/museum-self-explanation-system/control.c b/10/museum-self-explanation-system/control.c new file mode 100644 index 0000000..32a46c8 --- /dev/null +++ b/10/museum-self-explanation-system/control.c @@ -0,0 +1,434 @@ +#include +#include +#include + +// 设置音频采集开关的函数 +// card: 声卡名称 +// selem: 控制项名称 +// enable: 开关状态 +int set_capture_switch(const char* card, const char* selem, bool enable) { + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + + // 打开混音器 + if ((err = snd_mixer_open(&handle, 0)) < 0) { + fprintf(stderr, "Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + + // 附加控制接口到混音器 + if ((err = snd_mixer_attach(handle, card)) < 0) { + fprintf(stderr, "Mixer attach %s error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 注册混音器 + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + fprintf(stderr, "Mixer register error: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 加载混音器元素 + if ((err = snd_mixer_load(handle)) < 0) { + fprintf(stderr, "Mixer %s load error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 分配简单元素ID + snd_mixer_selem_id_alloca(&sid); + + // 设置简单元素的名称 + snd_mixer_selem_id_set_name(sid, selem); + + // 查找简单元素 + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + fprintf(stderr, "Unable to find simple control '%s',%i\n", selem, 0); + snd_mixer_close(handle); + return -ENOENT; + } + + // 设置采集开关(启用或禁用) + if ((err = snd_mixer_selem_set_capture_switch_all(elem, enable ? 1 : 0)) < 0) { + fprintf(stderr, "Unable to set capture switch: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 关闭混音器 + snd_mixer_close(handle); + + return 0; // 成功 +} + +// 设置音频回放开关的函数 +// card: 声卡名称 +// selem: 控制项名称 +// enable: 开关状态 +int set_playback_switch(const char* card, const char* selem, bool enable) { + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + + // 打开混音器 + if ((err = snd_mixer_open(&handle, 0)) < 0) { + fprintf(stderr, "Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + + // 附加控制接口到混音器 + if ((err = snd_mixer_attach(handle, card)) < 0) { + fprintf(stderr, "Mixer attach %s error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 注册混音器 + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + fprintf(stderr, "Mixer register error: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 加载混音器元素 + if ((err = snd_mixer_load(handle)) < 0) { + fprintf(stderr, "Mixer %s load error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 分配简单元素ID + snd_mixer_selem_id_alloca(&sid); + + // 设置简单元素的名称 + snd_mixer_selem_id_set_name(sid, selem); + + // 查找简单元素 + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + fprintf(stderr, "Unable to find simple control '%s',%i\n", selem, 0); + snd_mixer_close(handle); + return -ENOENT; + } + + // 设置回放开关(启用或禁用) + if ((err = snd_mixer_selem_set_playback_switch_all(elem, enable ? 1 : 0)) < 0) { + fprintf(stderr, "Unable to set playback switch: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 关闭混音器 + snd_mixer_close(handle); + + return 0; // 成功 +} + +// 获取音频采集音量 +// card: 声卡名称 +// selem: 控制项名称 +// 返回值: 当前音量 +int get_capture_volume(const char* card, const char* selem) +{ + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + + // 打开混音器 + if ((err = snd_mixer_open(&handle, 0)) < 0) { + fprintf(stderr, "Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + + // 附加控制接口到混音器 + if ((err = snd_mixer_attach(handle, card)) < 0) { + fprintf(stderr, "Mixer attach %s error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 注册混音器 + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + fprintf(stderr, "Mixer register error: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 加载混音器元素 + if ((err = snd_mixer_load(handle)) < 0) { + fprintf(stderr, "Mixer %s load error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 分配简单元素ID + snd_mixer_selem_id_alloca(&sid); + + // 设置简单元素的名称 + snd_mixer_selem_id_set_name(sid, selem); + + // 查找简单元素 + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + fprintf(stderr, "Unable to find simple control '%s',%i\n", selem, 0); + snd_mixer_close(handle); + return -ENOENT; + } + + // 获取采集通道音量 + long volume = 0; + if ((err = snd_mixer_selem_get_capture_volume(elem, 0, &volume)) < 0) { + fprintf(stderr, "Unable to get capture volume: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 关闭混音器 + snd_mixer_close(handle); + + return volume; // 成功返回音量 +} + +// 获取音频回放通道音量 +// card: 声卡名称 +// selem: 控制项名称 +// 返回值: 当前音量 +int get_playback_volume(const char* card, const char* selem) { + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + + // 打开混音器 + if ((err = snd_mixer_open(&handle, 0)) < 0) { + fprintf(stderr, "Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + + // 附加控制接口到混音器 + if ((err = snd_mixer_attach(handle, card)) < 0) { + fprintf(stderr, "Mixer attach %s error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 注册混音器 + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + fprintf(stderr, "Mixer register error: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 加载混音器元素 + if ((err = snd_mixer_load(handle)) < 0) { + fprintf(stderr, "Mixer %s load error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 分配简单元素ID + snd_mixer_selem_id_alloca(&sid); + + // 设置简单元素的名称 + snd_mixer_selem_id_set_name(sid, selem); + + // 查找简单元素 + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + fprintf(stderr, "Unable to find simple control '%s',%i\n", selem, 0); + snd_mixer_close(handle); + return -ENOENT; + } + + // 获取回放通道音量 + long volume = 0; + if ((err = snd_mixer_selem_get_playback_volume(elem, 0, &volume)) < 0) { + fprintf(stderr, "Unable to get playback volume: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 关闭混音器 + snd_mixer_close(handle); + + return volume; // 成功返回音量 +} + +// 设置音频采集音量 +// card: 声卡名称 +// selem: 控制项名称 +// volume: 设置音量 +// 返回值: 成功返回设置后的音量,失败返回错误码 +int set_capture_volume(const char* card, const char* selem, long volume) +{ + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + + // 打开混音器 + if ((err = snd_mixer_open(&handle, 0)) < 0) { + fprintf(stderr, "Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + + // 附加控制接口到混音器 + if ((err = snd_mixer_attach(handle, card)) < 0) { + fprintf(stderr, "Mixer attach %s error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 注册混音器 + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + fprintf(stderr, "Mixer register error: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 加载混音器元素 + if ((err = snd_mixer_load(handle)) < 0) { + fprintf(stderr, "Mixer %s load error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 分配简单元素ID + snd_mixer_selem_id_alloca(&sid); + + // 设置简单元素的名称 + snd_mixer_selem_id_set_name(sid, selem); + + // 查找简单元素 + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + fprintf(stderr, "Unable to find simple control '%s',%i\n", selem, 0); + snd_mixer_close(handle); + return -ENOENT; + } + + // 获取音量范围 + long min, max; + if ((err = snd_mixer_selem_get_capture_volume_range(elem, &min, &max)) < 0) + { + fprintf(stderr, "Unable to get capture volume range: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + if (volume < min) + { + volume = min; + } + + if (volume > max) + { + volume = max; + } + + // 设置采集通道音量 + if ((err = snd_mixer_selem_set_capture_volume_all(elem, volume)) < 0) { + fprintf(stderr, "Unable to set capture volume: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 关闭混音器 + snd_mixer_close(handle); + + return volume; // 成功 +} + +// 设置音频回放通道音量 +// card: 声卡名称 +// selem: 控制项名称 +// volume: 设置音量 +// 返回值: 成功返回设置后的音量,失败返回错误码 +int set_playback_volume(const char* card, const char* selem, long volume) +{ + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + + // 打开混音器 + if ((err = snd_mixer_open(&handle, 0)) < 0) { + fprintf(stderr, "Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + + // 附加控制接口到混音器 + if ((err = snd_mixer_attach(handle, card)) < 0) { + fprintf(stderr, "Mixer attach %s error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 注册混音器 + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + fprintf(stderr, "Mixer register error: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 加载混音器元素 + if ((err = snd_mixer_load(handle)) < 0) { + fprintf(stderr, "Mixer %s load error: %s\n", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 分配简单元素ID + snd_mixer_selem_id_alloca(&sid); + + // 设置简单元素的名称 + snd_mixer_selem_id_set_name(sid, selem); + + // 查找简单元素 + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + fprintf(stderr, "Unable to find simple control '%s',%i\n", selem, 0); + snd_mixer_close(handle); + return -ENOENT; + } + + // 获取音量范围 + long min, max; + if ((err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max)) < 0) + { + fprintf(stderr, "Unable to get playback volume range: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + if (volume < min) + { + volume = min; + } + + if (volume > max) + { + volume = max; + } + + // 设置回放通道音量 + if ((err = snd_mixer_selem_set_playback_volume_all(elem, volume)) < 0) { + fprintf(stderr, "Unable to set playback volume: %s\n", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + // 关闭混音器 + snd_mixer_close(handle); + + return volume; // 成功返回设置后音量 +} + +int main(int argc, char** argv) +{ + printf("Playback Vol: %d\n", get_playback_volume("hw:0", "Analog")); + printf("New Playback Vol: %d\n", set_playback_volume("hw:0", "Analog", atoi(argv[1]))); + + return 0; +} diff --git a/10/museum-self-explanation-system/http.c b/10/museum-self-explanation-system/http.c new file mode 100644 index 0000000..dbceee8 --- /dev/null +++ b/10/museum-self-explanation-system/http.c @@ -0,0 +1,110 @@ +#include +#include "http.h" + +//发送GET请求 +//url: 请求地址 +//headers: 增加的请求头部字段 +//psize: 响应正文大小,输出参数 +//返回值: 响应正文,需要调用者释放内存 +char* http_get(char* url, struct curl_slist* headers, size_t* psize) +{ + char *respdata; + size_t respsize; + FILE *fp = open_memstream(&respdata, &respsize); + if (!fp) + { + perror("open_memstream"); + return NULL; + } + + CURL *client = curl_easy_init(); + if (!client) + { + perror("curl_easy_init"); + fclose(fp); + return NULL; + } + + curl_easy_setopt(client, CURLOPT_URL, url); + + if (headers) + { + curl_easy_setopt(client, CURLOPT_HTTPHEADER, headers); + } + + // 将服务器返回的响应报文保存到文件流中 + curl_easy_setopt(client, CURLOPT_WRITEDATA, fp); + + CURLcode error = curl_easy_perform(client); + if (error != CURLE_OK) + { + fprintf(stderr, "curl_easy_perform: %s\n", curl_easy_strerror(error)); + curl_easy_cleanup(client); + fclose(fp); + return NULL; + } + + curl_easy_cleanup(client); + fclose(fp); + + //更新响应正文大小 + *psize = respsize; + + return respdata; +} + +//发送POST请求 +//url: 请求地址 +//headers: 增加的请求头部字段 +//data: 请求正文 +//psize: 请求和响应正文大小,输入和输出参数 +//返回值: 响应正文,需要调用者释放内存 +char* http_post(char* url, struct curl_slist* headers, char* data, size_t* psize) +{ + char *respdata; + size_t respsize; + FILE *fp = open_memstream(&respdata, &respsize); + if (!fp) + { + perror("open_memstream"); + return NULL; + } + + CURL *client = curl_easy_init(); + if (!client) + { + perror("curl_easy_init"); + fclose(fp); + return NULL; + } + + curl_easy_setopt(client, CURLOPT_URL, url); + curl_easy_setopt(client, CURLOPT_POST, 1); + curl_easy_setopt(client, CURLOPT_POSTFIELDS, data); + curl_easy_setopt(client, CURLOPT_POSTFIELDSIZE, *psize); + + if (headers) + { + curl_easy_setopt(client, CURLOPT_HTTPHEADER, headers); + } + + // 将服务器返回的响应报文保存到文件流中 + curl_easy_setopt(client, CURLOPT_WRITEDATA, fp); + + CURLcode error = curl_easy_perform(client); + if (error != CURLE_OK) + { + fprintf(stderr, "curl_easy_perform: %s\n", curl_easy_strerror(error)); + curl_easy_cleanup(client); + fclose(fp); + return NULL; + } + + curl_easy_cleanup(client); + fclose(fp); + + //更新响应正文大小 + *psize = respsize; + + return respdata; +} \ No newline at end of file diff --git a/10/museum-self-explanation-system/jrsc.c b/10/museum-self-explanation-system/jrsc.c new file mode 100644 index 0000000..b38e53a --- /dev/null +++ b/10/museum-self-explanation-system/jrsc.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include + +// 打印今日天气 +void print_today_weather(const char *response, size_t size) { + // 解析 JSON 响应 + cJSON *json = cJSON_ParseWithLength(response, size); + if (json == NULL) { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return; + } + + // 获取 "data" 对象 + cJSON *data = cJSON_GetObjectItemCaseSensitive(json, "data"); + if (!cJSON_IsObject(data)) { + fprintf(stderr, "JSON 格式错误: 找不到 'data' 对象\n"); + cJSON_Delete(json); + return; + } + + // 获取 "weatherData" 对象 + cJSON *weatherData = cJSON_GetObjectItemCaseSensitive(data, "weatherData"); + if (!cJSON_IsObject(weatherData)) { + fprintf(stderr, "JSON 格式错误: 找不到 'weatherData' 对象\n"); + cJSON_Delete(json); + return; + } + + // 获取天气数据字段 + cJSON *temperature = cJSON_GetObjectItemCaseSensitive(weatherData, "temperature"); + cJSON *weather = cJSON_GetObjectItemCaseSensitive(weatherData, "weather"); + cJSON *humidity = cJSON_GetObjectItemCaseSensitive(weatherData, "humidity"); + + if (!cJSON_IsNumber(temperature) || !cJSON_IsString(weather) || !cJSON_IsNumber(humidity)) { + fprintf(stderr, "JSON 格式错误: 找不到必要的天气信息\n"); + cJSON_Delete(json); + return; + } + + // 打印今日天气 + printf("今日天气:\n"); + printf("温度:%.1f °C\n", cJSON_GetNumberValue(temperature)); + printf("天气:%s\n", weather->valuestring); + printf("湿度:%.1f%%\n", cJSON_GetNumberValue(humidity)); + + // 释放 JSON 对象 + cJSON_Delete(json); +} + +int main(void) { + CURL *client; + CURLcode err; + char *response; + size_t size; + + // 创建内存流 + FILE *memstream = open_memstream(&response, &size); + if (memstream == NULL) { + perror("open_memstream"); + return 1; + } + + // 初始化 CURL + curl_global_init(CURL_GLOBAL_ALL); + client = curl_easy_init(); + + // 设置 CURL 选项 + curl_easy_setopt(client, CURLOPT_URL, "https://v2.jinrishici.com/info"); + curl_easy_setopt(client, CURLOPT_WRITEDATA, memstream); + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, "X-User-Token: tVgJ29i700dX3FnRPu8SgDoIv0HoQjOO"); + curl_easy_setopt(client, CURLOPT_HTTPHEADER, headers); + + // 执行 CURL 请求 + err = curl_easy_perform(client); + fclose(memstream); // 关闭内存流 + + if (err != CURLE_OK) { + fprintf(stderr, "curl_easy_perform() 失败: %s\n", curl_easy_strerror(err)); + } else { + // 打印今日天气 + print_today_weather(response, size); + } + + // 清理资源 + curl_slist_free_all(headers); + curl_easy_cleanup(client); + free(response); + curl_global_cleanup(); + + return 0; +} diff --git a/10/museum-self-explanation-system/key.c b/10/museum-self-explanation-system/key.c new file mode 100644 index 0000000..1bcb850 --- /dev/null +++ b/10/museum-self-explanation-system/key.c @@ -0,0 +1,106 @@ +#include // 包含libgpiod库的头文件 +#include // 包含标准输入输出库 +#include // 包含UNIX标准库,用于usleep函数 +#include "record.h" + +#define GPIO_LINE 9 // 定义GPIO线的编号,这里是PF9 + +int main(void) { + struct gpiod_chip *chip; // 定义指向GPIO芯片的指针 + struct gpiod_line *line; // 定义指向GPIO线的指针 + int value, last_value; // 定义当前值和上一次的值,用于检测状态变化 + snd_pcm_uframes_t period; //周期大小 + snd_pcm_t* capture; //音频采集设备 + char* buffer = NULL; //存放音频数据的缓冲区 + FILE* fp = NULL; //录音文件 + + // 打开GPIO芯片 + chip = gpiod_chip_open_by_label("GPIOF"); + if (!chip) { + perror("打开GPIO芯片失败"); + return 1; + } + + // 获取GPIO线 + line = gpiod_chip_get_line(chip, GPIO_LINE); + if (!line) { + perror("获取GPIO线失败"); + gpiod_chip_close(chip); + return 1; + } + + // 将GPIO线设置为输入模式 + if (gpiod_line_request_input(line, "key1")) { + perror("请求将GPIO线设置为输入模式失败"); + gpiod_chip_close(chip); + return 1; + } + + // 获取初始的GPIO线值 + last_value = gpiod_line_get_value(line); + + // 无限循环检测GPIO线值的变化 + while (1) { + // 获取当前的GPIO线值 + value = gpiod_line_get_value(line); + + // 如果当前值与上一次的值不同,说明按键状态发生了变化 + if (value != last_value) { + // 如果当前值为0,表示按键被按下 + if (value == 0) { + printf("key pressed\n"); + capture = record_open("hw:0,1", SND_PCM_FORMAT_S16_LE, 2, 44100, &period); + if (!capture) + { + continue; + } + + buffer = malloc(snd_pcm_frames_to_bytes(capture, period)); // 分配缓冲区 + if (!buffer) + { + perror("malloc"); + record_close(capture); + continue; + } + + fp = fopen("output.pcm", "wb"); + if (!fp) + { + perror("Error opening output file"); + free(buffer); // 释放缓冲区 + record_close(capture); // 关闭PCM设备 + continue; + } + } + // 如果当前值为1,表示按键被释放 + else { + printf("key released\n"); + record_close(capture); + capture = NULL; + } + // 更新上一次的值为当前值 + last_value = value; + } + + if (value == 0 && capture) + { + //如果按键按下并且音频采集设备已打开 + snd_pcm_sframes_t frames = snd_pcm_readi(capture, buffer, period); // 从PCM设备读取数据 + if (frames < 0) + { + fprintf(stderr, "Error from read: %s\n", snd_strerror(frames)); + snd_pcm_recover(capture, frames, 0); + } + + fwrite(buffer, snd_pcm_frames_to_bytes(capture, frames), 1, fp); // 将读取的数据写入文件 + } + // 延时100毫秒,防止检测过于频繁 + //usleep(100000); + } + + // 释放GPIO线资源 + gpiod_line_release(line); + // 关闭GPIO芯片 + gpiod_chip_close(chip); + return 0; +} diff --git a/10/museum-self-explanation-system/led.c b/10/museum-self-explanation-system/led.c new file mode 100644 index 0000000..45b549e --- /dev/null +++ b/10/museum-self-explanation-system/led.c @@ -0,0 +1,31 @@ +#include +#include //sleep + +int main() +{ + //打开LED设备文件 + FILE *fp = fopen("/sys/class/leds/led1/brightness", "w"); + if (fp == NULL) + { + perror("打开LED设备文件失败"); + return 1; + } + + while(1) + { + //打开LED + fprintf(fp, "1"); + fflush(fp); + usleep(100000); + + //关闭LED + fprintf(fp, "0"); + fflush(fp); + usleep(100000); + } + + //关闭LED设备文件 + fclose(fp); + + return 0; +} \ No newline at end of file diff --git a/10/museum-self-explanation-system/main.c b/10/museum-self-explanation-system/main.c new file mode 100644 index 0000000..7b2fd1f --- /dev/null +++ b/10/museum-self-explanation-system/main.c @@ -0,0 +1,5 @@ +#include + +int main(){ + printf("Hello, from museum!\n"); +} diff --git a/10/museum-self-explanation-system/play.c b/10/museum-self-explanation-system/play.c new file mode 100644 index 0000000..5ecc0ff --- /dev/null +++ b/10/museum-self-explanation-system/play.c @@ -0,0 +1,150 @@ +#include +#include +#include +#include // 用于处理错误码 + +//开始播放 +snd_pcm_t* play_open(const char* name, + snd_pcm_format_t format, + unsigned int channel, + unsigned int rate, + snd_pcm_uframes_t* period) +{ + snd_pcm_t *playback; // PCM设备句柄 + snd_pcm_hw_params_t *params; // PCM硬件参数 + int err; // 用于存储错误码 + int dir; + + // 打开PCM设备用于回放 + if ((err = snd_pcm_open(&playback, name, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + fprintf(stderr, "Error opening PCM device %s: %s\n", name, snd_strerror(err)); + return NULL; + } + + // 分配参数对象,并用默认值填充 + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_any(playback, params); + + // 设置参数 + // 设置访问类型:交错模式 + if ((err = snd_pcm_hw_params_set_access(playback, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + fprintf(stderr, "Error setting access: %s\n", snd_strerror(err)); + snd_pcm_close(playback); + return NULL; + } + // 设置数据格式:16位小端 + if ((err = snd_pcm_hw_params_set_format(playback, params, format)) < 0) { + fprintf(stderr, "Error setting format: %s\n", snd_strerror(err)); + snd_pcm_close(playback); + return NULL; + } + // 设置声道数:立体声 + if ((err = snd_pcm_hw_params_set_channels(playback, params, channel)) < 0) { + fprintf(stderr, "Error setting channels: %s\n", snd_strerror(err)); + snd_pcm_close(playback); + return NULL; + } + // 设置采样率 + if ((err = snd_pcm_hw_params_set_rate_near(playback, params, &rate, &dir)) < 0) { + fprintf(stderr, "Error setting rate: %s\n", snd_strerror(err)); + snd_pcm_close(playback); + return NULL; + } + + printf("sample rate: %d Hz\n", rate); + + //设置周期大小 + if ((err = snd_pcm_hw_params_set_period_size_near(playback, params, period, &dir)) < 0) { + fprintf(stderr, "Error setting period size: %s\n", snd_strerror(err)); + snd_pcm_close(playback); + return NULL; + } + + // 设置硬件参数 + if ((err = snd_pcm_hw_params(playback, params)) < 0) { + fprintf(stderr, "Error setting HW params: %s\n", snd_strerror(err)); + snd_pcm_close(playback); + return NULL; + } + + // 获取周期大小 + snd_pcm_hw_params_get_period_size(params, period, &dir); + + return playback; +} + +//停止播放 +void play_close(snd_pcm_t* playback) +{ + snd_pcm_drain(playback); // 排空PCM设备 + snd_pcm_close(playback); // 关闭PCM设备 +} + +#if 0 +int main() +{ + snd_pcm_t *playback; // PCM设备句柄 + snd_pcm_uframes_t period = 999; // 每个周期的帧数 + char *buffer; // 缓冲区,用于存储从文件中读取的音频数据 + FILE *pcm_file; // 输出PCM文件 + int err; // 用于存储错误码 + + playback = play_open("hw:0,0", SND_PCM_FORMAT_S16_LE, 2, 44100, &period); + if (!playback) + { + return 1; + } + + printf("period: %d frames\n", period); + + buffer = (char *) malloc(snd_pcm_frames_to_bytes(playback, period)); // 分配缓冲区 + if (!buffer) { + perror("malloc"); + play_close(playback); + return 1; + } + + // 打开输出文件 + pcm_file = fopen("output.pcm", "rb"); + if (!pcm_file) { + perror("Error opening output file"); + free(buffer); // 释放缓冲区 + play_close(playback); // 关闭PCM设备 + return 1; + } + + // 录制数据 + printf("Playing... Press Ctrl+C to stop.\n"); + while (1) { + size_t bytes = fread(buffer, 1, snd_pcm_frames_to_bytes(playback, period), pcm_file); + if (bytes == 0) + { + if (ferror(pcm_file)) + { + perror("fread"); + continue; + } + + if (feof(pcm_file)) + { + break; + } + } + + snd_pcm_sframes_t frames = snd_pcm_writei(playback, buffer, snd_pcm_bytes_to_frames(playback, bytes)); + if (frames < 0) + { + fprintf(stderr, "Error from write: %s\n", snd_strerror(frames)); + snd_pcm_recover(playback, frames, 0); + } + } + + // 清理资源 + free(buffer); // 释放缓冲区 + fclose(pcm_file); // 关闭文件 + play_close(playback); + + return 0; +} + +#endif diff --git a/10/museum-self-explanation-system/record.c b/10/museum-self-explanation-system/record.c new file mode 100644 index 0000000..cdcdb7b --- /dev/null +++ b/10/museum-self-explanation-system/record.c @@ -0,0 +1,135 @@ +#include +#include +#include // 用于处理错误码 +#include "record.h" + +//开始录音 +snd_pcm_t* record_open(const char* name, + snd_pcm_format_t format, + unsigned int channel, + unsigned int rate, + snd_pcm_uframes_t* period) +{ + snd_pcm_t *capture; // PCM设备句柄 + snd_pcm_hw_params_t *params; // PCM硬件参数 + int err; // 用于存储错误码 + int dir; + + // 打开PCM设备用于录音(捕捉) + if ((err = snd_pcm_open(&capture, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) { + fprintf(stderr, "Error opening PCM device %s: %s\n", name, snd_strerror(err)); + return NULL; + } + + // 分配参数对象,并用默认值填充 + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_any(capture, params); + + // 设置参数 + // 设置访问类型:交错模式 + if ((err = snd_pcm_hw_params_set_access(capture, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + fprintf(stderr, "Error setting access: %s\n", snd_strerror(err)); + snd_pcm_close(capture); + return NULL; + } + // 设置数据格式:16位小端 + if ((err = snd_pcm_hw_params_set_format(capture, params, format)) < 0) { + fprintf(stderr, "Error setting format: %s\n", snd_strerror(err)); + snd_pcm_close(capture); + return NULL; + } + // 设置声道数:立体声 + if ((err = snd_pcm_hw_params_set_channels(capture, params, channel)) < 0) { + fprintf(stderr, "Error setting channels: %s\n", snd_strerror(err)); + snd_pcm_close(capture); + return NULL; + } + // 设置采样率 + if ((err = snd_pcm_hw_params_set_rate_near(capture, params, &rate, &dir)) < 0) { + fprintf(stderr, "Error setting rate: %s\n", snd_strerror(err)); + snd_pcm_close(capture); + return NULL; + } + printf("sample rate: %d Hz\n", rate); + + //设置周期大小 + if ((err = snd_pcm_hw_params_set_period_size_near(capture, params, period, &dir)) < 0) { + fprintf(stderr, "Error setting period size: %s\n", snd_strerror(err)); + snd_pcm_close(capture); + return NULL; + } + + // 设置硬件参数 + if ((err = snd_pcm_hw_params(capture, params)) < 0) { + fprintf(stderr, "Error setting HW params: %s\n", snd_strerror(err)); + snd_pcm_close(capture); + return NULL; + } + + // 获取周期大小 + snd_pcm_hw_params_get_period_size(params, period, &dir); + + return capture; +} + +//停止录音 +void record_close(snd_pcm_t* capture) +{ + snd_pcm_drain(capture); // 排空PCM设备 + snd_pcm_close(capture); // 关闭PCM设备 +} + +#if 0 +int main() +{ + snd_pcm_t *capture; // PCM设备句柄 + snd_pcm_uframes_t period; // 每个周期的帧数 + char *buffer; // 缓冲区,用于存储采集到的音频数据 + FILE *pcm_file; // 输出PCM文件 + int err; // 用于存储错误码 + + capture = record_open("hw:0,1", SND_PCM_FORMAT_S16_LE, 2, 44100, &period); + if (!capture) + { + return 1; + } + + printf("period: %d frames\n", period); + + buffer = (char *) malloc(snd_pcm_frames_to_bytes(capture, period)); // 分配缓冲区 + if (!buffer) { + perror("malloc"); + record_close(capture); + return 1; + } + + // 打开输出文件 + pcm_file = fopen("output.pcm", "wb"); + if (!pcm_file) { + perror("Error opening output file"); + free(buffer); // 释放缓冲区 + record_close(capture); // 关闭PCM设备 + return 1; + } + + // 录制数据 + printf("Recording... Press Ctrl+C to stop.\n"); + while (1) { + snd_pcm_sframes_t frames = snd_pcm_readi(capture, buffer, period); // 从PCM设备读取数据 + if (frames < 0) + { + fprintf(stderr, "Error from read: %s\n", snd_strerror(frames)); + snd_pcm_recover(capture, frames, 0); + } + + fwrite(buffer, snd_pcm_frames_to_bytes(capture, frames), 1, pcm_file); // 将读取的数据写入文件 + } + + // 清理资源 + free(buffer); // 释放缓冲区 + fclose(pcm_file); // 关闭文件 + record_close(capture); + + return 0; +} +#endif diff --git a/10/museum-self-explanation-system/stt.c b/10/museum-self-explanation-system/stt.c new file mode 100644 index 0000000..338e520 --- /dev/null +++ b/10/museum-self-explanation-system/stt.c @@ -0,0 +1,153 @@ +#include +#include +#include //strdup +#include "config.h" +#include "token.h" +#include "http.h" +#include "stt.h" + +//读取音频文件 +//file: 音频文件路径 +//size: 音频文件大小 +//return: 音频文件内容, NULL 表示失败 +char* load_audio_file(const char* file, size_t* size) +{ + //打开音频文件 + FILE* fp = fopen(file, "rb"); + if (!fp) { + perror(file); + return NULL; + } + //获取文件大小 + fseek(fp, 0, SEEK_END); + *size = ftell(fp); + fseek(fp, 0, SEEK_SET); + //读取文件内容 + char* buffer = (char*)malloc(*size); + if (!buffer) { + perror("malloc"); + fclose(fp); + return NULL; + } + fread(buffer, 1, *size, fp); + fclose(fp); + return buffer; +} + +//发送请求消息 +//token: 获取的access token +//audio: 音频文件内容 +//size: 音频文件大小 +//return: 响应消息正文, NULL 表示失败 +static char* send_request(char* token, char* audio, size_t size) +{ + char* url = NULL; + asprintf(&url, "http://vop.baidu.com/server_api?cuid=hqyj&token=%s", token); + + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: audio/pcm; rate=16000"); + + char* response = http_post(url, headers, audio, &size); + + free(url); + curl_slist_free_all(headers); + + return response; +} + +//处理服务器返回的响应消息 +static char* process_response(char* response) +{ + char* retval; + + //解析 JSON 响应 + cJSON *json = cJSON_Parse(response); + if (!json) { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return NULL; + } + + //判断err_no字段 + cJSON* err_no = cJSON_GetObjectItem(json, "err_no"); + if (!err_no) { + fprintf(stderr, "err_no 字段不存在\n"); + cJSON_Delete(json); + return NULL; + } + //判断err_no的值 + if (err_no->valueint != 0) { + //打印错误信息 + cJSON* err_msg = cJSON_GetObjectItem(json, "err_msg"); + if (err_msg) + { + fprintf(stderr, "err_msg: %s\n", err_msg->valuestring); + } + cJSON_Delete(json); + return NULL; + } + + // 获取 "result" 字段中的第一个元素 + cJSON *result = cJSON_GetObjectItem(json, "result"); + if (!result) { + fprintf(stderr, "JSON 格式错误: 找不到'result' 字段\n"); + cJSON_Delete(json); + return NULL; + } + + if (cJSON_GetArraySize(result) > 0) { + // 获取第一个元素的 "content" 字段 + cJSON *content = cJSON_GetArrayItem(result, 0); + //保存结果 + retval = strdup(content->valuestring); + } + + cJSON_Delete(json); + + return retval; +} + +//将音频数据转换为字符串 +//audio: 存放音频数据的地址 +//size: 音频数据大小 +//返回值: 转换之后的字符串,转换失败返回NULL +char* speech_to_text(char* audio, size_t size) +{ + char* text; + //读取配置信息,API KEY 和 SECRET KEY + cJSON *config = read_config("config.json"); + if (!config) { + printf("config: %s\n", cJSON_Print(config)); + return NULL; + } + + cJSON* api_key = cJSON_GetObjectItem(config, "api_key"); + cJSON* secret_key = cJSON_GetObjectItem(config, "secret_key"); + if (!api_key ||!secret_key) { + fprintf(stderr, "配置文件错误: 找不到 'api_key' 或'secret_key' 字段\n"); + cJSON_Delete(config); + return NULL; + } + + //获取token + char* token = get_access_token(api_key->valuestring, secret_key->valuestring); + cJSON_Delete(config); + if (!token) { + fprintf(stderr, "获取 token 失败\n"); + return NULL; + } + + //调用百度语音识别 API + char* response = send_request(token, audio, size); + free(token); + if (!response) { + fprintf(stderr, "调用百度语音识别 API 失败\n"); + return NULL; + } + + //处理服务器返回的响应消息 + text = process_response(response); + + free(response); + + return text; +} diff --git a/10/museum-self-explanation-system/token.c b/10/museum-self-explanation-system/token.c new file mode 100644 index 0000000..d336af9 --- /dev/null +++ b/10/museum-self-explanation-system/token.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include "http.h" +#include "config.h" +#include "token.h" + +//成功返回access token,失败返回NULL +char *get_access_token(const char *ak, const char *sk) +{ + char* token = NULL; + + //设置URL + char* url = "https://aip.baidubce.com/oauth/2.0/token"; + char* form = NULL; + asprintf(&form, "grant_type=client_credentials&client_id=%s&client_secret=%s&", ak, sk); + + //发送请求报文 + size_t size = strlen(form); + char* response = http_post(url, NULL, form, &size); + free(form); + + if (!response) + { + return NULL; + } + + //解析响应报文 + cJSON* root = cJSON_ParseWithLength(response, size); + free(response); + + if (!root) + { + // 解析错误 + const char *error_ptr = cJSON_GetErrorPtr(); + if (error_ptr != NULL) + { + fprintf(stderr, "Error before: %s\n", error_ptr); + } + return NULL; + } + + cJSON* access_token = cJSON_GetObjectItem(root, "access_token"); + if (!access_token) + { + fprintf(stderr, "access_token attribute not found\n"); + cJSON_Delete(root); + return NULL; + } + + if (!cJSON_IsString(access_token)) + { + fprintf(stderr, "access_token attribute format error\n"); + cJSON_Delete(root); + return NULL; + } + + token = strdup(access_token->valuestring); + + // 删除解析后对象占用的内存 + cJSON_Delete(root); + + return token; +} diff --git a/10/museum-self-explanation-system/tts.c b/10/museum-self-explanation-system/tts.c new file mode 100644 index 0000000..e264312 --- /dev/null +++ b/10/museum-self-explanation-system/tts.c @@ -0,0 +1,248 @@ +#include +#include +#include +#include +#include +#include "config.h" +#include "http.h" +#include "play.h" + +char* appid = "1461471028"; + +//生成UUID +char* gen_uuid(void) +{ + static char uuid_str[37]; + uuid_t uuid; + uuid_generate(uuid); + uuid_unparse(uuid, uuid_str); + return uuid_str; +} + +// base64解码 +//base64: 需要解码的base64字符串 +//decoded: 解码后的数据 +//返回值: 解码后的数据大小 +size_t base64_decode(const char* base64, char** decoded) +{ + // 计算解码后的数据大小 + size_t decoded_size = (strlen(base64) / 4 + 1) * 3; + *decoded = (char*)malloc(decoded_size); + if (!*decoded) + { + return 0; + } + + // 解码base64字符串 + int size = b64_pton(base64, *decoded, decoded_size); + if (size < 0) + { + return 0; + } + + return size; +} + +//发送请求 +//text: 需要转为语音的文本 +//返回值: API返回的响应消息,失败返回 NULL +static char* send_request(char* text) +{ + //从配置文件中获取 API 密钥 + cJSON* config = read_config("config.json"); + if (!config) { + return NULL; + } + + cJSON* ttstoken = cJSON_GetObjectItem(config, "ttstoken"); + if (!ttstoken) { + fprintf(stderr, "无法获取ttstoken\n"); + return NULL; + } + + //printf("%s\n", ttstoken->valuestring); + + char* url = "https://openspeech.bytedance.com/api/v1/tts"; + + char* auth; + asprintf(&auth, "Authorization: Bearer;%s", ttstoken->valuestring); + + //添加请求头部字段 + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, auth); + free(auth); + + //创建请求体 + cJSON* obj = cJSON_CreateObject(); + + cJSON* app = cJSON_CreateObject(); + cJSON_AddStringToObject(app, "appid", appid); + cJSON_AddStringToObject(app, "token", ttstoken->valuestring); + cJSON_AddStringToObject(app, "cluster", "volcano_tts"); + cJSON_AddItemToObject(obj, "app", app); + + cJSON* user = cJSON_CreateObject(); + cJSON_AddStringToObject(user, "uid", "hqyj"); + cJSON_AddItemToObject(obj, "user", user); + + cJSON* audio = cJSON_CreateObject(); + //发音人 + cJSON_AddStringToObject(audio, "voice_type", "BV700_V2_streaming"); //灿灿2.0 + //方言 + //cJSON_AddStringToObject(audio, "language", "zh_xian"); + //情感 + //cJSON_AddStringToObject(audio, "emotion", "tear"); + cJSON_AddNumberToObject(audio, "rate", 16000); + cJSON_AddItemToObject(obj, "audio", audio); + + cJSON* request = cJSON_CreateObject(); + cJSON_AddStringToObject(request, "reqid", gen_uuid()); + cJSON_AddStringToObject(request, "text", text); + //SSML + //cJSON_AddStringToObject(request, "text_type", "ssml"); + cJSON_AddStringToObject(request, "operation", "query"); + cJSON_AddItemToObject(obj, "request", request); + + char* json = cJSON_Print(obj); + size_t size = strlen(json); + cJSON_Delete(obj); + puts(json); + + return http_post(url, headers, json, &size); +} + +//处理服务器返回的响应消息 +//response: API返回的响应消息 +//size: 音频数据的大小 +//返回值:处理后的语音数据 +static char* process_response(char* response, size_t* size) +{ + //解析 JSON 响应 + cJSON *obj = cJSON_Parse(response); + if (!obj) { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return NULL; + } + + cJSON* code = cJSON_GetObjectItem(obj, "code"); + if (!code) + { + fprintf(stderr, "JSON 格式错误: 找不到 'code' 字段\n"); + return NULL; + } + + if (code->valueint != 3000) + { + cJSON* message = cJSON_GetObjectItem(obj, "message"); + if (message) + { + fprintf(stderr, "message: %s\n", message->valuestring); + } + return NULL; + } + + cJSON* data = cJSON_GetObjectItem(obj, "data"); + if (!data) + { + fprintf(stderr, "JSON 格式错误: 找不到 'data' 字段\n"); + return NULL; + } + + //对data字段的值进行base64解码 + char* audio = NULL; + size_t audio_size = base64_decode(data->valuestring, &audio); + if (audio_size == 0) + { + fprintf(stderr, "base64解码失败\n"); + return NULL; + } + + cJSON_Delete(obj); + + *size = audio_size; + return audio; +} + +//语音合成 +void text_to_speech(char* text) +{ + //调用语音合成 API + char* response = send_request(text); + if (!response) { + fprintf(stderr, "调用语音合成 API 失败\n"); + return; + } + + //puts(response); + + //处理服务器返回的响应消息 + size_t size = 0; + char* audio = process_response(response, &size); + if (!audio) { + return; + } + + //播放音频 + snd_pcm_t *playback; // PCM设备句柄 + snd_pcm_uframes_t period = 999; // 每个周期的帧数 + char *buffer; // 缓冲区,用于存储从文件中读取的音频数据 + FILE *pcm_file; // 输出PCM文件 + int err; // 用于存储错误码 + // 打开音频数据 + pcm_file = fmemopen(audio, size, "rb"); + if (!pcm_file) { + perror("Error opening output file"); + return; + } + + playback = play_open("hw:0,0", SND_PCM_FORMAT_S16_LE, 1, 16000, &period); + if (!playback) + { + fclose(pcm_file); + return; + } + + printf("period: %d frames\n", period); + + buffer = (char *) malloc(snd_pcm_frames_to_bytes(playback, period)); // 分配缓冲区 + if (!buffer) { + perror("malloc"); + play_close(playback); + fclose(pcm_file); + return; + } + + // 录制数据 + while (1) { + size_t bytes = fread(buffer, 1, snd_pcm_frames_to_bytes(playback, period), pcm_file); + if (bytes == 0) + { + if (ferror(pcm_file)) + { + perror("fread"); + continue; + } + + if (feof(pcm_file)) + { + break; + } + } + + snd_pcm_sframes_t frames = snd_pcm_writei(playback, buffer, snd_pcm_bytes_to_frames(playback, bytes)); + if (frames < 0) + { + fprintf(stderr, "Error from write: %s\n", snd_strerror(frames)); + snd_pcm_recover(playback, frames, 0); + } + } + + // 清理资源 + free(buffer); // 释放缓冲区 + fclose(pcm_file); // 关闭文件 + play_close(playback); + + free(response); +} + diff --git a/10/museum-self-explanation-system/utils.c b/10/museum-self-explanation-system/utils.c new file mode 100644 index 0000000..61e1197 --- /dev/null +++ b/10/museum-self-explanation-system/utils.c @@ -0,0 +1,41 @@ +#include +#include +#include //b64_pton +#include + +//生成UUID +char* gen_uuid(void) +{ + static char uuid_str[37]; + uuid_t uuid; + uuid_generate(uuid); + uuid_unparse(uuid, uuid_str); + return uuid_str; +} + +// base64解码 +//base64: 需要解码的base64字符串 +//decoded: 解码后的数据 +//返回值: 解码后的数据大小 +size_t base64_decode(const char* base64, char* decoded) +{ + // 计算解码后的数据大小 + size_t decoded_size = (strlen(base64) / 4 + 1) * 3; + // 解码base64字符串 + int size = b64_pton(base64, decoded, decoded_size); + if (size < 0) + { + return 0; + } + + return size; +} + +int main() +{ + puts(gen_uuid()); + + char text[100]; + int size = base64_decode("6YGT5Y+v6YGT6Z2e5bi46YGT", text); + printf("%*s\n", size, text); +} diff --git a/10/museum-self-explanation-system/wake.c b/10/museum-self-explanation-system/wake.c new file mode 100644 index 0000000..b6dc306 --- /dev/null +++ b/10/museum-self-explanation-system/wake.c @@ -0,0 +1,236 @@ +#include "snowboy/snowboy-detect-c-wrapper.h" +#include +#include +#include "record.h" +#include "stt.h" +#include +#include +#include "config.h" +#include "http.h" +#include "tts.h" // 添加这个头文件包含 + +char* app_id = "0a9a80f0-2e03-435a-81a3-e2b402cc8f3a"; + +// 创建会话 +char* create_conversation(char* authtoken) { + char* url = "https://qianfan.baidubce.com/v2/app/conversation"; + char* auth; + asprintf(&auth, "Authorization: Bearer %s", authtoken); + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/json;charset=utf-8"); + headers = curl_slist_append(headers, auth); + + cJSON* obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "app_id", app_id); + char* json = cJSON_Print(obj); + + size_t size = strlen(json); + char* response = http_post(url, headers, json, &size); + cJSON_Delete(obj); + free(json); + free(auth); + curl_slist_free_all(headers); + + if (!response) { + return NULL; + } + + obj = cJSON_ParseWithLength(response, size); + free(response); + + if (!obj) { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return NULL; + } + + cJSON* conversation_id = cJSON_GetObjectItem(obj, "conversation_id"); + if (!conversation_id) { + fprintf(stderr, "conversation_id 字段不存在\n"); + cJSON_Delete(obj); + return NULL; + } + + char* retval = strdup(conversation_id->valuestring); + cJSON_Delete(obj); + return retval; +} + +// 调用对话API +char* chat(char* authtoken, char* conv_id, char* query) { + char* url = "https://qianfan.baidubce.com/v2/app/conversation/runs"; + char* auth; + asprintf(&auth, "Authorization: Bearer %s", authtoken); + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/json;charset=utf-8"); + headers = curl_slist_append(headers, auth); + + cJSON* obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "app_id", app_id); + cJSON_AddStringToObject(obj, "conversation_id", conv_id); + cJSON_AddStringToObject(obj, "query", query); + cJSON_AddBoolToObject(obj, "stream", false); + char* json = cJSON_Print(obj); + + size_t size = strlen(json); + char* response = http_post(url, headers, json, &size); + cJSON_Delete(obj); + free(json); + curl_slist_free_all(headers); + free(auth); + + if (!response) { + return NULL; + } + + obj = cJSON_ParseWithLength(response, size); + free(response); + + if (!obj) { + fprintf(stderr, "解析 JSON 失败: [%s]\n", cJSON_GetErrorPtr()); + return NULL; + } + + cJSON* answer = cJSON_GetObjectItem(obj, "answer"); + if (!answer) { + fprintf(stderr, "answer 字段不存在\n"); + puts(cJSON_Print(obj)); + cJSON_Delete(obj); + return NULL; + } + + char* retval = strdup(answer->valuestring); + cJSON_Delete(obj); + return retval; +} + +int main() { + // 创建snowboy检测器 + SnowboyDetect* detector = SnowboyDetectConstructor("common.res", "model.pmdl"); + if (!detector) { + return EXIT_FAILURE; + } + SnowboyDetectSetSensitivity(detector, "0.6"); + SnowboyDetectSetAudioGain(detector, 1); + + int bits = SnowboyDetectBitsPerSample(detector); + int channels = SnowboyDetectNumChannels(detector); + int rate = SnowboyDetectSampleRate(detector); + + printf("采样深度: %d\n", bits); + printf("声道数量: %d\n", channels); + printf("采样频率: %d\n", rate); + + snd_pcm_uframes_t period = 999; + snd_pcm_t* capture = record_open("hw:0,1", SND_PCM_FORMAT_S16_LE, channels, rate, &period); + if (!capture) { + return EXIT_FAILURE; + } + + char* buffer = malloc(snd_pcm_frames_to_bytes(capture, period)); + if (!buffer) { + perror("malloc"); + record_close(capture); + SnowboyDetectDestructor(detector); + return EXIT_FAILURE; + } + + int recording = 0; + int silence = 0; + FILE* memstream = NULL; + char* audio = NULL; + size_t audio_size = 0; + + // 读取配置文件中的平台密钥 + cJSON* config = read_config("config.json"); + if (!config) { + return EXIT_FAILURE; + } + + cJSON* authtoken = cJSON_GetObjectItem(config, "authtoken"); + if (!authtoken) { + fprintf(stderr, "配置文件错误: 找不到 'authtoken' 字段\n"); + cJSON_Delete(config); + return EXIT_FAILURE; + } + + printf("%s\n", authtoken->valuestring); + + // 创建会话 + char* conv_id = create_conversation(authtoken->valuestring); + if (!conv_id) { + fprintf(stderr, "创建会话失败\n"); + return EXIT_FAILURE; + } + printf("conv_id: %s\n", conv_id); + + while (1) { + snd_pcm_sframes_t frames = snd_pcm_readi(capture, buffer, period); + if (frames < 0) { + fprintf(stderr, "Error from read: %s\n", snd_strerror(frames)); + snd_pcm_recover(capture, frames, 0); + continue; + } + + int status = SnowboyDetectRunDetection(detector, (int16_t*)buffer, snd_pcm_frames_to_bytes(capture, frames) / sizeof(int16_t), 0); + if (status > 0) { + printf("检测到唤醒词,开始录音\n"); + recording = 1; + memstream = open_memstream(&audio, &audio_size); + if (!memstream) { + perror("open_memstream"); + continue; + } + } + + if (recording) { + if (status == -2) { + silence++; + } + + if (status == 0) { + silence = 0; + } + + if (silence > 50) { + printf("停止录音\n"); + recording = 0; + silence = 0; + if (memstream) { + fclose(memstream); + memstream = NULL; + } + + if (audio_size) { + snd_pcm_drop(capture); + char* text = speech_to_text(audio, audio_size); + if (text) { + puts(text); + + // 调用chat函数并打印对话反馈 + char* response = chat(authtoken->valuestring, conv_id, text); + if (response) { + printf("< %s\n", response); + + // 调用text_to_speech函数输出语音 + text_to_speech(response); + + free(response); + } + } + snd_pcm_prepare(capture); + } + } + + if (memstream) { + fwrite(buffer, 1, snd_pcm_frames_to_bytes(capture, frames), memstream); + } + } + } + + free(buffer); + record_close(capture); + SnowboyDetectDestructor(detector); + free(conv_id); + + return EXIT_SUCCESS; +} \ No newline at end of file -- Gitee From e382b11036286d68f6e22c392091585f85dcdd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=80=9D=E4=BC=9F?= <283569385@qq.com> Date: Sun, 7 Jul 2024 07:50:07 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=E4=B8=BB=E8=A6=81=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 程思伟 <283569385@qq.com> --- .../CMakeLists.txt | 41 +++++++++++++++++++ .../CMakePresets.json | 29 +++++++++++++ 10/museum-self-explanation-system/config.json | 6 +++ 10/museum-self-explanation-system/http.h | 9 ++++ 10/museum-self-explanation-system/play.h | 14 +++++++ 10/museum-self-explanation-system/record.h | 16 ++++++++ 10/museum-self-explanation-system/stt.h | 8 ++++ 10/museum-self-explanation-system/token.h | 7 ++++ 10/museum-self-explanation-system/tts.h | 9 ++++ 9 files changed, 139 insertions(+) create mode 100644 10/museum-self-explanation-system/CMakeLists.txt create mode 100644 10/museum-self-explanation-system/CMakePresets.json create mode 100644 10/museum-self-explanation-system/config.json create mode 100644 10/museum-self-explanation-system/http.h create mode 100644 10/museum-self-explanation-system/play.h create mode 100644 10/museum-self-explanation-system/record.h create mode 100644 10/museum-self-explanation-system/stt.h create mode 100644 10/museum-self-explanation-system/token.h create mode 100644 10/museum-self-explanation-system/tts.h diff --git a/10/museum-self-explanation-system/CMakeLists.txt b/10/museum-self-explanation-system/CMakeLists.txt new file mode 100644 index 0000000..6993afa --- /dev/null +++ b/10/museum-self-explanation-system/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.0.0) +project(voice-assistant VERSION 0.1.0 LANGUAGES C) + +# 添加包含目录 +include_directories(${PROJECT_SOURCE_DIR}/cJSON) + +add_executable(voice-assistant main.c) + +add_executable(control control.c) +target_link_libraries(control asound) + +#add_executable(record record.c) +#target_link_libraries(record asound) + +#add_executable(play play.c) +#target_link_libraries(play asound) + +add_executable(key key.c record.c) +target_link_libraries(key gpiod asound) + +add_subdirectory(snowboy) +add_executable(wake wake.c record.c stt.c token.c http.c config.c tts.c play.c) # 包含 tts.c 和 play.c +target_link_libraries(wake snowboy-wrapper snowboy-detect cblas m stdc++ asound curl cjson resolv uuid) # 确保包含 uuid 库 +target_compile_definitions(wake PRIVATE _GNU_SOURCE) + +add_executable(jrsc jrsc.c) +target_link_libraries(jrsc curl cjson) + +add_executable(led led.c) + +add_executable(chat chat.c config.c http.c) +target_link_libraries(chat cjson curl) +target_compile_definitions(chat PRIVATE _GNU_SOURCE) + +add_executable(utils utils.c) +target_link_libraries(utils uuid resolv) + +# 删除独立的 tts 可执行文件 +#add_executable(tts tts.c config.c http.c play.c) +#target_link_libraries(tts cjson curl uuid resolv asound) +#target_compile_definitions(tts PRIVATE _GNU_SOURCE) diff --git a/10/museum-self-explanation-system/CMakePresets.json b/10/museum-self-explanation-system/CMakePresets.json new file mode 100644 index 0000000..9cc6240 --- /dev/null +++ b/10/museum-self-explanation-system/CMakePresets.json @@ -0,0 +1,29 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "native", + "displayName": "GCC 10.2.1 x86_64-linux-gnu", + "description": "使用编译器: C = /usr/bin/gcc, CXX = /usr/bin/g++", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_C_COMPILER": "/usr/bin/gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/g++", + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "cross", + "displayName": "GCC 10.2.1 arm-linux-gnueabihf", + "description": "使用编译器: C = /usr/bin/arm-linux-gnueabihf-gcc, CXX = /usr/bin/arm-linux-gnueabihf-g++", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_C_COMPILER": "/usr/bin/arm-linux-gnueabihf-gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/arm-linux-gnueabihf-g++", + "CMAKE_BUILD_TYPE": "Debug" + } + } + ] +} \ No newline at end of file diff --git a/10/museum-self-explanation-system/config.json b/10/museum-self-explanation-system/config.json new file mode 100644 index 0000000..ea8d307 --- /dev/null +++ b/10/museum-self-explanation-system/config.json @@ -0,0 +1,6 @@ +{ + "api_key":"xBhaoJaQiAJNP1InjLD95Dbw", + "secret_key":"dNQ1Zb0eVR0bcmLyPX5ZhbRo7XN4Z1Nr", + "authtoken":"bce-v3/ALTAK-us37Ej0Da5JjK9xLj1xT0/db05d17d9c0037eb163fae3715259da36685f036", + "ttstoken":"Xz0ai7FZrTvdApMpi_OTSdnJiQFa2Eg3" +} \ No newline at end of file diff --git a/10/museum-self-explanation-system/http.h b/10/museum-self-explanation-system/http.h new file mode 100644 index 0000000..a3c8b73 --- /dev/null +++ b/10/museum-self-explanation-system/http.h @@ -0,0 +1,9 @@ +#ifndef HTTP_H +#define HTTP_H + +#include + +char* http_get(char* url, struct curl_slist* headers, size_t* psize); +char* http_post(char* url, struct curl_slist* headers, char* data, size_t* psize); + +#endif diff --git a/10/museum-self-explanation-system/play.h b/10/museum-self-explanation-system/play.h new file mode 100644 index 0000000..5f67f8f --- /dev/null +++ b/10/museum-self-explanation-system/play.h @@ -0,0 +1,14 @@ +#ifndef PLAY_H +#define PLAY_H + +#include + +snd_pcm_t* play_open(const char* name, + snd_pcm_format_t format, + unsigned int channel, + unsigned int rate, + snd_pcm_uframes_t* period); +//停止播放 +void play_close(snd_pcm_t* playback); + +#endif diff --git a/10/museum-self-explanation-system/record.h b/10/museum-self-explanation-system/record.h new file mode 100644 index 0000000..3c48d69 --- /dev/null +++ b/10/museum-self-explanation-system/record.h @@ -0,0 +1,16 @@ +#ifndef RECORD_H +#define RECORD_H + +#include + +//开始录音 +snd_pcm_t* record_open(const char* name, + snd_pcm_format_t format, + unsigned int channel, + unsigned int rate, + snd_pcm_uframes_t* period); + +//停止录音 +void record_close(snd_pcm_t* capture); + +#endif diff --git a/10/museum-self-explanation-system/stt.h b/10/museum-self-explanation-system/stt.h new file mode 100644 index 0000000..81c0a30 --- /dev/null +++ b/10/museum-self-explanation-system/stt.h @@ -0,0 +1,8 @@ +#ifndef STT_H +#define STT_H + +#include + +char* speech_to_text(char* audio, size_t size); + +#endif diff --git a/10/museum-self-explanation-system/token.h b/10/museum-self-explanation-system/token.h new file mode 100644 index 0000000..5b2442b --- /dev/null +++ b/10/museum-self-explanation-system/token.h @@ -0,0 +1,7 @@ +#ifndef TOKEN_H +#define TOKEN_H + +//成功返回access token,失败返回NULL +char *get_access_token(const char *ak, const char *sk); + +#endif diff --git a/10/museum-self-explanation-system/tts.h b/10/museum-self-explanation-system/tts.h new file mode 100644 index 0000000..4b04781 --- /dev/null +++ b/10/museum-self-explanation-system/tts.h @@ -0,0 +1,9 @@ +// tts.h + +#ifndef TTS_H +#define TTS_H + +// 声明 text_to_speech 函数 +void text_to_speech(char* text); + +#endif // TTS_H -- Gitee