///////////////////////////////////////////////////////////////////////////// // LocalTTSEngineSite.cpp: implementation of the CLocalTTSEngineSite class. // // Created by JOEM 02-2000 // Copyright (C) 2000 Microsoft Corporation // All Rights Reserved // //////////////////////////////////////////////////////////// JOEM 02-2000 // #include "LocalTTSEngineSite.h" #include #include #include "vapiIO.h" // If buffer is used, the min size is one sec const int g_iMinBufferSize = 1; const int g_iMaxBufferSize = 60; // let's keep it reasonable, ok! const float g_dMinBufferShift = .25; const double g_dChunkSize = .10; const int g_iBase = 16; // Default duration of chunks written from buffer to SAPI ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite // // Construction/Destruction // //////////////////////////////////////////////////////////// JOEM 02-2000 // CLocalTTSEngineSite::CLocalTTSEngineSite() { m_vcRef = 1; m_pMainOutputSite = NULL; m_pOutputFormatId = NULL; m_pOutputFormat = NULL; m_pcBuffer = NULL; m_ulBufferBytes = 0; m_ulBufferSeconds = 0; m_ulMinBufferShift = 0; m_ulDataEnd = 0; m_ulCurrentByte = 0; m_ulSkipForward = 0; m_pEventQueue = NULL; m_pCurrentEvent = NULL; m_pLastEvent = NULL; m_ullTotalBytesReceived = 0; m_ullPreviousBytesReceived = 0; m_ullBytesWritten = 0; m_ullBytesWrittenToSAPI = 0; m_lTotalBytesSkipped = 0; m_pTsm = NULL; m_flRateAdj = 1.0; // Regular rate unless user changes it. m_flVol = 1.0; // Full volume unless user changes it. } CLocalTTSEngineSite::~CLocalTTSEngineSite() { m_pMainOutputSite = NULL; if ( m_pOutputFormat ) { free( m_pOutputFormat ); m_pOutputFormat = NULL; m_pOutputFormatId = NULL; } Event* pEvent = m_pEventQueue; Event* pNextEvent = NULL; while ( pEvent ) { pNextEvent = pEvent->pNext; delete pEvent; pEvent = pNextEvent; } if ( m_pcBuffer ) { delete [] m_pcBuffer; m_pcBuffer = NULL; } if ( m_pTsm ) { delete m_pTsm; m_pTsm = NULL; } } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::QueryInterface // // //////////////////////////////////////////////////////////// JOEM 02-2000 // STDMETHODIMP CLocalTTSEngineSite::QueryInterface ( REFIID iid, void** ppv ) { if ( iid == IID_ISpTTSEngineSite ) { *ppv = (ISpTTSEngineSite*) this; } else if ( iid == IID_ISpTTSEngineSite ) { *ppv = (ISpTTSEngineSite*) this; } else if ( iid == IID_ISpEventSink ) { *ppv = (ISpEventSink*) this; } else { *ppv = NULL; return E_NOINTERFACE; } ((IUnknown*) *ppv)->AddRef(); return S_OK; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::AddRef //////////////////////////////////////////////////////////// JOEM 02-2000 // ULONG CLocalTTSEngineSite::AddRef(void) { return InterlockedIncrement(&m_vcRef); } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::Release //////////////////////////////////////////////////////////// JOEM 02-2000 // ULONG CLocalTTSEngineSite::Release(void) { if ( 0 == InterlockedDecrement(&m_vcRef) ) { delete this; return 0; } return (ULONG) m_vcRef; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::SetOutputSite // // Sets pointer to main SAPI output site. // Sets/resets data buffer and accumulators to zero. // //////////////////////////////////////////////////////////// JOEM 01-2001 // void CLocalTTSEngineSite::SetOutputSite(ISpTTSEngineSite* pOutputSite) { SPDBG_FUNC( "CLocalTTSEngineSite::SetOutputSite" ); SPDBG_ASSERT(pOutputSite); m_pMainOutputSite = pOutputSite; // flush the buffer m_ulCurrentByte = 0; m_ulDataEnd = 0; m_ulSkipForward = 0; memset( m_pcBuffer, 0, m_ulBufferBytes * sizeof(char) ); // flush the event queue and start over Event* pEvent = m_pEventQueue; Event* pNextEvent = NULL; while ( pEvent ) { pNextEvent = pEvent->pNext; delete pEvent; pEvent = pNextEvent; } m_pEventQueue = NULL; m_pCurrentEvent = NULL; m_pLastEvent = NULL; // reset counters m_ullTotalBytesReceived = 0; m_ullPreviousBytesReceived = 0; m_ullBytesWritten = 0; m_ullBytesWrittenToSAPI = 0; m_lTotalBytesSkipped = 0; m_flRateAdj = 1.0; // Regular rate unless user changes it. m_flVol = 1.0; // Full volume unless user changes it. } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::SetBufferSize // // Sets the size of the audio data buffer, and creates the buffer if the // output format is known (but Text output is not buffered). // // **Destroys contents of existing buffer.** That's ok, because this function // is not exposed to the application -- just to the engine, where speak calls // are synchronous. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::SetBufferSize(const ULONG ulSeconds) { SPDBG_FUNC( "CLocalTTSEngineSite::SetBufferSize" ); HRESULT hr = S_OK; // don't need to preserve contents of old buffer -- it's flushed between // speak calls anyway. if ( m_pcBuffer ) { delete [] m_pcBuffer; m_pcBuffer = NULL; } m_ulBufferBytes = 0; m_ulBufferSeconds = 0; m_ulMinBufferShift = 0; m_ulDataEnd = 0; m_ulCurrentByte = 0; if ( ulSeconds ) { if ( ulSeconds < g_iMinBufferSize ) { m_ulBufferSeconds = g_iMinBufferSize; } else if ( ulSeconds > g_iMaxBufferSize ) { m_ulBufferSeconds = g_iMaxBufferSize; } else { m_ulBufferSeconds = ulSeconds; } // check format -- don't set a buffer for text format if ( m_pOutputFormatId ) { if ( *m_pOutputFormatId != SPDFID_Text ) { // if the wav format is defined, allocate the buffer for the correct #secs of data if ( m_pOutputFormat ) { m_ulBufferBytes = m_ulBufferSeconds * m_pOutputFormat->nAvgBytesPerSec; m_pcBuffer = new char[m_ulBufferBytes]; if ( !m_pcBuffer ) { m_ulBufferBytes = 0; m_ulBufferSeconds = 0; hr = E_OUTOFMEMORY; } if ( SUCCEEDED(hr) ) { // Make the min shift approximately 1/4 of buffer // When the buffer is full, data shifts off the end by this amount. m_ulMinBufferShift = g_dMinBufferShift * m_ulBufferBytes; // data must shift by multiples of nBlockAlign m_ulMinBufferShift -= ( m_ulMinBufferShift % m_pOutputFormat->nBlockAlign ); } } } else { m_ulBufferSeconds = 0; // can't buffer text - but not an error(?) } } } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::SetOutputFormat // // Sets the format in which samples are sent to SAPI. // Also creates the rate changer. // ////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::SetOutputFormat(const GUID *pOutputFormatId, const WAVEFORMATEX *pOutputFormat) { SPDBG_FUNC( "CLocalTTSEngineSite::SetOutputFormat" ); HRESULT hr = S_OK; SPDBG_ASSERT(pOutputFormatId); if ( pOutputFormatId && *pOutputFormatId == SPDFID_Text ) { m_pOutputFormatId = pOutputFormatId; SetBufferSize(0); // deletes the current buffer, if any (can't buffer text) if ( m_pOutputFormat ) { delete m_pOutputFormat; m_pOutputFormat = NULL; } } else if ( pOutputFormat ) { // Do we need to change the format? if ( !m_pOutputFormat || m_pOutputFormat->wFormatTag != pOutputFormat->wFormatTag || m_pOutputFormat->nChannels != pOutputFormat->nChannels || m_pOutputFormat->nSamplesPerSec != pOutputFormat->nSamplesPerSec || m_pOutputFormat->nAvgBytesPerSec != pOutputFormat->nAvgBytesPerSec || m_pOutputFormat->nBlockAlign != pOutputFormat->nBlockAlign || m_pOutputFormat->wBitsPerSample != pOutputFormat->wBitsPerSample || m_pOutputFormat->cbSize != pOutputFormat->cbSize ) { // save these values to determine whether buffer and/or rate changer need adjusting // for the new format DWORD oldnAvgBytesPerSec = 0; DWORD oldnSamplesPerSec = 0; if ( m_pOutputFormat ) { oldnAvgBytesPerSec = m_pOutputFormat->nAvgBytesPerSec; oldnSamplesPerSec = m_pOutputFormat->nSamplesPerSec; } // free the current waveformatex if ( m_pOutputFormat ) { free(m_pOutputFormat); m_pOutputFormat = NULL; } // this needs to copy the output format, not just point to it, // because engine will pass in const pointer. m_pOutputFormat = (WAVEFORMATEX*) malloc( sizeof(WAVEFORMATEX) + pOutputFormat->cbSize ); if ( !m_pOutputFormat ) { hr = E_OUTOFMEMORY; } else { m_pOutputFormatId = pOutputFormatId; memcpy(m_pOutputFormat, pOutputFormat, sizeof(WAVEFORMATEX) + pOutputFormat->cbSize ); } if ( SUCCEEDED(hr) ) { // Re-adjust the buffer size for the new format, if necessary if ( oldnAvgBytesPerSec != m_pOutputFormat->nAvgBytesPerSec ) { hr = SetBufferSize(m_ulBufferSeconds); } } // once we know the sampling frequency, we can create the rate changer if ( SUCCEEDED(hr) ) { if ( !m_pTsm ) { m_pTsm = new CTsm( g_dRateScale[BASE_RATE], m_pOutputFormat->nSamplesPerSec ); } if ( !m_pTsm ) { hr = E_OUTOFMEMORY; } if ( SUCCEEDED(hr) ) { // Reset the rate changer sampling frequency, if necessary if ( oldnSamplesPerSec != m_pOutputFormat->nSamplesPerSec ) { if ( m_pTsm->SetSamplingFrequency( m_pOutputFormat->nSamplesPerSec ) != m_pOutputFormat->nSamplesPerSec ) { delete m_pTsm; m_pTsm = NULL; hr = E_FAIL; } } } } } } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::GetActions // // When using a buffer, mask all actions except abort. This module handles // the real-time changes, so it makes the engine output regular rate and vol // all the time. (But the engine handles XML-based rate/vol changes.) // // When not buffering, pass the call to SAPI, but OR'd with vol and rate. // //////////////////////////////////////////////////////////// JOEM 02-2000 // DWORD CLocalTTSEngineSite::GetActions( void ) { if ( m_pcBuffer ) { return m_pMainOutputSite->GetActions() & SPVES_ABORT; } else { // When prompt eng calls GetVolume or GetRate, SAPI turns off the // SPVES_VOLUME/SPVES_RATE flags. OR them here to force the TTS engine // to call GetRate and GetVolume too. return m_pMainOutputSite->GetActions() | SPVES_VOLUME | SPVES_RATE; } } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::GetRate (ISpTTSEngineSite::GetRate) // // When using a buffer, simply sets plRateAdjust to 0 to make the engine // use normal rate (module controls real-time rate changes). // // When not buffering, pass the call to SAPI. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::GetRate ( long* plRateAdjust ) { SPDBG_FUNC( "CLocalTTSEngineSite::GetRate" ); HRESULT hr = S_OK; if ( m_pcBuffer ) { *plRateAdjust = 0; } else { hr = m_pMainOutputSite->GetRate ( plRateAdjust ); } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::GetVolume (ISpTTSEngineSite::GetVolume) // // When using a buffer, simply sets punVolume to 100 to make the engine use // full volume (module controls real-time vol changes). // // When not buffering, pass the call to SAPI. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::GetVolume ( USHORT* punVolume ) { SPDBG_FUNC( "CLocalTTSEngineSite::GetVolume" ); HRESULT hr = S_OK; if ( m_pcBuffer ) { *punVolume = 100; } else { hr = m_pMainOutputSite->GetVolume ( punVolume ); } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::GetSkipInfo (ISpTTSEngineSite::GetSkipInfo) // // When using a buffer, simply returns 0 to prevent the engine from skipping // (module controls skips). // // When not buffering, pass the call to SAPI. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::GetSkipInfo ( SPVSKIPTYPE* peType, long* plNumItems ) { SPDBG_FUNC( "CLocalTTSEngineSite::GetSkipInfo" ); HRESULT hr = S_OK; if ( m_pcBuffer ) { *peType = SPVST_SENTENCE; *plNumItems = 0; } else { hr = m_pMainOutputSite->GetSkipInfo ( peType, plNumItems ); } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::CompleteSkip (ISpTTSEngineSite::CompleteSkip) // // When using a buffer, this just returns. // // When not buffering, pass the call to SAPI. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::CompleteSkip ( long lNumSkipped ) { SPDBG_FUNC( "CLocalTTSEngineSite::CompleteSkip" ); HRESULT hr = S_OK; if ( !m_pcBuffer ) { hr = m_pMainOutputSite->CompleteSkip ( lNumSkipped ); } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::AddEvents (ISpTTSEngineSite::AddEvents) // // Two engines (Prompt and TTS) might be writing to this output site. // Each will not know that the other is sending data, so events // won't have the correct ullAudioStreamOffset. // // Consequently, CLocalTTSEngineSite::UpdateBytesWritten must be called // after each time the Prompt engine issues a TTS->speak call and after // each time the PE calls CLocalTTSEngineSite::Write. // // Note that when this is used by a single engine, there is no need to // (or harm in) calling UpdateBytesWritten. Consequently, TTS engines // require no modification -- the Prompt Engine manages AudioStreamOffset // when there are 2 engines writing data. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::AddEvents(const SPEVENT* pEventArray, ULONG ulCount ) { SPDBG_FUNC( "CLocalTTSEngineSite::AddEvents" ); HRESULT hr = S_OK; ULONG i = 0; SPDBG_ASSERT(pEventArray || !ulCount); // Are we buffering? if ( m_pcBuffer ) { Event* pNewEvent = NULL; for ( i=0; SUCCEEDED(hr) && ipPrev = NULL; pNewEvent->pNext = NULL; memcpy(&pNewEvent->event, &pEventArray[i], sizeof(SPEVENT) ); // ullAudioStreamOffset adjusted here since two engines may be writing data pNewEvent->event.ullAudioStreamOffset += m_ullPreviousBytesReceived; // Put it in the event queue, sorted // Is this the only event in the queue? if ( !m_pEventQueue ) { m_pEventQueue = pNewEvent; m_pCurrentEvent = pNewEvent; m_pLastEvent = pNewEvent; } else { // Does the new event go last? if ( m_pLastEvent && m_pLastEvent->event.ullAudioStreamOffset <= pNewEvent->event.ullAudioStreamOffset ) { m_pLastEvent->pNext = pNewEvent; pNewEvent->pPrev = m_pLastEvent; m_pLastEvent = pNewEvent; } else { // figure out where it goes -- most likely it goes near the end, so step // backward through the list Event* pInsertHere = m_pLastEvent; while ( pInsertHere ) { if ( pInsertHere->event.ullAudioStreamOffset <= pNewEvent->event.ullAudioStreamOffset ) { pNewEvent->pPrev = pInsertHere; pInsertHere->pNext->pPrev = pNewEvent; pNewEvent->pNext = pInsertHere->pNext; pInsertHere->pNext = pNewEvent; } else { pInsertHere = pInsertHere->pPrev; } } // did we reach the front of the list? if ( !pInsertHere ) { // insert it at the beginning pNewEvent->pNext = m_pEventQueue; m_pEventQueue->pPrev = pNewEvent; m_pEventQueue = pNewEvent; } } // else } // else //if there's no current event, make the first in this array current. if ( !m_pCurrentEvent ) { m_pCurrentEvent = pNewEvent; } } // if ( SUCCEEDED(hr) ) } // for ( i=0; SUCCEEDED(hr) && i (pEventArray); for ( i=0; iAddEvents(pEvents, ulCount); } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::SendEvents // // Steps through the event buffer, sending events that should occur within // the current block of data. // // The event's ullAudioStreamOffset must be adjust by the count of bytes // skipped (includes skip forward and backward). The sum must then be scaled // to match the cumulative real-time rate adjustments that have occurred // (ratio of bytes-sent-to-SAPI to the unaltered byte count). // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::SendEvents() { SPDBG_FUNC( "CLocalTTSEngineSite::SendEvents" ); HRESULT hr = S_OK; ULONG ulEventPos = 0; ULONG ulOriginalOffset = 0; float flRateAdj = 1.0; // was there a previous rate adjustment to factor in? if ( m_ullBytesWrittenToSAPI != m_ullBytesWritten ) { flRateAdj = (float)m_ullBytesWrittenToSAPI / m_ullBytesWritten; } while ( SUCCEEDED(hr) && m_pCurrentEvent ) { // only send events up to the current byte count if ( m_pCurrentEvent->event.ullAudioStreamOffset <= (m_ullBytesWritten + m_lTotalBytesSkipped) ) { ulOriginalOffset = m_pCurrentEvent->event.ullAudioStreamOffset; // Adjust the cb by amount skipped // BytesSkipped should never be greater than the current event's position!! // If this happens, RescheduleEvents is not working properly. SPDBG_ASSERT(m_lTotalBytesSkipped <= (long)m_pCurrentEvent->event.ullAudioStreamOffset); m_pCurrentEvent->event.ullAudioStreamOffset -= m_lTotalBytesSkipped; // Adjust by proportion rate change m_pCurrentEvent->event.ullAudioStreamOffset *= flRateAdj; // Make sure it's a multiple of nBlockAlign m_pCurrentEvent->event.ullAudioStreamOffset -= (m_pCurrentEvent->event.ullAudioStreamOffset % m_pOutputFormat->nBlockAlign ); hr = m_pMainOutputSite->AddEvents(&m_pCurrentEvent->event, 1); // restore the original in case we need to skip back m_pCurrentEvent->event.ullAudioStreamOffset = ulOriginalOffset; m_pCurrentEvent = m_pCurrentEvent->pNext; } else { break; } } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::RescheduleEvents // // Resets the pointer to the current event. // //////////////////////////////////////////////////////////// JOEM 01-2001 // void CLocalTTSEngineSite::RescheduleEvents(Event* pStart) { SPDBG_FUNC( "CLocalTTSEngineSite::RescheduleEvents" ); Event* pEvent = pStart; // Reschedules (or skips) events based on bytes skipped (forward or backward) while ( pEvent && ( pEvent->event.ullAudioStreamOffset < m_ullBytesWritten + m_lTotalBytesSkipped ) ) { pEvent = pEvent->pNext; } m_pCurrentEvent = pEvent; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::FlushEvents // // Some events are flushed when corresponding audio data is pushed off the // end of the buffer. // //////////////////////////////////////////////////////////// JOEM 01-2001 // void CLocalTTSEngineSite::FlushEvents(const ULONG cb) { SPDBG_FUNC( "CLocalTTSEngineSite::FlushEvents" ); Event* pEvent = m_pEventQueue; Event* pNextEvent = NULL; while ( pEvent && pEvent->event.ullAudioStreamOffset <= cb ) { pNextEvent = pEvent->pNext; delete pEvent; pEvent = pNextEvent; if ( pEvent ) { pEvent->pPrev = NULL; } } if ( pEvent ) { m_pEventQueue = pEvent; } else { m_pEventQueue = NULL; m_pCurrentEvent = NULL; m_pLastEvent = NULL; } } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::Write // // Copies the data (pBuff) onto the end of the audio buffer (at current empty // location), pushing data off the front of the buffer to make room, // if necessary. // // If buffer size is too small, some of the data is written directly to // the SAPI output site and the rest will be buffered. // // If no buffer exists, all of the data is written directly to SAPI. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::Write( const void* pBuff, ULONG cb, ULONG *pcbWritten ) { SPDBG_FUNC( "CLocalTTSEngineSite::Write" ); HRESULT hr = S_OK; long lSpaceNeeded = 0; char* pcStart = NULL; ULONG ulBytesToWrite = 0; bool fAbort = false; char* pcBuff = (char*)pBuff; pcStart = pcBuff; m_ullTotalBytesReceived += cb; // Are we buffering the samples, or just sending them? if ( m_pcBuffer ) { // Do we need extra space? lSpaceNeeded = ( m_ulDataEnd + cb ) - m_ulBufferBytes; // if out of space, push some off the front to make room if ( lSpaceNeeded > 0 ) { // Is the buffer big enough? if ( cb > m_ulBufferBytes ) { ulBytesToWrite = cb - m_ulBufferBytes; hr = WriteToSAPI( pcBuff, ulBytesToWrite, &fAbort ); if ( SUCCEEDED(hr) ) { // we're going to replace the entire buffer with what's left // so the current data in the buffer is garbage now. m_ulDataEnd = 0; pcStart = &pcBuff[ulBytesToWrite]; // make pcStart point to what's left after WriteToSapi // Flush the entire event buffer FlushEvents(m_ullBytesWritten + m_lTotalBytesSkipped); } } else { // how much should we shift off the end? lSpaceNeeded = ( lSpaceNeeded > m_ulMinBufferShift ) ? lSpaceNeeded : m_ulMinBufferShift; // Move the stuff we're saving to the beginning of the buffer (overwrite the shifted amount) memmove( m_pcBuffer, &m_pcBuffer[ lSpaceNeeded ], (m_ulDataEnd - lSpaceNeeded) * sizeof(char) ); // The data end now shifts toward front of buffer m_ulDataEnd -= lSpaceNeeded; pcStart = pcBuff; // make pcStart point to all of the new data // Flush everything received (minus the new bytes) minus what's left to buffer FlushEvents(m_ullTotalBytesReceived - cb - (m_ulBufferBytes - lSpaceNeeded)); } } if ( SUCCEEDED(hr) ) { // go to the end of the data we're keeping m_ulCurrentByte = m_ulDataEnd; // copy new data onto end of buffer. memcpy( &m_pcBuffer[m_ulCurrentByte], pcStart, cb - ulBytesToWrite ); // Mark the new end m_ulDataEnd += (cb - ulBytesToWrite); if ( !fAbort ) { hr = WriteBuffer(); } } } else { hr = WriteToSAPI( pcBuff, cb, &fAbort ); } if ( SUCCEEDED(hr) && pcbWritten ) { *pcbWritten = cb; } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::WriteBuffer // // Sends data from the buffer to SAPI (via WriteToSAPI) in small chunks, // checking for skip/abort actions. (WriteToSAPI does vol/rate changes). // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::WriteBuffer() { SPDBG_FUNC( "CLocalTTSEngineSite::WriteBuffer" ); HRESULT hr = S_OK; ULONG nBytesToWrite = 0; bool fAbort = false; ULONG ulChunkSize = 0; ulChunkSize = g_dChunkSize * m_pOutputFormat->nAvgBytesPerSec; ulChunkSize -= (ulChunkSize % m_pOutputFormat->nBlockAlign); // write in multiples of nBlockAlign // Try to write one full chunk (approx. 0.1 sec) of audio per loop while ( SUCCEEDED(hr) && m_ulCurrentByte < m_ulDataEnd ) { // Stop speaking? if ( fAbort || ( m_pMainOutputSite->GetActions() & SPVES_ABORT ) ) { break; } // Still skipping forward? if ( m_ulSkipForward > 0 ) { if ( m_ulDataEnd - m_ulCurrentByte > m_ulSkipForward ) { // there is some data in buffer that can be written m_ulCurrentByte += m_ulSkipForward; // move the current byte forward by the skip amount m_lTotalBytesSkipped += m_ulSkipForward; // keep accumulating m_ulSkipForward = 0; // no more to skip RescheduleEvents(m_pCurrentEvent); // skip associated events } else { // skip all of the rest of the data in the buffer and await more m_ulSkipForward -= ( m_ulDataEnd - m_ulCurrentByte ); // reduce bytes left to skip m_lTotalBytesSkipped += ( m_ulDataEnd - m_ulCurrentByte ); // keep accumulating m_ulCurrentByte = m_ulDataEnd; // move to end of buffered data m_pCurrentEvent = m_pLastEvent; // move to end of event buffer break; // no data to be written } } // New skip? if( m_pMainOutputSite->GetActions() & SPVES_SKIP ) { SPVSKIPTYPE SkipType; long lSkipCount = 0; hr = m_pMainOutputSite->GetSkipInfo( &SkipType, &lSkipCount ); if ( SUCCEEDED(hr) ) { long lBytesToSkip = 0; long lSkipped = 0; lBytesToSkip = lSkipCount * m_pOutputFormat->nAvgBytesPerSec; lBytesToSkip *= m_flRateAdj; // we're skipping by seconds, so rate-adjust lBytesToSkip -= ( lBytesToSkip % m_pOutputFormat->nBlockAlign ); // need multiple of nBlockAlign // skipping forward? if ( lBytesToSkip > 0 ) { m_ulSkipForward += lBytesToSkip; lSkipped = lBytesToSkip; } else // skipping backward { // don't skip backward past the beginning if ( (long) m_ulCurrentByte + lBytesToSkip < 0 ) { lSkipped -= m_ulCurrentByte; // just go back to beginning m_ulCurrentByte = 0; } else { lSkipped = lBytesToSkip; m_ulCurrentByte += lSkipped; // lBytesSkipped is negative } m_lTotalBytesSkipped += lSkipped; RescheduleEvents(m_pEventQueue); } hr = m_pMainOutputSite->CompleteSkip( lSkipCount ); continue; } } if ( SUCCEEDED(hr) ) { // Do we have a full chunk of audio samples? if ( m_ulDataEnd - m_ulCurrentByte > ulChunkSize ) { // Yes? Write a full chunk nBytesToWrite = ulChunkSize; } else { // No? Write all that we have nBytesToWrite = m_ulDataEnd - m_ulCurrentByte; // this should always be a multiple of nBlockAlign already! SPDBG_ASSERT(nBytesToWrite == (nBytesToWrite - (nBytesToWrite % m_pOutputFormat->nBlockAlign) ) ); } if ( nBytesToWrite ) { hr = WriteToSAPI( (void*)&m_pcBuffer[m_ulCurrentByte], nBytesToWrite, &fAbort ); } } if ( SUCCEEDED(hr) ) { m_ulCurrentByte += nBytesToWrite; } } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ///////////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::WriteToSAPI // // Forwards the data to the SAPI output site, adjusting vol/rate if necessary. // Accumulates count of bytes to be written, and bytes actually written to SAPI. // //////////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::WriteToSAPI( const void* pvBuff, const ULONG cb, bool* pfAbort ) { SPDBG_FUNC( "CLocalTTSEngineSite::WriteToSAPI" ); HRESULT hr = S_OK; void* pvTsmBuff = NULL; void* pvVolBuff = NULL; int iNumOutSamples = 0; ULONG ulOutCb = 0; long lRateAdj = 0; const void* pvSendBuff = pvBuff; // this is what we'll send to SAPI, unless Tsm/Vol changes it SPDBG_ASSERT(pvBuff); SPDBG_ASSERT(cb); SPDBG_ASSERT(pfAbort); if ( m_pOutputFormatId && *m_pOutputFormatId != SPDFID_Text ) { iNumOutSamples = cb / m_pOutputFormat->nBlockAlign; if ( m_pcBuffer ) { // check for rate change if ( m_pMainOutputSite->GetActions() & SPVES_RATE ) { hr = m_pMainOutputSite->GetRate( &lRateAdj ); if ( SUCCEEDED(hr) && lRateAdj ) { ComputeRateAdj(lRateAdj, &m_flRateAdj); m_pTsm->SetScaleFactor( m_flRateAdj ); } } // real-time rate change if ( SUCCEEDED(hr) && m_pTsm && m_flRateAdj != 1.0 ) { int iTsmResult = 0; if ( iNumOutSamples > m_pTsm->GetFrameLen() ) { iTsmResult = m_pTsm->AdjustTimeScale(pvBuff, iNumOutSamples, &pvTsmBuff, &iNumOutSamples, m_pOutputFormat); if ( iTsmResult == 0 ) { hr = E_FAIL; } if ( SUCCEEDED(hr) ) { // send the Tsm modified buff instead of the original pvSendBuff = pvTsmBuff; } } } // real-time volume change if ( SUCCEEDED(hr) ) { if ( m_pMainOutputSite->GetActions() & SPVES_VOLUME ) { USHORT unVol = 0; hr = m_pMainOutputSite->GetVolume( &unVol ); if ( SUCCEEDED(hr) && unVol != 100 ) { m_flVol = ((float) unVol) / 100; // make dVol fractional } } if ( m_flVol != 1.0 ) { hr = ApplyGain( pvSendBuff, &pvVolBuff, iNumOutSamples ); // Did ApplyGain need to create a new buffer? if ( pvVolBuff ) { // send the volume modified buff instead of the original pvSendBuff = pvVolBuff; } } } } // if ( m_pcBuffer ) } // if ( m_pOutputFormatId && *m_pOutputFormatId != SPDFID_Text ) // stop speaking? if ( m_pMainOutputSite->GetActions() & SPVES_ABORT ) { *pfAbort = true; } if ( SUCCEEDED(hr) && !*pfAbort ) { ULONG ulOutCb = 0; m_ullBytesWritten += cb; if ( m_pOutputFormatId && *m_pOutputFormatId != SPDFID_Text ) { ulOutCb = iNumOutSamples * m_pOutputFormat->nBlockAlign; m_ullBytesWrittenToSAPI += ulOutCb; // send all the buffered events up to the current byte count hr = SendEvents(); } else { ulOutCb = cb; m_ullBytesWrittenToSAPI += cb; } hr = m_pMainOutputSite->Write(pvSendBuff, ulOutCb, NULL); } if ( pvTsmBuff ) { delete [] pvTsmBuff; pvTsmBuff = NULL; } if ( pvVolBuff ) { delete [] pvVolBuff; pvVolBuff = NULL; } SPDBG_REPORT_ON_FAIL( hr ); return hr; } ////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::ComputeRateAdj // // Computes the rate multiplier. // /////////////////////////////////////////////////////// JOEM 11-2000 // void CLocalTTSEngineSite::ComputeRateAdj(const long lRate, float* flRate) { SPDBG_FUNC( "CLocalTTSEngineSite::ComputeRateAdj" ); SPDBG_ASSERT(flRate); if ( lRate < 0 ) { if ( lRate < MIN_RATE ) { *flRate = 1.0 / g_dRateScale[0 - MIN_RATE]; } else { *flRate = 1.0 / g_dRateScale[0 - lRate]; } } else { if ( lRate > MAX_RATE ) { *flRate = g_dRateScale[MAX_RATE]; } else { *flRate = g_dRateScale[lRate]; } } } ////////////////////////////////////////////////////////////////////// // CLocalTTSEngineSite::ApplyGain // // Real-time volume adjustment. // ////////////////////////////////////////////////////// JOEM 01-2001 // STDMETHODIMP CLocalTTSEngineSite::ApplyGain(const void* pvInBuff, void** ppvOutBuff, const int iNumSamples) { SPDBG_FUNC( "CLocalTTSEngineSite::ApplyGain" ); HRESULT hr = S_OK; int i = 0; SPDBG_ASSERT(pvInBuff); SPDBG_ASSERT(iNumSamples); // Apply Volume if ( SUCCEEDED(hr) && m_flVol != 1.0 ) { long lGain = (long) ( m_flVol * (1 << g_iBase) ); if ( m_pOutputFormat->wFormatTag == WAVE_FORMAT_ALAW || m_pOutputFormat->wFormatTag == WAVE_FORMAT_MULAW ) { short* pnBuff = NULL; // need to convert samples int iOriginalFormatType = VapiIO::TypeOf (m_pOutputFormat); if ( iOriginalFormatType == -1 ) { hr = E_FAIL; } // Allocate the intermediate buffer if ( SUCCEEDED(hr) ) { pnBuff = new short[iNumSamples]; if ( !pnBuff ) { hr = E_OUTOFMEMORY; } } // Allocate the final (out) buffer if ( SUCCEEDED(hr) ) { *ppvOutBuff = new char[iNumSamples * VapiIO::SizeOf(iOriginalFormatType)]; if ( !*ppvOutBuff ) { hr = E_OUTOFMEMORY; } } // Convert to something we can use if ( SUCCEEDED(hr) ) { if ( 0 == VapiIO::DataFormatConversion ((char *)pvInBuff, iOriginalFormatType, (char*)pnBuff, VAPI_PCM16, iNumSamples) ) { hr = E_FAIL; } } // Apply gain if ( SUCCEEDED(hr) ) { for ( i=0; i> g_iBase ); } } // convert it back (from intermediate buff to final out buff) if ( SUCCEEDED(hr) ) { if ( 0 == VapiIO::DataFormatConversion ((char *)pnBuff, VAPI_PCM16, (char*)*ppvOutBuff, iOriginalFormatType, iNumSamples) ) { hr = E_FAIL; } } if ( pnBuff ) { delete [] pnBuff; pnBuff = NULL; } } else if ( m_pOutputFormat->wFormatTag == WAVE_FORMAT_PCM ) { // no converting necessary switch ( m_pOutputFormat->nBlockAlign ) { case 1: for ( i=0; i> g_iBase ); } break; case 2: for ( i=0; i> g_iBase ); } break; default: hr = E_FAIL; } } else { hr = E_FAIL; } } if ( FAILED(hr) ) { if ( *ppvOutBuff ) { delete [] *ppvOutBuff; *ppvOutBuff = NULL; } } SPDBG_REPORT_ON_FAIL( hr ); return hr; }