PDA

View Full Version : آموزش: ارسال و دریافت صوت از طریق سوکت (چت صوتی)



silsin
شنبه 10 مرداد 1394, 14:40 عصر
در مورد برنامه نویسی شبکه مخصوصا در این گونه قسمت ها به شخصه دیدم که منابع خیلی کمی وجود داره . البته به طبع منابع مربوط به ارسال و دریافت متن وجود داره اما در مورد ارسال و یا دریافت محتواهای دیگه مثل صوت یا تصویر و یا حتی ویدئو منبع مستقیم برای راهنمایی کردن متاسفانه وجود نداره و اغلب برنامه نویس های این بخش به صورت ابتکاری اقدام به انجام این کار می کنند.
در همین محتوا در این تاپیک یکی از روش های انجام این کار رو بررسی می کنیم که به نظر خیلی بهتر از روش های دیگه برای ارسال و دریافت صوت از طریق سوکته
سه بخش تشکیل دهنده این تاپیک عبارتند از :
- اشنایی با کتابخانه NAudio و انجام پروژه دریافت صوت از طریق میکروفون
- اماده سازی محیط ارسال و دریافت سوکت (اماده سازی برنامه های کلاینت و سرور)
- ارسال صوت و پخش ان پس از دریافت در سمت کلاینت

بخش اول :
از بین کامپوننت های مربوط به کار با صدا , NAudio یکی از محبوب ترین اونها به شمار میره . از اونجایی که این کامپوننت با در اختیار قرار دادن امکانات مختلف در زمینه کار با صدا می تونه بسیاری از نیازهای مربوط به این بخش رو برای سازنده گان برطرف کنه فکر می کنم مزیت اون رو نسبت به دیگر پلاگین ها نشون میده
برای دریافت این کتابخانه می تونید به ادرس زیر مراجعه کنید :

https://naudio.codeplex.com (https://naudio.codeplex.com/)


برای اشنایی با این کتابخانه , مثال کاربردی رو در زمینه ضبط صوت از طریق میکروفون انجام خواهیم داد
ابتدا کتابخانه NAudio و کلاس های مربوط به Wav را فراخوانی کنید

using NAudio;
using NAudio.Wave;


در قدم اول متغییر source را تعریف می کنیم
public WaveIn waveSource = null;
این متغییر تمامی اعمال مربوط به پخش و مدیریت صدا را انجام خواهد داد .
در مرحله بعد برای ذخیره در فایل باید متغییر stream متناسب با این را نیز تعریف کنیم . در اینجا کلاس ذخیره در فایل با نام WaveFileWriter شناخته می شود .
public WaveFileWriter waveFile = null;
این متغییر مسئول نگه داری فایل صوتی ما می باشد
پس از این کار اکنون نوبت به ان رسیده که عملکرد کلید شروع ضبط صدا را برنامه ریزی کنیم .
برای این کار در قسمت کد های مربوط به این کلید :
ابتدا :

waveSource = new WaveIn();

و سپس
waveSource.WaveFormat = new WaveFormat(14400, 1);


در اینجا مقدار اول waveFormat مربوط به مقدار بیت ریت صدا بوده و مقدار دومی نیز مربوط به کانال
Naudio برای دسترسی به مقادیر از سیستم EventHandler استفاده می کند بنابراین برای دریافت اطلاعت و حتی برای متوقف کردن فرایند دریافت اطلاعات یا همان متوقف کردن فرایند ضبط صدا از handler های زیر استفاده کنید



waveSource.DataAvailable += new EventHandler<WaveInEventArgs>(waveSource_DataAvailable);
waveSource.RecordingStopped += new EventHandler<StoppedEventArgs>(waveSource_RecordingStopped);


قبل از اینکه کلاس های مربوط به این handler ها را برنامه ریزی کنیم ابتدا مقادیر زیر را به ادامه کدهای قبلی اضافه کنید
waveFile = new WaveFileWriter(@"C:\Temp\Test0001.wav", waveSource.WaveFormat);
مقدار فوق مکان ذخیره سازی و فرمت ذخیره سازی فایل
waveSource.StartRecording();
این کد نیز شروع فرایند ضبط صدا را اعلام می کند . پس ما بعد از فشردن کلیک ضبط صدا را شروع خواهیم کرد
همچنین به وسیله این دستور
waveSource.StopRecording();
متوقف کردن این فرایند را اعلام می کنیم

