Örnek Olay - Technitone.com sitesini oluşturma

Sean Middleditch
Sean Middleditch
Technitone: Web ses deneyimi.

Technitone.com; WebGL, Tuval, Web Sockets, CSS3, JavaScript, Flash ve Chrome'daki yeni Web Audio API'sının birleşiminden oluşur.

Bu makalede prodüksiyonun her yönü üzerine değineceğiz: plan, sunucu, sesler, görseller ve etkileşimli kullanım için tasarlamak üzere kullandığımız iş akışının bir kısmı. Çoğu bölümde kod snippet'leri, bir demo ve bir indirme bulunur. Makalenin sonunda, hepsini tek bir zip dosyası halinde alabileceğiniz bir indirme bağlantısı bulunmaktadır.

gskinner.com Üretim Ekibi.

Düzen

Biz gskinner.com'da ses mühendisi değiliz, ancak bizi bir meydan okumayla cezbederek bir plan oluştururuz:

  • Kullanıcılar, Andre'nin ToneMatrix eserinden"esinlenerek" bir ızgarada tonları çizer.
  • Tonlar örneklenmiş enstrümanlara, davul setlerine, hatta kullanıcıların kendi kayıtlarına bağlıdır.
  • Birden fazla bağlı kullanıcı aynı şebekede aynı anda oynar
  • ...veya kendi başına keşfetmek için solo moduna geçebilir
  • Davetiyeli oturumlar, kullanıcıların bir grup düzenlemesine ve önceden planlanmamış bir müzik çalmasına olanak tanır

Ses filtreleri ve efektlerinin tonlarına uygulanan bir araç paneliyle kullanıcılara Web Audio API'sını keşfetme fırsatı sunuyoruz.

gskinner.com tarafından geliştirilen teknitone

Ayrıca:

  • Kullanıcıların bestelerini ve efektlerini veri olarak depolayıp istemciler arasında senkronize etme
  • Müthiş görünen şarkılar çizebilmeleri için renk seçenekleri sunun
  • Başkalarının çalışmalarını dinleyebilmesi, sevebilmesi ve hatta düzenleyebilmesi için bir galeri sunabilirsiniz

Tanıdık ızgara metaforuna bağlı kaldık, onu 3D uzayda gezdirdik, biraz ışık, doku ve tanecik efektleri ekledik, esnek (veya tam ekran) CSS ve JS destekli bir arayüzde barındırdık.

Arabayla seyahat

Araç, efekt ve ızgara verileri birleştirilir ve istemcide serileştirilir, ardından Socket.io'da birden fazla kullanıcı için çözüm sağlamak üzere özel Node.js arka ucuna gönderilir. Bu veriler, çok kullanıcılı oynatma sırasında kullanıcı arayüzünün, örneklerin ve efektlerin oluşturulmasından sorumlu olan göreli CSS, WebGL ve WebAudio katmanlarına dağıtılmadan önce her oyuncunun katkıları dahil olmak üzere istemciye geri gönderilir.

Yuvalarla gerçek zamanlı iletişim, istemcide JavaScript ve sunucuda JavaScript besler.

Technitone Sunucu Diyagramı

Sunucunun her yönü için Düğümü kullanıyoruz. Bu, statik bir web sunucusu ve yuva sunucumuzdur. Tamamen Düğüm üzerine inşa edilmiş tam bir web sunucusu olan Express'i kullanmaya karar verdik. Süper ölçeklenebilir, yüksek düzeyde özelleştirilebilir ve alt düzey sunucu özelliklerini sizin yerinize yönetir (Apache veya Windows Server gibi). Bu durumda geliştirici olarak yalnızca uygulamanızı oluşturmaya odaklanmanız gerekir.

Çok Kullanıcılı Demo (Tamam, gerçekten de sadece bir ekran görüntüsü)

Bu demo, Node.js sunucusu üzerinden çalıştırılmalıdır. Bu makale bir düğüm sunucusu olmadığından, Node.js'yi yükledikten, web sunucunuzu yapılandırdıktan ve yerel olarak çalıştırdıktan sonra demonun nasıl görüneceğine dair bir ekran görüntüsü ekledik. Demo yüklemenizi her yeni kullanıcı ziyaret ettiğinde yeni bir tablo eklenir ve herkesin çalışmaları birbirlerine görünür.

