Commit 37d22377 authored by Dipl.-Ing. Jonas Stienen's avatar Dipl.-Ing. Jonas Stienen
Browse files

Adding NetAudio sample server with simple connection to a sample generator...

Adding NetAudio sample server with simple connection to a sample generator class and example code in NetAudioTest
parent c2ba8c23
...@@ -111,6 +111,7 @@ endif( ) ...@@ -111,6 +111,7 @@ endif( )
if( ITA_DATA_SOURCES_WITH_NET_AUDIO ) if( ITA_DATA_SOURCES_WITH_NET_AUDIO )
list( APPEND ITADataSourcesHeader list( APPEND ITADataSourcesHeader
"include/ITANetAudioStream.h" "include/ITANetAudioStream.h"
"include/ITANetAudioSampleServer.h"
"include/ITANetAudioStreamingServer.h" "include/ITANetAudioStreamingServer.h"
) )
list( APPEND ITADataSourcesSources list( APPEND ITADataSourcesSources
......
/*
* ----------------------------------------------------------------
*
* ITA core libs
* (c) Copyright Institute of Technical Acoustics (ITA)
* RWTH Aachen University, Germany, 2015-2017
*
* ----------------------------------------------------------------
* ____ __________ _______
* // / //__ ___/ // _ |
* // / // / // /_| |
* // / // / // ___ |
* //__/ //__/ //__/ |__|
*
* ----------------------------------------------------------------
*
*/
#ifndef INCLUDE_WATCHER_ITA_NET_AUDIO_SAMPLE_SERVER
#define INCLUDE_WATCHER_ITA_NET_AUDIO_SAMPLE_SERVER
#include <ITADataSourcesDefinitions.h>
#include <ITANetAudioStreamingServer.h>
#include <ITADataSourceRealization.h>
//! Sample-generation callback function pointer class
class CITASampleProcessor : public ITADatasourceRealization
{
public:
inline CITASampleProcessor( const double dSampleRate, const int iNumChannels, const int iBlockLength )
: ITADatasourceRealization( ( unsigned int ) ( iNumChannels ), dSampleRate, ( unsigned int ) ( iBlockLength ) )
{
for( size_t c = 0; c < iNumChannels; c++ )
{
m_vvfSampleBuffer.push_back( std::vector< float >() );
for( size_t n = 0; n < iBlockLength; n++ )
m_vvfSampleBuffer[ c ].push_back( 0.0f );
}
};
inline ~CITASampleProcessor() {};
//! Process samples (overwrite this virtual method)
/**
* Method that is called in audio streaming context and requests
* to produce or copy audio samples into the internal buffer m_vvfSampleBuffer
*
* @param[in] pStreamInfo Information over streaming status, i.e. sample count and time stamp
*/
virtual void Process( const ITAStreamInfo* pStreamInfo ) =0;
protected:
std::vector< std::vector< float > > m_vvfSampleBuffer; //!< Multi-channel sample buffer
public:
//! Delegate internal buffer to audio stream
inline void ProcessStream( const ITAStreamInfo* pInfo )
{
Process( pInfo );
for( size_t c = 0; c < m_vvfSampleBuffer.size(); c++ )
{
float* pfData = GetWritePointer( ( unsigned int ) ( c ) );
for( size_t n = 0; n < m_vvfSampleBuffer[ c ].size(); n++ )
pfData[ n ] = m_vvfSampleBuffer[ c ][ n ];
}
IncrementWritePointer();
};
};
//! Network audio sample server (for providing samples via individual callback)
/**
* Audio sample transmitter for a networked sample callback function that can connect via TCP/IP.
*
* @sa CITANetAudioStream CITANetAudioStreamingServer
* @note not thread-safe
*/
class ITA_DATA_SOURCES_API CITANetAudioSampleServer : public CITANetAudioStreamingServer
{
public:
inline CITANetAudioSampleServer( CITASampleProcessor* pProcessor )
: m_pSampleProcessor( pProcessor )
{
SetInputStream( m_pSampleProcessor );
};
inline ~CITANetAudioSampleServer()
{};
private:
//! Prohibit public access to streaming context and delegate
inline void SetInputStream( ITADatasource* pDataSource )
{
CITANetAudioStreamingServer::SetInputStream( pDataSource );
};
//! Prohibit public access to streaming context and delegate
inline ITADatasource* GetInputStream() const
{
return CITANetAudioStreamingServer::GetInputStream();
};
CITASampleProcessor* m_pSampleProcessor; //!< Callback / sample processor
};
#endif // INCLUDE_WATCHER_ITA_NET_AUDIO_SAMPLE_SERVER
...@@ -44,7 +44,7 @@ class VistaConnectionIP; ...@@ -44,7 +44,7 @@ class VistaConnectionIP;
/** /**
* Audio sample transmitter for a networked signal source that can connect via TCP/IP. * Audio sample transmitter for a networked signal source that can connect via TCP/IP.
* *
* @sa CITANetAudioStream, CITANetAudioSampleServer * @sa CITANetAudioStream
* @note not thread-safe * @note not thread-safe
*/ */
class ITA_DATA_SOURCES_API CITANetAudioStreamingServer : public VistaThreadLoop class ITA_DATA_SOURCES_API CITANetAudioStreamingServer : public VistaThreadLoop
......
// $Id: $ /*
* ----------------------------------------------------------------
*
* ITA core libs
* (c) Copyright Institute of Technical Acoustics (ITA)
* RWTH Aachen University, Germany, 2015-2017
*
* ----------------------------------------------------------------
* ____ __________ _______
* // / //__ ___/ // _ |
* // / // / // /_| |
* // / // / // ___ |
* //__/ //__/ //__/ |__|
*
* ----------------------------------------------------------------
*
*/
#ifndef __ITA_STREAM_INFO_H__ #ifndef INCLUDE_WATCHER_ITA_STREAM_INFO
#define __ITA_STREAM_INFO_H__ #define INCLUDE_WATCHER_ITA_STREAM_INFO
#include <ITATypes.h> #include <ITATypes.h>
// Diese Datenklasse beschreibt den Zustand eines Audiostreams //! Time code information for audio streams
class ITAStreamInfo { class ITAStreamInfo
{
public: public:
// Anzahl der abgespielten Samples seit Beginn des Streamings uint64_t nSamples; //!< Number of samples processed
uint64_t nSamples; double dTimecode; //!< Time code
// TODO: Beschreiben inline ITAStreamInfo()
double dTimecode; : nSamples( 0 )
, dTimecode( 0 )
{};
//! Standard-Konstruktor (setzt alle Werte 0)
ITAStreamInfo() : nSamples(0), dTimecode(0) {}
//! Destruktor
virtual ~ITAStreamInfo() {}; virtual ~ITAStreamInfo() {};
}; };
#endif // __ITA_STREAM_INFO_H__ #endif // INCLUDE_WATCHER_ITA_STREAM_INFO
#include "ITADataSourceRealization.h" #include "ITADataSourceRealization.h"
#include <cassert>
#include <ITAFastMath.h> #include <ITAFastMath.h>
#include <cassert>
/* ITADatasourceRealization::ITADatasourceRealization( unsigned int uiChannels, double dSamplerate, unsigned int uiBlocklength, unsigned int uiCapacity )
ITADatasourceRealization::ITADatasourceRealization(unsigned int uiChannels,
unsigned int uiBlocklength,
unsigned int uiCapacity)
{
Init(uiChannels, uiBlocklength, uiCapacity);
}
*/
ITADatasourceRealization::ITADatasourceRealization(unsigned int uiChannels,
double dSamplerate,
unsigned int uiBlocklength,
unsigned int uiCapacity)
{ {
assert( dSamplerate > 0 ); assert( dSamplerate > 0 );
m_dSampleRate = dSamplerate; m_dSampleRate = dSamplerate;
m_oStreamProps.dSamplerate = dSamplerate; m_oStreamProps.dSamplerate = dSamplerate;
Init(uiChannels, uiBlocklength, uiCapacity); Init( uiChannels, uiBlocklength, uiCapacity );
} }
void ITADatasourceRealization::Init(unsigned int uiChannels, void ITADatasourceRealization::Init( unsigned int uiChannels, unsigned int uiBlocklength, unsigned int uiCapacity )
unsigned int uiBlocklength,
unsigned int uiCapacity)
{ {
assert( uiChannels > 0 ); assert( uiChannels > 0 );
assert( uiBlocklength > 0 ); assert( uiBlocklength > 0 );
...@@ -49,7 +33,7 @@ void ITADatasourceRealization::Init(unsigned int uiChannels, ...@@ -49,7 +33,7 @@ void ITADatasourceRealization::Init(unsigned int uiChannels,
m_oStreamProps.uiChannels = m_uiChannels; m_oStreamProps.uiChannels = m_uiChannels;
m_oStreamProps.uiBlocklength = m_uiBlocklength; m_oStreamProps.uiBlocklength = m_uiBlocklength;
m_uiBufferSize = uiBlocklength * (uiCapacity+1); m_uiBufferSize = uiBlocklength * ( uiCapacity + 1 );
m_pEventHandler = NULL; m_pEventHandler = NULL;
...@@ -72,16 +56,18 @@ void ITADatasourceRealization::Init(unsigned int uiChannels, ...@@ -72,16 +56,18 @@ void ITADatasourceRealization::Init(unsigned int uiChannels,
2005-2-14 2005-2-14
*/ */
m_pfBuffer = fm_falloc(m_uiBufferSize * m_uiChannels + /* >>> */ 1024 /* <<< */, false); m_pfBuffer = fm_falloc( m_uiBufferSize * m_uiChannels + /* >>> */ 1024 /* <<< */, false );
Reset(); Reset();
} }
ITADatasourceRealization::~ITADatasourceRealization() { ITADatasourceRealization::~ITADatasourceRealization()
fm_free(m_pfBuffer); {
fm_free( m_pfBuffer );
} }
void ITADatasourceRealization::Reset() { void ITADatasourceRealization::Reset()
{
m_uiReadCursor = 0; m_uiReadCursor = 0;
m_uiWriteCursor = 0; m_uiWriteCursor = 0;
...@@ -93,22 +79,26 @@ void ITADatasourceRealization::Reset() { ...@@ -93,22 +79,26 @@ void ITADatasourceRealization::Reset() {
m_iGBPEntrances = 0; m_iGBPEntrances = 0;
m_bGBPFirst = true; m_bGBPFirst = true;
fm_zero(m_pfBuffer, m_uiBufferSize * m_uiChannels + /* >>> */ 1024 /* <<< */); fm_zero( m_pfBuffer, m_uiBufferSize * m_uiChannels + /* >>> */ 1024 /* <<< */ );
} }
bool ITADatasourceRealization::HasStreamErrors() const { bool ITADatasourceRealization::HasStreamErrors() const
return (m_iBufferUnderflows > 0) || (m_iBufferOverflows > 0) || (m_iGBPReentrances > 0); {
return ( m_iBufferUnderflows > 0 ) || ( m_iBufferOverflows > 0 ) || ( m_iGBPReentrances > 0 );
} }
ITADatasourceRealizationEventHandler* ITADatasourceRealization::GetStreamEventHandler() const { ITADatasourceRealizationEventHandler* ITADatasourceRealization::GetStreamEventHandler() const
{
return m_pEventHandler; return m_pEventHandler;
} }
void ITADatasourceRealization::SetStreamEventHandler(ITADatasourceRealizationEventHandler* pHandler) { void ITADatasourceRealization::SetStreamEventHandler( ITADatasourceRealizationEventHandler* pHandler )
{
m_pEventHandler = pHandler; m_pEventHandler = pHandler;
} }
const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, const ITAStreamInfo* pStreamInfo) { const float* ITADatasourceRealization::GetBlockPointer( unsigned int uiChannel, const ITAStreamInfo* pStreamInfo )
{
assert( uiChannel < m_uiChannels ); assert( uiChannel < m_uiChannels );
/* /*
...@@ -117,7 +107,8 @@ const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, c ...@@ -117,7 +107,8 @@ const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, c
* *
* WICHTIG: Dies sollte nicht passieren. Fehler beim anwendenden Programmierer! * WICHTIG: Dies sollte nicht passieren. Fehler beim anwendenden Programmierer!
*/ */
if (++m_iGBPEntrances > 1) { if( ++m_iGBPEntrances > 1 )
{
--m_iGBPEntrances; --m_iGBPEntrances;
++m_iGBPReentrances; ++m_iGBPReentrances;
return NULL; return NULL;
...@@ -125,12 +116,16 @@ const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, c ...@@ -125,12 +116,16 @@ const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, c
// Hook/Handler aufrufen // Hook/Handler aufrufen
PreGetBlockPointer(); PreGetBlockPointer();
if (m_pEventHandler) m_pEventHandler->HandlePreGetBlockPointer(this, uiChannel); if( m_pEventHandler )
m_pEventHandler->HandlePreGetBlockPointer( this, uiChannel );
if (m_bGBPFirst) { if( m_bGBPFirst )
{
// Erster Eintritt in GBP seit letztem IBP => Daten produzieren // Erster Eintritt in GBP seit letztem IBP => Daten produzieren
ProcessStream(pStreamInfo); ProcessStream( pStreamInfo );
if (m_pEventHandler) m_pEventHandler->HandleProcessStream(this, pStreamInfo);
if( m_pEventHandler )
m_pEventHandler->HandleProcessStream( this, pStreamInfo );
m_bGBPFirst = false; m_bGBPFirst = false;
} }
...@@ -145,45 +140,51 @@ const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, c ...@@ -145,45 +140,51 @@ const float* ITADatasourceRealization::GetBlockPointer(unsigned int uiChannel, c
*/ */
unsigned int uiLocalReadCursor = m_uiReadCursor; unsigned int uiLocalReadCursor = m_uiReadCursor;
if (uiLocalReadCursor == m_uiWriteCursor) { if( uiLocalReadCursor == m_uiWriteCursor )
{
++m_iBufferUnderflows; ++m_iBufferUnderflows;
--m_iGBPEntrances; --m_iGBPEntrances;
return NULL; return NULL;
} }
--m_iGBPEntrances; --m_iGBPEntrances;
return m_pfBuffer + (uiChannel * m_uiBufferSize) + uiLocalReadCursor; return m_pfBuffer + ( uiChannel * m_uiBufferSize ) + uiLocalReadCursor;
} }
void ITADatasourceRealization::IncrementBlockPointer() { void ITADatasourceRealization::IncrementBlockPointer()
{
unsigned int uiLocalReadCursor = m_uiReadCursor; unsigned int uiLocalReadCursor = m_uiReadCursor;
if (uiLocalReadCursor == m_uiWriteCursor) if( uiLocalReadCursor == m_uiWriteCursor )
// Keine Daten im Ausgabepuffer? Kein Inkrement mglich! (Fehlerfall) // Keine Daten im Ausgabepuffer? Kein Inkrement mglich! (Fehlerfall)
++m_iBufferUnderflows; ++m_iBufferUnderflows;
else else
// Lesezeiger inkrementieren // Lesezeiger inkrementieren
m_uiReadCursor = (uiLocalReadCursor + m_uiBlocklength) % m_uiBufferSize; m_uiReadCursor = ( uiLocalReadCursor + m_uiBlocklength ) % m_uiBufferSize;
m_bGBPFirst = true; m_bGBPFirst = true;
PostIncrementBlockPointer(); PostIncrementBlockPointer();
if (m_pEventHandler) m_pEventHandler->HandlePostIncrementBlockPointer(this);
if( m_pEventHandler )
m_pEventHandler->HandlePostIncrementBlockPointer( this );
} }
float* ITADatasourceRealization::GetWritePointer(unsigned int uiChannel) { float* ITADatasourceRealization::GetWritePointer( unsigned int uiChannel )
{
assert( uiChannel < m_uiChannels ); assert( uiChannel < m_uiChannels );
return m_pfBuffer + (uiChannel * m_uiBufferSize) + m_uiWriteCursor; return m_pfBuffer + ( uiChannel * m_uiBufferSize ) + m_uiWriteCursor;
} }
void ITADatasourceRealization::IncrementWritePointer() { void ITADatasourceRealization::IncrementWritePointer()
{
// Lokaler Schreibcursor // Lokaler Schreibcursor
unsigned int uiLocalWriteCursor = m_uiWriteCursor; unsigned int uiLocalWriteCursor = m_uiWriteCursor;
unsigned int uiNewWriteCursor = (uiLocalWriteCursor + m_uiBlocklength) % m_uiBufferSize; unsigned int uiNewWriteCursor = ( uiLocalWriteCursor + m_uiBlocklength ) % m_uiBufferSize;
// Pufferberlauf // Pufferberlauf
if (uiNewWriteCursor == m_uiReadCursor) { if( uiNewWriteCursor == m_uiReadCursor )
{
++m_iBufferOverflows; ++m_iBufferOverflows;
return; return;
} }
...@@ -192,6 +193,6 @@ void ITADatasourceRealization::IncrementWritePointer() { ...@@ -192,6 +193,6 @@ void ITADatasourceRealization::IncrementWritePointer() {
m_uiWriteCursor = uiNewWriteCursor; m_uiWriteCursor = uiNewWriteCursor;
} }
void ITADatasourceRealizationEventHandler::HandlePreGetBlockPointer(ITADatasourceRealization* pSender, unsigned int uiChannel) {} void ITADatasourceRealizationEventHandler::HandlePreGetBlockPointer( ITADatasourceRealization*, unsigned int ) {}
void ITADatasourceRealizationEventHandler::HandlePostIncrementBlockPointer(ITADatasourceRealization* pSender) {} void ITADatasourceRealizationEventHandler::HandlePostIncrementBlockPointer( ITADatasourceRealization* ) {}
void ITADatasourceRealizationEventHandler::HandleProcessStream(ITADatasourceRealization* pSender, const ITAStreamInfo* pStreamInfo) {} void ITADatasourceRealizationEventHandler::HandleProcessStream( ITADatasourceRealization*, const ITAStreamInfo* ) {}
#include <ITANetAudioStreamingServer.h> #include <ITANetAudioStreamingServer.h>
#include <ITANetAudioSampleServer.h>
#include <ITANetAudioStream.h> #include <ITANetAudioStream.h>
#include <ITAPortaudioInterface.h> #include <ITAPortaudioInterface.h>
#include <ITAStreamFunctionGenerator.h> #include <ITAStreamFunctionGenerator.h>
...@@ -8,6 +9,7 @@ ...@@ -8,6 +9,7 @@
#include <ITAStreamProbe.h> #include <ITAStreamProbe.h>
#include <ITAStreamPatchBay.h> #include <ITAStreamPatchBay.h>
#include <ITAAsioInterface.h> #include <ITAAsioInterface.h>
#include <ITAStreamInfo.h>
#include <VistaBase/VistaStreamUtils.h> #include <VistaBase/VistaStreamUtils.h>
#include <VistaBase/VistaTimeUtils.h> #include <VistaBase/VistaTimeUtils.h>
...@@ -24,7 +26,7 @@ const static string g_sInputFilePath = "gershwin-mono.wav"; ...@@ -24,7 +26,7 @@ const static string g_sInputFilePath = "gershwin-mono.wav";
const static int g_iServerPort = 12480; const static int g_iServerPort = 12480;
const static double g_dSampleRate = 44100; const static double g_dSampleRate = 44100;
const static int g_iBlockLength = 512; const static int g_iBlockLength = 512;
const static int g_iChannels = 160; const static int g_iChannels = 2;
const static int g_iTargetLatencySamples = g_iBlockLength * 2; const static int g_iTargetLatencySamples = g_iBlockLength * 2;
const static int g_iRingerBufferCapacity = g_iBlockLength * 10; const static int g_iRingerBufferCapacity = g_iBlockLength * 10;
const static double g_dDuration = 10.0f; const static double g_dDuration = 10.0f;
...@@ -34,53 +36,103 @@ const static string g_sAudioInterface = "ASIO4ALL v2"; ...@@ -34,53 +36,103 @@ const static string g_sAudioInterface = "ASIO4ALL v2";
//const static string g_sAudioInterface = "ASIO Hammerfall DSP"; //const static string g_sAudioInterface = "ASIO Hammerfall DSP";
const static bool g_bUseUDP = false; const static bool g_bUseUDP = false;
class CServer : public VistaThread class CSampleGenerator : public CITASampleProcessor
{ {
public: public:
inline CServer( const string& sInputFilePath ) inline CSampleGenerator()
: CITASampleProcessor( g_dSampleRate, g_iChannels, g_iBlockLength )
{};
inline void Process( const ITAStreamInfo* pStreamInfo )
{
for( size_t c = 0; c < m_vvfSampleBuffer.size(); c++ )
{
for( size_t n = 0; n < m_vvfSampleBuffer[ c ].size(); n++ )
{
float fSample = ( c == 0 ? 1.0f : -1.0f ) * ( 1.0f - 2.0f * n / float( GetBlocklength() ) );
m_vvfSampleBuffer[ c ][ n ] = fSample;
}
}
};
};
class CSampleServerExample : public VistaThread
{
public:
inline CSampleServerExample()
{
m_pSampleGenerator = new CSampleGenerator();
m_pSampleServer = new CITANetAudioSampleServer( m_pSampleGenerator );
m_pSampleServer->SetDebuggingEnabled( true );
m_pSampleServer->SetTargetLatencySamples( g_iTargetLatencySamples );
m_pSampleServer->SetServerLogBaseName( "ITANetAudioTest_Server" );
Run();
};
inline ~CSampleServerExample()
{ {
pStreamingServer = new CITANetAudioStreamingServer; delete m_pSampleServer;
pStreamingServer->SetDebuggingEnabled( true ); delete m_pSampleGenerator;
pStreamingServer->SetTargetLatencySamples( g_iTargetLatencySamples ); };
pStreamingServer->SetServerLogBaseName( "ITANetAudioTest_Server" );
pInputFile = new ITAFileDatasource( sInputFilePath, g_iBlockLength ); void ThreadBody()
pInputFile->SetIsLooping( true ); {
assert( pInputFile->GetNumberOfChannels() == 1 ); vstr::out() << "[ NetAudioTestServer ] Starting net audio sample server and waiting for client connections on '" << g_sServerName << "' on port " << g_iServerPort << endl;
pMuliplier = new ITAStreamMultiplier1N( pInputFile, g_iChannels ); m_pSampleServer->Start( g_sServerName, g_iServerPort, g_dSyncTimout, g_bUseUDP );
pInputStreamProbe = new ITAStreamProbe( pMuliplier, "ITANetAudioTest.serverstream.wav" ); };
pStreamingServer->SetInputStream( pInputStreamProbe );
CITANetAudioSampleServer* m_pSampleServer;
CSampleGenerator* m_pSampleGenerator;
};
class CStreamServerExample : public VistaThread
{
public:
inline CStreamServerExample( const string& sInputFilePath )
{
m_pStreamingServer = new CITANetAudioStreamingServer;
m_pStreamingServer->SetDebuggingEnabled( true );
m_pStreamingServer->SetTargetLatencySamples( g_iTargetLatencySamples );
m_pStreamingServer->SetServerLogBaseName( "ITANetAudioTest_Server" );
m_pInputFile = new ITAFileDatasource( sInputFilePath, g_iBlockLength );
m_pInputFile->SetIsLooping( true );
assert( m_pInputFile->GetNumberOfChannels() == 1 );
m_pMultiplier = new ITAStreamMultiplier1N( m_pInputFile, g_iChannels );
m_pInputStreamProbe = new ITAStreamProbe( m_pMultiplier, "ITANetAudioTest.serverstream.wav" );
m_pStreamingServer->SetInputStream( m_pInputStreamProbe );
Run(); Run();
}; };
inline ~CServer( ) inline ~CStreamServerExample()
{ {
delete pInputFile; delete m_pInputFile;
delete pMuliplier; delete m_pMultiplier;
delete pStreamingServer; delete m_pStreamingServer;
delete pInputStreamProbe; delete m_pInputStreamProbe;
}; };
void ThreadBody( ) void ThreadBody( )
{ {
vstr::out() << "[ NetAudioTestServer ] Starting net audio server and waiting for client connections on '" << g_sServerName << "' on port " << g_iServerPort << endl; vstr::out() << "[ NetAudioTestServer ] Starting net audio server and waiting for client connections on '" << g_sServerName << "' on port " << g_iServerPort << endl;
pStreamingServer->Start( g_sServerName, g_iServerPort, g_dSyncTimout, g_bUseUDP ); m_pStreamingServer->Start( g_sServerName, g_iServerPort, g_dSyncTimout, g_bUseUDP );
}; };