Handler مربوط به متوقف کردن ضبط صدا :

void waveSource_RecordingStopped(object sender, StoppedEventArgs e)
{



if (waveSource != null)
{

waveSource.Dispose();
waveSource = null;
}

if (waveFile != null)
{

waveFile.Dispose();
waveFile = null;
}


}


نکته کلیدی این بخش این است که زمانی که فرایند ضبط صدا تمامی شد تمامی stream ها و به قولی نگه دارنده ها باید خالی شده و یا null شود . بنابراین به وسیله دستور Dispose این کار را انجام خواهیم داد یعنی تمامی بافرهای موجود در متغییر را خالی می کنیم .

Handler مربوط به ضبط صدا

void waveSource_DataAvailable(object sender, WaveInEventArgs e)
{
if (waveFile != null)
{


waveFile.Write(e.Buffer, 0, e.BytesRecorded);


waveFile.Flush();

}
}


تنها نکته این بخش فقط فرایند BinaryWriter که به وسیله waveFile در بافر دریافتی انجام خواهد شد . این بافر دریافتی نیز به وسیله WaveInEventArg وارد این متد خواهد شد
اکنون می توانید با اجرا کردن پروژه نتیجه را به خوبی مشاهده کنید

پایان بخش اول

silsin
سه شنبه 27 مرداد 1394, 19:51 عصر
بعد از کلی تاخیر بخش دوم رو شروع می کنیم
کلاسی با نام SendData ایجاد کنید . این کلاس مسئول ارسال اطلاعات ما می باشد

private NetworkStream NS;

public NetworkStream ns_stream
{
get { return NS; }
set { NS = value; }
}
private TcpClient tcp_client;

public TcpClient Tcp_client
{
get { return tcp_client; }
set { tcp_client = value; }
}
private BinaryWriter b_writer;

public BinaryWriter B_writer
{
get { return b_writer; }
set { b_writer = value; }
}
private Byte[] data_ary;

public Byte[] Data_ary
{
get { return data_ary; }
set { data_ary = value; }
}



public void Send(string IP, int port, string text)
{
Data_ary = Encoding.UTF8.GetBytes(text);
Tcp_client = new TcpClient(IP, port);
ns_stream = Tcp_client.GetStream();
B_writer = new BinaryWriter(ns_stream);
B_writer.Write(Data_ary);
B_writer.Close();
ns_stream.Flush();
ns_stream.Close();
Tcp_client.Close();

}





تابع ارسالی که در این متد اورده ایم را بررسی می کنیم . متدی که ما به کار گیری می کنیم بدین صورت است که ما یک stream شامل داده های صوتی را ارسال می کنیم (البته برنامه اصلی قابلیت ارسال متن رو هم داره واسه همین متد ارسال اینطوری وشته شده ) دیگه توضیح زیادی به نظرم نمی خواد این متد


کلاس دیگری با نام voice ایجاد می کنیم


private string ip;
private string path = Application.StartupPath + "\\buffer.wav";


متغییر اول برای دریافت ip
متغییر دوم نیز برای تعیین مسیر بافر
(دلیل استفاده از بافر را در ادامه بررسی خواهیم کرد)


public string Ip
{
get { return ip; }
set { ip = value; }
}
private int port ;
private Thread rec_thread;
public int VPort
{
get { return port; }
set { port = value; }
}




در ادامه متغییر های مربوطه را نیز تعریف می کنیم که این متغییر ها از بیرون از کلاس نیز قابل دسترسی خواهند بود VPort در اینجا مربوط به پورتی است که به ان متصل میشویم

private TcpListener listener = null;
private WaveIn sourceStream = null;
public WaveFileWriter waveWriter = null;
private System.Windows.Forms.Timer c_v=null;
private Socket connector, sc, sock = null;



یک tcplistener برای به اصطلاح گوش دهنده به اتصال های دریافتی
یک waveIn برای دریافت ورودی صوتی از میکروفون
waveFileWriter برای ذخیره بافر
یک تایمر
و مجموعه سوکت ها که در اادامه استفاده از هر کدام را بررسی می کنیم