Node.js Demonun Ekran Görüntüsü

Düğüm kolay. Socket.io ve özel POST isteklerini bir arada kullanarak senkronizasyon için karmaşık rutinler oluşturmamız gerekmedi. Socket.io bunu şeffaf bir şekilde işler; JSON ise geçiş yapar.

Ne kadar kolaydı? Bunu izleyin.

3 satırlık JavaScript ile Express ile çalışan bir web sunucumuz var.

//Tell  our Javascript file we want to use express.
var express = require('express');

//Create our web-server
var server = express.createServer();

//Tell express where to look for our static files.
server.use(express.static(__dirname + '/static/'));

Gerçek zamanlı iletişim için socket.io'yu bağlamak üzere birkaç önerimiz daha var.

var io = require('socket.io').listen(server);
//Start listening for socket commands
io.sockets.on('connection', function (socket) {
    //User is connected, start listening for commands.
    socket.on('someEventFromClient', handleEvent);

});

Artık HTML sayfasından gelen bağlantıları dinlemeye başladık.

<!-- Socket-io will serve it-self when requested from this url. -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>

 <!-- Create our socket and connect to the server -->
 var sock = io.connect('http://localhost:8888');
 sock.on("connect", handleConnect);

 function handleConnect() {
    //Send a event to the server.
    sock.emit('someEventFromClient', 'someData');
 }
 ```

## Sound check

A big unknown was the effort entailed with using the Web Audio API. Our initial findings confirmed that [Digital Signal Processing](http://en.wikipedia.org/wiki/Digital_Signal_Processing) (DSP) is very complex, and we were likely in way over our heads. Second realization: [Chris Rogers](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html) has already done the heavy lifting in the API.
Technitone isn't using any really complex math or audioholicism; this functionality is easily accessible to interested developers. We really just needed to brush up on some terminology and [read the docs](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). Our advice? Don't skim them. Read them. Start at the top and end at the bottom. They are peppered with diagrams and photos, and it's really cool stuff.

If this is the first you've heard of the Web Audio API, or don't know what it can do, hit up Chris Rogers' [demos](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html). Looking for inspiration? You'll definitely find it there.

### Web Audio API Demo

Load in a sample (sound file)…

```js
/**
 * The XMLHttpRequest allows you to get the load
 * progress of your file download and has a responseType
 * of "arraybuffer" that the Web Audio API uses to
 * create its own AudioBufferNode.
 * Note: the 'true' parameter of request.open makes the
 * request asynchronous - this is required!
 */
var request = new XMLHttpRequest();
request.open("GET", "mySample.mp3", true);
request.responseType = "arraybuffer";
request.onprogress = onRequestProgress; // Progress callback.
request.onload = onRequestLoad; // Complete callback.
request.onerror = onRequestError; // Error callback.
request.onabort = onRequestError; // Abort callback.
request.send();

// Use this context to create nodes, route everything together, etc.
var context = new webkitAudioContext();

// Feed this AudioBuffer into your AudioBufferSourceNode:
var audioBuffer = null;

function onRequestProgress (event) {
    var progress = event.loaded / event.total;
}

function onRequestLoad (event) {
    // The 'true' parameter specifies if you want to mix the sample to mono.
    audioBuffer = context.createBuffer(request.response, true);
}

function onRequestError (event) {
    // An error occurred when trying to load the sound file.
}

...modüler yönlendirmeyi ayarlayın...

/**
 * Generally you'll want to set up your routing like this:
 * AudioBufferSourceNode > [effect nodes] > CompressorNode > AudioContext.destination
 * Note: nodes are designed to be able to connect to multiple nodes.
 */

// The DynamicsCompressorNode makes the loud parts
// of the sound quieter and quiet parts louder.
var compressorNode = context.createDynamicsCompressor();
compressorNode.connect(context.destination);

// [other effect nodes]

// Create and route the AudioBufferSourceNode when you want to play the sample.

...bir çalışma zamanı etkisi uygulayın (bir dürtü tepkisi kullanarak evrim)...

