Örnek Olay - Technitone.com sitesini oluşturma

Sean Middleditch
Sean Middleditch
Technitone: Web'de ses deneyimi.

Technitone.com, WebGL, Canvas, Web Soketleri, CSS3, JavaScript, Flash ve Chrome'daki yeni Web Audio API'nin bir birleşimidir.

Bu makalede, prodüksiyonun her yönüne değineceğiz: plan, sunucu, sesler, görseller ve etkileşimli tasarımlar için yararlandığımız iş akışlarından bazıları. Çoğu bölümde kod snippet'leri, bir demo ve indirme bulunur. Makalenin sonunda, tüm bunları tek bir ZIP dosyası olarak indirebileceğiniz bir indirme bağlantısı bulunmaktadır.

gskinner.com prodüksiyon ekibi.

Konser

gskinner.com'da ses mühendisi değiliz ancak bir sorunla karşılaştığınızda bize ulaşabilirsiniz.

  • Kullanıcılar, Andre'nin ToneMatrix'inden "ilham alarak tonları bir ızgara üzerinde noktalarla gösterir.
  • Tonlar, örneklenmiş enstrümanlara, davul setlerine ve hatta kullanıcıların kendi kayıtlarına bağlanır.
  • Aynı ızgara üzerinde aynı anda birden fazla bağlı kullanıcının oynaması
  • …veya tek başına moduna geçerek kendi başlarına keşfe çıkabilirler.
  • Davetli oturumlar, kullanıcıların bir grup oluşturmasına ve anında jam session yapmasına olanak tanır.

Kullanıcılara, ses filtreleri ve efektleri tonlarına uygulayan bir araç paneli aracılığıyla Web Audio API'yi keşfetme fırsatı sunuyoruz.

gskinner.com tarafından Technitone

Ayrıca:

  • Kullanıcıların kompozisyonlarını ve efektlerini veri olarak depolayın ve istemciler arasında senkronize edin
  • Güzel şarkılar çizebilmeleri için birkaç renk seçeneği sunun.
  • Kullanıcıların diğer kullanıcıların çalışmalarını dinleyebilmesi, beğenebilmesi ve hatta düzenleyebilmesi için bir galeri sunun.

Bilinen ızgara metaforunu kullandık, ızgarayı 3D uzayda yüzen bir öğe haline getirdik, ışıklandırma, doku ve parçacık efektleri ekledik, esnek (veya tam ekran) bir CSS ve JS tabanlı arayüze yerleştirdik.

Arabayla seyahat

Enstrüman, efekt ve ızgara verileri istemcide birleştirilir ve serileştirilir, ardından Socket.io gibi birden fazla kullanıcı için çözülmesi amacıyla özel Node.js arka ucuma gönderilir. Bu veriler, her oyuncunun katkıları dahil edilerek istemciye geri gönderilir ve daha sonra çok kullanıcılı oynatma sırasında kullanıcı arayüzünü, örnekleri ve efektleri oluşturmaktan sorumlu ilgili CSS, WebGL ve WebAudio katmanlarına dağıtılır.

Soketlerle gerçek zamanlı iletişim, istemcide JavaScript'i ve sunucuda JavaScript'i besler.

Technitone Sunucu Diyagramı

Sunucunun her yönü için Node'u kullanırız. Statik web sunucusu ve soket sunucumuz bir aradadır. Sonunda Express'i kullanmaya karar verdik. Express, tamamen Node üzerinde oluşturulmuş tam bir web sunucusudur. Süper ölçeklenebilir ve yüksek düzeyde özelleştirilebilirdir. Ayrıca, alt düzey sunucu özelliklerini sizin için yönetir (tıpkı Apache veya Windows Server'ın yaptığı gibi). Geliştirici olarak tek yapmanız gereken uygulamanızı oluşturmaya odaklanmaktır.

Çoklu Kullanıcı Demo (Tamam, aslında sadece bir ekran görüntüsü)

Bu demonun bir Node sunucusunda çalıştırılması gerekir. Bu makale bir Node sunucusu olmadığından, Node.js'i yükledikten, web sunucunuzu yapılandırdıktan ve yerel olarak çalıştırdıktan sonra demonun nasıl göründüğünü gösteren bir ekran görüntüsü ekledik. Demo kurulumunuzu her yeni kullanıcı ziyaret ettiğinde yeni bir ızgara eklenir ve herkesin çalışması diğer kullanıcılar tarafından görülebilir.

Node.js Demo'nun ekran görüntüsü

Node kullanımı kolaydır. Socket.io ve özel POST isteklerinin bir kombinasyonunu kullanarak senkronizasyon için karmaşık rutinler oluşturmamız gerekmedi. Socket.io bunu şeffaf bir şekilde yönetir; JSON aktarılır.

Ne kadar kolay? Bunu izleyin.

3 satır 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ç tane daha.

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);

});

