# Ses

Botun bir ses kanalına girip müzik, duyuru ya da ses efekti yayınlayabilir. Ses dosyası ya da akış senin makinende çözülür ve doğrudan medya sunucusuna gönderilir; N'oluyo'nun API sunucularından geçmez.

> **Önemli:** Bir bot seste **yalnızca yayın yapar, kimseyi duyamaz.** Botun aldığı token'da `canSubscribe: false` yazılıdır ve medya sunucusu botun hiçbir ses, görüntü ya da veri izine abone olmasına izin vermez. Kodunda abone olmayı denesen bile tek bir ses karesi gelmez.

## Akış

Ses bağlantısı iki adımda kurulur: önce gateway'den ses kanalına girmek istediğini söylersin, sonra gelen adres ve token ile LiveKit odasına bağlanırsın.

1. Gateway bağlantında `op 4` gönderirsin (`intents: 0` yeterlidir; ses için ek bir intent gerekmez):

```json
{
  "op": 4,
  "d": {
    "guild_id": "21531879014400001",
    "channel_id": "21531879014400099",
    "self_mute": false
  }
}
```

2. İzinlerin tamamsa ve sınırlar aşılmadıysa aynı oturuma `VOICE_SERVER_UPDATE` olayı gelir:

```json
{
  "guild_id": "21531879014400001",
  "channel_id": "21531879014400099",
  "endpoint": "wss://noluyo.net/rtc",
  "token": "<10 dakikalık LiveKit token'ı>",
  "bitrate": 96000
}
```

3. Bir kural ya da sınır engellerse bağlantı kapanmaz; `VOICE_JOIN_ERROR` olayı gelir:

```json
{
  "guild_id": "21531879014400001",
  "channel_id": "21531879014400099",
  "code": "channel_bot_limit",
  "message": "This voice channel already has the maximum number of bots."
}
```

4. Kanaldan ayrılmak için önce LiveKit odasından kop, ardından `channel_id: null` ile `op 4` gönder:

```json
{
  "op": 4,
  "d": {
    "guild_id": "21531879014400001",
    "channel_id": null
  }
}
```

Bir bot her sunucuda aynı anda tek bir ses kanalında olabilir; farklı sunucularda ise aynı anda birden çok ses kanalında bulunabilir. Odaya girerken aldığın `token` 10 dakika geçerlidir; bağlandıktan sonra açık kalan odalarda LiveKit bağlantıyı kendisi yeniler, 10 dakika içinde hiç bağlanmazsan token geçersiz olur.

## Gereken izinler

Ses kanalına girip yayın yapmak için botun o kanalda üç izne ihtiyacı vardır:

- `ViewChannel` (`1024`) — kanalı görme
- `Connect` (`1048576`) — ses kanalına bağlanma
- `Speak` (`2097152`) — seste yayın yapma

Üçünün toplamı `3146752` eder; ekleme bağlantısında `izin=3146752` kullanabilirsin.

Bot odaya girdikten sonra izinleri değişirse şunlar olur:

- **`Speak` kaldırılırsa** (veya bot zaman aşımına alınırsa) bot odada kalır ama yayın izni anında kapatılır; sesi kesilir.
- **`Connect` veya `ViewChannel` kaldırılırsa** bot odadan hemen çıkarılır.

Botlar yalnızca sunuculardaki ses kanallarına (`type: 2`) girebilir; DM aramalarına katılamaz, kamera ya da ekran paylaşımı açamaz ve yalnızca tek bir mikrofon ses izi yayınlayabilir.

## Odaya bağlanmak ve dosya çalmak

Aşağıdaki örnek bir ses dosyasını `ffmpeg` ile 48 kHz stereo PCM'e çevirip odaya yayınlar ve dosya bitince kanaldan ayrılır. Yanına [Gateway](https://noluyo.net/gelistirici/gateway.md#tam-bir-istemci) sayfasındaki `gateway.mjs` dosyasını koy.

Önce LiveKit'in Node paketini ve makinenin paket yöneticisinden `ffmpeg` aracını kur:

```bash
npm install @livekit/rtc-node@1.2.1
```

Dosyayı `cal.mjs` adıyla kaydet:

