FAAD2解码AAC得到PCM数据帧采用Microsoft.DirectX.DirectSound播放时有嘟嘟声、噪音的问题

本文主要是介绍FAAD2解码AAC得到PCM数据帧采用Microsoft.DirectX.DirectSound播放时有嘟嘟声、噪音的问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

DirectX(Direct eXtension,简称DX)是由微软公司创建的多媒体编程接口,是一种应用程序接口(API)

问题描述:

1.PC端产测软件,通过P2P接收到设备发送过来的音频数据帧(AAC,16KHZ,16bit位宽、单通道),使用faad/faad2解码库解码后的音频帧,

播放出来有频率很快的一直嘟嘟嘟的声音,听起来断断续续(不知道怎么描述)

2.关于faad解码后的数据总是双通道的问题,见我上一篇博客(https://blog.csdn.net/spy_007_/article/details/109177862),不过最后我是直接

使用解码出来的双通道PCM数据进行播放的(该篇后续的整个过程都是使用的双通道数据,读者如果使用单通道数据进行播放可以结合上一篇博客进行修改)

 

问题解决:

将PCM数据先提交给DirectX底层接口播放时(waveOutPrepareHeader),提交完之后就会返回,并不是等到本次提交的数据完全播放结束才会返回,

也就是说传入的数据buf,A:我们上层并不能立马释放,B:也不能就只使用一块BUF循环接收数据,否则,底层播放的数据就会遭到破坏,声音异常。

解决办法就是多申请几块buf,让循环使用:

 

注意:上图函数OnWriteSoundData接收的PCM数据buf 每次都是不同的,上层申请了15个buf,依次循环使用,如下:

 

 源码如下:

#pragma once#include "pch.h"
#include "hi_voice_api.h"//标志使用哪一种解码方式
#define AUDIO_DECODE_USE_AAC	1
#define AUDIO_DECODE_USE_G711	0typedef struct _AudioFrame
{char*pcm;int pcm_len;int pts;}AudioFrame;#if AUDIO_DECODE_USE_AAC
#include "faad.h"
class AudioDecode_AAC
{
public:NeAACDecHandle decoder = NULL;
public:AudioDecode_AAC(){}~AudioDecode_AAC(){}//AAC解码器初始化,需要传入一帧数据帧(带ADTS帧头),作为初始化的入参long AudioDecode_AAC_Init(unsigned char *frame,unsigned long size,unsigned long *samplerate,unsigned char *channels);int AudioDecode_AAC_Exit();void* AudioDecode_AAC_Decode(NeAACDecFrameInfo *hInfo,unsigned char *buffer,unsigned long buffer_size);};#endif#if AUDIO_DECODE_USE_G711class AudioDecode_g711
{private:hiVOICE_G711_STATE_S vgs;bool ready;
public:AudioDecode_g711():ready(false){}~AudioDecode_g711(){}int Create();int Decode(void* buf, int len, int pts, int audType, AudioFrame*af);int malloc_buf(int pcm_len, AudioFrame*ret_buf);int free_buf(AudioFrame*buf);int Destroy();};#endif
#include "pch.h"
#include "AudioDecode.h"
#include "typeport.h"#if AUDIO_DECODE_USE_AAC#define MAX_CHANNELS 2static int adts_sample_rates[] = { 96000,88200,64000,48000,44100,32000,24000,22050,16000,12000,11025,8000,7350,0,0,0 };//用于接收AAC解码出来的pcm数据:
#define MAX_PCM_BUF_NUM	(15)		
#define ONE_PCM_BUF_LEN	(2048*2)
static char* pcm_buf[MAX_PCM_BUF_NUM] = {0};
static int pb_producer_index = 0; //生产者使用的索引号long AudioDecode_AAC::AudioDecode_AAC_Init(unsigned char *frame,unsigned long size,unsigned long *samplerate,unsigned char *channels)
{if (!decoder){//初始化PCM接收Buf:int i;for (i=0;i< MAX_PCM_BUF_NUM;i++){pcm_buf[i] = (char*)malloc(ONE_PCM_BUF_LEN);if (!pcm_buf[i]){printf("malloc failed!");return -1;}memset(pcm_buf[i],0, ONE_PCM_BUF_LEN);}//open decoderdecoder = NeAACDecOpen();NeAACDecConfigurationPtr conf = NeAACDecGetCurrentConfiguration(decoder);conf->defObjectType = LC;conf->defSampleRate = 8000;//8000; //real samplerate/2conf->outputFormat = FAAD_FMT_16BIT; //conf->downMatrix = 0; //不进行自动扩展到双通道 ???conf->useOldADTSFormat = 0; //ADTS长度为0:56bit(1代表是58bit)conf->dontUpSampleImplicitSBR = 1;NeAACDecSetConfiguration(decoder, conf);//initialize decoderreturn NeAACDecInit(decoder, frame,size, samplerate, channels);}else{ERROR_LOG("AAC decoder already inited!\n");return -1;}}int AudioDecode_AAC::AudioDecode_AAC_Exit()
{int i;for (i = 0; i < MAX_PCM_BUF_NUM; i++){free(pcm_buf[i]);pcm_buf[i] = NULL;}NeAACDecClose(decoder);decoder = NULL;return 0;
}/*** fetch one ADTS frame*/
int check_ADTS_len(unsigned char* buffer, size_t buf_size)
{size_t size = 0;if (!buffer){perror("illegall parameter!\n");return -1;}if (buf_size < 7){perror("illegall parameter!\n");return -1;}if ((buffer[0] == 0xff) && ((buffer[1] & 0xf0) == 0xf0)){// profile; 2 uimsbf// sampling_frequency_index; 4 uimsbf// private_bit; 1 bslbf// channel_configuration; 3 uimsbf// original/copy; 1 bslbf// home; 1 bslbf// copyright_identification_bit; 1 bslbf// copyright_identification_start; 1 bslbf// frame_length; 13 bslbfsize |= (((buffer[3] & 0x03)) << 11);//high 2 bitsize |= (buffer[4] << 3);//middle 8 bitsize |= ((buffer[5] & 0xe0) >> 5);//low 3bit//printf("len1=%x\n", (buffer[3] & 0x03));//printf("len2=%x\n", buffer[4]);//printf("len3=%x\n", (buffer[5] & 0xe0) >> 5);//printf("get_one_ADTS_frame buf_size(%d) parse ADTS-->size(%d)\n", buf_size,(int)size);}//int samplerate = adts_sample_rates[(buffer[2] & 0x3c) >> 2]; //解析ADTS中的采样率信息//printf("samplerate = %d\n", samplerate); //16000if (buf_size != size){printf("parse ADTS : buf_size(%d) != size(%d)\n", buf_size, size);return -1;}return 0;
}unsigned int parse_ADTS_len(unsigned char* buffer)
{size_t size = 0;if (!buffer){perror("illegall parameter!\n");return -1;}if ((buffer[0] == 0xff) && ((buffer[1] & 0xf0) == 0xf0)){// profile; 2 uimsbf// sampling_frequency_index; 4 uimsbf// private_bit; 1 bslbf// channel_configuration; 3 uimsbf// original/copy; 1 bslbf// home; 1 bslbf// copyright_identification_bit; 1 bslbf// copyright_identification_start; 1 bslbf// frame_length; 13 bslbfsize |= (((buffer[3] & 0x03)) << 11);//high 2 bitsize |= (buffer[4] << 3);//middle 8 bitsize |= ((buffer[5] & 0xe0) >> 5);//low 3bit//printf("len1=%x\n", (buffer[3] & 0x03));//printf("len2=%x\n", buffer[4]);//printf("len3=%x\n", (buffer[5] & 0xe0) >> 5);printf(" parse ADTS-->size(%d)\n", (int)size);}else{return -1;}//int samplerate = adts_sample_rates[(buffer[2] & 0x3c) >> 2]; //解析ADTS中的采样率信息//printf("samplerate = %d\n", samplerate); //16000return 0;
}/************ 左右声道合并* data:出入的待处理的数据* len:传入数据的长度* right_left:0,合并到左声道*             1,合并到右声道*********/
int my_audio_digital_Channel_merging_add(void *data, unsigned int  len, unsigned char right_left)
{if (data == NULL) {return -1;}int  valuetemp;short *buf;buf = (short *)data;len >>= 1; //byte to pointfor (unsigned int  i = 0; i < len; i += 2) {valuetemp = (buf[i] + buf[i + 1]);//防止16位数据溢出if (valuetemp < -32768) {valuetemp = -32768;}else if (valuetemp > 32767) {valuetemp = 32767;}//或者也可以全部数据除2//valuetemp = valuetemp / 2;buf[i + right_left] = (short)valuetemp;buf[i + 1 - right_left] = (short)0;}return 0;}//成功:返回解码得到的PCM数据指针 ; 失败 :NULL
//frame_info:返回帧信息参数void* AudioDecode_AAC::AudioDecode_AAC_Decode(NeAACDecFrameInfo *frame_info,unsigned char *frame,unsigned long size)
{check_ADTS_len(frame,size);/*----进行解码操作----------------------------------------*///解析下一帧数据长度char* return_pcm_data = NULL;void* pcm_data = NeAACDecDecode(decoder, frame_info, frame, size);//printf(" frame_info->samples = %d frame_info->channels = %d\n", frame_info->samples,frame_info->channels);//初始化中dontUpSampleImplicitSBR = 1时返回2048;dontUpSampleImplicitSBR=0时返回4096if (size != frame_info->bytesconsumed)//每次传入一帧数据,这两个值每次都相等{printf("error!$$$$$$$$$$$$$$$$$$$$$$$$$size(%d) frame_info->bytesconsumed(%d)\n", size, frame_info->bytesconsumed);}if (frame_info->error > 0){printf("error!$$$$$$$$$ %s\n", NeAACDecGetErrorMessage(frame_info->error));}else if (pcm_data && frame_info->samples > 0){#if 1	//直接返回双通道的数据//对数据进行备份到缓存bufif (frame_info->samples * sizeof(short) > ONE_PCM_BUF_LEN){printf("PCM buf overflow!!!!\n");return NULL;}memset(pcm_buf[pb_producer_index],0, ONE_PCM_BUF_LEN);memcpy(pcm_buf[pb_producer_index], pcm_data, frame_info->samples*sizeof(short));return_pcm_data = pcm_buf[pb_producer_index];pb_producer_index++;if (pb_producer_index >= MAX_PCM_BUF_NUM){pb_producer_index = 0;}#else	//转换成单通道数据(faad解码总是强制性变成双通道输出)if (frame_info->channels == 2) //双通道数据转换成单通道{if (frame_info->samples/2 * sizeof(short) > ONE_PCM_BUF_LEN){printf("PCM buf overflow!!!!\n");return NULL;}memset(pcm_buf[pb_producer_index], 0, ONE_PCM_BUF_LEN);return_pcm_data = pcm_buf[pb_producer_index];pb_producer_index++;if (pb_producer_index >= MAX_PCM_BUF_NUM){pb_producer_index = 0;}//从双声道的数据中提取单通道 int i, j;for (i = 0, j = 0; i < 4096 && j < 2048; i += 4, j += 2){//每次拷贝2字节数据到frame_mono(16bit位宽,即每个通道数据一个采样2字节)return_pcm_data[j] = ((char*)pcm_data)[i];return_pcm_data[j + 1] = ((char*)pcm_data)[i + 1];}frame_info->samples = frame_info->samples/2;//1024; //只留下单通道数据frame_info->channels = 1;}#endif	return (void*)return_pcm_data;}return NULL;
}#endif#if AUDIO_DECODE_USE_G711
int AudioDecode_g711::Create()
{int ret = HI_VOICE_DecReset(&vgs, G711_A);if (HI_SUCCESS != ret){ERROR_LOG("HI_VOICE_DecReset fail: %#x\n", ret);return -1;} ready = true;return 0;
}int AudioDecode_g711::malloc_buf(int pcm_len, AudioFrame*ret_buf)
{ret_buf->pcm_len = pcm_len;ret_buf->pcm = (char*)malloc(pcm_len);if (!ret_buf->pcm){ERROR_LOG("malloc failed!\n");return -1;}return 0;
}int AudioDecode_g711::free_buf(AudioFrame*buf)
{if (buf){free(buf->pcm);buf->pcm = NULL;}return 0;
}int AudioDecode_g711::Decode(void* buf, int len, int pts, int audType, AudioFrame*af)
{if (NULL == af){return -1;}audType = audType;if (!ready) return -1;char pcm[1024];HI_S16 l = len / 2;int ret = HI_VOICE_DecodeFrame(&vgs, (HI_S16*)buf, (HI_S16*)pcm, &l);if (HI_SUCCESS == ret){af->pcm = (char*)malloc(l * 2);if (malloc_buf(l * 2, af) < 0){return -1;}memcpy(af->pcm, pcm, l * 2);af->pts = pts;return 0;}return -1;
}int AudioDecode_g711::Destroy()
{ready = false;return 0;
}#endif 
#pragma once#include<mmsystem.h>
#include<mmreg.h>
#pragma  comment(lib, "winmm.lib")#define WM_PLAYSOUND_STARTPLAYING	WM_USER+600
#define WM_PLAYSOUND_STOPPLAYING	WM_USER+601
#define WM_PLAYSOUND_PLAYBLOCK		WM_USER+602
#define WM_PLAYSOUND_ENDTHREAD		WM_USER+603#define MAX_PCM_LPHDR_NUM 15// CPlaySound
class CPlaySound : public CWinThread
{DECLARE_DYNCREATE(CPlaySound)
public:CPlaySound();~CPlaySound();virtual BOOL InitInstance();virtual int ExitInstance();
private:void displayError(int code, char mesg[]);WAVEFORMATEX		m_WaveFormatEx;BOOL				m_IsPlaying;HWAVEOUT			m_hPlay;CStdioFile			m_PlayLog;WAVEHDR pcm_lpHdr[MAX_PCM_LPHDR_NUM] = {0};	//用于接收帧的缓存buf数组int cur_pcm_lpHdr_index = 0;	//当前用于接收传入数据的buf下标
protected:afx_msg void OnStartPlaying(WPARAM wParam, LPARAM lParam);afx_msg void OnStopPlaying(WPARAM wParam, LPARAM lParam);afx_msg void OnEndPlaySoundData(WPARAM wParam, LPARAM lParam);afx_msg void OnWriteSoundData(WPARAM wParam, LPARAM lParam);afx_msg void OnEndThread(WPARAM wParam, LPARAM lParam);DECLARE_MESSAGE_MAP()
};
// PlaySound.cpp : 实现文件
//#include "pch.h"
#include "PlaySound.h"
#include "typeport.h"// CPlaySoundIMPLEMENT_DYNCREATE(CPlaySound, CWinThread)CPlaySound::CPlaySound()
{//打开播放日志m_PlayLog.Open(TEXT("playsound.log"), CFile::modeCreate | CFile::modeWrite);m_PlayLog.WriteString(TEXT("\n In the constructor of Play sound"));//初始化音频格式结构体memset(&m_WaveFormatEx, 0, sizeof(m_WaveFormatEx));m_WaveFormatEx.wFormatTag = WAVE_FORMAT_PCM;m_WaveFormatEx.nChannels = 2;//2;//1;m_WaveFormatEx.wBitsPerSample = 16;//8;m_WaveFormatEx.nSamplesPerSec = 16000;//16000; //16000;//8000;m_WaveFormatEx.nBlockAlign = m_WaveFormatEx.nChannels * m_WaveFormatEx.wBitsPerSample / 8;m_WaveFormatEx.nAvgBytesPerSec = m_WaveFormatEx.nSamplesPerSec * m_WaveFormatEx.nBlockAlign;	//8000;m_WaveFormatEx.cbSize = 0;m_IsPlaying = FALSE;
}CPlaySound::~CPlaySound()
{}BOOL CPlaySound::InitInstance()
{// TODO: 在此执行任意逐线程初始化return TRUE;
}int CPlaySound::ExitInstance()
{// TODO: 在此执行任意逐线程清理return CWinThread::ExitInstance();
}void CPlaySound::OnStartPlaying(WPARAM wParam, LPARAM lParam)
{MMRESULT mmReturn = 0;if (m_IsPlaying) //已经开始播放则直接返回return; //FALSE;m_PlayLog.WriteString(TEXT("\n Starting playing"));//打开音频输出设备mmReturn = ::waveOutOpen(&m_hPlay, WAVE_MAPPER,&m_WaveFormatEx, ::GetCurrentThreadId(), 0, CALLBACK_THREAD);if (mmReturn) //打开设备失败{DEBUG_LOG("audio waveOutOpen failed!\n");displayError(mmReturn, "PlayStart");}else{m_IsPlaying = TRUE;DWORD volume = 0xffffffff;waveOutSetVolume(m_hPlay, volume);//设置输出设备的输出量}
}void CPlaySound::displayError(int code, char mesg[])
{TCHAR errorbuffer[MAX_PATH];TCHAR errorbuffer1[MAX_PATH];waveOutGetErrorText(code, errorbuffer, MAX_PATH);wsprintf(errorbuffer1, TEXT("PLAY : %s :%x:%s"), mesg, code, errorbuffer);AfxMessageBox(errorbuffer1);
}/*
6、结束输出前先用waveOutReset重置输出设备,重置能够使输出设备全部buffer输出结束,
所以在waveOutReset后要延迟一段时间,然后调用waveOutClose关闭设备。
*/
void CPlaySound::OnStopPlaying(WPARAM wParam, LPARAM lParam)
{MMRESULT mmReturn = 0;if (m_IsPlaying == FALSE)return;// FALSE;//m_PlayLog.WriteString(TEXT("\n Stopped  playing"));DEBUG_LOG("Audio Stopped  playing !\n");mmReturn = ::waveOutReset(m_hPlay);//重置输出设备,重置能够使输出设备全部buffer输出结束if (!mmReturn){m_IsPlaying = FALSE;Sleep(300); //等待所有buffer输出完成mmReturn = ::waveOutClose(m_hPlay);//关闭设备}
}/*5、当提交给设备的数据输出结束,设备会发送一条MM_WOM_DONE消息反馈给设备,
设备应该用waveOutUnprepareHeader将提交给设备输出的数据清除。
*/
void CPlaySound::OnEndPlaySoundData(WPARAM wParam, LPARAM lParam)
{LPWAVEHDR lpHdr = (LPWAVEHDR)lParam;if (lpHdr){::waveOutUnprepareHeader(m_hPlay, lpHdr, sizeof(WAVEHDR));//音频输出结束,清空buffer}return;//ERROR_SUCCESS;
}void CPlaySound::OnWriteSoundData(WPARAM wParam, LPARAM lParam)
{MMRESULT mmResult = 0;if (m_IsPlaying == FALSE){ERROR_LOG("m_IsPlaying == FALSE");return; //FALSE;}//m_PlayLog.WriteString(TEXT("\nplaying sound data...."));//DEBUG_LOG("playing sound data.... length(%d)\n", length);// Prepare wave header for playing WAVEHDR *lpHdr = &pcm_lpHdr[cur_pcm_lpHdr_index];cur_pcm_lpHdr_index++;if (cur_pcm_lpHdr_index >= MAX_PCM_LPHDR_NUM){cur_pcm_lpHdr_index = 0;}memset(lpHdr, 0, sizeof(WAVEHDR));lpHdr->lpData = (char *)lParam;lpHdr->dwBufferLength = (int)wParam;//printf("lpHdr->dwBufferLength = %d\n", lpHdr->dwBufferLength);//将要输出的数据写入buffermmResult = ::waveOutPrepareHeader(m_hPlay, lpHdr, sizeof(WAVEHDR));if (mmResult){m_PlayLog.WriteString(TEXT("\nError while preparing header"));ERROR_LOG("Error while preparing header\n");return;//ERROR_SUCCESS;}//将输出数据发送给输出设备mmResult = ::waveOutWrite(m_hPlay, lpHdr, sizeof(WAVEHDR));if (mmResult){ERROR_LOG("Error while writing to device");m_PlayLog.WriteString(TEXT("\nError while writing to device"));return;//ERROR_SUCCESS;				}return;//ERROR_SUCCESS;
}void CPlaySound::OnEndThread(WPARAM wParam, LPARAM lParam)
{// If already playing then stop it...if (m_IsPlaying)OnStopPlaying(0, 0);m_PlayLog.WriteString(TEXT("\nEnding the play device"));DEBUG_LOG("Audio Ending the play device\n");// Quit this thread...::PostQuitMessage(0);return;//TRUE;
}BEGIN_MESSAGE_MAP(CPlaySound, CWinThread)ON_THREAD_MESSAGE(WM_PLAYSOUND_STARTPLAYING, OnStartPlaying)ON_THREAD_MESSAGE(WM_PLAYSOUND_STOPPLAYING, OnStopPlaying)ON_THREAD_MESSAGE(WM_PLAYSOUND_PLAYBLOCK, OnWriteSoundData)ON_THREAD_MESSAGE(MM_WOM_DONE, OnEndPlaySoundData)ON_THREAD_MESSAGE(WM_PLAYSOUND_ENDTHREAD, OnEndThread)
END_MESSAGE_MAP()// CPlaySound 消息处理程序

 

这篇关于FAAD2解码AAC得到PCM数据帧采用Microsoft.DirectX.DirectSound播放时有嘟嘟声、噪音的问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/280671

相关文章

Python获取中国节假日数据记录入JSON文件

《Python获取中国节假日数据记录入JSON文件》项目系统内置的日历应用为了提升用户体验,特别设置了在调休日期显示“休”的UI图标功能,那么问题是这些调休数据从哪里来呢?我尝试一种更为智能的方法:P... 目录节假日数据获取存入jsON文件节假日数据读取封装完整代码项目系统内置的日历应用为了提升用户体验,

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Python使用自带的base64库进行base64编码和解码

《Python使用自带的base64库进行base64编码和解码》在Python中,处理数据的编码和解码是数据传输和存储中非常普遍的需求,其中,Base64是一种常用的编码方案,本文我将详细介绍如何使... 目录引言使用python的base64库进行编码和解码编码函数解码函数Base64编码的应用场景注意

用js控制视频播放进度基本示例代码

《用js控制视频播放进度基本示例代码》写前端的时候,很多的时候是需要支持要网页视频播放的功能,下面这篇文章主要给大家介绍了关于用js控制视频播放进度的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言html部分:JavaScript部分:注意:总结前言在javascript中控制视频播放

Java利用JSONPath操作JSON数据的技术指南

《Java利用JSONPath操作JSON数据的技术指南》JSONPath是一种强大的工具,用于查询和操作JSON数据,类似于SQL的语法,它为处理复杂的JSON数据结构提供了简单且高效... 目录1、简述2、什么是 jsONPath?3、Java 示例3.1 基本查询3.2 过滤查询3.3 递归搜索3.4

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

Python Dash框架在数据可视化仪表板中的应用与实践记录

《PythonDash框架在数据可视化仪表板中的应用与实践记录》Python的PlotlyDash库提供了一种简便且强大的方式来构建和展示互动式数据仪表板,本篇文章将深入探讨如何使用Dash设计一... 目录python Dash框架在数据可视化仪表板中的应用与实践1. 什么是Plotly Dash?1.1