Şimdi HTML sayfasından gelen bağlantıları dinlemeye başlayacağız.

<!-- 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önlendirme ayarlarını yapı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.

…çalışma zamanında efekt uygulama (impulse response kullanılarak convolve)…

/**
 * 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 (gecikme) uygulayın…

/**
 * 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 ardından sesli hale getirin.

/**
 * 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 planlamaya dayanır. Sesleri her vuruşta işlemek için tempomuza eşit bir zamanlayıcı aralığı ayarlamak yerine, sesleri bir sırada yönetip planlayan daha küçük bir aralık belirleriz. Bu sayede API, CPU'yu sesli hale getirme görevi vermeden önce ses verilerini çözme ve filtreleri ve efektleri işleme gibi ön hazırlık çalışmalarını gerçekleştirebilir. Bu vuruş sonunda geldiğinde, net sonucu hoparlörlere sunmak için ihtiyaç duyduğu tüm bilgilere sahip olur.

Genel olarak her şeyin optimize edilmesi gerekiyordu. CPU'larımızı çok zorladığımızda, programa ayak uydurmak için işlemler atlandı (pop, tıklama, çizilme). Chrome'da başka bir sekmeye atladığınızda tüm bu çılgınlığı durdurmak için ciddi çaba sarf ettik.

Işık gösterisi

Ön planda ve merkezde ızgaramız ve parçacık tünelimizle karşılaşırsınız. Bu, Technitone'un WebGL katmanıdır.

WebGL, GPU'yu işlemciyle birlikte çalışacak şekilde görevlendirerek web'de görselleri oluşturmaya yönelik diğer yaklaşımların çoğundan önemli ölçüde daha üstün performans sunar. Bu performans artışı, çok daha dik bir öğrenme eğrisi olan çok daha karmaşık bir geliştirme maliyetiyle birlikte gelir. 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'a benzer bir çözüm sunar.

WebGL Demosu

WebGL içeriği bir kanvasta (HTML5 kanvası) oluşturulur ve aşağıdaki temel yapı taşlarından oluşur:

  • nesne köşe noktaları (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 öğelerin kısayolları")
    • arabellekler (bağlam verilerini GPU'ya aktarmak için kullanılan ardışık düzenler)
    • ana kod (istenen etkileşimli öğeye özel iş mantığı)
    • "draw" yöntemi (gölgelendiricileri etkinleştirir ve tuvale piksel çizer)

WebGL içeriğini ekranda oluşturma işleminin temel süreci aşağıdaki gibidir:

  1. Perspektif matrisini ayarlayın (3D uzaya bakan kameranın ayarlarını düzenleyerek resim düzlemini tanımlar).
  2. Konum matrisini ayarlayın (konumların ölçüleceği 3D koordinatlarda bir kaynak tanımlayın).
  3. Verileri (köşe konumu, renk, dokular vb.) doldurarak gölgelendiriciler aracılığıyla bağlama aktarın.
  4. Gölgelendiricilerle arabelleklerdeki verileri ayıklayıp düzenleyin ve GPU'ya aktarın.
  5. Bağlama, gölgelendiricileri etkinleştirmesini, verilerle çalışmasını ve kanvası güncellemesini söylemek için draw yöntemini çağırın.

Bu özellik şu şekilde çalışı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ı geometri ve görünümleri 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]);

…arabellekleri veri ile doldurup 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 draw 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 birbirinin üzerine yığılmasını istemiyorsanız her karede kanvası 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 ve etkileşimli mantıkta JavaScript ile oluşturuldu.

Başından itibaren, kullanıcıların ızgarayla mümkün olduğunca hızlı bir şekilde etkileşime geçmesi gerektiğine karar verdik. Başlangıç ekranı, talimat veya eğitim yok. Sadece "Başlayın". Arayüz yüklüyse onları yavaşlatan bir şey yoktur.

Bu nedenle, ilk kez uygulamayı kullanan kullanıcılara etkileşimlerinde nasıl rehberlik edileceğine dikkatlice bakmamız gerekiyordu. CSS imleci özelliğinin, kullanıcının WebGL alanındaki fare konumuna göre değişmesini sağlama gibi ince ipuçları ekledik. İmleç ızgaranın üzerindeyse imleci el imlecine değiştiririz (kullanıcılar tonları işaretleyerek etkileşim kurabilir). Fareyle ızgaranın etrafındaki boşluğa gelindiğinde, imleci yön oku imleci ile değiştiririz (kullanıcıların ızgarayı döndürebileceğini veya katmanlara ayırabileceğini belirtmek için).

Gösteriye Hazırlık

LESS (CSS ön işlemcisi) ve CodeKit (steroidlerle desteklenen web geliştirme), tasarım dosyalarını şablon HTML/CSS'ye dönüştürme süresini önemli ölçüde kısalttı. Bu özellikler, değişkenlerden, karma öğelerden (işlevler) ve hatta matematikten yararlanarak CSS'yi çok daha çok yönlü bir şekilde düzenlememize, yazmamıza ve optimize etmemize olanak tanır.

Sahne Efektleri

CSS3 geçişleri ve backbone.js'i kullanarak uygulamaya hayat veren ve kullanıcılara hangi enstrümanı kullandıklarını gösteren görsel ipuçları sunan bazı basit efektler oluşturduk.

Technitone 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 performans üzerinde çok az veya hiç etkisi olmadan yönetti.

Arayüz öğelerindeki renk geçişlerinin çoğu, arka plan renkleri değiştirilerek oluşturulmuştur. Bu arka plan renginin üzerine, arka plan renginin parlamasına izin vermek için stratejik şeffaflık alanlarına sahip arka plan resimleri yerleştiririz.

HTML: Temel

Demo için üç renk bölgesine ihtiyacımız vardı: kullanıcı tarafından seçilen iki renk bölgesi ve üçüncü bir karma renk bölgesi. Görselimiz için CSS3 geçişlerini destekleyen ve en az sayıda HTTP isteği gönderen, aklımıza gelen en basit DOM yapısını 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 içeren basit yapı

Her bölgeyi doğru konumuna yerleştirmek için mutlak konumlandırmayı kullandık ve arka plan resmini her bölgede hizalamak için background-position özelliğini ayarladık. Bu sayede, her biri aynı arka plan resmine sahip olan tüm bölgeler tek bir öğe gibi görünür.

.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 dinleyen GPU hızlandırılmış geçişler uygulandı. Renklerin karışmasının zaman aldığı izlenimi yaratmak için .color-mixed öğesinin süresini artırdık ve geçiş hızını 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);
}

CSS3 geçişleri için mevcut tarayıcı desteği ve önerilen kullanım hakkında bilgi edinmek üzere HTML5please'i ziyaret edin.

JavaScript: Kullanımı

Renkleri dinamik olarak atama işlemi basittir. DOM'da renk sınıfımızı içeren tüm öğeleri arar ve arka plan rengini kullanıcının renk seçimlerine göre ayarlarız. Geçiş efektimizi, bir sınıf ekleyerek DOM'daki herhangi bir öğeye 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 karıştırılmış renk değerini hesaplar ve elde edilen değeri uygun DOM öğesine atarız.

// 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 Görselleştirme: Üç Renk Değiştiren Kutuya Kişilik Kazandırma

Amacımız, bitişik renk bölgelerine zıt 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ından gösterilmesine olanak tanır.

Resim Saydamlıkları

Renkli kutular, farklı renklerin birleştiği yerlerde sert kenarlar oluşturuyor. Bu durum, gerçekçi ışıklandırma efektlerinin önüne geçer ve görseli tasarlarken karşılaştığımız en büyük zorluklardan biriydi.

Renk Bölgeleri

Çözüm, görseli renk bölgelerinin kenarlarının şeffaf alanlarda gösterilmesine hiçbir zaman izin vermeyecek şekilde tasarlamaktı.

Renk Bölgesi Kenarları

Derleme için planlama yapmak çok önemliydi. Tasarımcı, geliştirici ve illüstratör arasında yapılan kısa bir planlama oturumu, ekibin tüm öğelerin bir araya getirildiğinde birlikte çalışabilmesi için nasıl oluşturulması gerektiğini anlamasına yardımcı oldu.

Katman adlandırmasının CSS oluşturma hakkında bilgi verebileceğine dair örnek olarak Photoshop dosyasına göz atın.

Renk Bölgesi Kenarları

Encore

Chrome kullanmayan kullanıcılar için uygulamanın özünü tek bir statik resimde özetleme hedefi belirledik. Izgara düğümü ana öğe haline geldi, arka plan karoları uygulamanın amacına işaret ediyor ve yansımada bulunan perspektif, ızgaranın sürükleyici 3D ortamına gönderme yapıyor.

Renk Bölgesi Kenarları.

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

Grup

Bu e-postayı okuduğunuz için teşekkürler. Yakında birlikte müzik yapacağız.