diff options
| author | 2009-11-30 17:30:08 +0000 | |
|---|---|---|
| committer | 2009-11-30 17:30:08 +0000 | |
| commit | 5e3ada86ffaee845bd6e7ec0a0ee5797ea1f1fe3 (patch) | |
| tree | 9dd8b10377b2ee88059ae07526e465aa7aa7c66e | |
| parent | fix for #624 (diff) | |
| download | KVIrc-5e3ada86ffaee845bd6e7ec0a0ee5797ea1f1fe3.tar.gz KVIrc-5e3ada86ffaee845bd6e7ec0a0ee5797ea1f1fe3.tar.bz2 KVIrc-5e3ada86ffaee845bd6e7ec0a0ee5797ea1f1fe3.zip | |
dcc video: added experimental ogg/theora support. Open Problems:
- audio sync still missing
- Slow as hell, since yuv<->rgb conversion is done in software
git-svn-id: https://svn.kvirc.de/svn/trunk/kvirc@3641 17fca916-40b9-46aa-a4ea-0a15b648b75c
| -rw-r--r-- | CMakeLists.txt | 27 | ||||
| -rw-r--r-- | cmake/modules/FindTheora.cmake | 42 | ||||
| -rw-r--r-- | src/kvilib/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/kvilib/ext/avdevice/videodevice.cpp | 2 | ||||
| -rw-r--r-- | src/kvilib/ext/kvi_oggtheora.cpp | 961 | ||||
| -rw-r--r-- | src/kvilib/ext/kvi_oggtheora.h | 183 | ||||
| -rw-r--r-- | src/modules/dcc/codec.cpp | 71 | ||||
| -rw-r--r-- | src/modules/dcc/codec.h | 17 | ||||
| -rw-r--r-- | src/modules/dcc/video.cpp | 425 | ||||
| -rw-r--r-- | src/modules/dcc/video.h | 19 |
10 files changed, 1682 insertions, 67 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fa4cc553..dfee0de2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -828,6 +828,32 @@ ELSE() ENDIF() ############################################################################ +# Ogg/Vorbis and Ogg/Theora support +############################################################################ +OPTION(WANT_OGG_THEORA "Compile Ogg/Vorbis+Theora Support" OFF) +IF(WITH_OGG_THEORA) + SET(WANT_OGG_THEORA ON) +ENDIF() + +FIND_PACKAGE(Theora) + +IF(WANT_OGG_THEORA) + IF(THEORA_FOUND) + SET(CMAKE_STATUS_OGG_THEORA_SUPPORT "Yes") + LIST(APPEND LIBS ${THEORA_LIBRARY}) + INCLUDE_DIRECTORIES(${THEORA_INCLUDE_DIR}) + ELSE() + SET(COMPILE_DISABLE_OGG_THEORA 1) + LIST(APPEND CMAKE_KVIRC_BUILD_FLAGS WITHOUT_OGG_THEORA) + SET(CMAKE_STATUS_OGG_THEORA_SUPPORT "No") + ENDIF() +ELSE() + SET(COMPILE_DISABLE_OGG_THEORA 1) + LIST(APPEND CMAKE_KVIRC_BUILD_FLAGS WITHOUT_OGG_THEORA) + SET(CMAKE_STATUS_OGG_THEORA_SUPPORT "User Disabled") +ENDIF() + +############################################################################ # ix86 asm support ############################################################################ OPTION(WANT_IX86_ASM "Compile ix86 (NOT x86_64!) asm Support" OFF) @@ -1283,6 +1309,7 @@ MESSAGE(STATUS " System memmove Support : ${CMAKE_STATUS_SYSTEM_MEMMOVE_SU MESSAGE(STATUS " GSM Support : ${CMAKE_STATUS_GSM_SUPPORT}") MESSAGE(STATUS " DCC Voice Support : ${CMAKE_STATUS_DCC_VOICE_SUPPORT}") MESSAGE(STATUS " DCC Video Support : ${CMAKE_STATUS_DCC_VIDEO_SUPPORT}") +MESSAGE(STATUS " Ogg/Theora Support : ${CMAKE_STATUS_OGG_THEORA_SUPPORT}") MESSAGE(STATUS " Apple Universal Binary : ${CMAKE_STATUS_BUILD_UNIVERSAL_BINARY}") MESSAGE(STATUS " ix86 asm Support : ${CMAKE_STATUS_ix86_ASM_SUPPORT}") MESSAGE(STATUS " Memory Profile Support : ${CMAKE_STATUS_MEMORY_PROFILE_SUPPORT}") diff --git a/cmake/modules/FindTheora.cmake b/cmake/modules/FindTheora.cmake new file mode 100644 index 000000000..b83a477db --- /dev/null +++ b/cmake/modules/FindTheora.cmake @@ -0,0 +1,42 @@ +# Taken from Kde4 Multimedia source files. Modified to use the new theora 1.1 api +# by Fabio Bas (ctrlaltca at libero dot it). +# You can find a copy of the mentioned COPYING-CMAKE-SCRIPTS file here: +# http://websvn.kde.org/trunk/KDE/kdemultimedia/cmake/modules/ +# Original copyright header follows: + +# Option for build or not Theora + +# Copyright (c) 2006,2007 Laurent Montel, <montel@kde.org> +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +IF(THEORA_INCLUDE_DIR AND THEORA_LIB_LIBRARIES_ENC AND THEORA_LIB_LIBRARIES_DEC AND THEORA_VORBIS_LIBRARIES AND THEORA_OGG_LIBRARIES) + # Already in cache, be silent + set(Theora_FIND_QUIETLY TRUE) +ENDIF() + +FIND_PATH(THEORA_INCLUDE_DIR theora/theoraenc.h) +FIND_LIBRARY(THEORA_OGG_LIBRARIES NAMES ogg ) +FIND_LIBRARY(THEORA_VORBIS_LIBRARIES NAMES vorbis) +FIND_LIBRARY(THEORA_VORBIS_LIBRARIES_ENC NAMES vorbisenc) +FIND_LIBRARY(THEORA_LIB_LIBRARIES_ENC NAMES theoraenc) +FIND_LIBRARY(THEORA_LIB_LIBRARIES_DEC NAMES theoradec) + +IF(THEORA_LIB_LIBRARIES AND THEORA_VORBIS_LIBRARIES AND THEORA_VORBIS_LIBRARIES_ENC AND THEORA_LIB_LIBRARIES_ENC AND THEORA_LIB_LIBRARIES_DEC AND THEORA_INCLUDE_DIR) + set(THEORA_LIBRARY ${THEORA_LIB_LIBRARIES_ENC} ${THEORA_LIB_LIBRARIES_DEC} ${THEORA_OGG_LIBRARIES} ${THEORA_VORBIS_LIBRARIES} ${THEORA_VORBIS_LIBRARIES_ENC}) + set(THEORA_FOUND TRUE) +ENDIF() + +IF(THEORA_FOUND) + IF(NOT Theora_FIND_QUIETLY) + MESSAGE(STATUS "Found theora: includes in ${THEORA_INCLUDE_DIR}, library in ${THEORA_LIBRARY}") + ENDIF() +ELSE() + IF(Theora_FIND_REQUIRED) + MESSAGE(STATUS "Theora not found") + ENDIF() +ENDIF() + +MARK_AS_ADVANCED(THEORA_INCLUDE_DIR THEORA_LIBRARY) diff --git a/src/kvilib/CMakeLists.txt b/src/kvilib/CMakeLists.txt index 57eb48521..4e5cbe38a 100644 --- a/src/kvilib/CMakeLists.txt +++ b/src/kvilib/CMakeLists.txt @@ -17,6 +17,7 @@ SET(kvilib_MOC_HDRS ext/kvi_animatedpixmap.h ext/kvi_animatedpixmapcache.h ext/kvi_crypt.h + ext/kvi_oggtheora.h ext/kvi_regusersdb.h ext/kvi_sharedfiles.h irc/kvi_ircuserdb.h @@ -73,6 +74,7 @@ SET(kvilib_SRCS ext/kvi_mediatype.cpp ext/kvi_miscutils.cpp ext/kvi_msgtype.cpp + ext/kvi_oggtheora.cpp ext/kvi_osinfo.cpp ext/kvi_pixmap.cpp ext/kvi_animatedpixmap.cpp diff --git a/src/kvilib/ext/avdevice/videodevice.cpp b/src/kvilib/ext/avdevice/videodevice.cpp index 941ea2a7b..d25b05b78 100644 --- a/src/kvilib/ext/avdevice/videodevice.cpp +++ b/src/kvilib/ext/avdevice/videodevice.cpp @@ -1184,7 +1184,7 @@ int VideoDevice::getImage(QImage *qimage) } uchar *bits=qimage->bits(); -// qDebug() << "Capturing in " << pixelFormatName(m_currentbuffer.pixelformat); +//qDebug() << "Capturing in " << pixelFormatName(m_currentbuffer.pixelformat); switch(m_currentbuffer.pixelformat) { case PIXELFORMAT_NONE : break; diff --git a/src/kvilib/ext/kvi_oggtheora.cpp b/src/kvilib/ext/kvi_oggtheora.cpp new file mode 100644 index 000000000..eb0fcb86b --- /dev/null +++ b/src/kvilib/ext/kvi_oggtheora.cpp @@ -0,0 +1,961 @@ +//============================================================================= +// +// File : kvi_oggtheora.cpp +// Creation date : Sat Nov 21 2009 22:53:21 CEST by Fabio Bas +// +// This file is part of the KVirc irc client distribution +// Copyright (C) 2009 Fabio Bas (ctrlaltca at libero dot it) +// +// This program is FREE software. You can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your opinion) any later version. +// +// This program is distributed in the HOPE that it will be USEFUL, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// Based on the example encoder/player from Xiph.Org Foundation: +//============================================================================= +// +// function: example encoder application; makes an Ogg Theora/Vorbis +// file from YUV4MPEG2 and WAV input +// last mod: $Id: encoder_example.c 16517 2009-08-25 19:48:57Z giles $ +// +//============================================================================= +// +// function: example SDL player application; plays Ogg Theora files (with +// optional Vorbis audio second stream) +// last mod: $Id: player_example.c 16628 2009-10-10 05:50:52Z gmaxwell $ +// +//============================================================================= + +#include <string.h> +#include <stdlib.h> +#include <time.h> +#include <sys/time.h> +#include <math.h> + +#include "kvi_oggtheora.h" + +#include "kvi_databuffer.h" + + +static void rgb32toyuv444(QRgb * rgbPt, unsigned char *yuvPt, int w, int h) +{ + for(int i = 0; i < h; i++) { + for (int j = 0; j < w; j++) { + yuvPt[i * w + j] = (unsigned char) (( 66 * qRed(*rgbPt) + 129 * qGreen(*rgbPt) + 25 * qBlue(*rgbPt) + 128) >> 8) + 16; // y + yuvPt[(i + h) * w + j] = (unsigned char) ((-38 * qRed(*rgbPt) - 74 * qGreen(*rgbPt) + 112 * qBlue(*rgbPt) + 128) >> 8) + 128; // u + yuvPt[(i + 2 * h) * w + j] = (unsigned char) ((112 * qRed(*rgbPt) - 94 * qGreen(*rgbPt) - 18 * qBlue(*rgbPt) + 128) >> 8) + 128; // v + //this moves the pointer forward 4 bytes (ARGB) + rgbPt++; + } + } + return; +} + +KviTheoraEncoder::KviTheoraEncoder(KviDataBuffer * stream, int iWidth, int iHeight, int iFpsN, int iFpsD, int iParN, int iParD) +{ + //used to check functions results + int ret; + videoflag=0; + audioflag=0; + videoYuv=0; + frame_state=-1; + frames=0; + + //audio + audio=false; + audio_ch=1; + audio_hz=8000; + audio_q=.6f; + samples_sofar=0; + + //user settings + m_pStream=stream; + geometry.pic_w=iWidth; + geometry.pic_h=iHeight; + //frame per second + int video_fps_n=iFpsN; + int video_fps_d=iFpsD; + //aspect ratio + int video_par_n=iParN; + int video_par_d=iParD; + + + // preset settings + int video_q=48; + ogg_uint32_t keyframe_frequency=64; + + y4m_dst_buf_read_sz = geometry.pic_w * geometry.pic_h * 3; + y4m_aux_buf_sz = y4m_aux_buf_read_sz=0; + + // buffer allocations + y4m_dst_buf_read_sz = geometry.pic_w * geometry.pic_h * 3; + y4m_aux_buf_sz = y4m_aux_buf_read_sz=0; + // The size of the final frame buffers is always computed from the destination chroma decimation type. + y4m_dst_buf_sz = geometry.pic_w * geometry.pic_h * 3; + + // Set up Ogg output stream + srand(time(NULL)); + ogg_stream_init(&to,rand()); + + + // initialize Vorbis assuming we have audio to compress. + if(audio) + { + ogg_stream_init(&vo,rand()); + vorbis_info_init(&vi); + ret = vorbis_encode_init_vbr(&vi,audio_ch,audio_hz,audio_q); + if(ret) + { + qDebug("error initializing vorbis audio"); + return; + } + vorbis_comment_init(&vc); + vorbis_analysis_init(&vd,&vi); + vorbis_block_init(&vd,&vb); + } + + // Set up Theora encoder + // Theora has a divisible-by-sixteen restriction for the encoded frame size + // scale the picture size up to the nearest /16 and calculate offsets + geometry.frame_w = (geometry.pic_w + 15) &~ 0xF; + geometry.frame_h = (geometry.pic_h + 15) &~ 0xF; + // Force the offsets to be even so that chroma samples line up like we expect. + geometry.pic_x = (geometry.frame_w - geometry.pic_w) >> 1 &~ 1; + geometry.pic_y = (geometry.frame_h - geometry.pic_h) >> 1 &~ 1; + + // Fill in a th_info structure with details on the format of the video you wish to encode. + th_info_init(&ti); + ti.frame_width=geometry.frame_w; + ti.frame_height=geometry.frame_h; + ti.pic_width=geometry.pic_w; + ti.pic_height=geometry.pic_h; + ti.pic_x=geometry.pic_x; + ti.pic_y=geometry.pic_y; + ti.fps_numerator=video_fps_n; + ti.fps_denominator=video_fps_d; + ti.aspect_numerator=video_par_n; + ti.aspect_denominator=video_par_d; + ti.colorspace=TH_CS_UNSPECIFIED; + //use vbr, not cbr + ti.target_bitrate=0; + ti.quality=video_q; + ti.keyframe_granule_shift=ilog(keyframe_frequency-1); + ti.pixel_fmt=TH_PF_444; + // Allocate a th_enc_ctx handle with th_encode_alloc(). + td=th_encode_alloc(&ti); + if(td==0){ + qDebug("Could not th_encode_alloc()."); + return; + } + + th_info_clear(&ti); + // Perform any additional encoder configuration required with th_encode_ctl(). + // setting just the granule shift only allows power-of-two keyframe spacing. Set the actual requested spacing. + ret=th_encode_ctl(td,TH_ENCCTL_SET_KEYFRAME_FREQUENCY_FORCE,&keyframe_frequency,sizeof(keyframe_frequency-1)); + if(ret<0){ + qDebug("Could not set keyframe interval to %d.",(int)keyframe_frequency); + } + // write the bitstream header packets with proper page interleave + th_comment_init(&tc); + + // Repeatedly call th_encode_flushheader() to retrieve all the header packets. + // first packet will get its own page automatically + if(th_encode_flushheader(td,&tc,&op)<=0){ + qDebug("Internal Theora library error."); + return; + } + + ogg_stream_packetin(&to,&op); + if(ogg_stream_pageout(&to,&og)!=1){ + qDebug("Internal Ogg library error."); + return; + } + + m_pStream->append(og.header,og.header_len); + m_pStream->append(og.body,og.body_len); + + // create the remaining theora headers + for(;;){ + ret=th_encode_flushheader(td,&tc,&op); + if(ret<0){ + qDebug("Internal Theora library error."); + return; + } + else if(!ret)break; + ogg_stream_packetin(&to,&op); + } + + if(audio){ + ogg_packet header; + ogg_packet header_comm; + ogg_packet header_code; + vorbis_analysis_headerout(&vd,&vc,&header,&header_comm,&header_code); + ogg_stream_packetin(&vo,&header); // automatically placed in its own page + if(ogg_stream_pageout(&vo,&og)!=1){ + qDebug("Internal Ogg library error."); + return; + } + m_pStream->append(og.header,og.header_len); + m_pStream->append(og.body,og.body_len); + // remaining vorbis header packets + ogg_stream_packetin(&vo,&header_comm); + ogg_stream_packetin(&vo,&header_code); + } + + // Flush the rest of our headers. This ensures the actual data in each stream + // will start on a new page, as per spec. + for(;;){ + int result = ogg_stream_flush(&to,&og); + if(result<0){ + // can't get here + qDebug("Internal Ogg library error."); + return; + } + if(result==0)break; + m_pStream->append(og.header,og.header_len); + m_pStream->append(og.body,og.body_len); + } + + if(audio) + { + for(;;){ + int result=ogg_stream_flush(&vo,&og); + if(result<0){ + // can't get here + qDebug("Internal Ogg library error."); + return; + } + if(result==0)break; + m_pStream->append(og.header,og.header_len); + m_pStream->append(og.body,og.body_len); + } + } +} + +KviTheoraEncoder::~KviTheoraEncoder() +{ + // Call th_encode_free() to release all encoder memory. + th_encode_free(td); + + // clear out state + + if(audio) + { + ogg_stream_clear(&vo); + vorbis_block_clear(&vb); + vorbis_dsp_clear(&vd); + vorbis_comment_clear(&vc); + vorbis_info_clear(&vi); + } + + ogg_stream_clear(&to); + th_comment_clear(&tc); +} + +void KviTheoraEncoder::addVideoFrame(QRgb * rgb32, int videoSize) +{ + /* + * For each uncompressed frame: + * o Submit the uncompressed frame via th_encode_ycbcr_in() + * o Repeatedly call th_encode_packetout() to retrieve any video data packets that are ready. + */ + + if(!videoYuv) + videoYuv = new quint8[geometry.pic_w * geometry.pic_h * YUV444_BPP]; + + rgb32toyuv444(rgb32, videoYuv, geometry.pic_w, geometry.pic_h); +// qDebug("addFrame p%p size%d yuv%p",rgb32,videoSize,videoYuv); + + double videotime; + ogg_page videopage; + + // is there a video page flushed? If not, fetch one if possible + videoflag=fetch_and_process_video(videoYuv,&videopage,&to,td,videoflag); + + // no pages? Must be end of stream. + if(!videoflag) return; + + videotime=videoflag?th_granule_time(td,ogg_page_granulepos(&videopage)):-1; + + // flush a video page + m_pStream->append(videopage.header,videopage.header_len); + m_pStream->append(videopage.body,videopage.body_len); + videoflag=0; +} + +void KviTheoraEncoder::addAudioFrame(unsigned char* audioPkt, int audioSize) +{ + /* + * For each uncompressed frame: + * o Submit the uncompressed frame via th_encode_ycbcr_in() + * o Repeatedly call th_encode_packetout() to retrieve any video data packets that are ready. + */ + if(!audio) + return; + + double audiotime; + ogg_page audiopage; + + // is there an audio page flushed? If not, fetch one if possible + audioflag=fetch_and_process_audio(audioPkt, audioSize,&audiopage,&vo,&vd,&vb,audioflag); + + // no pages? Must be end of stream. + if(!audioflag) return; + + audiotime=audioflag?vorbis_granule_time(&vd,ogg_page_granulepos(&audiopage)):-1; + + m_pStream->append(audiopage.header,audiopage.header_len); + m_pStream->append(audiopage.body,audiopage.body_len); + audioflag=0; +} + +int KviTheoraEncoder::fetch_and_process_video_packet(quint8 * videoYuv,th_enc_ctx *td,ogg_packet *op) +{ + th_ycbcr_buffer ycbcr; + + if(frame_state==-1) + { + /* initialize the double frame buffer */ + yuvframe[0]=(unsigned char *)malloc(y4m_dst_buf_sz); + yuvframe[1]=(unsigned char *)malloc(y4m_dst_buf_sz); + yuvframe[2]=(unsigned char *)malloc(y4m_aux_buf_sz); + frame_state=0; + } + /* read and process more video */ + /* video strategy reads one frame ahead so we know when we're + at end of stream and can mark last video frame as such + (vorbis audio has to flush one frame past last video frame + due to overlap and thus doesn't need this extra work */ + + /* have two frame buffers full (if possible) before + proceeding. after first pass and until eos, one will + always be full when we get here */ + for(;frame_state<2;) + { + /*Read the frame data that needs no conversion.*/ + memcpy(yuvframe[frame_state],videoYuv,y4m_dst_buf_read_sz); + /*Read the frame data that does need conversion.*/ + memcpy(yuvframe[2],videoYuv,y4m_aux_buf_read_sz); + frames++; + frame_state++; + } + /* check to see if there are dupes to flush */ + if(th_encode_packetout(td,frame_state<1,op)>0)return 1; + if(frame_state<1) + { + /* can't get here unless YUV4MPEG stream has no video */ + qDebug("Video input contains no frames."); + return 0; + } + /* Theora is a one-frame-in,one-frame-out system; submit a frame + for compression and pull out the packet */ + + /*We submit the buffer to the library as if it were padded, but we do not + actually allocate space for the padding. + This is okay, because with the 1.0 API the library will never read data from the padded + region.*/ + ycbcr[0].width=geometry.frame_w; + ycbcr[0].height=geometry.frame_h; + ycbcr[0].stride=geometry.pic_w; + ycbcr[0].data=yuvframe[0] - geometry.pic_x -geometry.pic_y * geometry.pic_w; + + ycbcr[1].width=geometry.frame_w; + ycbcr[1].height=geometry.frame_h; + ycbcr[1].stride=geometry.pic_w; + ycbcr[1].data=yuvframe[0] + (geometry.pic_w * geometry.pic_h) - geometry.pic_x - geometry.pic_y * geometry.frame_w; + + ycbcr[2].width=geometry.frame_w; + ycbcr[2].height=geometry.frame_h; + ycbcr[2].stride=geometry.pic_w; + ycbcr[2].data=ycbcr[1].data+(geometry.frame_w * geometry.frame_h); + + th_encode_ycbcr_in(td,ycbcr); + { + unsigned char *temp=yuvframe[0]; + yuvframe[0]=yuvframe[1]; + yuvframe[1]=temp; + frame_state--; + } + /* if there was only one frame, it's the last in the stream */ + return th_encode_packetout(td,frame_state<1,op); +} + +int KviTheoraEncoder::fetch_and_process_video(quint8 * videoYuv,ogg_page *videopage, +ogg_stream_state *to,th_enc_ctx *td,int videoflag){ + ogg_packet op; + int ret; + /* is there a video page flushed? If not, work until there is. */ + while(!videoflag){ + if(ogg_stream_pageout(to,videopage)>0) return 1; + if(ogg_stream_eos(to)) return 0; + ret=fetch_and_process_video_packet(videoYuv,td,&op); + if(ret<=0)return 0; + ogg_stream_packetin(to,&op); + } + return videoflag; +} + +int KviTheoraEncoder::ilog(unsigned _v) +{ + int ret; + for(ret=0;_v;ret++)_v>>=1; + return ret; +} + +int KviTheoraEncoder::fetch_and_process_audio(quint8 * audio, int size, ogg_page *audiopage, + ogg_stream_state *vo, + vorbis_dsp_state *vd, + vorbis_block *vb, + int audioflag) +{ + ogg_packet op; + int i,j; + qDebug("fetch_and_process_audio audioflag=%d",audioflag); + signed char readbuffer[4096]; + int bytesread=qMin(size, 4096); + memcpy(readbuffer,audio, bytesread); + + while(!audioflag) + { + /* process any audio already buffered */ + if(ogg_stream_pageout(vo,audiopage)>0) return 1; + if(ogg_stream_eos(vo))return 0; + + { + /* read and process more audio */ + qDebug("Processing audio frame size %d/4096",size); + signed char *readptr=readbuffer; + int sampread=bytesread/2/audio_ch; + float **vorbis_buffer; + int count=0; + + if(bytesread<=0) + { + /* end of file. this can be done implicitly, but it's + easier to see here in non-clever fashion. Tell the + library we're at end of stream so that it can handle the + last frame and mark end of stream in the output properly */ + vorbis_analysis_wrote(vd,0); + } else { + samples_sofar += sampread; + + if(sampread>0) + { + vorbis_buffer=vorbis_analysis_buffer(vd,sampread); + /* uninterleave samples */ + for(i=0;i<sampread;i++) + { + for(j=0;j<audio_ch;j++) + { + vorbis_buffer[j][i]=((readptr[count+1]<<8)| + (0x00ff&(int)readptr[count]))/32768.f; + count+=2; + } + } + vorbis_analysis_wrote(vd,sampread); + } + } + + while(vorbis_analysis_blockout(vd,vb)==1) + { + + /* analysis, assume we want to use bitrate management */ + vorbis_analysis(vb,NULL); + vorbis_bitrate_addblock(vb); + + /* weld packets into the bitstream */ + while(vorbis_bitrate_flushpacket(vd,&op)) + ogg_stream_packetin(vo,&op); + } + } + //FIXME remove this + return 0; + } + return audioflag; +} + + +//------------------------------------------------------------------------------ +#define OC_CLAMP255(_x) ((unsigned char)((((_x)<0)-1)&((_x)|-((_x)>255)))) + +KviTheoraDecoder::KviTheoraDecoder(KviDataBuffer * signal) +{ + m_pSignal=signal; + theora_p=0; + vorbis_p=0; + stateflag=0; + ts=NULL; + thda=false; + thtic=false; + + /* single frame video buffering */ + videobuf_ready=0; + videobuf_granulepos=-1; + videobuf_time=0; + + /* single audio fragment audio buffering */ + audiobuf_fill=0; + audiobuf_ready=0; + audiobuf_granulepos=0; /* time position of last sample */ + + frames = 0; + dropped = 0; + + audiofd_totalsize=-1; + audiofd=-1; + audiofd_timer_calibrate=-1; + + /* start up Ogg stream synchronization layer */ + ogg_sync_init(&oy); + + /* init supporting Vorbis structures needed in header parsing */ + vorbis_info_init(&vi); + vorbis_comment_init(&vc); + + /* init supporting Theora structures needed in header parsing */ + th_comment_init(&tc); + th_info_init(&ti); + + for(int i=0;i<256;i++) + { + lu_Y[i] = 2441889*(i- 16) + 1048576; + lu_R[i] = 3347111*(i-128); + lu_GV[i] = -1704917*(i-128); + lu_GU[i] = -821585*(i-128); + lu_B[i] = 4230442*(i-128); + } +} + +KviTheoraDecoder::~KviTheoraDecoder() +{ + if(vorbis_p){ + ogg_stream_clear(&vo); + vorbis_block_clear(&vb); + vorbis_dsp_clear(&vd); + vorbis_comment_clear(&vc); + vorbis_info_clear(&vi); + } + if(theora_p){ + ogg_stream_clear(&to); + th_decode_free(td); + th_comment_clear(&tc); + th_info_clear(&ti); + } + + ogg_sync_clear(&oy); +} + +void KviTheoraDecoder::addData(KviDataBuffer * stream) +{ +// qDebug("adddata signal %p stream size %d",m_pSignal, stream->size() ); + if(stream->size()==0)return; + /* Ogg file open; parse the headers */ + /* Only interested in Vorbis/Theora streams */ + + if(!stateflag){ + + char *buffer=ogg_sync_buffer(&oy,stream->size()); + memcpy(buffer,stream->data(),stream->size()); + ogg_sync_wrote(&oy,stream->size()); + stream->clear(); + + while(ogg_sync_pageout(&oy,&og)>0){ + ogg_stream_state test; + + /* is this a mandated initial header? If not, stop parsing */ + if(!ogg_page_bos(&og)){ + /* don't leak the page; get it into the appropriate stream */ +// qDebug("queue_page && return"); + queue_page(&og); + stateflag=1; + return; + } + + ogg_stream_init(&test,ogg_page_serialno(&og)); + ogg_stream_pagein(&test,&og); + ogg_stream_packetout(&test,&op); + + + /* identify the codec: try theora */ + if(!theora_p && th_decode_headerin(&ti,&tc,&ts,&op)>=0){ +// qDebug("is theora, ts=%p &ts=%p",ts, &ts); + /* it is theora */ + memcpy(&to,&test,sizeof(test)); + theora_p=1; + }else if(!vorbis_p && vorbis_synthesis_headerin(&vi,&vc,&op)>=0){ +// qDebug("is vorbis"); + /* it is vorbis */ + memcpy(&vo,&test,sizeof(test)); + vorbis_p=1; + }else{ +// qDebug("is other"); + /* whatever it is, we don't care about it */ + ogg_stream_clear(&test); + } + } + /* fall through to non-bos page parsing */ + } + +// qDebug("checkpoint 2, theora_p=%d vorbis_p=%d",theora_p, vorbis_p); + /* we're expecting more header packets. */ + if((theora_p && theora_p<3) || (vorbis_p && vorbis_p<3)) + { + int ret; + + /* look for further theora headers */ + while(theora_p && (theora_p<3) && (ret=ogg_stream_packetout(&to,&op))) + { + if(ret<0){ + qDebug("Error parsing Theora stream headers; corrupt stream?"); + return; + } + if(!th_decode_headerin(&ti,&tc,&ts,&op)){ + qDebug("Error parsing Theora stream headers; corrupt stream?"); + return; + } + theora_p++; + } + + /* look for more vorbis header packets */ + while(vorbis_p && (vorbis_p<3) && (ret=ogg_stream_packetout(&vo,&op))) + { + if(ret<0) + { + qDebug("Error parsing Vorbis stream headers; corrupt stream?"); + return; + } + if(vorbis_synthesis_headerin(&vi,&vc,&op)) + { + qDebug("Error parsing Vorbis stream headers; corrupt stream?"); + return; + } + vorbis_p++; + if(vorbis_p==3)break; + } + + /* The header pages/packets will arrive before anything else we + care about, or the stream is not obeying spec */ + if(ogg_sync_pageout(&oy,&og)>0) + { +// qDebug("queue_page"); + queue_page(&og); /* demux into the appropriate stream */ + } else { + qDebug("TheoraDecoder: need more data!"); + //need more data + return; + } + } + +// qDebug("checkpoint 3"); + /* and now we have it all. initialize decoders */ + if(theora_p && !thda){ + thda=true; + td=th_decode_alloc(&ti,ts); + qDebug("Ogg logical stream %lx is Theora %dx%d %.02f fps", + to.serialno,ti.pic_width,ti.pic_height, + (double)ti.fps_numerator/ti.fps_denominator); + + geometry.pic_w = ti.pic_width; + geometry.pic_h = ti.pic_height; + geometry.frame_w = ti.frame_width; + geometry.frame_h = ti.frame_height; + geometry.pic_x = ti.pic_x; + geometry.pic_y = ti.pic_y; + + px_fmt=ti.pixel_fmt; + switch(ti.pixel_fmt){ + case TH_PF_444: qDebug(" 4:4:4 video"); break; + default: + qDebug(" video (UNSUPPORTED Chroma sampling!)"); + return; + break; + } + th_decode_ctl(td,TH_DECCTL_GET_PPLEVEL_MAX,&pp_level_max,sizeof(pp_level_max)); + pp_level=pp_level_max; + th_decode_ctl(td,TH_DECCTL_SET_PPLEVEL,&pp_level,sizeof(pp_level)); + pp_inc=0; + + int w = ((ti.pic_x + ti.pic_width + 1) &~ 1) - (ti.pic_x &~ 1); + int h = ((ti.pic_y + ti.pic_height + 1) &~ 1) - (ti.pic_y &~ 1); + RGBbuffer = (unsigned char *) calloc(sizeof(char), w * h * 4); + } else if(!thtic) + { + thtic=true; + /* tear down the partial theora setup */ + th_info_clear(&ti); + th_comment_clear(&tc); + + th_setup_free(ts); + } + + if(vorbis_p){ + vorbis_synthesis_init(&vd,&vi); + vorbis_block_init(&vd,&vb); + qDebug("Ogg logical stream %lx is Vorbis %d channel %ld Hz audio.", + vo.serialno,vi.channels,vi.rate); + }else{ + /* tear down the partial vorbis setup */ + vorbis_info_clear(&vi); + vorbis_comment_clear(&vc); + } + +// stateflag=0; /* playback has not begun */ + + /* we want a video and audio frame ready to go at all times. If + we have to buffer incoming, buffer the compressed data (ie, let + ogg do the buffering) */ + + while(vorbis_p && !audiobuf_ready){ + int ret; + float **pcm; + + // if there's pending, decoded audio, grab it + if((ret=vorbis_synthesis_pcmout(&vd,&pcm))>0){ + int count=audiobuf_fill/2; + int maxsamples=(audiofd_fragsize-audiobuf_fill)/2/vi.channels; + for(i=0;i<ret && i<maxsamples;i++) + for(j=0;j<vi.channels;j++){ + int val=rint(pcm[j][i]*32767.f); + if(val>32767)val=32767; + if(val<-32768)val=-32768; + audiobuf[count++]=val; + } + vorbis_synthesis_read(&vd,i); + audiobuf_fill+=i*vi.channels*2; + if(audiobuf_fill==audiofd_fragsize)audiobuf_ready=1; + if(vd.granulepos>=0) + audiobuf_granulepos=vd.granulepos-ret+i; + else + audiobuf_granulepos+=i; + + }else{ + + // no pending audio; is there a pending packet to decode? + if(ogg_stream_packetout(&vo,&op)>0){ + if(vorbis_synthesis(&vb,&op)==0) // test for success! + vorbis_synthesis_blockin(&vd,&vb); + }else // we need more data; break out to suck in another page + break; + } + } + + if(theora_p && !videobuf_ready){ + if(stream->size()>0) + { + char *buffer=ogg_sync_buffer(&oy,stream->size()); + memcpy(buffer,stream->data(),stream->size()); + ogg_sync_wrote(&oy,stream->size()); + stream->clear(); + + while(ogg_sync_pageout(&oy,&og)>0){ + queue_page(&og); + } + } +// qDebug("loop4 theora_p=%d videobuf_ready=%d",theora_p, videobuf_ready); + // theora is one in, one out... + if(ogg_stream_packetout(&to,&op)>0){ +// qDebug("ogg_stream_packetout >0"); + if(pp_inc){ + pp_level+=pp_inc; + th_decode_ctl(td,TH_DECCTL_SET_PPLEVEL,&pp_level, + sizeof(pp_level)); + pp_inc=0; + } + /*HACK: This should be set after a seek or a gap, but we might not have + a granulepos for the first packet (we only have them for the last + packet on a page), so we just set it as often as we get it. + To do this right, we should back-track from the last packet on the + page and compute the correct granulepos for the first packet after + a seek or a gap.*/ + if(op.granulepos>=0){ + th_decode_ctl(td,TH_DECCTL_SET_GRANPOS,&op.granulepos,sizeof(op.granulepos)); + } + if(th_decode_packetin(td,&op,&videobuf_granulepos)==0){ +// qDebug("th_decode_packetin ==0"); + videobuf_time=th_granule_time(td,videobuf_granulepos); + frames++; + + /* is it already too old to be useful? This is only actually + useful cosmetically after a SIGSTOP. Note that we have to + decode the frame even if we don't show it (for now) due to + keyframing. Soon enough libtheora will be able to deal + with non-keyframe seeks. */ + + if(videobuf_time>=get_time()) + { +// qDebug("videobuf_time>=get_time"); + videobuf_ready=1; + }else{ +// qDebug("videobuf_time<get_time"); + /*If we are too slow, reduce the pp level.*/ + pp_inc=pp_level>0?-1:0; + dropped++; + } + } + + }else { +// qDebug("ogg_stream_packetout <=0"); + //need more data :) + return; + } + } + + if(!videobuf_ready && !audiobuf_ready)return; +/* + if(!videobuf_ready || !audiobuf_ready){ + // no data yet for somebody. Grab another page + return; + } +*/ + /* If playback has begun, top audio buffer off immediately. */ +// if(stateflag) audio_write_nonblocking(); + + /* are we at or past time for this video frame? */ + if(stateflag && videobuf_ready) {// && videobuf_time<=get_time()){ + video_write(); + videobuf_ready=0; + } +/* + if(stateflag && + (audiobuf_ready || !vorbis_p) && + (videobuf_ready || !theora_p) && + ){ + // we have an audio frame ready (which means the audio buffer is + // full), it's not time to play video, so wait until one of the + // audio buffer is ready or it's near time to play video + + // set up select wait on the audiobuffer and a timeout for video + struct timeval timeout; + fd_set writefs; + fd_set empty; + int n=0; + + FD_ZERO(&writefs); + FD_ZERO(&empty); + if(audiofd>=0){ + FD_SET(audiofd,&writefs); + n=audiofd+1; + } + + if(theora_p){ + double tdiff; + long milliseconds; + tdiff=videobuf_time-get_time(); + //If we have lots of extra time, increase the post-processing level. + if(tdiff>ti.fps_denominator*0.25/ti.fps_numerator){ + pp_inc=pp_level<pp_level_max?1:0; + } + else if(tdiff<ti.fps_denominator*0.05/ti.fps_numerator){ + pp_inc=pp_level>0?-1:0; + } + milliseconds=tdiff*1000-5; + if(milliseconds>500)milliseconds=500; + if(milliseconds>0){ + timeout.tv_sec=milliseconds/1000; + timeout.tv_usec=(milliseconds%1000)*1000; + + n=select(n,&empty,&writefs,&empty,&timeout); + if(n)audio_calibrate_timer(0); + } + }else{ + select(n,&empty,&writefs,&empty,NULL); + } + } +*/ + // if our buffers either don't exist or are ready to go,we can begin playback + if((!theora_p || videobuf_ready) && + (!vorbis_p || audiobuf_ready))stateflag=1; +} + +/* helper: push a page into the appropriate steam */ +/* this can be done blindly; a stream won't accept a page + that doesn't belong to it */ +int KviTheoraDecoder::queue_page(ogg_page *page) +{ + if(theora_p)ogg_stream_pagein(&to,page); + if(vorbis_p)ogg_stream_pagein(&vo,page); + return 0; +} + +/* get relative time since beginning playback, compensating for A/V + drift */ +double KviTheoraDecoder::get_time(){ + ogg_int64_t last=0; + ogg_int64_t up=0; + ogg_int64_t now; + struct timeval tv; + + gettimeofday(&tv,0); + now=tv.tv_sec*1000+tv.tv_usec/1000; + + if(audiofd_timer_calibrate==-1)audiofd_timer_calibrate=last=now; + + if(audiofd<0){ + /* no audio timer to worry about, we can just use the system clock */ + /* only one complication: If the process is suspended, we should + reset timing to account for the gap in play time. Do it the + easy/hack way */ + if(now-last>1000)audiofd_timer_calibrate+=(now-last); + last=now; + } + + if(now-up>200){ +/* + double timebase=(now-audiofd_timer_calibrate)*.001; + int hundredths=timebase*100-(long)timebase*100; + int seconds=(long)timebase%60; + int minutes=((long)timebase/60)%60; + int hours=(long)timebase/3600; + + qDebug(" Playing: %d:%02d:%02d.%02d \r",hours,minutes,seconds,hundredths); +*/ + up=now; + } + + return (now-audiofd_timer_calibrate)*.001; +} + +void KviTheoraDecoder::video_write(void) +{ + th_ycbcr_buffer yuv; + int y_offset; + th_decode_ycbcr_out(td,yuv); + + y_offset=(geometry.pic_x&~1)+yuv[0].stride*(geometry.pic_y&~1); + + for(int i=0;i<geometry.pic_h;i++) + { + unsigned char *in_y = (unsigned char *)yuv[0].data+y_offset+yuv[0].stride*i; + unsigned char *in_u = (unsigned char *)yuv[1].data+y_offset+yuv[1].stride*i; + unsigned char *in_v = (unsigned char *)yuv[2].data+y_offset+yuv[2].stride*i; + unsigned char *out = RGBbuffer+(geometry.pic_w * i * ARGB32_BPP); + for (int j=0;j<geometry.pic_w;j++) + { + int r, g, b; + int y = lu_Y[in_y[j]]; + b=(y + lu_B[in_u[j]])>>21; + out[j*4] = OC_CLAMP255(b); + g=(y + lu_GV[in_v[j]] + lu_GU[in_u[j]])>>21; + out[j*4+1] = OC_CLAMP255(g); + r=(y + lu_R[in_v[j]])>>21; + out[j*4+2] = OC_CLAMP255(r); + out[j*4+3] = 255; //alpha + } + + } + + int size = sizeof(char) * geometry.pic_w * geometry.pic_h * ARGB32_BPP; + +// qDebug("VIDEO WRITE size=%d",size); + m_pSignal->clear(); + m_pSignal->append(RGBbuffer, size); +}
\ No newline at end of file diff --git a/src/kvilib/ext/kvi_oggtheora.h b/src/kvilib/ext/kvi_oggtheora.h new file mode 100644 index 000000000..3ad2eee0b --- /dev/null +++ b/src/kvilib/ext/kvi_oggtheora.h @@ -0,0 +1,183 @@ +#ifndef KVI_OGGTHEORA_H_ +#define KVI_OGGTHEORA_H_ +//============================================================================= +// +// File : kvi_oggtheora.h +// Creation date : Sat Nov 21 2009 22:53:21 CEST by Fabio Bas +// +// This file is part of the KVirc irc client distribution +// Copyright (C) 2009 Fabio Bas (ctrlaltca at libero dot it) +// +// This program is FREE software. You can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your opinion) any later version. +// +// This program is distributed in the HOPE that it will be USEFUL, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +//============================================================================= + +#include "kvi_settings.h" + +#include "theora/theoradec.h" +#include "theora/theoraenc.h" +#include "vorbis/codec.h" +#include "vorbis/vorbisenc.h" + +#include <QColor> + +#define OC_MINI(_a,_b) ((_a)>(_b)?(_b):(_a)) +#define OC_MAXI(_a,_b) ((_a)<(_b)?(_b):(_a)) +#define OC_CLAMPI(_a,_b,_c) (OC_MAXI(_a,OC_MINI(_b,_c))) + +#define YUV444_BPP 3 +#define ARGB32_BPP 4 + +class KviDataBuffer; + +typedef struct _KviTheoraGeometry { + //original geometry, set by user + int pic_w; + int pic_h; + //calculated geometry: frame + int frame_w; + int frame_h; + //calculated geometry: offset + int pic_x; + int pic_y; +} KviTheoraGeometry; + +class KVILIB_API KviTheoraEncoder { +public: + KviTheoraEncoder(KviDataBuffer * stream, int iWidth=320, int iHeight=240, int iFpsN=5, int iFpsD=1, int iParN=4, int iParD=3); + virtual ~KviTheoraEncoder(); + void addVideoFrame(QRgb * rgb32, int videoSize); + void addAudioFrame(unsigned char* audioPkt, int audioSize); +private: + int fetch_and_process_video(quint8 * videoYuv,ogg_page *videopage,ogg_stream_state *to,th_enc_ctx *td,int videoflag); + int fetch_and_process_video_packet(quint8 * videoYuv,th_enc_ctx *td,ogg_packet *op); + int fetch_and_process_audio(quint8 * audio, int size, ogg_page *audiopage, ogg_stream_state *vo, vorbis_dsp_state *vd, vorbis_block *vb, int audioflag); +private: + KviTheoraGeometry geometry; + KviDataBuffer * m_pStream; + quint8 * videoYuv; + + bool audio; + int audio_ch; + int audio_hz; + float audio_q; + ogg_int64_t samples_sofar; + + int frame_state; + ogg_int64_t frames; + unsigned char *yuvframe[3]; + + // The size of each converted frame buffer. + size_t y4m_dst_buf_sz; + // The amount to read directly into the converted frame buffer. + size_t y4m_dst_buf_read_sz; + // The size of the auxilliary buffer. + size_t y4m_aux_buf_sz; + // The amount to read into the auxilliary buffer. + size_t y4m_aux_buf_read_sz; + + ogg_stream_state to; // take physical pages, weld into a logical stream of packets + ogg_stream_state vo; // take physical pages, weld into a logical stream of packets + ogg_page og; // one Ogg bitstream page. Vorbis packets are inside + ogg_packet op; // one raw packet of data for decode + + th_enc_ctx *td; + th_info ti; + th_comment tc; + + vorbis_info vi; // struct that stores all the static vorbis bitstream settings + vorbis_comment vc; // struct that stores all the user comments + vorbis_dsp_state vd; // central working state for the packet->PCM decoder + vorbis_block vb; // local working space for packet->PCM decode + + int audioflag; + int videoflag; +private: + static int ilog(unsigned _v); +}; + + +class KVILIB_API KviTheoraDecoder { +public: + KviTheoraDecoder(KviDataBuffer * signal); + virtual ~KviTheoraDecoder(); + void addData(KviDataBuffer * stream); +private: + int queue_page(ogg_page *page); + double get_time(); + void video_write(void); +private: + KviDataBuffer * m_pSignal; + KviTheoraGeometry geometry; + unsigned char * RGBbuffer; + + // Ogg and codec state for demux/decode + ogg_sync_state oy; + ogg_page og; + ogg_stream_state vo; + ogg_stream_state to; + th_info ti; + th_comment tc; + th_dec_ctx *td; + th_setup_info *ts; + vorbis_info vi; + vorbis_dsp_state vd; + vorbis_block vb; + vorbis_comment vc; + th_pixel_fmt px_fmt; + + int theora_p; + int vorbis_p; + int stateflag; + + int lu_Y[256]; + int lu_R[256]; + int lu_GU[256]; + int lu_GV[256]; + int lu_B[256]; + + // single frame video buffering + int videobuf_ready; + ogg_int64_t videobuf_granulepos; + double videobuf_time; + + // single audio fragment audio buffering + int audiobuf_fill; + int audiobuf_ready; + ogg_int16_t *audiobuf; + ogg_int64_t audiobuf_granulepos; // time position of last sample + + int pp_level_max; + int pp_level; + int pp_inc; + int i,j; + ogg_packet op; + + int frames; + int dropped; + + long audiofd_totalsize; + int audiofd_fragsize; /* read and write only complete fragments + so that SNDCTL_DSP_GETOSPACE is + accurate immediately after a bank + switch */ + int audiofd; + ogg_int64_t audiofd_timer_calibrate; + bool thda; + bool thtic; + +}; + +#endif /* KVI_OGGTHEORA_H_ */ diff --git a/src/modules/dcc/codec.cpp b/src/modules/dcc/codec.cpp index e70a532c9..a1a0897e8 100644 --- a/src/modules/dcc/codec.cpp +++ b/src/modules/dcc/codec.cpp @@ -76,6 +76,7 @@ void KviDccVoiceNullCodec::decode(KviDataBuffer * stream,KviDataBuffer * signal) { if(stream->size() < 1)return; signal->append(stream->data(),stream->size()); + stream->resize(0); } @@ -99,7 +100,11 @@ KviDccVideoCodec::~KviDccVideoCodec() { } -void KviDccVideoCodec::encode(KviDataBuffer *,KviDataBuffer *) +void KviDccVideoCodec::encodeVideo(KviDataBuffer *,KviDataBuffer *) +{ +} + +void KviDccVideoCodec::encodeAudio(KviDataBuffer *,KviDataBuffer *) { } @@ -122,49 +127,63 @@ const char * KviDccVideoCodec::name() return m_szName.ptr(); } -KviDccVideoJpegCodec::KviDccVideoJpegCodec() +KviDccVideoTheoraCodec::KviDccVideoTheoraCodec() : KviDccVideoCodec() { - m_szName = "jpeg"; + m_szName = "theora"; + m_pEncoder = 0; + m_pDecoder = 0; } -KviDccVideoJpegCodec::~KviDccVideoJpegCodec() +KviDccVideoTheoraCodec::~KviDccVideoTheoraCodec() { + if(m_pEncoder) + delete m_pEncoder; + m_pEncoder=0; + + if(m_pDecoder) + delete m_pDecoder; + m_pDecoder=0; } -void KviDccVideoJpegCodec::encode(KviDataBuffer * signal,KviDataBuffer * stream) +void KviDccVideoTheoraCodec::encodeVideo(KviDataBuffer * videoSignal,KviDataBuffer * stream) { - if(signal->size() < 1)return; - stream->append(signal->data(),signal->size()); - signal->resize(0); + if(videoSignal->size() < 1) return; + + if(!m_pEncoder) + m_pEncoder = new KviTheoraEncoder(stream); + + m_pEncoder->addVideoFrame((QRgb *) videoSignal->data(),videoSignal->size()); + videoSignal->clear(); } -void KviDccVideoJpegCodec::decode(KviDataBuffer * stream,KviDataBuffer * signal) +void KviDccVideoTheoraCodec::encodeAudio(KviDataBuffer * audioSignal,KviDataBuffer * stream) { - static unsigned const char jpg_magic_init[4] = { 0xFF, 0xD8, 0xFF, 0xE0}; //SOI + APP0 - static unsigned const char jpg_magic_end[2] = { 0xFF, 0xD9}; //EOI + if(audioSignal->size() < 1) return; + + if(!m_pEncoder) + m_pEncoder = new KviTheoraEncoder(stream); + m_pEncoder->addAudioFrame(audioSignal->data(),audioSignal->size()); + audioSignal->clear(); +} + +void KviDccVideoTheoraCodec::decode(KviDataBuffer * stream,KviDataBuffer * signal) +{ if(stream->size() < 1)return; - - int start = stream->find(jpg_magic_init, 4); - int end = stream->find(jpg_magic_end, 2); - if(start != -1 && end != -1) - { - //remove junk before jpeg start - stream->remove(start); - int len = end - start + 1; - signal->clear(); - signal->append(stream->data(),stream->size()); - stream->remove(len); - } + + if(!m_pDecoder) + m_pDecoder = new KviTheoraDecoder(signal); + + m_pDecoder->addData(stream); } -int KviDccVideoJpegCodec::encodedFrameSize() +int KviDccVideoTheoraCodec::encodedFrameSize() { return 0; } -int KviDccVideoJpegCodec::decodedFrameSize() +int KviDccVideoTheoraCodec::decodedFrameSize() { return 0; -} +}
\ No newline at end of file diff --git a/src/modules/dcc/codec.h b/src/modules/dcc/codec.h index 68da97c8d..cff70073d 100644 --- a/src/modules/dcc/codec.h +++ b/src/modules/dcc/codec.h @@ -25,6 +25,7 @@ //============================================================================= #include "kvi_string.h" +#include "kvi_oggtheora.h" #include "kvi_databuffer.h" class KviDccVoiceCodec @@ -63,21 +64,27 @@ protected: KviStr m_szName; public: const char * name(); - virtual void encode(KviDataBuffer * signal,KviDataBuffer * stream); + virtual void encodeVideo(KviDataBuffer * videoSignal, KviDataBuffer * stream); + virtual void encodeAudio(KviDataBuffer * audioSignal, KviDataBuffer * stream); virtual void decode(KviDataBuffer * stream,KviDataBuffer * signal); virtual int encodedFrameSize(); virtual int decodedFrameSize(); }; -class KviDccVideoJpegCodec : public KviDccVideoCodec +class KviDccVideoTheoraCodec : public KviDccVideoCodec { public: - KviDccVideoJpegCodec(); - virtual ~KviDccVideoJpegCodec(); + KviDccVideoTheoraCodec(); + virtual ~KviDccVideoTheoraCodec(); public: - virtual void encode(KviDataBuffer * signal,KviDataBuffer * stream); + virtual void encodeVideo(KviDataBuffer * videoSignal, KviDataBuffer * stream); + virtual void encodeAudio(KviDataBuffer * audioSignal, KviDataBuffer * stream); virtual void decode(KviDataBuffer * stream,KviDataBuffer * signal); virtual int encodedFrameSize(); virtual int decodedFrameSize(); +private: + KviTheoraEncoder *m_pEncoder; + KviTheoraDecoder *m_pDecoder; }; + #endif //_CODEC_H_ diff --git a/src/modules/dcc/video.cpp b/src/modules/dcc/video.cpp index 1835af942..c0fc48633 100644 --- a/src/modules/dcc/video.cpp +++ b/src/modules/dcc/video.cpp @@ -53,16 +53,46 @@ extern KviDccBroker * g_pDccBroker; //Check for the *SS Api....we don't want to fail here... +#ifndef COMPILE_DISABLE_DCC_VOICE + #ifdef HAVE_LINUX_SOUNDCARD_H + #include <linux/soundcard.h> + #else + #ifdef HAVE_SYS_SOUNDCARD_H + #include <sys/soundcard.h> + #else + #ifdef HAVE_SOUNDCARD_H + #include <soundcard.h> + #else + //CAN NOT COMPILE :( + #define COMPILE_DISABLE_DCC_VOICE + #if !defined(COMPILE_ON_WINDOWS) && !defined(COMPILE_ON_MINGW) + #warning "Cannot find the soundcard.h header; you will NOT be able to use DCC Voice" + #endif + #endif + #endif + #endif +#endif + + +//#define KVI_AUDIO_DEVICE "/dev/dsp" +// 32 fragments , 512 bytes +#define KVI_SNDCTL_FRAG_SIZE 0x00B00009 +#define KVI_FRAGMENT_SIZE_IN_BYTES 512 +#define KVI_FORMAT AFMT_S16_LE +#define KVI_NUM_CHANNELS 1 + bool kvi_dcc_video_is_valid_codec(const char * codecName) { if(kvi_strEqualCI("jpeg",codecName))return true; + if(kvi_strEqualCI("theora",codecName))return true; return false; } -static KviDccVideoCodec * kvi_dcc_video_get_codec(const char *) +static KviDccVideoCodec * kvi_dcc_video_get_codec(const char *codecName) { + Q_UNUSED(codecName); // if(kvi_strEqualCI("jpeg",codecName))return new KviDccVideoJpegCodec(); - return new KviDccVideoJpegCodec(); + return new KviDccVideoTheoraCodec(); } @@ -76,6 +106,11 @@ KviDccVideoThread::KviDccVideoThread(KviWindow * wnd,kvi_socket_t fd,KviDccVideo m_pInfoMutex = new KviMutex(); m_bRecordingRequestPending = false; +#ifndef COMPILE_DISABLE_DCC_VOICE + m_bSoundcardChecked = false; + m_soundFd = -1; + m_soundFdMode = 0; +#endif //local video input if(!g_pVideoDevicePool) { @@ -106,6 +141,163 @@ KviDccVideoThread::~KviDccVideoThread() #endif } + +bool KviDccVideoThread::checkSoundcard() +{ +#ifdef COMPILE_DISABLE_DCC_VOICE + return false; +#else + bool bOpened = false; + if(m_soundFd == -1) + { + if(!openSoundcard(O_RDONLY))return false; + bOpened = true; + } + + int caps; + + m_bSoundcardChecked = true; + + if(ioctl(m_soundFd,SNDCTL_DSP_GETCAPS,&caps) < 0) + { + postMessageEvent(__tr2qs_ctx("WARNING: failed to check the soundcard duplex capabilities: if this is a half-duplex soundcard, use the DCC VOICE option to force half-duplex algorithm","dcc").toUtf8().data()); + if(bOpened)closeSoundcard(); + return false; + } + + if(!(caps & DSP_CAP_DUPLEX)) + { + m_pOpt->bForceHalfDuplex = true; // the device is half duplex...use it in that way + postMessageEvent(__tr2qs_ctx("Half duplex soundcard detected, you will not be able to talk and listen at the same time","dcc").toUtf8().data()); + } + + if(bOpened)closeSoundcard(); + + return true; +#endif +} + + +bool KviDccVideoThread::openSoundcard(int mode) +{ +#ifdef COMPILE_DISABLE_DCC_VOICE + return false; +#else + int speed = m_pOpt->iSampleRate; + static int chans=KVI_NUM_CHANNELS; + static int fmt=KVI_FORMAT; + static int frag = KVI_SNDCTL_FRAG_SIZE; + + + if(m_soundFd != -1) + { + if(m_soundFdMode == mode)return true; // already open + closeSoundcard(); + } + + m_soundFd = ::open(m_pOpt->szSoundDevice.ptr(),mode | O_NONBLOCK); + if(m_soundFd < 0)return false; + + if(!m_pOpt->bForceHalfDuplex) + { + if(ioctl(m_soundFd,SNDCTL_DSP_SETDUPLEX,0) < 0)goto exit_false; + } + + if(ioctl(m_soundFd,SNDCTL_DSP_SETFRAGMENT,&frag)<0)goto exit_false; + if(ioctl(m_soundFd,SNDCTL_DSP_SETFMT,&fmt)<0)goto exit_false; + if(ioctl(m_soundFd,SNDCTL_DSP_CHANNELS,&chans)<0)goto exit_false; + if(ioctl(m_soundFd,SNDCTL_DSP_SPEED,&speed)<0)goto exit_false; + if(speed != m_pOpt->iSampleRate) + { + KviStr tmp(KviStr::Format,__tr2qs_ctx("WARNING: failed to set the requested sample rate (%d): the device used closest match (%d)","dcc").toUtf8().data(), + m_pOpt->iSampleRate,speed); + postMessageEvent(tmp.ptr()); + } + + // TODO: #warning "We could also support blocking operations mode" + + m_soundFdMode = mode; + + + return true; + +exit_false: + closeSoundcard(); + return false; +#endif +} + +bool KviDccVideoThread::openSoundcardForWriting() +{ +#ifndef COMPILE_DISABLE_DCC_VOICE + return openSoundcardWithDuplexOption(O_WRONLY,O_RDONLY); +#else + return false; +#endif +} + +bool KviDccVideoThread::openSoundcardForReading() +{ +#ifndef COMPILE_DISABLE_DCC_VOICE + return openSoundcardWithDuplexOption(O_RDONLY,O_WRONLY); +#else + return false; +#endif +} + +bool KviDccVideoThread::openSoundcardWithDuplexOption(int openMode,int failMode) +{ +#ifndef COMPILE_DISABLE_DCC_VOICE + if(m_soundFd == -1) + { + // soundcard not open yet...open for write (at least) + if(m_pOpt->bForceHalfDuplex) + { + // Forcing half duplex... (user option or the card does not support full duplex mode) + if(!openSoundcard(openMode))return false; + } else { + // Try read/write open + if(!openSoundcard(O_RDWR)) + { + // half-duplex sound card ? + if(!m_bSoundcardChecked) + { + // We haven't checked the full-duplex support yet... + // Try to open in RDONLY o WRONLY mode + if(!openSoundcard(openMode))return false; + if(!checkSoundcard()) + { + postMessageEvent(__tr2qs_ctx("Ops...failed to test the soundcard capabilities...expect problems...","dcc").toUtf8().data()); + } + } // else the test has been done and it is a full duplex card that is just busy + } + } + } else { + // Hmmm...already open + // If it is open in O_RDWR or O_WRONLY mode...it is ok for us + // but if it is open in O_RDONLY mode...we can do nothing...just wait + return (m_soundFdMode != failMode); + } + + return true; +#else + return false; +#endif +} + +void KviDccVideoThread::closeSoundcard() +{ +#ifndef COMPILE_DISABLE_DCC_VOICE + if(m_soundFd != -1) + { + ::close(m_soundFd); + m_soundFd = -1; + m_soundFdMode = 0; + } +#endif +} + + bool KviDccVideoThread::readWriteStep() { #ifndef COMPILE_DISABLE_DCC_VIDEO @@ -118,11 +310,11 @@ bool KviDccVideoThread::readWriteStep() while(bCanRead) { unsigned int actualSize = m_inFrameBuffer.size(); - m_inFrameBuffer.resize(actualSize + 1024); - int readLen = kvi_socket_recv(m_fd,(void *)(m_inFrameBuffer.data() + actualSize),1024); + m_inFrameBuffer.resize(actualSize + 4096); + int readLen = kvi_socket_recv(m_fd,(void *)(m_inFrameBuffer.data() + actualSize),4096); if(readLen > 0) { - if(readLen < 1024)m_inFrameBuffer.resize(actualSize + readLen); + if(readLen < 4096)m_inFrameBuffer.resize(actualSize + readLen); m_pOpt->pCodec->decode(&m_inFrameBuffer,&m_inSignalBuffer); //#warning "A maximum length for the signal buffer is actually needed!!!" } else { @@ -160,11 +352,10 @@ bool KviDccVideoThread::videoStep() { if(m_inSignalBuffer.size() > 0) { - QImage img; - img.loadFromData(m_inSignalBuffer.data(), m_inSignalBuffer.size()); + QImage img(m_inSignalBuffer.data(), 320, 240, 1280, QImage::Format_ARGB32); +// qDebug("IMG data %p size %d", m_inSignalBuffer.data(), m_inSignalBuffer.size()); if(!img.isNull()) m_inImage = img; - m_inSignalBuffer.clear(); } } @@ -174,22 +365,177 @@ bool KviDccVideoThread::videoStep() //grab the image, compress using the codec g_pVideoDevicePool->getFrame(); g_pVideoDevicePool->getImage(&m_outImage); + if(m_outImage.numBytes() > 0) + { + m_videoOutSignalBuffer.append((const unsigned char*) m_outImage.bits(), m_outImage.numBytes()); + m_pOpt->pCodec->encodeVideo(&m_videoOutSignalBuffer,&m_outFrameBuffer); + } + } - QByteArray ba; - QBuffer buffer(&ba); - buffer.open(QIODevice::WriteOnly); - //0 to obtain small compressed files, - //100 for large uncompressed files, - //and -1 (the default) to use the default settings. - m_outImage.save(&buffer, "JPEG", 20); -// qDebug("framesize %d",ba.size()); - if(ba.size() > 0) +#endif // !COMPILE_DISABLE_DCC_VIDEO + return true; +} + +bool KviDccVideoThread::audioStep() +{ +#ifndef COMPILE_DISABLE_DCC_VIDEO +#ifndef COMPILE_DISABLE_DCC_VOICE + // Are we playing ? +/* + if(m_bPlaying) + { + // Do we have something to write ? + audio_buf_info info; + if(m_inSignalBuffer.size() > 0) { - m_outSignalBuffer.append((const unsigned char*) ba.data(), ba.size()); - m_pOpt->pCodec->encode(&m_outSignalBuffer,&m_outFrameBuffer); + // Get the number of fragments that can be written to the soundcard without blocking + + if(ioctl(m_soundFd,SNDCTL_DSP_GETOSPACE,&info) < 0) + { + debug("get o space failed"); + info.bytes = KVI_FRAGMENT_SIZE_IN_BYTES; // dummy... if this is not correct...well...we will block for 1024/16000 of a sec + info.fragments = 1; + info.fragsize = KVI_FRAGMENT_SIZE_IN_BYTES; + } + if(info.fragments > 0) + { + int toWrite = info.fragments * info.fragsize; + //debug("Can write %d bytes",toWrite); + if(m_inSignalBuffer.size() < toWrite)toWrite = m_inSignalBuffer.size(); + int written = write(m_soundFd,m_inSignalBuffer.data(),toWrite); + if(written > 0)m_inSignalBuffer.remove(written); + else { +//#warning "Do something for -1 here ?" +//#warning "Usleep ?" + } + } //else { + // No stuff can be written...we are running too fast ? + // m_uSleepTime += 100; // sleep for a while + //} + } else { + // hmmmm....playing , but nothing to write , possible underrun or EOF + // a nice idea would be to use SNDCTL_DSP_GETODELAY here... + // but it appears to be broken on some audio devices + if(ioctl(m_soundFd,SNDCTL_DSP_GETOSPACE,&info) < 0)info.fragstotal = info.fragments; // dummy...but what should we do ? + if(info.fragstotal == info.fragments) + { + // underrun or EOF: close the device + stopPlaying(); + } + } + } else { + // do we have anything to play ? + if(m_inSignalBuffer.size() > 0) + { + if(m_inSignalBuffer.size() >= m_pOpt->iPreBufferSize) + { + // yep...stuff to play... open the soundcard , if possible + startPlaying(); + + m_iLastSignalBufferSize = m_inSignalBuffer.size(); + } else { + // have stuff to play , but it's not enough to fill the pre-buffer + // + struct timeval tv; + gettimeofday(&tv,0); + + long int sigBufferTime = (tv.tv_sec * 1000) + (tv.tv_usec / 1000); + + if(m_inSignalBuffer.size() == m_iLastSignalBufferSize) + { + // the same signal buffer size... check the time + // m_pOpt->iPreBufferSize / 16 gives us the preBufferTime in msecs + // we calc the remaining preBufferTime by subtracting the + // size of buffer already filled and we also add 50 milliseconds... smart heuristic + int preBufferTime = ((m_pOpt->iPreBufferSize - m_iLastSignalBufferSize) / 16) + 50; + // if the buffer size hasn't changed since preBufferTime + // it's time to start playing anyway, since there is + // either a network stall or it was just a really short data stream + if((sigBufferTime - m_iLastSignalBufferTime) > preBufferTime) + { + startPlaying(); + if(m_bPlaying)m_iLastSignalBufferSize = 0; + } + } else { + // signal buffer size differs...we have received new packets + // and still pre-buffering + m_iLastSignalBufferSize = m_inSignalBuffer.size(); + m_iLastSignalBufferTime = sigBufferTime; + } + } + } } +*/ + // Are we recording ? + if(m_bRecording) + { + fd_set rs; + FD_ZERO(&rs); + FD_SET(m_soundFd,&rs); + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 10; + int ret = select(m_soundFd + 1,&rs,0,0,&tv); + if(ret > 0) + { + // This is rather easy... + audio_buf_info info; + if(ioctl(m_soundFd,SNDCTL_DSP_GETISPACE,&info) < 0) + { + debug("Ispace failed"); + info.fragments = 0; // dummy... + info.bytes = 0; + } + //debug("INFO: fragments: %d, fragstotal: %d, fragsize: %d, bytes: %d",info.fragments,info.fragstotal,info.fragsize,info.bytes); + + if(info.fragments == 0 && info.bytes == 0) + { + // force a dummy read: the device needs to be triggered + info.fragments = 1; + } + + if(info.fragments > 0) + { + int oldSize = m_audioOutSignalBuffer.size(); + int available = info.fragments * info.fragsize; + m_audioOutSignalBuffer.addSize(available); + int readed = read(m_soundFd,m_audioOutSignalBuffer.data() + oldSize,available); + + if(readed < available) + { + // huh ? ...error ? + if(readed >= 0)m_audioOutSignalBuffer.resize(oldSize + readed); + else { + if((errno == EINTR) || (errno == EAGAIN)) + { + m_audioOutSignalBuffer.resize(oldSize); + } else { +//#warning "Critical error...do something reasonable!" + m_audioOutSignalBuffer.resize(oldSize); + } + } + } +/* + debug("Signal buffer:"); + for(int i=0;i<200;i+=2) + { + if(i >= m_outSignalBuffer.size())break; + printf("%04x ",*(((unsigned short *)(m_outSignalBuffer.data() + i)))); + if((i % 6) == 0)printf("\n"); + } + debug("END\n"); +*/ + m_pOpt->pCodec->encodeAudio(&m_audioOutSignalBuffer,&m_outFrameBuffer); + } + }// else { + // Nothing to read + // m_uSleepTime += 100; + // } + } + +#endif // !COMPILE_DISABLE_DCC_VOICE #endif // !COMPILE_DISABLE_DCC_VIDEO return true; } @@ -199,9 +545,11 @@ void KviDccVideoThread::startRecording() #ifndef COMPILE_DISABLE_DCC_VIDEO //debug("Start recording"); if(m_bRecording)return; // already started + + //FIXME this is mandatory only for audio! + openSoundcardForReading(); + - //if(openSoundcardForReading()) - if(1) { // debug("Posting event"); KviThreadDataEvent<int> * e = new KviThreadDataEvent<int>(KVI_DCC_THREAD_EVENT_ACTION); @@ -210,8 +558,8 @@ void KviDccVideoThread::startRecording() m_bRecording = true; m_bRecordingRequestPending = false; - } else { - m_bRecordingRequestPending = true; +// } else { +// m_bRecordingRequestPending = true; } #endif } @@ -228,7 +576,8 @@ void KviDccVideoThread::stopRecording() postEvent(KviDccThread::parent(),e); m_bRecording = false; -// if(!m_bPlaying)closeSoundcard(); + //FIXME this is mandatory only if we are using audio + if(!m_bPlaying)closeSoundcard(); #endif } @@ -237,12 +586,15 @@ void KviDccVideoThread::startPlaying() #ifndef COMPILE_DISABLE_DCC_VIDEO //debug("Start playing"); if(m_bPlaying)return; - - KviThreadDataEvent<int> * e = new KviThreadDataEvent<int>(KVI_DCC_THREAD_EVENT_ACTION); - e->setData(new int(KVI_DCC_VIDEO_THREAD_ACTION_START_PLAYING)); - postEvent(KviDccThread::parent(),e); - m_bPlaying = true; - + //FIXME this is mandatory only if we are using audio + openSoundcardForWriting(); + //if() + { + KviThreadDataEvent<int> * e = new KviThreadDataEvent<int>(KVI_DCC_THREAD_EVENT_ACTION); + e->setData(new int(KVI_DCC_VIDEO_THREAD_ACTION_START_PLAYING)); + postEvent(KviDccThread::parent(),e); + m_bPlaying = true; + } #endif } @@ -257,6 +609,8 @@ void KviDccVideoThread::stopPlaying() postEvent(KviDccThread::parent(),e); m_bPlaying = false; + //FIXME this is mandatory only if we are using audio + if(!m_bRecording)closeSoundcard(); #endif } @@ -288,16 +642,15 @@ void KviDccVideoThread::run() } if(!readWriteStep())goto exit_dcc; + //if(!videoStep() || !audioStep())goto exit_dcc; if(!videoStep())goto exit_dcc; m_pInfoMutex->lock(); -// m_iInputBufferSize = m_inSignalBuffer.size(); -// m_iOutputBufferSize = m_outSignalBuffer.size(); m_pInfoMutex->unlock(); //if(m_uSleepTime)usleep(m_uSleepTime); - //qDebug("in %d out %d in_sig %d out_sig %d", m_inFrameBuffer.size(), m_outFrameBuffer.size(), m_inSignalBuffer.size(), m_outSignalBuffer.size()); -// usleep(200); +// qDebug("in %d out %d in_sig %d out_sig %d", m_inFrameBuffer.size(), m_outFrameBuffer.size(), m_inSignalBuffer.size(), m_videoOutSignalBuffer.size()); + usleep(200); // Start recording if the request was not fulfilled yet if(m_bRecordingRequestPending)startRecording(); @@ -594,6 +947,10 @@ void KviDccVideo::connected() KviDccVideoThreadOptions * opt = new KviDccVideoThreadOptions; opt->pCodec = kvi_dcc_video_get_codec(m_pDescriptor->szCodec.ptr()); + opt->bForceHalfDuplex = KVI_OPTION_BOOL(KviOption_boolDccVoiceForceHalfDuplex); + opt->iPreBufferSize = KVI_OPTION_UINT(KviOption_uintDccVoicePreBufferSize); + opt->szSoundDevice = KVI_OPTION_STRING(KviOption_stringDccVoiceSoundDevice).toUtf8().data(); + opt->iSampleRate = 8000; output(KVI_OUT_DCCMSG,__tr2qs_ctx("Actual codec used is '%s'","dcc"),opt->pCodec->name()); diff --git a/src/modules/dcc/video.h b/src/modules/dcc/video.h index 40967aada..02f93cd77 100644 --- a/src/modules/dcc/video.h +++ b/src/modules/dcc/video.h @@ -62,6 +62,10 @@ typedef struct _KviDccVideoThreadOptions { QString szVideoDevice; KviDccVideoCodec * pCodec; + bool bForceHalfDuplex; + int iPreBufferSize; + int iSampleRate; + KviStr szSoundDevice; } KviDccVideoThreadOptions; class KviDccVideoThread : public KviDccThread @@ -75,7 +79,8 @@ protected: KviDataBuffer m_outFrameBuffer; KviDataBuffer m_inFrameBuffer; KviDataBuffer m_inSignalBuffer; - KviDataBuffer m_outSignalBuffer; + KviDataBuffer m_videoOutSignalBuffer; + KviDataBuffer m_audioOutSignalBuffer; bool m_bPlaying; bool m_bRecording; bool m_bRecordingRequestPending; @@ -84,18 +89,30 @@ protected: // stuff protected by the mutex: int m_iInputBufferSize; int m_iOutputBufferSize; + //audio + int m_soundFd; + int m_soundFdMode; + bool m_bSoundcardChecked; protected: QImage m_inImage; QImage m_outImage; protected: bool readWriteStep(); bool videoStep(); + bool audioStep(); void startRecording(); void stopRecording(); void startPlaying(); void stopPlaying(); inline bool isPlaying() { return m_bPlaying; }; virtual void run(); + //audio + bool checkSoundcard(); + bool openSoundcardWithDuplexOption(int openMode,int failMode); + bool openSoundcard(int mode); + bool openSoundcardForWriting(); + bool openSoundcardForReading(); + void closeSoundcard(); }; |