```js title=cal.mjs
import { spawn } from "node:child_process";
import { AudioFrame, AudioSource, LocalAudioTrack, Room, TrackPublishOptions, TrackSource, dispose } from "@livekit/rtc-node";
import { connect } from "./gateway.mjs";

const { GUILD_ID, VOICE_CHANNEL_ID, AUDIO_FILE } = process.env;
const RATE = 48000;
const CHANNELS = 2;
const SAMPLES = 960; // 20 ms
const FRAME_BYTES = SAMPLES * CHANNELS * 2;

const gateway = await connect({
  intents: 0, // Ses için intent gerekmez.
  onEvent(type, d) {
    if (type === "READY") gateway.send(4, { guild_id: GUILD_ID, channel_id: VOICE_CHANNEL_ID, self_mute: false });
    if (type === "VOICE_SERVER_UPDATE") play(d).catch(fail);
    if (type === "VOICE_JOIN_ERROR") fail(new Error(`${d.code}: ${d.message}`));
  },
});

function fail(err) {
  console.error(err.message);
  process.exit(1);
}

async function play({ endpoint, token, bitrate }) {
  const room = new Room();
  await room.connect(endpoint, token, { autoSubscribe: false, dynacast: false });

  const source = new AudioSource(RATE, CHANNELS);
  const track = LocalAudioTrack.createAudioTrack("muzik", source);
  const options = new TrackPublishOptions({
    source: TrackSource.SOURCE_MICROPHONE,
    audioEncoding: { maxBitrate: BigInt(bitrate) }, // Kanalın ses kalitesini aşma.
  });
  await room.localParticipant.publishTrack(track, options);

  const ffmpeg = spawn("ffmpeg", ["-v", "error", "-i", AUDIO_FILE, "-f", "s16le", "-ar", String(RATE), "-ac", String(CHANNELS), "-"], {
    stdio: ["ignore", "pipe", "inherit"],
  });
  let rest = Buffer.alloc(0);
  for await (const chunk of ffmpeg.stdout) {
    rest = Buffer.concat([rest, chunk]);
    while (rest.length >= FRAME_BYTES) {
      const pcm = rest.subarray(0, FRAME_BYTES);
      const samples = new Int16Array(pcm.buffer.slice(pcm.byteOffset, pcm.byteOffset + FRAME_BYTES));
      await source.captureFrame(new AudioFrame(samples, RATE, CHANNELS, SAMPLES));
      rest = rest.subarray(FRAME_BYTES);
    }
  }

  await source.waitForPlayout();
  await track.close();
  await room.disconnect();
  gateway.send(4, { guild_id: GUILD_ID, channel_id: null });
  await dispose();
  process.exit(0);
}
```

Çalıştırmak için:

```bash
export BOT_TOKEN='kopyaladigin_token'
export GUILD_ID='sunucu_id'
export VOICE_CHANNEL_ID='ses_kanali_id'
export AUDIO_FILE='./ornek.mp3'
node cal.mjs
```

Ses dosyası senin makinende okunur; N'oluyo'nun API sunucusuna yüklenmez ve bizde saklanmaz. Yayınladığın sesin kullanım ve telif haklarına sahip olman senin sorumluluğundadır.

## Sınırlar

İnsanların sesli sohbetini korumak için bot ses bağlantıları aşağıdaki sınırlarla çalışır:

| Anahtar | Sınır | Açıklama |
|---|---|---|
| `channel` | 2 | Bir ses kanalında aynı anda bulunabilecek en çok bot |
| `guild` | 4 | Bir sunucudaki tüm ses kanallarında aynı anda bulunabilecek en çok bot |
| `app` | 50 | Bir uygulamanın tüm sunucularda aynı anda tutabileceği ses bağlantısı |
| `platform` | 100 | Tüm botların ortak eşzamanlı ses bağlantısı tavanı (`NOLUYO_BOT_VOICE_MAX`) |
| `alone` | 5 | Kanalda hiç insan kalmadığında botun çıkarılmadan önce beklediği dakika |
| `op4` | 5 / 20 | Bot ve sunucu başına 20 saniyede gönderilebilecek `op 4` çerçevesi |
| `tracks` | 10 / 60 | Bot ve oda başına dakikada yapılabilecek ses izi yayını; aşılırsa bot odadan çıkarılır |
| `bitrate` | 96 | Botlara verilen en yüksek ses bit hızı (kbps); kanal daha düşükse kanalın değeri gelir |

`op 4` hız sınırını aşarsan bağlantı kapanmaz, `RATE_LIMITED` (`opcode: 4`) olayı gelir; `retry_after` saniye bekleyip yeniden gönder. Kanalda tek başına 5 dakika kalan bot kendiliğinden sesten çıkarılır, böylece boş odalar ortak kapasiteyi doldurmaz.

## Bot odada ne görür, kişi ne görür

**Botun gördükleri:** Bot ses alamaz ve kaydedemez. Ancak LiveKit odasında katılımcı olduğu için o odada o an kimin bağlı olduğunu (kullanıcı kimliği ve görünen ad), susturulmuş olup olmadığını ve **kimin ne zaman konuştuğunu** (ses içeriği olmadan yalnızca aktif konuşmacı bilgisini) görür. Ekleme ekranı `Connect` izni isteyen her botta sunucu yöneticisine bunu açıkça söyler.

**Kişilerin gördükleri:**

- Web, masaüstü (oyun içi arayüz dahil) ve Android'de bot ses listesinde ve arama ekranında **BOT** etiketiyle görünür; etiketin üstüne gelindiğinde ya da bilgi penceresinde botun yalnızca yayın yaptığı ve kimseyi duyamadığı yazar.
- Bot, ses kanalının kişi sayacına (`1 / 5` gibi) **dahil edilmez** ve dolu bir kanala insan girmesini engellemez.
- İsteyen her kullanıcı botun ses seviyesini kendi cihazında kısıp açabilir ya da botu yalnızca kendisi için susturabilir.
- Botun satırında kulaklık kapalı (`self_deaf: true`) durumu her zaman açıktır; bu bir hata değil, botun kimseyi duyamadığının teknik karşılığıdır.