public void Send(string ip,int port)
{
this.Ip = ip;
this.VPort = port;

c_v = new System.Windows.Forms.Timer();
c_v.Interval = 1000;
c_v.Enabled = false;
c_v.Tick += c_v_Tick;
Recordwav();
}


به وسیله این متغییر ما روند ارسال را بررسی می کنیم
- تعیین ای پی ها و پورت ها
مقدار دهی اولیه تایمر (این تایمر برای تکرار متد استفاده می شود )


private void Recordwav()
{
sourceStream = new WaveIn();
int devicenum=0;

for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++)
{
if (NAudio.Wave.WaveIn.GetCapabilities(i).ProductName .Contains("icrophone"))
devicenum = i;
}
sourceStream.DeviceNumber = devicenum;
sourceStream.WaveFormat = new WaveFormat(22000, WaveIn.GetCapabilities(devicenum).Channels);
sourceStream.DataAvailable += new EventHandler<WaveInEventArgs>(sourceStream_DataAvailable);

waveWriter = new WaveFileWriter(path, sourceStream.WaveFormat);

sourceStream.StartRecording();

c_v.Start();

}





به وسیله این متد شماره دستگاه های فعال را پیدا می کنیم

for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++)
{
if (NAudio.Wave.WaveIn.GetCapabilities(i).ProductName .Contains("icrophone"))
devicenum = i;
}



توجه کنید که نیازی نیست بیت ریت بالایی برای فرمت ارسالی تعیین کنیم
sourceStream.WaveFormat = new WaveFormat(22000, WaveIn.GetCapabilities(devicenum).Channels);



موارد دیگه رو در مثال قبلی توضیح دادم

تایمر رو هم به وسیله
c_v.Start(); فعال می کنیم

در این متد که همان متد تکراری است که به وسیله تایمر ایجاد کردیم دو متد صدا می زنیم

void c_v_Tick(object sender, EventArgs e)
{
this.Dispose();
Send_Bytes();

}


متد اول برای خالی کردن بافر
متد دوم برای ارسال بایت
متد اول رو بررسی می کنی م


public void Dispose()
{
c_v.Stop();
if (sourceStream != null)
{
sourceStream.StopRecording();
sourceStream.Dispose();
}
if (waveWriter != null)
{
waveWriter.Dispose();

}
GC.SuppressFinalize(this);
}

به وسیله این متد ما بافر رو خالی می کنیم . کاربرد این متد را در ادامه به طور کامل بررسی می کنیم

ادامه دارد .......

silsin
سه شنبه 27 مرداد 1394, 21:53 عصر
ترکیدم از بس تو این فرم اموزش شبکه گذاشتم :لبخند:
ادامه میدیم
نکاتی که وجود داره در مورد این روند تکرار در اینجا هم مشخص خواهد شد ما ابتدا dispose کرده و سپس ارسال می کنیم
ابتدا ارسال رو توضیح خواهیم داد

private void Send_Bytes()
{
Data_ary = File.ReadAllBytes(path);

connector = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ie = new IPEndPoint(IPAddress.Parse(this.Ip), this.VPort);
ie.Address = IPAddress.Loopback;
connector.Connect(ie);
connector.Send(Data_ary, 0, Data_ary.Length, 0);
connector.Close();

Recordwav();
}


نکته ای که یادم رفت بگم اینه که این کلاس باید از کلاس SendData ارث بری بشه چون باید بتونه وِیژگی ها و متغییر هایی که در اونه رو استفاده کنیم در اینجا Data_ary هم در همون کلاس تعریف شده .
برای مقدار دهی این متغییر بایتی ما از ویژگی ReadAllBytes از کلاس File استفاده کردیم . این کلاس در کتابخانه IO وجود داره . نیازمندی استفاده از این ویژگی اینه که برنامه هیچ دسترسی به فایل مذکور نداشته باشه همونطور که گفتم این ویزگی میاد داده های موجود در فایل رو در قالب بایت وارد متغییر فوق می کنه . برای اینکه دسترسی به فایل رو برداریم باید از متد Dispose استفاده کنیم و این متد وظیفه رها کردن حافظه های در اختیار گرفته رو بر عهده داره اینطوری میشه از این کلاس و دستور استفاده کرد .
در خطوط بعدی تعاریف متداول شبکه + اتصال ها و همچنین ارسال اسطلاعات به وسیله سوکت Connector
توجه کنید که این متد رو برای ارسال استفاده می کنیم (ربطی به گیرنده و فرستنده نداره اگر باهوش باشید می تونید دو طرفه استفاده اش کنید)