/**
 * Your routing now looks like this:
 * AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
 */

var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;

...başka bir çalışma zamanı efekti uygulayın (gecikme)...

/**
 * The delay effect needs some special routing.
 * Unlike most effects, this one takes the sound data out
 * of the flow, reinserts it after a specified time (while
 * looping it back into itself for another iteration).
 * You should add an AudioGainNode to quieten the
 * delayed sound...just so things don't get crazy :)
 *
 * Your routing now looks like this:
 * AudioBufferSourceNode -> ConvolverNode > CompressorNode > AudioContext.destination
 *                       |  ^
 *                       |  |___________________________
 *                       |  v                          |
 *                       -> DelayNode > AudioGainNode _|
 */

var delayGainNode = context.createGainNode();
delayGainNode.gain.value = 0.7; // Quieten the feedback a bit.
delayGainNode.connect(convolverNode);

var delayNode = context.createDelayNode();
delayNode.delayTime = 0.5; // Re-sound every 0.5 seconds.
delayNode.connect(delayGainNode);

delayGainNode.connect(delayNode); // make the loop

...ve daha sonra duyulabilir olmasını sağlayın.

/**
 * Once your routing is set up properly, playing a sound
 * is easy-shmeezy. All you need to do is create an
 * AudioSourceBufferNode, route it, and tell it what time
 * (in seconds relative to the currentTime attribute of
 * the AudioContext) it needs to play the sound.
 *
 * 0 == now!
 * 1 == one second from now.
 * etc...
 */

var sourceNode = context.createBufferSource();
sourceNode.connect(convolverNode);
sourceNode.connect(delayNode);
sourceNode.buffer = audioBuffer;
sourceNode.noteOn(0); // play now!

Technitone'da oynatma konusundaki yaklaşımımız tamamen programlamaya dayanır. Her vuruşta sesleri işleme hızımıza eşit bir zamanlayıcı aralığı ayarlamak yerine, sesleri bir sıraya göre yöneten ve planlayan daha küçük bir aralık belirleriz. Bu, CPU'ya sesi gerçekten duyulabilir hale getirme görevini vermeden önce, API'nin ses verilerini çözme, filtreleri ve efektleri işleme gibi ön işleri gerçekleştirmesine olanak tanır. Nihayet bu gelişme olduğunda konuşmacılara net sonucu sunmak için gereken tüm bilgiler mevcut.

Genel olarak her şeyin optimize edilmesi gerekiyordu. CPU'larımızı çok zorladığımızda programa ayak uydurmak için işlemler atlanıyordu (pop, tıklama, kazındı). Chrome'da başka bir sekmeye geçerseniz tüm çılgınlığı durdurmak için ciddi çaba harcıyoruz.

Işık gösterisi

Önde ve ortada ızgara ve parçacık tünelimiz yer alıyor. Bu, Technitone'un WebGL katmanıdır.

WebGL, GPU'yu işlemciyle birlikte çalışacak şekilde görevlendirerek, web'de görsel oluşturma konusundaki diğer yaklaşımların çoğundan çok daha üstün performans sunar. Bu performans kazancı, çok daha dik bir öğrenme eğrisiyle önemli ölçüde daha ayrıntılı geliştirmenin maliyetini beraberinde getirir. Bununla birlikte, web'de etkileşime gerçekten tutkuluysanız ve mümkün olduğunca az performans kısıtlaması istiyorsanız WebGL, Flash ile karşılaştırılabilir bir çözüm sunar.

WebGL Demosu