## Yönetici ne yapabilir

Sunucuda yetkisi olan yöneticiler seste duran bir bot üzerinde üç işlem yapabilir:

- **Sunucuda sustur (`MuteMembers`):** Botun yayın izni hemen kapatılır, sesi kesilir. Susturma kaldırılınca yeniden yayın yapabilir. **Sunucuda sağırlaştır** seçeneği botlar için gizlidir çünkü bot zaten hiçbir zaman duyamaz.
- **Bağlantıyı kes (`MoveMembers`):** Bot LiveKit odasından çıkarılır (`Disconnected`) ve ses satırı silinir. Botun kendi kendine hemen geri girmemeli; yalnızca bir kullanıcı komutu istediğinde yeniden `op 4` göndermelidir.
- **Başka ses kanalına taşı (`MoveMembers`):** Bot eski odadan çıkarılır ve botun açık gateway oturumuna yeni kanal için yeni bir `VOICE_SERVER_UPDATE` gönderilir. `cal.mjs` gibi `VOICE_SERVER_UPDATE` dinleyen bir bot eski `Room` nesnesini kapatıp gelen yeni `endpoint` ve `token` ile yeni odaya bağlanarak yayını sürdürebilir.

Botun token'ı yenilenirse, uygulaması durdurulursa ya da bot sunucudan çıkarılırsa açık tüm ses bağlantıları anında kesilir.

## Hata kodları (VOICE_JOIN_ERROR)

`op 4` isteği reddedildiğinde `VOICE_JOIN_ERROR` olayındaki `code` alanı nedenini söyler:

| Kod | Anlamı | Ne yapmalı |
|---|---|---|
| `unknown_guild` | Sunucu yok ya da bot bu sunucunun üyesi değil. | Sunucu kimliğini kontrol et, tekrar deneme. |
| `unknown_channel` | Kanal bu sunucuda yok. | Kanal kimliğini kontrol et. |
| `invalid_channel_type` | Kanal bir ses kanalı (`type: 2`) değil. | Metin kanalı ya da kategori yerine ses kanalı seç. |
| `missing_access` | Botun bu kanalı görme (`ViewChannel`) izni yok. | Kanal izinlerini kontrol et. |
| `missing_permissions` | Botun kanalda `Connect` izni yok ya da zaman aşımında. | Botun rolüne `Connect` ve `Speak` ver. |
| `channel_bot_limit` | Bu ses kanalında zaten 2 bot var. | Başka bir ses kanalı seç ya da diğer bot çıkana dek bekle. |
| `guild_bot_limit` | Bu sunucuda zaten 4 bot seste. | Sunucudaki diğer botlardan biri sesten çıkana dek bekle. |
| `app_voice_limit` | Uygulamanın eşzamanlı 50 ses bağlantısı doldu. | Boşta kalan bağlantıları kapat. |
| `bot_voice_full` | Platform genelindeki bot ses kapasitesi o an dolu. | Biraz bekleyip yeniden dene, sık döngüye girme. |
| `voice_unavailable` | Ses hizmeti o an kullanılamıyor ya da geçici bir hata oluştu. | Birkaç saniye bekleyip yeniden dene. |

## Bu sayfayı yapay zekaya verirsen

Bir yapay zeka aracından N'oluyo için ses veya müzik botu yazmasını isterken bu sayfayı ve [Gateway](https://noluyo.net/gelistirici/gateway.md) sayfasını birlikte ver. Şu kuralları özellikle hatırlat:

- Bot token'ı koda yazılmaz, `process.env.BOT_TOKEN` ile ortam değişkeninden okunur.
- Ses kanalına girmek HTTP isteği değildir; gateway üzerinden `op 4` gönderilir ve `VOICE_SERVER_UPDATE` beklenir. `POST /channels/{id}/voice` kişi rotasıdır ve bot token'ını `403 bot_forbidden` ile reddeder.
- Bot seste kimseyi duyamaz: `setSubscribed(true)` işe yaramaz, ses tanıma veya kayıt botu istendiğinde "N'oluyo'da botlar seste kimseyi duyamaz, bu özellik yapılamaz" denmelidir.
- `TrackPublishOptions` içinde `source: TrackSource.SOURCE_MICROPHONE` ve `maxBitrate: BigInt(bitrate)` kullanılmalıdır; kamera veya ekran kaynağı reddedilir.

## Sırada ne var

- [JavaScript kütüphanesi](https://noluyo.net/gelistirici/kutuphane-js.md) ve [Python kütüphanesi](https://noluyo.net/gelistirici/kutuphane-python.md): `bot.voice.join` ile tek satırda ses yayını
- [Gateway](https://noluyo.net/gelistirici/gateway.md): `op 4` çerçevesinin gittiği WebSocket bağlantısı
- [Hatalar](https://noluyo.net/gelistirici/hatalar.md): tüm REST ve `VOICE_JOIN_ERROR` hata kodları
