~32.000 şarkıyı iki geçişli loudnorm + alimiter ile yeniden encode et
Kod değişikliği gerektirmez — bağımsız script
Sorun: Şarkılar arasında ses seviyesi farkı var. Bazıları patlıyor, bazıları çok kısık.
Çözüm: Her şarkıyı ölçüp aynı ses seviyesine getiren bir script. Orijinal dosyalara dokunmuyor.
Süre: 3 paralel terminal ile ~15 saat (gece başlatılabilir).
Risk: Sıfır. Orijinal MP3'ler korunuyor. Sorun çıkarsa script'i durdur, eski dosyalar yerinde.
loudnorm=I=-16:TP=-1.5:LRA=14:measured_I=X:measured_TP=X:measured_LRA=X:measured_thresh=X:linear=false,alimiter=limit=0.95:attack=5:release=50
📁 Orijinal (DOKUNULMAZ) storage/tenant1001/app/public/muzibu/songs/song_xxxxx.mp3 📁 Re-encode Edilenler (script bunları üretir) storage/tenant1001/app/public/muzibu/hls/{song_id}/ ├── playlist.m3u8 ← high kalite (orijinal bitrate, 4sn segment) ├── segment-*.ts ← high segment'ler ├── ultralow/playlist.m3u8 ← 32kbps, mono, 22050Hz ├── low/playlist.m3u8 ← 64kbps, mono, 22050Hz ├── mid/playlist.m3u8 ← 128kbps, stereo, 44100Hz ├── master.m3u8 ← 4 varyant referansı ├── enc.bin ← DOKUNULMAZ (encryption key) └── enc.keyinfo ← DOKUNULMAZ storage/tenant1001/app/public/muzibu/songs/ ├── mp3_128/{song_id}.mp3 ← 128kbps MP3 fallback └── mp3_64/{song_id}.mp3 ← 64kbps MP3 son çare fallback
Bu dosyayı production sunucunun ana dizinine (/var/www/vhosts/muzibu.com/httpdocs/) kopyala.
<?php
/**
* Bağımsız Re-encode Script — Production Sunucu İçin
*
* Laravel kodlarını DEĞİŞTİRMEDEN tüm şarkıları iki geçişli loudnorm + alimiter ile yeniden encode eder.
*
* Kullanım:
* php re-encode-songs.php # Tüm şarkılar
* php re-encode-songs.php --limit=100 # İlk 100 şarkı
* php re-encode-songs.php --offset=100 --limit=100 # 101-200 arası
* php re-encode-songs.php --dry-run # Sadece say, encode yapma
* php re-encode-songs.php --skip-high # High re-encode atla
* php re-encode-songs.php --only-mp3 # Sadece MP3 64k+128k üret
* php re-encode-songs.php --song=34455 # Tek şarkı test
*
* Paralel çalıştırma (3 terminal):
* Terminal 1: php re-encode-songs.php --offset=0 --limit=11000
* Terminal 2: php re-encode-songs.php --offset=11000 --limit=11000
* Terminal 3: php re-encode-songs.php --offset=22000 --limit=11000
*/
// ============================================================
// AYARLAR
// ============================================================
$TENANT_ID = 1001;
$LOUDNORM_TARGET = '-16';
$LOUDNORM_TP = '-1.5';
$LOUDNORM_LRA = '14';
$LIMITER = 'alimiter=limit=0.95:attack=5:release=50';
$HLS_SEGMENT_TIME = 4;
$FFMPEG = '/usr/bin/ffmpeg';
$VARIANTS = [
'ultralow' => ['bitrate' => '32k', 'sample_rate' => 22050, 'channels' => 1],
'low' => ['bitrate' => '64k', 'sample_rate' => 22050, 'channels' => 1],
'mid' => ['bitrate' => '128k', 'sample_rate' => 44100, 'channels' => 2],
];
// ============================================================
// ARGÜMAN PARSE
// ============================================================
$options = getopt('', ['limit:', 'offset:', 'dry-run', 'skip-high', 'only-mp3', 'song:', 'help']);
if (isset($options['help'])) { echo "Kullanım için dosya başını oku.\n"; exit(0); }
$limit = isset($options['limit']) ? (int)$options['limit'] : 0;
$offset = isset($options['offset']) ? (int)$options['offset'] : 0;
$dryRun = isset($options['dry-run']);
$skipHigh = isset($options['skip-high']);
$onlyMp3 = isset($options['only-mp3']);
$singleSong = $options['song'] ?? null;
// ============================================================
// LARAVEL BOOT
// ============================================================
echo "🎵 Ses Eşitleme Re-encode Script v3\n";
echo str_repeat('=', 50) . "\n";
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$tenant = App\Models\Tenant::find($TENANT_ID);
if (!$tenant) { echo "❌ Tenant {$TENANT_ID} bulunamadı!\n"; exit(1); }
tenancy()->initialize($tenant);
echo "✅ Tenant: {$TENANT_ID}\n";
// ============================================================
// ŞARKILARI BUL
// ============================================================
$query = DB::connection('tenant')
->table('muzibu_songs')
->whereNotNull('hls_path')
->where('hls_path', '!=', '')
->orderBy('song_id');
if ($singleSong) $query->where('song_id', $singleSong);
if ($offset > 0) $query->skip($offset);
if ($limit > 0) $query->take($limit);
$songs = $query->get();
$total = $songs->count();
echo "📊 Toplam şarkı: {$total}\n";
if ($offset > 0) echo " Offset: {$offset}\n";
if ($limit > 0) echo " Limit: {$limit}\n";
echo " Mod: " . ($dryRun ? 'DRY-RUN' : ($onlyMp3 ? 'SADECE MP3' : ($skipHigh ? 'VARIANT+MP3' : 'TAM RE-ENCODE'))) . "\n";
echo str_repeat('=', 50) . "\n\n";
if ($dryRun) { echo "🔍 Dry-run: İşlem yapılmadı.\n"; exit(0); }
// ============================================================
// YARDIMCI FONKSİYONLAR
// ============================================================
function buildTwoPassFilter(string $inputPath): string
{
global $FFMPEG, $LOUDNORM_TARGET, $LOUDNORM_TP, $LOUDNORM_LRA, $LIMITER;
$base = "loudnorm=I={$LOUDNORM_TARGET}:TP={$LOUDNORM_TP}:LRA={$LOUDNORM_LRA}";
$cmd = "{$FFMPEG} -hide_banner -i " . escapeshellarg($inputPath) .
" -af \"{$base}:print_format=json\" -f null /dev/null 2>&1";
$output = shell_exec($cmd);
if ($output && preg_match('/\{[^{}]*"input_i"\s*:\s*"[^"]*"[^{}]*\}/s', $output, $match)) {
$json = json_decode($match[0], true);
if ($json && isset($json['input_i'], $json['input_tp'], $json['input_lra'], $json['input_thresh'])) {
return sprintf('%s:measured_I=%s:measured_TP=%s:measured_LRA=%s:measured_thresh=%s:linear=false,%s',
$base, $json['input_i'], $json['input_tp'], $json['input_lra'], $json['input_thresh'], $LIMITER);
}
}
echo " ⚠️ Ölçüm başarısız, tek geçiş\n";
return "{$base},{$LIMITER}";
}
function findMp3(object $song): ?string
{
global $TENANT_ID;
$paths = [
storage_path("app/public/muzibu/songs/{$song->file_path}"),
storage_path("../tenant{$TENANT_ID}/app/public/muzibu/songs/{$song->file_path}"),
];
foreach ($paths as $p) { if (!empty($song->file_path) && file_exists($p)) return $p; }
return null;
}
function findHlsDir(object $song): ?string
{
global $TENANT_ID;
$relDir = str_contains($song->hls_path, 'playlist.m3u8')
? dirname($song->hls_path) : rtrim($song->hls_path, '/');
$paths = [
storage_path("app/public/{$relDir}"),
storage_path("../tenant{$TENANT_ID}/app/public/{$relDir}"),
storage_path("../tenant{$TENANT_ID}/app/public/muzibu/hls/{$song->song_id}"),
];
foreach ($paths as $p) {
if (file_exists($p) && file_exists("{$p}/playlist.m3u8")) return $p;
}
return null;
}
function detectBitrate(string $filePath): int
{
$cmd = "ffprobe -v quiet -print_format json -show_format " . escapeshellarg($filePath);
$data = json_decode(shell_exec($cmd), true);
$kbps = round((int)($data['format']['bit_rate'] ?? 128000) / 1000);
if ($kbps <= 128) return 128;
if ($kbps <= 160) return 160;
if ($kbps <= 192) return 192;
if ($kbps <= 256) return 256;
if ($kbps <= 320) return 320;
return $kbps;
}
function reEncodeHigh(string $mp3Path, string $hlsDir, int $songId): bool
{
global $FFMPEG, $HLS_SEGMENT_TIME;
$keyInfo = "{$hlsDir}/enc.keyinfo";
if (!file_exists($keyInfo)) { echo " ❌ enc.keyinfo yok\n"; return false; }
foreach (glob("{$hlsDir}/segment-*.ts") as $seg) unlink($seg);
$bitrate = detectBitrate($mp3Path);
$filters = buildTwoPassFilter($mp3Path);
$cmd = sprintf('%s -hide_banner -y -i %s -map 0:a -c:a aac -b:a %dk -profile:a aac_low -ar 48000 -ac 2 -af %s -hls_key_info_file %s -start_number 0 -hls_time %d -hls_list_size 0 -hls_playlist_type vod -hls_segment_filename %s -f hls %s 2>&1',
$FFMPEG, escapeshellarg($mp3Path), $bitrate, escapeshellarg($filters),
escapeshellarg($keyInfo), $HLS_SEGMENT_TIME,
escapeshellarg("{$hlsDir}/segment-%03d.ts"), escapeshellarg("{$hlsDir}/playlist.m3u8"));
exec($cmd, $output, $ret);
return $ret === 0;
}
function generateVariant(string $mp3Path, string $hlsDir, string $name, array $config): bool
{
global $FFMPEG, $HLS_SEGMENT_TIME;
$varDir = "{$hlsDir}/{$name}";
if (file_exists("{$varDir}/playlist.m3u8")) {
foreach (glob("{$varDir}/segment-*.ts") as $seg) unlink($seg);
@unlink("{$varDir}/playlist.m3u8");
}
if (!file_exists($varDir)) mkdir($varDir, 0755, true);
$keyInfoPath = null;
$mainKeyInfo = "{$hlsDir}/enc.keyinfo";
if (file_exists($mainKeyInfo)) {
$lines = explode("\n", trim(file_get_contents($mainKeyInfo)));
$sep = str_contains($lines[0], '?') ? '&' : '?';
$keyInfoPath = "{$varDir}/enc.keyinfo";
file_put_contents($keyInfoPath, implode("\n", [
"{$lines[0]}{$sep}v={$name}", $lines[1], bin2hex(random_bytes(16))
]));
}
$filters = buildTwoPassFilter($mp3Path);
$cmd = sprintf('%s -hide_banner -y -i %s -c:a aac -b:a %s -profile:a aac_low -ar %d -ac %d -vn -af %s',
$FFMPEG, escapeshellarg($mp3Path), $config['bitrate'], $config['sample_rate'], $config['channels'],
escapeshellarg($filters));
if ($keyInfoPath) $cmd .= ' -hls_key_info_file ' . escapeshellarg($keyInfoPath);
$cmd .= sprintf(' -hls_time %d -hls_list_size 0 -hls_playlist_type vod -hls_segment_filename %s %s 2>&1',
$HLS_SEGMENT_TIME, escapeshellarg("{$varDir}/segment-%03d.ts"), escapeshellarg("{$varDir}/playlist.m3u8"));
exec($cmd, $output, $ret);
return $ret === 0;
}
function generateMaster(string $hlsDir): void
{
global $VARIANTS;
$highBitrate = 256000;
$pf = "{$hlsDir}/playlist.m3u8";
if (file_exists($pf)) {
$content = file_get_contents($pf); $dur = 0; $sz = 0;
if (preg_match_all('/#EXTINF:([\d.]+),/', $content, $m))
foreach ($m[1] as $d) $dur += (float)$d;
foreach (glob("{$hlsDir}/segment-*.ts") as $s) $sz += filesize($s);
if ($dur > 0 && $sz > 0) $highBitrate = (int)round($sz * 8 / $dur);
}
$bw = ['ultralow' => 32000, 'low' => 64000, 'mid' => 128000];
$master = "#EXTM3U\n";
foreach ($VARIANTS as $n => $c) {
if (file_exists("{$hlsDir}/{$n}/playlist.m3u8"))
$master .= "#EXT-X-STREAM-INF:BANDWIDTH={$bw[$n]},CODECS=\"mp4a.40.2\",NAME=\"{$n}\"\n{$n}/playlist.m3u8\n";
}
if (file_exists($pf))
$master .= "#EXT-X-STREAM-INF:BANDWIDTH={$highBitrate},CODECS=\"mp4a.40.2\",NAME=\"high\"\nplaylist.m3u8\n";
file_put_contents("{$hlsDir}/master.m3u8", $master);
}
function generateMp3(string $mp3Path, int $songId, string $quality): bool
{
global $FFMPEG, $TENANT_ID;
$dir = storage_path("../tenant{$TENANT_ID}/app/public/muzibu/songs/mp3_{$quality}");
if (!file_exists($dir)) mkdir($dir, 0755, true);
$outPath = "{$dir}/{$songId}.mp3";
if (file_exists($outPath)) unlink($outPath);
$filters = buildTwoPassFilter($mp3Path);
$sr = $quality === '64' ? 22050 : 44100;
$ch = $quality === '64' ? 1 : 2;
$cmd = sprintf('%s -hide_banner -y -i %s -b:a %sk -ar %d -ac %d -af %s -map_metadata -1 -vn %s 2>&1',
$FFMPEG, escapeshellarg($mp3Path), $quality, $sr, $ch,
escapeshellarg($filters), escapeshellarg($outPath));
exec($cmd, $output, $ret);
return $ret === 0;
}
// ============================================================
// ANA DÖNGÜ
// ============================================================
$success = 0; $failed = 0; $startTime = microtime(true);
foreach ($songs as $i => $song) {
$num = $i + 1;
$elapsed = round(microtime(true) - $startTime);
$eta = $num > 1 ? round(($elapsed / $num) * ($total - $num) / 60) : '?';
echo "[{$num}/{$total}] Song #{$song->song_id} (ETA: {$eta}dk)\n";
$mp3Path = findMp3($song);
if (!$mp3Path) { echo " ❌ MP3 bulunamadı\n"; $failed++; continue; }
$hlsDir = findHlsDir($song);
if (!$hlsDir && !$onlyMp3) { echo " ❌ HLS dizini yok\n"; $failed++; continue; }
if (!$onlyMp3 && !$skipHigh) {
echo " 🔄 High re-encode... ";
if (reEncodeHigh($mp3Path, $hlsDir, $song->song_id)) { echo "✅\n"; }
else { echo "❌\n"; $failed++; continue; }
}
if (!$onlyMp3) {
foreach ($VARIANTS as $name => $config) {
echo " 🔄 {$name}... ";
echo generateVariant($mp3Path, $hlsDir, $name, $config) ? "✅\n" : "❌\n";
}
generateMaster($hlsDir);
echo " 📋 master.m3u8 ✅\n";
}
echo " 🔄 MP3 128k... ";
echo generateMp3($mp3Path, $song->song_id, '128') ? "✅\n" : "❌\n";
echo " 🔄 MP3 64k... ";
echo generateMp3($mp3Path, $song->song_id, '64') ? "✅\n" : "❌\n";
$success++; echo "\n";
}
$totalTime = round((microtime(true) - $startTime) / 60, 1);
echo str_repeat('=', 50) . "\n";
echo "✅ Tamamlandı: {$success} başarılı, {$failed} başarısız ({$totalTime} dk)\n";
Yukarıdaki script'i kopyalayıp production sunucunun ana dizinine yapıştır:
cd /var/www/vhosts/muzibu.com/httpdocs/ nano re-encode-songs.php # İçeriği yapıştır, kaydet
# Rastgele bir şarkı ID bul php artisan tenants:run "hls:add-variants --dry-run" --tenants=1001 2>&1 | head -20 # Tek şarkıyla test et php re-encode-songs.php --song=ŞARKI_ID_BURAYA
[1/1] Song #XXXXX (ETA: ?dk) 🔄 High re-encode... ✅ 🔄 ultralow... ✅ 🔄 low... ✅ 🔄 mid... ✅ 📋 master.m3u8 ✅ 🔄 MP3 128k... ✅ 🔄 MP3 64k... ✅ ================================================== ✅ Tamamlandı: 1 başarılı, 0 başarısız (X.X dk)
1. Script çalıştı mı? (Evet/Hayır) Varsa hata mesajını yapıştır. 2. Çıktıdaki tüm satırlar ✅ mi? ❌ olan varsa hangisi? 3. Süre ne kadar sürdü? (X.X dk) 4. ffprobe ile doğrulama: HLS dizinindeki playlist.m3u8 dosyasının yolunu yaz: ffmpeg -hide_banner -i [PLAYLIST_YOLU] -af volumedetect -f null /dev/null 2>&1 | grep mean_volume 5. Sunucu PHP versiyonu: php -v | head -1 6. FFmpeg versiyonu: ffmpeg -version | head -1 7. Toplam HLS'li şarkı sayısı: php re-encode-songs.php --dry-run
php re-encode-songs.php --limit=10
Hepsi ✅ ise devam. ❌ varsa DURMA, hata mesajını paylaş.
Her terminali screen veya tmux içinde aç (SSH kopsa bile devam etsin):
# Terminal 1 screen -S encode1 php re-encode-songs.php --offset=0 --limit=11000 # Terminal 2 screen -S encode2 php re-encode-songs.php --offset=11000 --limit=11000 # Terminal 3 screen -S encode3 php re-encode-songs.php --offset=22000 --limit=11000
screen ipuçları:
Ctrl+A D → Oturumu arkada bırak (detach)
screen -r encode1 → Tekrar bağlan
screen -ls → Tüm oturumları listele
HLSDIR="storage/tenant1001/app/public/muzibu/hls" SONGDIR="storage/tenant1001/app/public/muzibu/songs" echo "HLS klasör: $(ls -d $HLSDIR/*/ | wc -l)" echo "ultralow: $(find $HLSDIR -maxdepth 2 -name ultralow -type d | wc -l)" echo "low: $(find $HLSDIR -maxdepth 2 -name low -type d | wc -l)" echo "mid: $(find $HLSDIR -maxdepth 2 -name mid -type d | wc -l)" echo "master: $(find $HLSDIR -name master.m3u8 | wc -l)" echo "mp3_128: $(ls $SONGDIR/mp3_128/*.mp3 2>/dev/null | wc -l)" echo "mp3_64: $(ls $SONGDIR/mp3_64/*.mp3 2>/dev/null | wc -l)"
# Script'i sil (güvenlik) rm re-encode-songs.php # Cache temizle php artisan cache:clear php artisan view:clear php artisan responsecache:clear
| Şarkı Sayısı | 1 Terminal | 3 Paralel |
|---|---|---|
| 1 şarkı | ~2 dk | — |
| 10 şarkı | ~20 dk | — |
| 1.000 şarkı | ~33 saat | ~11 saat |
| ~32.000 şarkı | ~44 saat | ~15 saat |
* Test sunucusu ölçümü: 1 şarkı = 1.9 dk. Production sunucu daha hızlı olabilir.
O şarkının HLS encryption dosyası eksik. --skip-high ile devam edebilirsin, sadece variant ve MP3 üretir.
Orijinal MP3 dosyası silinmiş. O şarkıyı atlar, devam eder.
ffmpeg -version ile versiyon kontrol et. Minimum 4.x gerekli.
Sorun yok. Aynı komutu tekrar çalıştır — script mevcut dosyaları siler ve yeniden oluşturur. Kaldığı yerden devam etmek için --offset kullan.
Orijinal MP3'ler yerinde. Eski HLS zaten üzerine yazıldı. Eski haline döndürmek için eski filtreli script ile tekrar encode etmek gerekir (ama genellikle gerekmez).