و همچنین RecordWav نیز برای تکرار خوندن اطلاعات به کار میره . یعنی زمانی که تایمر یک ثانیه رو شمرد بعد از اون ابتدا حافظه ها به وسیله متد Dispose رها میشن اطلاعات موجود از فایل بافر خونده میشه و پس از اون ارسال و دوباره همین روند به وسیله فراخوانی متد ارسال تکرار خواهد شد


توجه کنید با توجه به اینکه در مثال پست اول روند کار با کتابخانه Naudio را توضیح دادم از دوباره پرداختن به روند اجرایی متد های لازم برای انجام و اعمال ضبط و ذخیره صدا رو ذکر نکردم مثل این متد که باید با توجه به پست اول استفاده از اون رو یادتون نره

private void sourceStream_DataAvailable(object sender, WaveInEventArgs e)
{
if (waveWriter == null) return;


mFile.Write(e.Buffer, 0, e.BytesRecorded);





}



تا اینجای کار روند ارسال رو برنامه ریزی کردیم . اکنون روند دریافت رو انجام خواهیم داد .
پس از ارسال استریم در شبکه مسلما باید در طرف دیگر نیز استریم دریافت شود
اگر یادتون باشه ما یک ترد تعریف کردیم . در اینجا برای دریافت نیز ما یک ترد شامل روند دریافت رو استارت خواهیم کرد

public void Receive(int port) {
this.VPort = port;
rec_thread = new Thread(new ThreadStart(VoiceReceive));
rec_thread.Start();

}



که این ترد یک متد برای دریافت صدا را استارت می کنه

private void VoiceReceive()
{

sc = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ie = new IPEndPoint(0, this.VPort);

sc.Bind(ie);

sc.Listen(0);
sock = sc.Accept();
ns_stream = new NetworkStream(sock);


WriteBytes();
sc.Close();

while (true)
{
VoiceReceive();
}


}


در این متد
تعاریف سوکت به عنوان نقطه نهایی یا گوش دهنده به پورت انجام خواهد شد
استریم از سوکت دریافت شده و در کلاس NetworkStream قرار خواهد گرفت
همچنین به وسیله حلقه تکرار while این روند تکرار خواهد شد
نکته بعدی در مورد متد WriteBytes بوده که به شرح زیر می باشد

private void WriteBytes()
{
if (ns_stream != null)
{
SoundPlayer sp = new SoundPlayer(ns_stream);
sp.Play();
}
}

به وسیله این متد استریم دریافت شده را بهوسیله کلاس صوتی و تصویری اجرا می کنیم . به بیان ساده تر این متد وظیفه اجرا صدای دریافت شده ما را خواهد داشت .

کلیات کار رو توضیح دادم در پست بعدی جزئیات باقی مونده به همراه سورس کامل برنامه رو قرار خواهم داد
پست بعدی تا چند ساعت دیگه ....

silsin
چهارشنبه 28 مرداد 1394, 00:06 صبح
حالا می خواهیم برنامه رو راه اندازی کنیم
برای شروع ارسال از کد زیر استفاده می کنیم

v.Send(getIp(), 2000);

این v بیانگر کلاس voice ماست یعنی Voice v = new Voice();
متدی در اون به کار رفته :

public string getIp()
{
string hostname = Dns.GetHostName();
IPHostEntry hostentery = Dns.GetHostEntry(hostname);
IPAddress[] ip = hostentery.AddressList;
return ip[ip.Length - 1].ToString();
}



از این متد برای به دست اوردن ای پی شبکه استفاده می کنیم .صرفا ای پی خودمون در اینجا(البته یه فرق هایی هم این در شبکه های محلی داره که خب جاش تو این مقاله نیست)
با اجرای این روند شما می تونید ارسال رو انجام بدید
برای دریافت هم ما صرفا در برنامه مقصد از این کد استفاده میک نیم
v.Receive(2000);
که مجددا v همان نماینده کلاس Voice می باشد

سورس کامل برنامه :
134432

و تمام
سوالی بود در همین تاپیک بپرسین تا بجوابیم
با تشکر