WebGL içeriği bir tuval olarak (kelimenin tam anlamıyla HTML5 Tuvali) oluşturulur ve şu temel yapı taşlarından oluşur:

  • nesne köşeleri (geometri)
  • konum matrisleri (3D koordinatlar)
    • gölgelendiriciler (doğrudan GPU'ya bağlı geometri görünümünün açıklaması)
    • bağlam ("GPU'nun referans verdiği öğelere giden kısayollar")
    • arabellekler (bağlam verilerini GPU'ya geçirmek için kullanılan ardışık düzenler)
    • ana kod (istenen etkileşimli uygulamaya özgü iş mantığı)
    • "draw" yöntemi (gölgelendiricileri etkinleştirir ve tuvale pikseller çizer)

Ekranda WebGL içeriğini oluşturmak için temel işlem şu şekildedir:

  1. Perspektif matrisini ayarlayın (3D alana bakan kameraya ilişkin ayarları düzenleyerek görüntü düzlemini tanımlar).
  2. Konum matrisini ayarlayın (3D koordinatlarda konumların göreli olarak ölçüldüğü bir başlangıç noktası belirtin).
  3. Gölgelendiriciler üzerinden bağlama geçmek için arabellekleri verilerle (köşe konumu, renk, dokular...) doldurun.
  4. Gölgelendiricilerle arabelleklerden verileri çıkartıp düzenleyin ve GPU'ya aktarın.
  5. Bağlama, gölgelendiricileri etkinleştirmesini, verilerle çalışmayı ve kanvası güncellemesini söylemek için çizim yöntemini çağırın.

Uygulama şu şekilde görünür:

Perspektif matrisini ayarlayın...

// Aspect ratio (usually based off the viewport,
// as it can differ from the canvas dimensions).
var aspectRatio = canvas.width / canvas.height;

// Set up the camera view with this matrix.
mat4.perspective(45, aspectRatio, 0.1, 1000.0, pMatrix);

// Adds the camera to the shader. [context = canvas.context]
// This will give it a point to start rendering from.
context.uniformMatrix4fv(shader.pMatrixUniform, 0, pMatrix);

...konum matrisini ayarlayın...

// This resets the mvMatrix. This will create the origin in world space.
mat4.identity(mvMatrix);

// The mvMatrix will be moved 20 units away from the camera (z-axis).
mat4.translate(mvMatrix, [0,0,-20]);

// Sets the mvMatrix in the shader like we did with the camera matrix.
context.uniformMatrix4fv(shader.mvMatrixUniform, 0, mvMatrix);

...bazı geometrik şekilleri ve görünüm tanımlayın...

// Creates a square with a gradient going from top to bottom.
// The first 3 values are the XYZ position; the last 4 are RGBA.
this.vertices = new Float32Array(28);
this.vertices.set([-2,-2, 0,    0.0, 0.0, 0.7, 1.0,
                   -2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2,-2, 0,    0.0, 0.0, 0.7, 1.0
                  ]);

// Set the order of which the vertices are drawn. Repeating values allows you
// to draw to the same vertex again, saving buffer space and connecting shapes.
this.indices = new Uint16Array(6);
this.indices.set([0,1,2, 0,2,3]);

...tamponları verilerle doldurun ve bağlama aktarın...

// Create a new storage space for the buffer and assign the data in.
context.bindBuffer(context.ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ARRAY_BUFFER, this.vertices, context.STATIC_DRAW);

// Separate the buffer data into its respective attributes per vertex.
context.vertexAttribPointer(shader.vertexPositionAttribute,3,context.FLOAT,0,28,0);
context.vertexAttribPointer(shader.vertexColorAttribute,4,context.FLOAT,0,28,12);

// Create element array buffer for the index order.
context.bindBuffer(context.ELEMENT_ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ELEMENT_ARRAY_BUFFER, this.indices, context.STATIC_DRAW);

...ve çizim yöntemini çağırın

// Draw the triangles based off the order: [0,1,2, 0,2,3].
// Draws two triangles with two shared points (a square).
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0);

Alfa tabanlı görsellerin üst üste yığılmasını istemiyorsanız her kareyi temizlemeyi unutmayın.

Gösteri mekanı

Izgara ve parçacık tünelinin yanı sıra diğer tüm kullanıcı arayüzü öğeleri HTML / CSS'de oluşturulmuştur ve JavaScript'te etkileşimli mantıktır.

En baştan, kullanıcıların mümkün olduğunca hızlı bir şekilde tabloyla etkileşimde bulunması gerektiğine karar verdik. Başlangıç ekranı, talimat veya eğitim yok. Sadece "Git". Arayüz yüklendiyse, bunları yavaşlatan bir şey olmamalıdır.

Bu nedenle, ilk kez gelen bir kullanıcıyı etkileşimleri sırasında nasıl yönlendireceğimizi dikkatle incelememiz gerekiyordu. CSS imleç özelliğinin, kullanıcının WebGL alanındaki konumuna göre değişmesi gibi küçük işaretler ekledik. İmleç ızgaranın üzerindeyse el imlecine geçiririz (çünkü bu iki imleç, tonları çizerek etkileşime geçebilir). Izgaranın etrafındaki boşlukta fareyle üzerine gelindiğinde, fareyi yönlü çapraz imleçle değiştiririz (bu, öğenin dönebileceğini veya ızgarayı katmanlara bölebildiğini gösterir).

Şova Hazırlanma

DAHA AZ (bir CSS ön işlemcisi) ve CodeKit (steroidlerde web geliştirme), tasarım dosyalarını sökük HTML/CSS'ye çevirmek için gereken süreyi kısaltır. Bu araçlar CSS'yi çok daha yönlü bir şekilde düzenlememizi, yazmamızı ve optimize etmemizi sağlıyor. Böylece değişkenlerden, karışımlardan (fonksiyonlar) ve hatta matematikten bile faydalanabiliyoruz.

Sahne Efektleri

CSS3 geçişlerini ve backbone.js'yi kullanarak, uygulamaya hayat veren ve kullanıcılara kullandıkları enstrümanı belirten görsel sıralar sunan bazı basit efektler oluşturduk.

Technitone&#39;un renkleri.

Backbone.js, renk değişikliği etkinliklerini yakalamamıza ve yeni rengi uygun DOM öğelerine uygulamamıza olanak tanır. GPU hızlandırmalı CSS3 geçişleri, renk stili değişikliklerini ele aldı ve performansı neredeyse hiç etkilemedi.

Arayüz öğelerindeki renk geçişlerinin çoğu, arka plan renklerinin geçişiyle oluşturulmuştur. Bu arka plan renginin üzerine, arka plan renginin parlamasını sağlamak için stratejik şeffaflık alanlarına sahip arka plan resimlerini yerleştiririz.

HTML: Temel

Demo için üç renk bölgesine ihtiyacımız vardı: iki kullanıcı renk bölgesi ve üçüncü bir karma renk bölgesi. CSS3 geçişlerini destekleyecek düşünebileceğimiz en basit DOM yapısını ve çizimimiz için en az HTTP isteğini oluşturduk.

<!-- Basic HTML Setup -->
<div class="illo color-mixed">
  <div class="illo color-primary"></div>
  <div class="illo color-secondary"></div>
</div>

CSS: Stil ile Basit Yapı

Her bölgeyi doğru konumuna yerleştirmek için mutlak konumlandırma kullandık ve arka plan konumu özelliğini her bir bölgedeki arka plan resmini hizalayacak şekilde düzenledik. Bu, tüm bölgelerin (her biri aynı arka plan resmine sahip) tek bir öğe gibi görünmesine neden olur.

.illo {
  background: url('../img/illo.png') no-repeat;
  top:        0;
  cursor:     pointer;
}
  .illo.color-primary, .illo.color-secondary {
    position: absolute;
    height:   100%;
  }
  .illo.color-primary {
    width:                350px;
    left:                 0;
    background-position:  top left;
  }
  .illo.color-secondary {
    width:                355px;
    right:                0;
    background-position:  top right;
  }

Renk değişikliği etkinliklerini işleyen GPU ile hızlandırılmış geçişler uygulandı. Renklerin karıştırılmasının zaman aldığı izlenimi oluşturmak için süreyi uzattık ve .color-mixed üzerinde yumuşatmayı değiştirdik.

/* Apply Transitions To Backgrounds */
.color-primary, .color-secondary {
  -webkit-transition: background .5s linear;
  -moz-transition:    background .5s linear;
  -ms-transition:     background .5s linear;
  -o-transition:      background .5s linear;
}

.color-mixed {
  position:           relative;
  width:              750px;
  height:             600px;
  -webkit-transition: background 1.5s cubic-bezier(.78,0,.53,1);
  -moz-transition:    background 1.5s cubic-bezier(.78,0,.53,1);
  -ms-transition:     background 1.5s cubic-bezier(.78,0,.53,1);
  -o-transition:      background 1.5s cubic-bezier(.78,0,.53,1);
}

Mevcut tarayıcı desteği ve CSS3 geçişleriyle ilgili önerilen kullanım için lütfen HTML5 adresini ziyaret edin.

JavaScript: İşe Yaramasını Sağlama

Renkleri dinamik olarak atamak basit bir işlemdir. DOM'da renk sınıfımıza sahip herhangi bir öğeyi ararız ve arka plan rengini kullanıcının renk seçimlerine göre belirleriz. Bir sınıf ekleyerek DOM'deki tüm öğelere geçiş efektimizi uygularız. Bu sayede hafif, esnek ve ölçeklenebilir bir mimari oluşturulur.

function createPotion() {

    var primaryColor = $('.picker.color-primary > li.selected').css('background-color');
    var secondaryColor = $('.picker.color-secondary > li.selected').css('background-color');
    console.log(primaryColor, secondaryColor);
    $('.illo.color-primary').css('background-color', primaryColor);
    $('.illo.color-secondary').css('background-color', secondaryColor);

    var mixedColor = mixColors (
            parseColor(primaryColor),
            parseColor(secondaryColor)
    );

    $('.color-mixed').css('background-color', mixedColor);
}

Birincil ve ikincil renkler seçildikten sonra, bunların karma renk değerini hesaplar ve elde edilen değer, uygun DOM öğesine atanır.

// take our rgb(x,x,x) value and return an array of numeric values
function parseColor(value) {
    return (
            (value = value.match(/(\d+),\s*(\d+),\s*(\d+)/)))
            ? [value[1], value[2], value[3]]
            : [0,0,0];
}

// blend two rgb arrays into a single value
function mixColors(primary, secondary) {

    var r = Math.round( (primary[0] * .5) + (secondary[0] * .5) );
    var g = Math.round( (primary[1] * .5) + (secondary[1] * .5) );
    var b = Math.round( (primary[2] * .5) + (secondary[2] * .5) );

    return 'rgb('+r+', '+g+', '+b+')';
}

HTML/CSS Mimarisi için çizim: Üç Renk Değiştiren Kutuya Kişilik Verme

Hedefimiz, bitişik renk bölgelerine kontrast renkler yerleştirildiğinde bütünlüğünü koruyan eğlenceli ve gerçekçi bir ışıklandırma efekti oluşturmaktı.

24 bit PNG, HTML öğelerimizin arka plan renginin resmin şeffaf alanlarında gösterilmesine olanak tanır.

Görüntü Saydamlıkları

Renkli kutular, farklı renklerin bir arada bulunduğu sert kenarlar oluşturur. Bu yöntem, gerçekçi ışık efektlerinin önüne geçiyor ve çizimi tasarlarken karşılaşılan en büyük zorluklardan biriydi.

Renk Bölgeleri

Çözüm, çizimi, renk bölgelerinin kenarlarının şeffaf alanlardan hiçbir zaman görünmesine izin vermeyecek şekilde tasarlamaktı.

Renk Bölgesi Kenarları

Derlemenin planlanması kritik öneme sahipti. Tasarımcı, geliştirici ve çizer arasında yapılan kısa bir planlama oturumu, ekibin monte edildiğinde birlikte çalışması için her şeyin nasıl inşa edilmesi gerektiğini anlamasına yardımcı oldu.

Katman adlandırmanın CSS yapısıyla ilgili bilgileri nasıl iletebileceğini gösteren bir örnek olarak Photoshop dosyasını inceleyin.

Renk Bölgesi Kenarları

Encore

Chrome kullanmayan kullanıcılar için ise uygulamanın özünü tek bir statik resimde süzmek amacıyla bir hedef belirledik. Izgara düğümü kahraman haline geldi, arka plan karoları uygulamanın amacını ve ızgaranın etkileyici 3D ortamındaki yansımada bulunan perspektif ipuçları veriyor.

Renk Bölgesi Kenarları.

Technitone hakkında daha fazla bilgi edinmek istiyorsanız blogumuzu takip etmeye devam edin.

Grup

Okuduğunuz için teşekkür ederiz. Belki yakında sizinle sohbet edebiliriz!