Перейти к содержимому
Zone of Games Forum
NightGuardianX

Утилита для импорта озвучки из Metro Awakening

Рекомендованные сообщения

Всем привет, я новенький на этом форуме, так что прошу сильно не бить.

Недавно занялся озвучкой игровых проектов, в последний раз взгляд упал на свежевыпущенную Metro (она на unreal engine 5). Увидел пост по Pacific Drive, хотел уточнить — есть ли что-то универсальное или схожее для извлечения звуков? 

Поделиться сообщением


Ссылка на сообщение
В 06.11.2024 в 13:14, NightGuardianX сказал:

есть ли что-то универсальное или схожее для извлечения звуков?

Для анрила есть Fmodel чтобы доставать файлы из pak и utoc/ucas

Звуки часто в формате wwise (.wem контейнеры)

Для конвертации звука в wav есть, например, vgmstream

В играх начиная от unreal 4.27+ звук может быть в Bink Audio (.binka контейнер)

Для конвертации звука в wav из Bink Audio есть binkadec (vgmstream не понимает .binka контейнер от unreal)

.binka контейнер от unreal (ABEU — Unreal Engine Bink Audio) не путать со старым binka контейнером от Miles Sound System (1FCB)

 

Metro Awakening на unreal 5.2.1

ресурсы в IOStore файлах (папка Metro Awakening\Impact\Content\Paks\)

не шифрованные, сжатые Oodle

звук Unreal Engine Bink Audio в нескольких utoc/ucas:

по пути Impact\Content\Maps\ — сюжет

и Impact\Content\Assets\Audio\VO\ — стандартные фразы

субтитры в Impact\Content\L10N (папка ru — русские, uk — украинские) в uasset с постфиксом “_Sub”

английские субтитры в папках рядом со звуком

Fmodel умеет экспортировать звук в .binka: нужно два раза кликнуть на uasset звука и в папке экспорта будет соответствующий binka файл

Чтобы Fmodel экспортировал сразу в wav, нужно закинуть binkadec.exe в папку .data в папке экспорта (Output Directory в настройках)

 

 

 

Изменено пользователем xoixa

Поделиться сообщением


Ссылка на сообщение
Скрытый текст

извлечь папку субтитров, например Impact\Content\L10N\ru:

в Fmodel (пкм на папку > Export Raw Data (.uasset) )

в папке экспорта, рядом с папкой ru, создать файл Sub.bms со следующим содержимым:


#Metro Awakening subtitles text export
#Quickbms script

get SubName basename
get Size asize
log MEMORY_FILE 0 0
put SubName string  MEMORY_FILE
put 0x09 byte MEMORY_FILE // tab

string SubName - 4 // remove _Sub postfix

//parse uasset file
get Null long
get Offset long
math Offset + 120
goto Offset

for
	//read string
	get StrSize long
	if StrSize < 0 // unicode
		math StrSize * -2
		getDString Str StrSize
		set Str unicode Str
	else
		getDString Str StrSize
	endif
	
	put Str string MEMORY_FILE
	savepos Pos

	//try next string
	math Pos + 100
	if Pos > Size		
		put 0x0D byte MEMORY_FILE
		break // end 
	else
		put 0x6e5c short MEMORY_FILE // \n
		goto Pos
		continue // read next string
	endif
next

#write to file
get MFSize asize MEMORY_FILE
append
encryption replace "\x00" "" //remove Zero bytes
log Sub.txt 0 MFSize MEMORY_FILE

там же создать файл export_sub.bat со следующим содержимым:


@echo off
for /R %%1 in (*_Sub.uasset) do quickbms.exe -o Sub.bms %%1
@pause

положить рядом quickbms.exe и запустить export_sub.bat

субтитры будут в файле Subs.txt

для английских субтитров эти же файлы поместить в папку Impact\Content\Assets\Audio\VO и Impact\Content\Maps\

скачать русские субтитры

 

Скрытый текст

извлечь все папки Impact\Content\Maps из всех utoc/ucas:

в Fmodel (пкм на папку > Export Raw Data (.uasset) )

 

в папке экспорта, в папке Maps, создать файл export_binka.bms со следующим содержимым:


#Metro Awakening bink audio export
#Quickbms script
#Use on uasset file
#uasset has binka header
#ubulk is audio data

get NAME basename
string NAME + ".binka"
findloc OFFSET binary "ABEU" // magick
get SIZE asize
math SIZE - OFFSET

open FDDE ubulk 1 Exist
if Exist = 0 //no ubulk
	log NAME OFFSET SIZE
else // has ubulk
	math SIZE - 8 //size of ubulk two times
	log NAME OFFSET SIZE
	
	get SIZE asize 1
	append
	log NAME 0 SIZE 1 //append ubulk audio data
endif 

там же создать файл towav.bat со следующим содержимым:


@echo off
if not exist wav mkdir wav
for /R %%1 in (*.uasset) do quickbms -o export_binka.bms %%1
for /R %%1 in (*.binka) do binkadec -i %%1 -o wav/%%~n1.wav
del *.binka
@pause

положить рядом quickbms.exe и binkadec.exe  и запустить towav.bat

готовые файлы будут в папке wav

повторить с папкой Impact\Content\Assets\Audio\VO\  из pakchunk0-Windows utoc/ucas

 

Изменено пользователем xoixa
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение
В 08.11.2024 в 18:39, NightGuardianX сказал:

@xoixa Спасибо, буду разбираться!

Только тут, нашел что-то рабочее. А как запаковать обратно, чтобы в игру добавить. Мне только как wav обратно в .uasset и .bulk. Хочу сделать озвучку для небольшой игры Scholar's Mate.

Изменено пользователем vananagornyi

Поделиться сообщением


Ссылка на сообщение
В 05.01.2025 в 15:16, vananagornyi сказал:

А как запаковать обратно, чтобы в игру добавить. Мне только как wav обратно в .uasset и .bulk. Хочу сделать озвучку для небольшой игры Scholar's Mate.

Как wav не получится. Нужно конвертировать обратно в .binka контейнер от unreal

Насколько я знаю, это можно сделать только в самом Unreal Engine из egs

Есть исходники декодера, на основе которых, можно(наверное), написать энкодер

Скрытый текст

#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>

#include <cassert>

#include <fstream>

#include <argparse/argparse.hpp>
#include <memory>
#include <string>
#include <vector>

#include "binka_ue_decode.h"
#include "binka_ue_file_header.h"

using int16 = int16_t;
using int32 = int32_t;

constexpr int MAX_BINK_AUDIO_CHANNELS = 16;
inline uint32_t Align32(size_t size) { return (uint32_t)((size + 31) & ~31); }

inline void *PTR_ADD(void *ptr, uint32_t off) {
  return (void *)(((uint8_t *)(ptr)) + (off));
}

struct BinkAudioDecoder {
  // # of low level bink audio streams (max 2 chans each)
  uint8 StreamCount;
  // # of current bytes in the res
  uint32 OutputReservoirValidBytes;
  // # max size of the res.
  uint32 OutputReservoirTotalBytes;
  // offsets to the various pieces of the decoder
  uint16 ToDeinterlaceBufferOffset32;
  uint16 ToSeekTableOffset32;
  uint16 ToOutputReservoirOffset32;
  // # of entries in the seek table - the entries are byte sizes, so
  // when decoding the seek table we convert to file offsets. This means
  // we actually have +1 entries in the decoded seek table due to the standard
  // "span -> edge" count thing.
  uint16 SeekTableCount;
  // # of low level bink blocks one seek table entry spans
  uint16 FramesPerSeekTableEntry;
  // # of frames we need to eat before outputting data due to a sample
  // accurate seek.
  uint16 ConsumeFrameCount;

  uint8 *StreamChannels() { return (uint8 *)(this + 1); }
  void **Decoders() {
    return (void **)PTR_ADD(
        this, Align32(sizeof(uint8) * StreamCount + sizeof(BinkAudioDecoder)));
  }
  uint8 *OutputReservoir() {
    return (uint8 *)(PTR_ADD(this, ToOutputReservoirOffset32 * 32U));
  }
  int16 *DeinterlaceBuffer() {
    return (int16 *)(PTR_ADD(this, ToDeinterlaceBufferOffset32 * 32U));
  }
  uint32 *
  SeekTable() // [SeekTableCount + 1] if SeekTableCount != 0, otherwise invalid.
  {
    return (uint32 *)(PTR_ADD(this, ToSeekTableOffset32 * 32U));
  }
};

struct audioinfo {
  int SampleRate;
  int TrueSampleCount;
  int SampleDataSize;
  int NumChannels;
  float Duration;
  int AudioDataOffset;

public:
  friend std::string to_string(const audioinfo &info) {
    std::string str = "";
    str += "SampleRate: " + std::to_string(info.SampleRate) + "\n";
    str += "TrueSampleCount: " + std::to_string(info.TrueSampleCount) + "\n";
    str += "SampleDataSize: " + std::to_string(info.SampleDataSize) + "\n";
    str += "NumChannels: " + std::to_string(info.NumChannels) + "\n";
    str += "Duration: " + std::to_string(info.Duration) + "\n";
    str += "AudioDataOffset: " + std::to_string(info.AudioDataOffset) + "\n";
    return str;
  }
};

bool ParseHeader(const uint8 *InSrcBufferData, uint32 InSrcBufferDataSize,
                 struct audioinfo *info) {
  auto SrcBufferData = InSrcBufferData;
  auto SrcBufferDataSize = InSrcBufferDataSize;
  auto SrcBufferOffset = 0;
  auto CurrentSampleCount = 0;

  assert(InSrcBufferDataSize >= sizeof(BinkAudioFileHeader));
  if (InSrcBufferDataSize < sizeof(BinkAudioFileHeader)) {
    return false;
  }

  BinkAudioFileHeader *Header = (BinkAudioFileHeader *)InSrcBufferData;
  if (Header->tag != 'UEBA') {
    return false;
  }
  if (Header->version != 1) {
    return false;
  }

  auto SampleRate = Header->rate;
  auto TrueSampleCount = Header->sample_count;
  auto NumChannels = Header->channels;
  auto MaxCompSpaceNeeded = Header->max_comp_space_needed;

  uint32 SeekTableSize = Header->seek_table_entry_count * sizeof(uint16);
  if (sizeof(BinkAudioFileHeader) + SeekTableSize > InSrcBufferDataSize) {
    return false;
  }

  // Store the offset to where the audio data begins
  auto AudioDataOffset = sizeof(BinkAudioFileHeader) + SeekTableSize;

  // Write out the the header info
  if (info) {
    info->SampleRate = Header->rate;
    info->NumChannels = Header->channels;
    info->SampleDataSize =
        Header->sample_count * info->NumChannels * sizeof(int16);
    info->Duration = (float)Header->sample_count / info->SampleRate;
    info->AudioDataOffset = AudioDataOffset;
    info->TrueSampleCount = TrueSampleCount;
  }

  return true;
}

bool CreateDecoder(const uint8_t *SrcBufferData, uint32_t SrcBufferDataSize,
                   uint32_t NumChannels, uint32_t SampleRate,
                   uint8_t *&RawMemory, BinkAudioDecoder *&Decoder,
                   uint32_t &SrcBufferOffset) {

  auto GetMaxFrameSizeSamples = [SampleRate]() -> uint32_t {
    if (SampleRate >= 44100) {
      return 1920;
    } else if (SampleRate >= 22050) {
      return 960;
    } else {
      return 480;
    }
  };
  UEBinkAudioDecodeInterface *BinkInterface = nullptr;

  BinkInterface = UnrealBinkAudioDecodeInterface();

  if (BinkInterface == nullptr) {
    return false; // only happens if we dont have libs.
  }

  BinkAudioFileHeader *Header = (BinkAudioFileHeader *)SrcBufferData;

  // Bink is max stereo per stream
  uint32 StreamCount = (NumChannels + 1) >> 1;

  // Figure memory for buffers:

  // Deinterlace - buffer space for interleaving multi stream binks in to a
  // standard interleaved format.
  uint32 DeinterlaceMemory = 0;
  if (StreamCount > 1) {
    DeinterlaceMemory = GetMaxFrameSizeSamples() * sizeof(int16) * 2;
  }

  // Space to store a decoded block.
  uint32 OutputReservoirMemory =
      NumChannels * GetMaxFrameSizeSamples() * sizeof(int16);

  // Space for the decoder state
  uint32 DecoderMemoryTotal = 0;
  uint32 DecoderMemoryPerStream[MAX_BINK_AUDIO_CHANNELS / 2];
  {
    uint32 RemnChannels = NumChannels;
    for (uint32 StreamIndex = 0; StreamIndex < StreamCount; StreamIndex++) {
      uint32 StreamChannels = RemnChannels;
      if (StreamChannels > 2) {
        StreamChannels = 2;
      }
      RemnChannels -= StreamChannels;
      DecoderMemoryPerStream[StreamIndex] =
          BinkInterface->MemoryFn(SampleRate, StreamChannels);
      DecoderMemoryTotal += DecoderMemoryPerStream[StreamIndex];
    }
  }

  // Space for the decoder pointers
  uint32 PtrMemory = Align32(sizeof(void *) * StreamCount);

  // Space for ourselves + the channel count for each stream.
  uint32 StructMemory =
      Align32(sizeof(BinkAudioDecoder) + sizeof(uint8) * StreamCount);

  // Space for decoded seek table
  uint32 SeekTableMemory = 0;
  if (Header->seek_table_entry_count) {
    SeekTableMemory =
        Align32(sizeof(uint32) * (Header->seek_table_entry_count + 1));
  }

  uint32 TotalMemory = DecoderMemoryTotal + PtrMemory + StructMemory +
                       OutputReservoirMemory + DeinterlaceMemory +
                       SeekTableMemory;

  //
  // Allocate and save offsets
  //
  RawMemory = (uint8 *)malloc(TotalMemory);
  memset(RawMemory, 0, TotalMemory);

  Decoder = (BinkAudioDecoder *)RawMemory;

  Decoder->StreamCount = StreamCount;
  Decoder->OutputReservoirTotalBytes = OutputReservoirMemory;
  Decoder->SeekTableCount = Header->seek_table_entry_count;
  Decoder->FramesPerSeekTableEntry = Header->blocks_per_seek_table_entry;

  // See layout discussion in class declaration
  void **Decoders = Decoder->Decoders();
  uint8 *CurrentMemory = (uint8 *)PTR_ADD(Decoders, PtrMemory);
  uint8 *Channels = Decoder->StreamChannels();

  // Init decoders
  {
    uint8 RemnChannels = NumChannels;
    for (uint32 StreamIndex = 0; StreamIndex < StreamCount; StreamIndex++) {
      uint32 StreamChannels = RemnChannels;
      if (StreamChannels > 2) {
        StreamChannels = 2;
      }
      RemnChannels -= StreamChannels;

      Channels[StreamIndex] = StreamChannels;
      Decoders[StreamIndex] = (void **)CurrentMemory;
      CurrentMemory += DecoderMemoryPerStream[StreamIndex];
      BinkInterface->OpenFn(Decoders[StreamIndex], SampleRate, StreamChannels,
                            true, true);
    }
  }

  Decoder->ToOutputReservoirOffset32 =
      (uint16)((CurrentMemory - RawMemory) / 32);
  CurrentMemory += OutputReservoirMemory;

  Decoder->ToDeinterlaceBufferOffset32 =
      (uint16)((CurrentMemory - RawMemory) / 32);
  CurrentMemory += DeinterlaceMemory;

  Decoder->ToSeekTableOffset32 = (uint16)((CurrentMemory - RawMemory) / 32);
  CurrentMemory += SeekTableMemory;

  // Decode the seek table
  if (Decoder->SeekTableCount) {
    uint32 *SeekTable = Decoder->SeekTable();
    uint16 *EncodedSeekTable =
        (uint16 *)(SrcBufferData + sizeof(BinkAudioFileHeader));
    uint32 CurrentSeekOffset = 0;

    // the seek table has deltas from last, and we want absolutes
    for (uint32 i = 0; i < Decoder->SeekTableCount; i++) {
      SeekTable[i] = CurrentSeekOffset;
      CurrentSeekOffset += EncodedSeekTable[i];
    }
    SeekTable[Decoder->SeekTableCount] = CurrentSeekOffset;
  }

  SrcBufferOffset = sizeof(BinkAudioFileHeader) +
                    Header->seek_table_entry_count * sizeof(uint16);
  return true;
}

int Decode(BinkAudioDecoder *&Decoder, const uint8 *CompressedData,
           const int32 CompressedDataSize, uint32_t NumChannels,
           uint32_t SampleRate, const uint16_t MaxCompSpaceNeeded,
           uint8 *OutPCMData, const int32 OutputPCMDataSize) {
  auto GetMaxFrameSizeSamples = [SampleRate]() -> uint32_t {
    if (SampleRate >= 44100) {
      return 1920;
    } else if (SampleRate >= 22050) {
      return 960;
    } else {
      return 480;
    }
  };
  UEBinkAudioDecodeInterface *BinkInterface = 0;

  BinkInterface = UnrealBinkAudioDecodeInterface();

  if (BinkInterface == 0) {
    return 0; // only happens if we dont have libs.
  }

  uint32 RemnOutputPCMDataSize = OutputPCMDataSize;
  uint32 RemnCompressedDataSize = CompressedDataSize;
  const uint8 *CompressedDataEnd = CompressedData + CompressedDataSize;

  //
  // In the event we need to copy to a stack buffer, we alloca() it here so
  // that it's not inside the loop (for static analysis). We don't touch the
  // memory until we need it so it's just a couple instructions for the
  // alloca().
  // (+8 for max block header size)
  uint8 *StackBlockBuffer =
      (uint8 *)alloca(MaxCompSpaceNeeded + BINK_UE_DECODER_END_INPUT_SPACE + 8);

  const uint32 DecodeSize =
      GetMaxFrameSizeSamples() * sizeof(int16) * NumChannels;
  while (RemnOutputPCMDataSize) {
    //
    // Drain the output reservoir before attempting a decode.
    //
    if (Decoder->OutputReservoirValidBytes) {
      uint32 CopyBytes = Decoder->OutputReservoirValidBytes;
      if (CopyBytes > RemnOutputPCMDataSize) {
        CopyBytes = RemnOutputPCMDataSize;
      }

      memcpy(OutPCMData, Decoder->OutputReservoir(), CopyBytes);

      // We use memmove here because we expect it to be very rare that we
      // don't consume the entire output reservoir in a call, so it's not
      // worth managing a cursor. Just move down the remnants, which we expect
      // to be zero.
      if (Decoder->OutputReservoirValidBytes != CopyBytes) {
        std::memmove(Decoder->OutputReservoir(),
                     Decoder->OutputReservoir() + CopyBytes,
                     Decoder->OutputReservoirValidBytes - CopyBytes);
      }
      Decoder->OutputReservoirValidBytes -= CopyBytes;

      RemnOutputPCMDataSize -= CopyBytes;
      OutPCMData += CopyBytes;

      if (RemnOutputPCMDataSize == 0) {
        // we filled entirely from the output reservoir
        break;
      }
    }

    if (RemnCompressedDataSize == 0) {
      // This is the normal termination condition
      break;
    }

    

    if (BinkAudioValidateBlock(MaxCompSpaceNeeded, CompressedData,
                               RemnCompressedDataSize) !=
        BINK_AUDIO_BLOCK_VALID) {
      // The splitting system should ensure that we only ever get complete
      // blocks - so this is bizarre.
      // UE_LOG(LogBinkAudioDecoder, Warning,
      //        TEXT("Got weird buffer, validate returned %d"),
      //        BinkAudioValidateBlock(MaxCompSpaceNeeded, CompressedData,
      //                               RemnCompressedDataSize));
      
      std::cout << "Got weird buffer, validate returned "
                << std::to_string(BinkAudioValidateBlock(
                       MaxCompSpaceNeeded, CompressedData,
                       RemnCompressedDataSize))
                << std::endl;
      break;
    }

    uint8 const *BlockStart = 0;
    uint8 const *BlockEnd = 0;
    uint32 TrimToSampleCount = 0;
    BinkAudioCrackBlock(CompressedData, &BlockStart, &BlockEnd,
                        &TrimToSampleCount);
    uint8 const *BlockBase = CompressedData;

    //
    // We need to make sure there's room available for Bink to read past the
    // end of the buffer (for vector decoding). If there's not, we need to
    // copy to a temp buffer.
    //
    bool HasRoomForDecode =
        (CompressedDataEnd - BlockEnd) > BINK_UE_DECODER_END_INPUT_SPACE;
    if (HasRoomForDecode == false) {
      // This looks weird, but in order for the advancement logic to work,
      // we need to replicate the entire block including the header.
      size_t BlockOffset = BlockStart - BlockBase;
      size_t BlockSize = BlockEnd - BlockBase;
      if (BlockSize > MaxCompSpaceNeeded + 8) // +8 for max block header size
      {
        // UE_LOG(LogBinkAudioDecoder, Error,
        //        TEXT("BAD! Validated block exceeds header max block size (%d
        //        "
        //             "vs %d)"),
        //        BlockSize, MaxCompSpaceNeeded);
        std::cout << "BAD! Validated block exceeds header max block size ("
                  << BlockSize << " vs " << MaxCompSpaceNeeded << ")"
                  << std::endl;
        // bErrorStateLatch = true;
        break;
      }

      memcpy(StackBlockBuffer, BlockBase, BlockSize);

      // this is technically not needed, but just so that any analysis shows
      // that we've initialized all the memory we touch.
      memset(StackBlockBuffer + BlockSize, 0, BINK_UE_DECODER_END_INPUT_SPACE);

      BlockBase = StackBlockBuffer;
      BlockStart = StackBlockBuffer + BlockOffset;
      BlockEnd = StackBlockBuffer + BlockSize;
    }

    //
    // If we're a simple single stream and we have enough output space,
    // just decode directly in to our destination to avoid some
    // copies.
    //
    // We also have to have a "simple" decode - i.e. aligned and no trimming.
    //
    if (Decoder->StreamCount == 1 && DecodeSize <= RemnOutputPCMDataSize &&
        TrimToSampleCount == ~0U && (((size_t)OutPCMData) & 0xf) == 0 &&
        Decoder->ConsumeFrameCount == 0) {
      uint32 DecodedBytes =
          BinkInterface->DecodeFn(Decoder->Decoders()[0], OutPCMData,
                                  RemnOutputPCMDataSize, &BlockStart, BlockEnd);

      if (DecodedBytes == 0) {

        // This means that our block check above succeeded and we still failed
        // - corrupted data!
        return 0;
      }

      uint32 InputConsumed = (uint32)(BlockStart - BlockBase);

      OutPCMData += DecodedBytes;
      CompressedData += InputConsumed;
      RemnCompressedDataSize -= InputConsumed;
      RemnOutputPCMDataSize -= DecodedBytes;
      continue;
    }

    // Otherwise we route through the output reservoir.
    if (Decoder->StreamCount == 1) {
      uint32 DecodedBytes = BinkInterface->DecodeFn(
          Decoder->Decoders()[0], Decoder->OutputReservoir(),
          Decoder->OutputReservoirTotalBytes, &BlockStart, BlockEnd);
      if (DecodedBytes == 0) {

        // This means that our block check above succeeded and we still failed
        // - corrupted data!
        return 0;
      }

      uint32 InputConsumed = (uint32)(BlockStart - BlockBase);

      CompressedData += InputConsumed;
      RemnCompressedDataSize -= InputConsumed;

      Decoder->OutputReservoirValidBytes = DecodedBytes;
    } else {
      // multistream requires interlacing the stereo/mono streams
      int16 *DeinterlaceBuffer = Decoder->DeinterlaceBuffer();
      uint8 *CurrentOutputReservoir = Decoder->OutputReservoir();

      uint32 LocalNumChannels = NumChannels;
      for (uint32 i = 0; i < Decoder->StreamCount; i++) {
        uint32 StreamChannels = Decoder->StreamChannels()[i];

        uint32 DecodedBytes = BinkInterface->DecodeFn(
            Decoder->Decoders()[i], (uint8 *)DeinterlaceBuffer,
            GetMaxFrameSizeSamples() * sizeof(int16) * 2, &BlockStart,
            BlockEnd);

        if (DecodedBytes == 0) {
          // This means that our block check above succeeded and we still
          // failed - corrupted data!
          return 0;
        }

        // deinterleave in to the output reservoir.
        if (StreamChannels == 1) {
          int16 *Read = DeinterlaceBuffer;
          int16 *Write = (int16 *)CurrentOutputReservoir;
          uint32 Frames = DecodedBytes / sizeof(int16);
          for (uint32 FrameIndex = 0; FrameIndex < Frames; FrameIndex++) {
            Write[LocalNumChannels * FrameIndex] = Read[FrameIndex];
          }
        } else {
          // stereo int16 pairs
          int32 *Read = (int32 *)DeinterlaceBuffer;
          int16 *Write = (int16 *)CurrentOutputReservoir;
          uint32 Frames = DecodedBytes / sizeof(int32);
          for (uint32 FrameIndex = 0; FrameIndex < Frames; FrameIndex++) {
            *(int32 *)(Write + LocalNumChannels * FrameIndex) =
                Read[FrameIndex];
          }
        }

        CurrentOutputReservoir += sizeof(int16) * StreamChannels;

        Decoder->OutputReservoirValidBytes += DecodedBytes;
      }

      uint32 InputConsumed = (uint32)(BlockStart - BlockBase);
      CompressedData += InputConsumed;
      RemnCompressedDataSize -= InputConsumed;
    } // end if multi stream

    // Check if we are trimming the tail due to EOF
    if (TrimToSampleCount != ~0U) {
      // Ignore the tail samples by just dropping our reservoir size
      Decoder->OutputReservoirValidBytes =
          TrimToSampleCount * sizeof(int16) * NumChannels;
    }

    // Check if we need to eat some frames due to a sample-accurate seek.
    if (Decoder->ConsumeFrameCount) {
      const uint32 BytesPerFrame = sizeof(int16) * NumChannels;
      uint32 ValidFrames = Decoder->OutputReservoirValidBytes / BytesPerFrame;
      if (Decoder->ConsumeFrameCount < ValidFrames) {
        memmove(Decoder->OutputReservoir(),
                Decoder->OutputReservoir() +
                    Decoder->ConsumeFrameCount * BytesPerFrame,
                (ValidFrames - Decoder->ConsumeFrameCount) * BytesPerFrame);
        Decoder->OutputReservoirValidBytes -=
            Decoder->ConsumeFrameCount * BytesPerFrame;
      } else {
        Decoder->OutputReservoirValidBytes = 0;
      }
      Decoder->ConsumeFrameCount = 0;
    }
    // Fall through to the next loop to copy the decoded pcm data out of the
    // reservoir.
  } // while need output pcm data

  // We get here if we filled the output buffer or not.
  // FDecodeResult Result;
  // Result.NumPcmBytesProduced = OutputPCMDataSize - RemnOutputPCMDataSize;
  // Result.NumAudioFramesProduced =
  //     Result.NumPcmBytesProduced / (sizeof(int16) * NumChannels);
  // Result.NumCompressedBytesConsumed =
  //     CompressedDataSize - RemnCompressedDataSize;
  std::cout << "NumPcmBytesProduced: "
            << OutputPCMDataSize - RemnOutputPCMDataSize << std::endl;
  // std::cout << "NumAudioFramesProduced: " << Result.NumAudioFramesProduced
  // << std::endl; std::cout << "NumCompressedBytesConsumed: " <<
  // Result.NumCompressedBytesConsumed << std::endl;

  return OutputPCMDataSize - RemnOutputPCMDataSize;
}

std::string description = 
R"(binkadec: experimental Unreal Engine 4/5 "UEBA/.binka" audio decoder

Tested with:
  - THE FINALS (UE5.0))";
int main(int argc, char **argv) {
  // binkadec -i input.bink -o output.s16 -f raw
  argparse::ArgumentParser program("binkadec");
  program.add_description(description);
  program.add_argument("-i", "--input").required().help("input file");
  program.add_argument("-o", "--output").required().help("output file");
  program.add_argument("-v", "--verbose")
      .default_value(false)
      .implicit_value(true)
      .help("verbose output");
  //format: raw/wav
  program.add_argument("-f", "--format")
      .default_value("raw")
      .help("output format(raw)");

  try {
    program.parse_args(argc, argv);
  } catch (const std::runtime_error &err) {
    std::cout << err.what() << std::endl;
    std::cout << program;
    exit(0);
  }
  std::string input = program.get<std::string>("--input");
  std::filesystem::path input_path(input);
  std::string output = program.get<std::string>("--output");
  std::filesystem::path output_path(output);
  bool verbose = program.get<bool>("--verbose");
  if (verbose) {
    std::cout << "Input: " << input_path << std::endl;
    std::cout << "Output: " << output_path << std::endl;
  }
  std::ifstream input_file(input_path, std::ios::binary);
  if (!input_file.is_open()) {
    std::cout << "Failed to open input file" << std::endl;
    exit(0);
  }
  std::shared_ptr<uint8_t> input_data;
  uint32_t input_data_size;
  {
    input_file.seekg(0, std::ios::end);
    input_data_size = input_file.tellg();
    input_file.seekg(0, std::ios::beg);
    input_data = std::shared_ptr<uint8_t>(new uint8_t[input_data_size],
                                          std::default_delete<uint8_t[]>());
    input_file.read((char *)input_data.get(), input_data_size);
  }

  struct audioinfo info;
  if (!ParseHeader(input_data.get(), input_data_size, &info)) {
    std::cout << "Failed to parse header" << std::endl;
    exit(0);
  }
  if (verbose) {
    std::cout << to_string(info) << std::endl;
  }else{
    std::cout << "SampleRate: " << info.SampleRate << std::endl;
    std::cout << "NumChannels: " << info.NumChannels << std::endl;
    std::cout << "Duration: " << info.Duration << std::endl;
  }

  uint8_t *RawMemory = nullptr;
  BinkAudioDecoder *Decoder = nullptr;
  uint32_t SrcBufferOffset = 0;
  if (!CreateDecoder(input_data.get(), input_data_size, info.NumChannels,
                     info.SampleRate, RawMemory, Decoder, SrcBufferOffset)) {
    std::cout << "Failed to create decoder" << std::endl;
    exit(0);
  }

  if (verbose) {
    std::cout << "RawMemory: " << (void *)RawMemory << std::endl;
    std::cout << "Decoder: " << (void *)Decoder << std::endl;
    std::cout << "SrcBufferOffset: " << SrcBufferOffset << std::endl;
    std::cout << "Decoder->StreamCount: " << Decoder->StreamCount << std::endl;
    std::cout << "Decoder->OutputReservoirValidBytes: "
              << Decoder->OutputReservoirValidBytes << std::endl;
    std::cout << "Decoder->OutputReservoirTotalBytes: "
              << Decoder->OutputReservoirTotalBytes << std::endl;
    std::cout << "Decoder->ToDeinterlaceBufferOffset32: "
              << Decoder->ToDeinterlaceBufferOffset32 << std::endl;
    std::cout << "Decoder->ToSeekTableOffset32: "
              << Decoder->ToSeekTableOffset32 << std::endl;
    std::cout << "Decoder->ToOutputReservoirOffset32: "
              << Decoder->ToOutputReservoirOffset32 << std::endl;
    std::cout << "Decoder->SeekTableCount: " << Decoder->SeekTableCount
              << std::endl;
    std::cout << "Decoder->FramesPerSeekTableEntry: "
              << Decoder->FramesPerSeekTableEntry << std::endl;
    std::cout << "Decoder->ConsumeFrameCount: " << Decoder->ConsumeFrameCount
              << std::endl;
  }

  //malloc for compressed data — new buffer
  std::unique_ptr<uint8_t> compressedData =
      std::unique_ptr<uint8_t>(new uint8_t[input_data_size - SrcBufferOffset]);
  //Copy compressed data, but skip anything from "SEEK" to 0x9999.
  //Does not know why it is needed because Decoder->SeekTableCount is 0.
  bool in_skip = false;
  size_t dst_offset = 0;

  for (size_t i = SrcBufferOffset; i < input_data_size; i++) {
    if (!in_skip && i + 4 < input_data_size &&
        std::memcmp(&input_data.get()[i], "SEEK", 4) == 0) {
      in_skip = true;
      i += 3; // Skip "SEEK"
    } else if (in_skip && i + 2 < input_data_size && input_data.get()[i] == 0x99 &&
               input_data.get()[i + 1] == 0x99) {
      in_skip = false;
      compressedData.get()[dst_offset++] = 0x99;
      compressedData.get()[dst_offset++] = 0x99;
      i += 1; // Skip 0x9999
    } else if (!in_skip) {
      compressedData.get()[dst_offset++] = input_data.get()[i];
    }
  }

  if (verbose) {
    std::cout << "compressedDataSize: " << dst_offset << std::endl;
  }


  uint32_t NumChannels = info.NumChannels;
  uint32_t SampleRate = info.SampleRate;
  uint16_t MaxCompSpaceNeeded = info.SampleDataSize;
  uint8_t *OutPCMData = (uint8_t *)malloc(info.SampleDataSize);
  int32_t OutputPCMDataSize = info.SampleDataSize;
  int32_t DecodedBytes =
      Decode(Decoder, compressedData.get(), dst_offset, NumChannels, SampleRate,
             MaxCompSpaceNeeded, OutPCMData, OutputPCMDataSize);
  if (DecodedBytes == 0) {
    std::cout << "Failed to decode" << std::endl;
    exit(0);
  }
  std::ofstream output_file(output_path, std::ios::binary);
  if (!output_file.is_open()) {
    std::cout << "Failed to open output file" << std::endl;
    exit(0);
  }
  output_file.write((char *)OutPCMData, DecodedBytes);
  output_file.close();
  input_file.close();
  std::cout << "Done. Play the result with:" << std::endl;
  std::cout << "ffplay -f s16le -ar " << SampleRate << " -ac "
            << NumChannels << " -i " << output_path << std::endl;
}

 

 

Поделиться сообщением


Ссылка на сообщение
В 28.01.2025 в 18:08, xoixa сказал:

Как wav не получится. Нужно конвертировать обратно в .binka контейнер от unreal

Насколько я знаю, это можно сделать только в самом Unreal Engine из egs

Есть исходники декодера, на основе которых, можно(наверное), написать энкодер

  main.cpp (Показать содержимое)


#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>

#include <cassert>

#include <fstream>

#include <argparse/argparse.hpp>
#include <memory>
#include <string>
#include <vector>

#include "binka_ue_decode.h"
#include "binka_ue_file_header.h"

using int16 = int16_t;
using int32 = int32_t;

constexpr int MAX_BINK_AUDIO_CHANNELS = 16;
inline uint32_t Align32(size_t size) { return (uint32_t)((size + 31) & ~31); }

inline void *PTR_ADD(void *ptr, uint32_t off) {
  return (void *)(((uint8_t *)(ptr)) + (off));
}

struct BinkAudioDecoder {
  // # of low level bink audio streams (max 2 chans each)
  uint8 StreamCount;
  // # of current bytes in the res
  uint32 OutputReservoirValidBytes;
  // # max size of the res.
  uint32 OutputReservoirTotalBytes;
  // offsets to the various pieces of the decoder
  uint16 ToDeinterlaceBufferOffset32;
  uint16 ToSeekTableOffset32;
  uint16 ToOutputReservoirOffset32;
  // # of entries in the seek table - the entries are byte sizes, so
  // when decoding the seek table we convert to file offsets. This means
  // we actually have +1 entries in the decoded seek table due to the standard
  // "span -> edge" count thing.
  uint16 SeekTableCount;
  // # of low level bink blocks one seek table entry spans
  uint16 FramesPerSeekTableEntry;
  // # of frames we need to eat before outputting data due to a sample
  // accurate seek.
  uint16 ConsumeFrameCount;

  uint8 *StreamChannels() { return (uint8 *)(this + 1); }
  void **Decoders() {
    return (void **)PTR_ADD(
        this, Align32(sizeof(uint8) * StreamCount + sizeof(BinkAudioDecoder)));
  }
  uint8 *OutputReservoir() {
    return (uint8 *)(PTR_ADD(this, ToOutputReservoirOffset32 * 32U));
  }
  int16 *DeinterlaceBuffer() {
    return (int16 *)(PTR_ADD(this, ToDeinterlaceBufferOffset32 * 32U));
  }
  uint32 *
  SeekTable() // [SeekTableCount + 1] if SeekTableCount != 0, otherwise invalid.
  {
    return (uint32 *)(PTR_ADD(this, ToSeekTableOffset32 * 32U));
  }
};

struct audioinfo {
  int SampleRate;
  int TrueSampleCount;
  int SampleDataSize;
  int NumChannels;
  float Duration;
  int AudioDataOffset;

public:
  friend std::string to_string(const audioinfo &info) {
    std::string str = "";
    str += "SampleRate: " + std::to_string(info.SampleRate) + "\n";
    str += "TrueSampleCount: " + std::to_string(info.TrueSampleCount) + "\n";
    str += "SampleDataSize: " + std::to_string(info.SampleDataSize) + "\n";
    str += "NumChannels: " + std::to_string(info.NumChannels) + "\n";
    str += "Duration: " + std::to_string(info.Duration) + "\n";
    str += "AudioDataOffset: " + std::to_string(info.AudioDataOffset) + "\n";
    return str;
  }
};

bool ParseHeader(const uint8 *InSrcBufferData, uint32 InSrcBufferDataSize,
                 struct audioinfo *info) {
  auto SrcBufferData = InSrcBufferData;
  auto SrcBufferDataSize = InSrcBufferDataSize;
  auto SrcBufferOffset = 0;
  auto CurrentSampleCount = 0;

  assert(InSrcBufferDataSize >= sizeof(BinkAudioFileHeader));
  if (InSrcBufferDataSize < sizeof(BinkAudioFileHeader)) {
    return false;
  }

  BinkAudioFileHeader *Header = (BinkAudioFileHeader *)InSrcBufferData;
  if (Header->tag != 'UEBA') {
    return false;
  }
  if (Header->version != 1) {
    return false;
  }

  auto SampleRate = Header->rate;
  auto TrueSampleCount = Header->sample_count;
  auto NumChannels = Header->channels;
  auto MaxCompSpaceNeeded = Header->max_comp_space_needed;

  uint32 SeekTableSize = Header->seek_table_entry_count * sizeof(uint16);
  if (sizeof(BinkAudioFileHeader) + SeekTableSize > InSrcBufferDataSize) {
    return false;
  }

  // Store the offset to where the audio data begins
  auto AudioDataOffset = sizeof(BinkAudioFileHeader) + SeekTableSize;

  // Write out the the header info
  if (info) {
    info->SampleRate = Header->rate;
    info->NumChannels = Header->channels;
    info->SampleDataSize =
        Header->sample_count * info->NumChannels * sizeof(int16);
    info->Duration = (float)Header->sample_count / info->SampleRate;
    info->AudioDataOffset = AudioDataOffset;
    info->TrueSampleCount = TrueSampleCount;
  }

  return true;
}

bool CreateDecoder(const uint8_t *SrcBufferData, uint32_t SrcBufferDataSize,
                   uint32_t NumChannels, uint32_t SampleRate,
                   uint8_t *&RawMemory, BinkAudioDecoder *&Decoder,
                   uint32_t &SrcBufferOffset) {

  auto GetMaxFrameSizeSamples = [SampleRate]() -> uint32_t {
    if (SampleRate >= 44100) {
      return 1920;
    } else if (SampleRate >= 22050) {
      return 960;
    } else {
      return 480;
    }
  };
  UEBinkAudioDecodeInterface *BinkInterface = nullptr;

  BinkInterface = UnrealBinkAudioDecodeInterface();

  if (BinkInterface == nullptr) {
    return false; // only happens if we dont have libs.
  }

  BinkAudioFileHeader *Header = (BinkAudioFileHeader *)SrcBufferData;

  // Bink is max stereo per stream
  uint32 StreamCount = (NumChannels + 1) >> 1;

  // Figure memory for buffers:

  // Deinterlace - buffer space for interleaving multi stream binks in to a
  // standard interleaved format.
  uint32 DeinterlaceMemory = 0;
  if (StreamCount > 1) {
    DeinterlaceMemory = GetMaxFrameSizeSamples() * sizeof(int16) * 2;
  }

  // Space to store a decoded block.
  uint32 OutputReservoirMemory =
      NumChannels * GetMaxFrameSizeSamples() * sizeof(int16);

  // Space for the decoder state
  uint32 DecoderMemoryTotal = 0;
  uint32 DecoderMemoryPerStream[MAX_BINK_AUDIO_CHANNELS / 2];
  {
    uint32 RemnChannels = NumChannels;
    for (uint32 StreamIndex = 0; StreamIndex < StreamCount; StreamIndex++) {
      uint32 StreamChannels = RemnChannels;
      if (StreamChannels > 2) {
        StreamChannels = 2;
      }
      RemnChannels -= StreamChannels;
      DecoderMemoryPerStream[StreamIndex] =
          BinkInterface->MemoryFn(SampleRate, StreamChannels);
      DecoderMemoryTotal += DecoderMemoryPerStream[StreamIndex];
    }
  }

  // Space for the decoder pointers
  uint32 PtrMemory = Align32(sizeof(void *) * StreamCount);

  // Space for ourselves + the channel count for each stream.
  uint32 StructMemory =
      Align32(sizeof(BinkAudioDecoder) + sizeof(uint8) * StreamCount);

  // Space for decoded seek table
  uint32 SeekTableMemory = 0;
  if (Header->seek_table_entry_count) {
    SeekTableMemory =
        Align32(sizeof(uint32) * (Header->seek_table_entry_count + 1));
  }

  uint32 TotalMemory = DecoderMemoryTotal + PtrMemory + StructMemory +
                       OutputReservoirMemory + DeinterlaceMemory +
                       SeekTableMemory;

  //
  // Allocate and save offsets
  //
  RawMemory = (uint8 *)malloc(TotalMemory);
  memset(RawMemory, 0, TotalMemory);

  Decoder = (BinkAudioDecoder *)RawMemory;

  Decoder->StreamCount = StreamCount;
  Decoder->OutputReservoirTotalBytes = OutputReservoirMemory;
  Decoder->SeekTableCount = Header->seek_table_entry_count;
  Decoder->FramesPerSeekTableEntry = Header->blocks_per_seek_table_entry;

  // See layout discussion in class declaration
  void **Decoders = Decoder->Decoders();
  uint8 *CurrentMemory = (uint8 *)PTR_ADD(Decoders, PtrMemory);
  uint8 *Channels = Decoder->StreamChannels();

  // Init decoders
  {
    uint8 RemnChannels = NumChannels;
    for (uint32 StreamIndex = 0; StreamIndex < StreamCount; StreamIndex++) {
      uint32 StreamChannels = RemnChannels;
      if (StreamChannels > 2) {
        StreamChannels = 2;
      }
      RemnChannels -= StreamChannels;

      Channels[StreamIndex] = StreamChannels;
      Decoders[StreamIndex] = (void **)CurrentMemory;
      CurrentMemory += DecoderMemoryPerStream[StreamIndex];
      BinkInterface->OpenFn(Decoders[StreamIndex], SampleRate, StreamChannels,
                            true, true);
    }
  }

  Decoder->ToOutputReservoirOffset32 =
      (uint16)((CurrentMemory - RawMemory) / 32);
  CurrentMemory += OutputReservoirMemory;

  Decoder->ToDeinterlaceBufferOffset32 =
      (uint16)((CurrentMemory - RawMemory) / 32);
  CurrentMemory += DeinterlaceMemory;

  Decoder->ToSeekTableOffset32 = (uint16)((CurrentMemory - RawMemory) / 32);
  CurrentMemory += SeekTableMemory;

  // Decode the seek table
  if (Decoder->SeekTableCount) {
    uint32 *SeekTable = Decoder->SeekTable();
    uint16 *EncodedSeekTable =
        (uint16 *)(SrcBufferData + sizeof(BinkAudioFileHeader));
    uint32 CurrentSeekOffset = 0;

    // the seek table has deltas from last, and we want absolutes
    for (uint32 i = 0; i < Decoder->SeekTableCount; i++) {
      SeekTable[i] = CurrentSeekOffset;
      CurrentSeekOffset += EncodedSeekTable[i];
    }
    SeekTable[Decoder->SeekTableCount] = CurrentSeekOffset;
  }

  SrcBufferOffset = sizeof(BinkAudioFileHeader) +
                    Header->seek_table_entry_count * sizeof(uint16);
  return true;
}

int Decode(BinkAudioDecoder *&Decoder, const uint8 *CompressedData,
           const int32 CompressedDataSize, uint32_t NumChannels,
           uint32_t SampleRate, const uint16_t MaxCompSpaceNeeded,
           uint8 *OutPCMData, const int32 OutputPCMDataSize) {
  auto GetMaxFrameSizeSamples = [SampleRate]() -> uint32_t {
    if (SampleRate >= 44100) {
      return 1920;
    } else if (SampleRate >= 22050) {
      return 960;
    } else {
      return 480;
    }
  };
  UEBinkAudioDecodeInterface *BinkInterface = 0;

  BinkInterface = UnrealBinkAudioDecodeInterface();

  if (BinkInterface == 0) {
    return 0; // only happens if we dont have libs.
  }

  uint32 RemnOutputPCMDataSize = OutputPCMDataSize;
  uint32 RemnCompressedDataSize = CompressedDataSize;
  const uint8 *CompressedDataEnd = CompressedData + CompressedDataSize;

  //
  // In the event we need to copy to a stack buffer, we alloca() it here so
  // that it's not inside the loop (for static analysis). We don't touch the
  // memory until we need it so it's just a couple instructions for the
  // alloca().
  // (+8 for max block header size)
  uint8 *StackBlockBuffer =
      (uint8 *)alloca(MaxCompSpaceNeeded + BINK_UE_DECODER_END_INPUT_SPACE + 8);

  const uint32 DecodeSize =
      GetMaxFrameSizeSamples() * sizeof(int16) * NumChannels;
  while (RemnOutputPCMDataSize) {
    //
    // Drain the output reservoir before attempting a decode.
    //
    if (Decoder->OutputReservoirValidBytes) {
      uint32 CopyBytes = Decoder->OutputReservoirValidBytes;
      if (CopyBytes > RemnOutputPCMDataSize) {
        CopyBytes = RemnOutputPCMDataSize;
      }

      memcpy(OutPCMData, Decoder->OutputReservoir(), CopyBytes);

      // We use memmove here because we expect it to be very rare that we
      // don't consume the entire output reservoir in a call, so it's not
      // worth managing a cursor. Just move down the remnants, which we expect
      // to be zero.
      if (Decoder->OutputReservoirValidBytes != CopyBytes) {
        std::memmove(Decoder->OutputReservoir(),
                     Decoder->OutputReservoir() + CopyBytes,
                     Decoder->OutputReservoirValidBytes - CopyBytes);
      }
      Decoder->OutputReservoirValidBytes -= CopyBytes;

      RemnOutputPCMDataSize -= CopyBytes;
      OutPCMData += CopyBytes;

      if (RemnOutputPCMDataSize == 0) {
        // we filled entirely from the output reservoir
        break;
      }
    }

    if (RemnCompressedDataSize == 0) {
      // This is the normal termination condition
      break;
    }

    

    if (BinkAudioValidateBlock(MaxCompSpaceNeeded, CompressedData,
                               RemnCompressedDataSize) !=
        BINK_AUDIO_BLOCK_VALID) {
      // The splitting system should ensure that we only ever get complete
      // blocks - so this is bizarre.
      // UE_LOG(LogBinkAudioDecoder, Warning,
      //        TEXT("Got weird buffer, validate returned %d"),
      //        BinkAudioValidateBlock(MaxCompSpaceNeeded, CompressedData,
      //                               RemnCompressedDataSize));
      
      std::cout << "Got weird buffer, validate returned "
                << std::to_string(BinkAudioValidateBlock(
                       MaxCompSpaceNeeded, CompressedData,
                       RemnCompressedDataSize))
                << std::endl;
      break;
    }

    uint8 const *BlockStart = 0;
    uint8 const *BlockEnd = 0;
    uint32 TrimToSampleCount = 0;
    BinkAudioCrackBlock(CompressedData, &BlockStart, &BlockEnd,
                        &TrimToSampleCount);
    uint8 const *BlockBase = CompressedData;

    //
    // We need to make sure there's room available for Bink to read past the
    // end of the buffer (for vector decoding). If there's not, we need to
    // copy to a temp buffer.
    //
    bool HasRoomForDecode =
        (CompressedDataEnd - BlockEnd) > BINK_UE_DECODER_END_INPUT_SPACE;
    if (HasRoomForDecode == false) {
      // This looks weird, but in order for the advancement logic to work,
      // we need to replicate the entire block including the header.
      size_t BlockOffset = BlockStart - BlockBase;
      size_t BlockSize = BlockEnd - BlockBase;
      if (BlockSize > MaxCompSpaceNeeded + 8) // +8 for max block header size
      {
        // UE_LOG(LogBinkAudioDecoder, Error,
        //        TEXT("BAD! Validated block exceeds header max block size (%d
        //        "
        //             "vs %d)"),
        //        BlockSize, MaxCompSpaceNeeded);
        std::cout << "BAD! Validated block exceeds header max block size ("
                  << BlockSize << " vs " << MaxCompSpaceNeeded << ")"
                  << std::endl;
        // bErrorStateLatch = true;
        break;
      }

      memcpy(StackBlockBuffer, BlockBase, BlockSize);

      // this is technically not needed, but just so that any analysis shows
      // that we've initialized all the memory we touch.
      memset(StackBlockBuffer + BlockSize, 0, BINK_UE_DECODER_END_INPUT_SPACE);

      BlockBase = StackBlockBuffer;
      BlockStart = StackBlockBuffer + BlockOffset;
      BlockEnd = StackBlockBuffer + BlockSize;
    }

    //
    // If we're a simple single stream and we have enough output space,
    // just decode directly in to our destination to avoid some
    // copies.
    //
    // We also have to have a "simple" decode - i.e. aligned and no trimming.
    //
    if (Decoder->StreamCount == 1 && DecodeSize <= RemnOutputPCMDataSize &&
        TrimToSampleCount == ~0U && (((size_t)OutPCMData) & 0xf) == 0 &&
        Decoder->ConsumeFrameCount == 0) {
      uint32 DecodedBytes =
          BinkInterface->DecodeFn(Decoder->Decoders()[0], OutPCMData,
                                  RemnOutputPCMDataSize, &BlockStart, BlockEnd);

      if (DecodedBytes == 0) {

        // This means that our block check above succeeded and we still failed
        // - corrupted data!
        return 0;
      }

      uint32 InputConsumed = (uint32)(BlockStart - BlockBase);

      OutPCMData += DecodedBytes;
      CompressedData += InputConsumed;
      RemnCompressedDataSize -= InputConsumed;
      RemnOutputPCMDataSize -= DecodedBytes;
      continue;
    }

    // Otherwise we route through the output reservoir.
    if (Decoder->StreamCount == 1) {
      uint32 DecodedBytes = BinkInterface->DecodeFn(
          Decoder->Decoders()[0], Decoder->OutputReservoir(),
          Decoder->OutputReservoirTotalBytes, &BlockStart, BlockEnd);
      if (DecodedBytes == 0) {

        // This means that our block check above succeeded and we still failed
        // - corrupted data!
        return 0;
      }

      uint32 InputConsumed = (uint32)(BlockStart - BlockBase);

      CompressedData += InputConsumed;
      RemnCompressedDataSize -= InputConsumed;

      Decoder->OutputReservoirValidBytes = DecodedBytes;
    } else {
      // multistream requires interlacing the stereo/mono streams
      int16 *DeinterlaceBuffer = Decoder->DeinterlaceBuffer();
      uint8 *CurrentOutputReservoir = Decoder->OutputReservoir();

      uint32 LocalNumChannels = NumChannels;
      for (uint32 i = 0; i < Decoder->StreamCount; i++) {
        uint32 StreamChannels = Decoder->StreamChannels()[i];

        uint32 DecodedBytes = BinkInterface->DecodeFn(
            Decoder->Decoders()[i], (uint8 *)DeinterlaceBuffer,
            GetMaxFrameSizeSamples() * sizeof(int16) * 2, &BlockStart,
            BlockEnd);

        if (DecodedBytes == 0) {
          // This means that our block check above succeeded and we still
          // failed - corrupted data!
          return 0;
        }

        // deinterleave in to the output reservoir.
        if (StreamChannels == 1) {
          int16 *Read = DeinterlaceBuffer;
          int16 *Write = (int16 *)CurrentOutputReservoir;
          uint32 Frames = DecodedBytes / sizeof(int16);
          for (uint32 FrameIndex = 0; FrameIndex < Frames; FrameIndex++) {
            Write[LocalNumChannels * FrameIndex] = Read[FrameIndex];
          }
        } else {
          // stereo int16 pairs
          int32 *Read = (int32 *)DeinterlaceBuffer;
          int16 *Write = (int16 *)CurrentOutputReservoir;
          uint32 Frames = DecodedBytes / sizeof(int32);
          for (uint32 FrameIndex = 0; FrameIndex < Frames; FrameIndex++) {
            *(int32 *)(Write + LocalNumChannels * FrameIndex) =
                Read[FrameIndex];
          }
        }

        CurrentOutputReservoir += sizeof(int16) * StreamChannels;

        Decoder->OutputReservoirValidBytes += DecodedBytes;
      }

      uint32 InputConsumed = (uint32)(BlockStart - BlockBase);
      CompressedData += InputConsumed;
      RemnCompressedDataSize -= InputConsumed;
    } // end if multi stream

    // Check if we are trimming the tail due to EOF
    if (TrimToSampleCount != ~0U) {
      // Ignore the tail samples by just dropping our reservoir size
      Decoder->OutputReservoirValidBytes =
          TrimToSampleCount * sizeof(int16) * NumChannels;
    }

    // Check if we need to eat some frames due to a sample-accurate seek.
    if (Decoder->ConsumeFrameCount) {
      const uint32 BytesPerFrame = sizeof(int16) * NumChannels;
      uint32 ValidFrames = Decoder->OutputReservoirValidBytes / BytesPerFrame;
      if (Decoder->ConsumeFrameCount < ValidFrames) {
        memmove(Decoder->OutputReservoir(),
                Decoder->OutputReservoir() +
                    Decoder->ConsumeFrameCount * BytesPerFrame,
                (ValidFrames - Decoder->ConsumeFrameCount) * BytesPerFrame);
        Decoder->OutputReservoirValidBytes -=
            Decoder->ConsumeFrameCount * BytesPerFrame;
      } else {
        Decoder->OutputReservoirValidBytes = 0;
      }
      Decoder->ConsumeFrameCount = 0;
    }
    // Fall through to the next loop to copy the decoded pcm data out of the
    // reservoir.
  } // while need output pcm data

  // We get here if we filled the output buffer or not.
  // FDecodeResult Result;
  // Result.NumPcmBytesProduced = OutputPCMDataSize - RemnOutputPCMDataSize;
  // Result.NumAudioFramesProduced =
  //     Result.NumPcmBytesProduced / (sizeof(int16) * NumChannels);
  // Result.NumCompressedBytesConsumed =
  //     CompressedDataSize - RemnCompressedDataSize;
  std::cout << "NumPcmBytesProduced: "
            << OutputPCMDataSize - RemnOutputPCMDataSize << std::endl;
  // std::cout << "NumAudioFramesProduced: " << Result.NumAudioFramesProduced
  // << std::endl; std::cout << "NumCompressedBytesConsumed: " <<
  // Result.NumCompressedBytesConsumed << std::endl;

  return OutputPCMDataSize - RemnOutputPCMDataSize;
}

std::string description = 
R"(binkadec: experimental Unreal Engine 4/5 "UEBA/.binka" audio decoder

Tested with:
  - THE FINALS (UE5.0))";
int main(int argc, char **argv) {
  // binkadec -i input.bink -o output.s16 -f raw
  argparse::ArgumentParser program("binkadec");
  program.add_description(description);
  program.add_argument("-i", "--input").required().help("input file");
  program.add_argument("-o", "--output").required().help("output file");
  program.add_argument("-v", "--verbose")
      .default_value(false)
      .implicit_value(true)
      .help("verbose output");
  //format: raw/wav
  program.add_argument("-f", "--format")
      .default_value("raw")
      .help("output format(raw)");

  try {
    program.parse_args(argc, argv);
  } catch (const std::runtime_error &err) {
    std::cout << err.what() << std::endl;
    std::cout << program;
    exit(0);
  }
  std::string input = program.get<std::string>("--input");
  std::filesystem::path input_path(input);
  std::string output = program.get<std::string>("--output");
  std::filesystem::path output_path(output);
  bool verbose = program.get<bool>("--verbose");
  if (verbose) {
    std::cout << "Input: " << input_path << std::endl;
    std::cout << "Output: " << output_path << std::endl;
  }
  std::ifstream input_file(input_path, std::ios::binary);
  if (!input_file.is_open()) {
    std::cout << "Failed to open input file" << std::endl;
    exit(0);
  }
  std::shared_ptr<uint8_t> input_data;
  uint32_t input_data_size;
  {
    input_file.seekg(0, std::ios::end);
    input_data_size = input_file.tellg();
    input_file.seekg(0, std::ios::beg);
    input_data = std::shared_ptr<uint8_t>(new uint8_t[input_data_size],
                                          std::default_delete<uint8_t[]>());
    input_file.read((char *)input_data.get(), input_data_size);
  }

  struct audioinfo info;
  if (!ParseHeader(input_data.get(), input_data_size, &info)) {
    std::cout << "Failed to parse header" << std::endl;
    exit(0);
  }
  if (verbose) {
    std::cout << to_string(info) << std::endl;
  }else{
    std::cout << "SampleRate: " << info.SampleRate << std::endl;
    std::cout << "NumChannels: " << info.NumChannels << std::endl;
    std::cout << "Duration: " << info.Duration << std::endl;
  }

  uint8_t *RawMemory = nullptr;
  BinkAudioDecoder *Decoder = nullptr;
  uint32_t SrcBufferOffset = 0;
  if (!CreateDecoder(input_data.get(), input_data_size, info.NumChannels,
                     info.SampleRate, RawMemory, Decoder, SrcBufferOffset)) {
    std::cout << "Failed to create decoder" << std::endl;
    exit(0);
  }

  if (verbose) {
    std::cout << "RawMemory: " << (void *)RawMemory << std::endl;
    std::cout << "Decoder: " << (void *)Decoder << std::endl;
    std::cout << "SrcBufferOffset: " << SrcBufferOffset << std::endl;
    std::cout << "Decoder->StreamCount: " << Decoder->StreamCount << std::endl;
    std::cout << "Decoder->OutputReservoirValidBytes: "
              << Decoder->OutputReservoirValidBytes << std::endl;
    std::cout << "Decoder->OutputReservoirTotalBytes: "
              << Decoder->OutputReservoirTotalBytes << std::endl;
    std::cout << "Decoder->ToDeinterlaceBufferOffset32: "
              << Decoder->ToDeinterlaceBufferOffset32 << std::endl;
    std::cout << "Decoder->ToSeekTableOffset32: "
              << Decoder->ToSeekTableOffset32 << std::endl;
    std::cout << "Decoder->ToOutputReservoirOffset32: "
              << Decoder->ToOutputReservoirOffset32 << std::endl;
    std::cout << "Decoder->SeekTableCount: " << Decoder->SeekTableCount
              << std::endl;
    std::cout << "Decoder->FramesPerSeekTableEntry: "
              << Decoder->FramesPerSeekTableEntry << std::endl;
    std::cout << "Decoder->ConsumeFrameCount: " << Decoder->ConsumeFrameCount
              << std::endl;
  }

  //malloc for compressed data — new buffer
  std::unique_ptr<uint8_t> compressedData =
      std::unique_ptr<uint8_t>(new uint8_t[input_data_size - SrcBufferOffset]);
  //Copy compressed data, but skip anything from "SEEK" to 0x9999.
  //Does not know why it is needed because Decoder->SeekTableCount is 0.
  bool in_skip = false;
  size_t dst_offset = 0;

  for (size_t i = SrcBufferOffset; i < input_data_size; i++) {
    if (!in_skip && i + 4 < input_data_size &&
        std::memcmp(&input_data.get()[i], "SEEK", 4) == 0) {
      in_skip = true;
      i += 3; // Skip "SEEK"
    } else if (in_skip && i + 2 < input_data_size && input_data.get()[i] == 0x99 &&
               input_data.get()[i + 1] == 0x99) {
      in_skip = false;
      compressedData.get()[dst_offset++] = 0x99;
      compressedData.get()[dst_offset++] = 0x99;
      i += 1; // Skip 0x9999
    } else if (!in_skip) {
      compressedData.get()[dst_offset++] = input_data.get()[i];
    }
  }

  if (verbose) {
    std::cout << "compressedDataSize: " << dst_offset << std::endl;
  }


  uint32_t NumChannels = info.NumChannels;
  uint32_t SampleRate = info.SampleRate;
  uint16_t MaxCompSpaceNeeded = info.SampleDataSize;
  uint8_t *OutPCMData = (uint8_t *)malloc(info.SampleDataSize);
  int32_t OutputPCMDataSize = info.SampleDataSize;
  int32_t DecodedBytes =
      Decode(Decoder, compressedData.get(), dst_offset, NumChannels, SampleRate,
             MaxCompSpaceNeeded, OutPCMData, OutputPCMDataSize);
  if (DecodedBytes == 0) {
    std::cout << "Failed to decode" << std::endl;
    exit(0);
  }
  std::ofstream output_file(output_path, std::ios::binary);
  if (!output_file.is_open()) {
    std::cout << "Failed to open output file" << std::endl;
    exit(0);
  }
  output_file.write((char *)OutPCMData, DecodedBytes);
  output_file.close();
  input_file.close();
  std::cout << "Done. Play the result with:" << std::endl;
  std::cout << "ffplay -f s16le -ar " << SampleRate << " -ac "
            << NumChannels << " -i " << output_path << std::endl;
}

 

 

Да, спасибо. Я что-то сделал. Но без Unreal Engine Audio Binka SDK не получается — для компиляции нужны:

#include "binka_ue_encode.h"
#include "binka_ue_file_header.h"
Изменено пользователем vananagornyi
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

ребята кто очень хочет озвучку
давайте навалимся на synthvoiceru
https://boosty.to/synthvoiceru
пишите под постами чтоб они организовали сбор

Поделиться сообщением


Ссылка на сообщение
В 07.11.2024 в 22:32, xoixa сказал:

Для анрила есть Fmodel чтобы доставать файлы из pak и utoc/ucas

Звуки часто в формате wwise (.wem контейнеры)

Для конвертации звука в wav есть, например, vgmstream

В играх начиная от unreal 4.27+ звук может быть в Bink Audio (.binka контейнер)

Для конвертации звука в wav из Bink Audio есть binkadec (vgmstream не понимает .binka контейнер от unreal)

.binka контейнер от unreal (ABEU — Unreal Engine Bink Audio) не путать со старым binka контейнером от Miles Sound System (1FCB)

 

Metro Awakening на unreal 5.2.1

ресурсы в IOStore файлах (папка Metro Awakening\Impact\Content\Paks\)

не шифрованные, сжатые Oodle

звук Unreal Engine Bink Audio в нескольких utoc/ucas:

по пути Impact\Content\Maps\ — сюжет

и Impact\Content\Assets\Audio\VO\ — стандартные фразы

субтитры в Impact\Content\L10N (папка ru — русские, uk — украинские) в uasset с постфиксом “_Sub”

английские субтитры в папках рядом со звуком

Fmodel умеет экспортировать звук в .binka: нужно два раза кликнуть на uasset звука и в папке экспорта будет соответствующий binka файл

Чтобы Fmodel экспортировал сразу в wav, нужно закинуть binkadec.exe в папку .data в папке экспорта (Output Directory в настройках)

@xoixa подскажи пожалуйста, а обратно как из wav в binka конвертить? Я при помощи binkadec в wav форматнул, перевёл на русский, теперь хочу обратно в binka, потом сложить в проект UE5, запечь и сделать uasset\uexp\ubulk. Я так делал в UE4 правда с wwise. Но как обратно в binka конвертить — вообще нигде инфы нет...

Поделиться сообщением


Ссылка на сообщение
16 часов назад, nekkk сказал:

Вот, попробуйте, я сделал это с помощью IA.

Here it is, give it a try, I made it with the help of an IA.
https://www.mediafire.com/file/ffq5wbu1ebx4grm/binkaenc.exe/file

SEEK блоки не создает если использовать —seek-table и от какой версии Unreal Engine взято SDK?
Вот пример с SEEK: VO_BK_1_1_Tina_01_Tina.binka
И за саму программу большое спасибо.

Изменено пользователем ponaromixxx

Поделиться сообщением


Ссылка на сообщение
1 hour ago, ponaromixxx said:

SEEK does not create blocks if you use —seek-table and from which version of Unreal Engine is the SDK taken?
Here is an example with SEEK: VO_BK_1_1_Tina_01_Tina.binka
And many thanks for the program itself.

Encoder действительно что-то создает, если вы используете —seek-table, но это определенно отличается от предоставленного вами примера.

Я постараюсь исправить это, но если вы тоже хотите помочь, я могу отправить вам исходный код в приватной форме.

Кстати, я использовал SDK 5.5.4.

 

Encoder does create something if you use —seek-table, but it is definitely different from the example you provided.

I will try to correct it, but if you want to help too I can send you the source code in private.

By the way, I have used the SDK 5.5.4.

Поделиться сообщением


Ссылка на сообщение
4 часа назад, nekkk сказал:

Encoder действительно что-то создает, если вы используете —seek-table, но это определенно отличается от предоставленного вами примера.

Я постараюсь исправить это, но если вы тоже хотите помочь, я могу отправить вам исходный код в приватной форме.

Кстати, я использовал SDK 5.5.4.

 

Encoder does create something if you use —seek-table, but it is definitely different from the example you provided.

I will try to correct it, but if you want to help too I can send you the source code in private.

By the way, I have used the SDK 5.5.4.

Я тоже начинал писать Encoder по SDK, но забросил из-за нехватки времени, так же я decoder переносил под последнюю версию, но там не сложно было. Я сам больше на C# пишу, С++ только изучаю, но могу попробовать помочь, если смогу.

Поделиться сообщением


Ссылка на сообщение

Создайте аккаунт или войдите в него для комментирования

Вы должны быть пользователем, чтобы оставить комментарий

Создать аккаунт

Зарегистрируйтесь для получения аккаунта. Это просто!

Зарегистрировать аккаунт

Войти

Уже зарегистрированы? Войдите здесь.

Войти сейчас


  • Сейчас популярно

  • Продвигаемые темы

  • Последние сообщения

    • А вот Тирниэль выше привел вполне себе статистику, сам я поленился проверять. О нет, это очевидный факт, зайди в стим, в новинки не популярные. В день выходит НЕСКОЛЬКО ДЕСЯТКОВ И ИНОГДА ДАЖЕ СОТЕН Ты про них никогда не узнаешь, но они входят в статистику твою, когда ты говоришь как много есть игр Большая часть сделана на коленке, неройсетями или даже это просто клоны клонов, клонов, там не надо быть семи пядей во лбу, чтобы каждый сказал, что это мусор. Но даже если это не мусор, они нам не интересны, в них не вложены никакие бюджеты, все относительно стоящие игры, известны, они привлекают внимание, даже если оно не достаточно для окупаемости.
    • Правильно: твои единичные примеры - не аргумент. Как и мои возможные в ответ. "Мои примеры" все на странице Стим "ранний доступ", красноречивее некуда  .   Крайне оценочное суждение, выгодное исключительно тебе в этом споре. Игр множество, с каждым днём всё больше и любая может "выстрелить", что регулярно и происходит.    Почему это, все важны, особенно в статистике. Напомню, что изначально речь шла об отношении к РД. Ты же в последних постах цепляешься за конкретный сегмент разработчиков, чаще всего использующих эту форму распространения. Тема мягко говоря уехала  .   Он является не защищённым для пользователя, сейчас уже защищённее, и выгодным в первую очередь разработчику, а там где большая выгода и перевес на чью либо сторону - раздолье для не чистых на руку, что и происходит, если ознакомиться со списком игр в Стим "ранний доступ".
    • @piton4 Следи за мыслью  — раз  ты потерял нить  —  это ключевая, далее твое продолжаем, посты с примерами сам найдешь это не суть может еще известных мне перечислить? Хотя это тоже не суть. дальше из пустого в порожнее по большей части Ключевое уточнение, которое ты пытаешься опровергать, то что тебя вдруг понесло доказывать как много игр не в РД в метроидвании, так это тебя Даскер покусал докапываться до мелочей игнорируя общую картинку, чтобы показать какой он в споре молодец Ты мне доказываешь что 90% таких игр, отвечая на мой пост — без раннего доступа, у меня такие игры — ЭТО ИНДИ относительно среднего бюджета, я это повторил несколько раз, что ты доказал за это время? Что не инди метроидвании выходт без раннего доступа, а инди метроидваний у тебя не намного больше, чем инди метроидваний в раннем доступе (три?) Ну поздравляю, че.. был не прав, хотя ранее вот этого уже было
        Там где я погорячился со ВСЕ — в самом начале или  ПОЛОВИНА МЕТРОИДВАНИЙ — ну что я сам признал сразу, что погорячился, что ты доказал то, что я уже ранее признал? А как же все твои неправильные тезисы, будем доказывать или еще к чему прикопаемся? Сказочник и синонимы, в других постах в том числе, это как несколько раз сказочник, никакой разницы. Софистика какая-то.  Возвращаю тебе перчатку “90% игр выходит без раннего доступа” — это верно но не в ответах мне, потому что они четко обозначили, какие игры выходят, инди — среднего бюджета, принимаем мой тезис или будет примеры подбирать в большом количестве?
    • @Tirniel я вообще не понял, что ты сейчас написал. Какая связь между тем, что 90% игр Б не выходят в доступе, и тем, что игры этой категории в основном выходят в раннем доступе?   И ты веришь? )  
    • Твоя статистика имеет логические ошибки и основана не на фактах. Даже если допустить, что “90%” Б игр не выходят в ранний доступ, но при этом игры этой категории и есть основной потребитель раннего доступа, то эти “90%” — бесполезная информация, которая сама по себе не говорит ни о чём, т.к. в оставшихся 10-ти процентах основная доля игр будет именно из категории раннего доступа, а потому нужно сравнение в одинаковых категориях: ранний доступ разных по бюджетам игр с не ранним доступом разных по бюджетам игр. Например, много видел трипл А игр в раннем доступе? Да, они есть, но какой их процент на фоне Б игр, например? В т.ч. советую всё-таки не упоминать точные проценты, когда отсутствуют данные, и по сути идёт лишь предположение. Гугл, к примеру, говорит о том, что 14-18 процентов всех игр стима выходят в раннем доступе. Б играми он именует все инди. Далее берём процент всех инди в раннем доступе (50-60% от всех), процент релиза инди игр относительно прочих в стиме (98-99%) и выводим то, что твои 90% взяты с потолка. Ну а также, анализируя данные гугла, приходим к выводам о том, что он данные также берёт с потолка, т.к. они не сходятся между собой.
    • есть такое. Думаю, это реально вина возраста и багаж просмотренного, где-то усталость от клише и банальностей жанров. Это как в советских фильмах для подростков вроде “Кортика” и “Бронзовой птицы”, по сути дети порой разговаривают как академики. Можно еще вспомнить поздние “Неуловимые мстители”. Смотрится круто, особенно когда ты сам еще детё, хочется походить на героев, подражать им, с этой целью так и делают, но становясь старше, понимаешь, что в реальности так не бывает и НЕ разговаривают так между собой в таком возрасте.  реальных подростков можно увидеть в Ералаше. Веселые, тупые (что нормально для возраста), беззаботные и т.д. вот это про него. Я подобное смотрю, в надежде увидеть что-то стоящее, хотя бы в духе “Город в котором меня нет”, к которому тоже немало вопросов. Но снова  лютый кринж, на вроде когда очередной неудачник, при этом закончивший не абы какую там академию, уже состоявший в каких-нибудь там отношениях, и даже успевший поработать в престижной компании… камбекнувшись в школьные годы… тупеет, становится девственником (что по факту понятно), больше по разуму, и главное его цель это друзьяшки и улыбка одноклассницы, а если еще и за ручки удастся подержаться… то там прям на весь экран  я про эмоции если что    
    • Обновление: мелкие исправления, новый штурман.

      Русификатор звука для Assetto Corsa Rally
      Русские штурманы Илья Баландин (izolda), Штурман Вика и Hrenovalke.
      Скачать Яндекс Диск Гугл Диск

      Озвучка:
      -Сергей День.
      -Hrenovalke.
      Адаптация:
      -Siroga

      ВАЖНО!!!
      Заменяется голоса итальянского, немецкого и французского штурманов.
      Выберите в опциях игры:
      Илья Баландин - «ЯЗЫК ШТУРМАНА — ИТАЛЬЯНСКИЙ».
      Штурман Вика - «ЯЗЫК ШТУРМАНА — НЕМЕЦКИЙ».
      Hrenovalke - «ЯЗЫК ШТУРМАНА — ФРАНЦУЗСКИЙ».
      Установка:
      Распакуйте архив в папку с игрой, заменив существующие файлы.
      Предварительно, если нужно, сделайте резервную копию FMB_Pacenotes.assets.bank.
      Выберите в опциях игры: «ЯЗЫК ШТУРМАНА — ИТАЛЬЯНСКИЙ, НЕМЕЦКИЙ ИЛИ ФРАНЦУЗСКИЙ».

         
    • Я показал, что метроидвании в подавляющем большинстве, не выходят в раннем доступе. То, что не все они были инди, это уже другой вопрос. И по качеству сопоставимы.  То, что у тебя "не поворачивается" язык - это твоя проблема.  Не назвал ты и 20-ти. А даже если назвал - 20 это не 40.  Я смотрю ты любитель сильно преувеличить, когда тебе это выгодно.   Я  очевидные вещи доказывать не собираюсь, и выискивать сотни примеров.   Я тебя назвал сказочником 1 раз. А ещё несколько раз написал, что ты ерунду говоришь. Так это по факту. Про метрошки и про то, что ты написал изначально про "все игры в раннем доступе" (или как там?), так это ерунда. Будешь спорить?  А про то, что ты сказочник...  А как тебя назвать, если ты упомянул игр 15(5 игр Торчлайт), а пишешь про 40? Да, я докопался до этого, но уж не обессудь, ты сам виноват ) В раннем доступе в основном выходит всякая "рогалическая" и изометрическая хрень - торчлайты, хейдесы, и тп.
    • Тебе нельзя, не пущу. Если кроме меня всяких торговых автоматов будут смотреть и другие, то какой же хаос тут начнётся ж  Из всего списка кроме торгового автомата ничего больше смотреть не стал. Он, кстати, на общем фоне ещё норм. На самом дне лежит анимация про перерождение во всё подряд: попадалась анимация (не помню название, в коротких видео попался ролик), где гг за сезон успел побывать от микрофона и женского белья до ещё более неожиданных вещей (-дцать перерождений за сезон — это сильно). Ну и ещё из экзотики “ребро героя”. Это из того, что даже я смотреть не стал, даже первые серии. При всей моей любви ко всяким перерождалкам необычным, где обычно первые серии — самый сок, а дальше можно не трогать. Яйцо дракона запомнилось ещё из общего перечня (ага, не левое, мб правое ). Но в вышедших сезонах контента было с гулькин нос, так что смотреть смысла нет.   Ледяную стену с пару недель назад смотреть попытался, но что-то не затягивает. Несколько серий посмотрел и эмоции смешанные. ВидимоЮ слишком сильно перерос эти сюжеты, где из мухи делают слона, а вокруг половина людей или добрячки с ментальным возрастом на пару десятилетий старше их самих или сразу малолетние уё*ки для контраста. Всегда выбивало из колеи, когда в школьных анимациях для любовных интересов ставят персонажей, которые явно лишены гормональных бунтов (чрезмерно адекватные для своих лет), уже умеют в какую-то мораль, уже рассуждают как 30+ -летние умудрёные опытом люди, но при этом рассуждают о какой-то ерунде полнейшей. Проще говоря, баланс между возрастом фактическим и ментальным не соблюдён и часто персонажи кидаются из крайности в крайность. При этом в относительно новом течении подобных анимаций с перерождениями в персонажей новел (перерождения в злодеек особенно часто встречаются), где взрослые люди попадают в школьников, картина обычно обратная. Те резко тупеют и ведут себя, будто у них вообще не было их жизненного опыта на -дцать лет большего. Советую начать не с графонистой версии, а с оригинальных каракуль от самого вана. Как ни странно, но качественно куда сильнее воспринимается, не смотря на слабую графику по сути зарисовок. Но особенно вставляет, когда ван в некоторых кадрах показывает, что он так-то и нормально рисовать умеет. Контраст прям сильный выходит. Куда мощнее, чем моменты в перерисовке другим художником, где тот местами Сайтанму рисует в стиле обычной графики Вана (где уровень графики резко падает).
  • Изменения статусов

    • Jimmi Hopkins  »  SerGEAnt

      Это не просто перевод, а полноценная авторская сценарная адаптация. Диалоги переписаны так, чтобы персонажи звучали живо, остро и в характере. Добавлен чёрный юмор там, где авторы постеснялись. Убраны лорные противоречия, докручены мотивации. В результате игра стала умнее, злее и смешнее оригинала.
      · 0 ответов
    • ElikaStudio

      Долгожданный релиз полного сезона состоялся!
      https://youtu.be/mwBk2stm2OQ?si=qpJojB_XDABaC0We
      https://vk.com/video-48153754_456239394?sh=4&list=c62797c2b7d0725d6e
      Life is Strange: Before the Storm:
      Эпизод 1: "Прoбуждeниe"
      Эпизод 2: "О дивный новый мир"
      Эпизод 3: "Ад пуст"
      Бонусный эпизод: "Прощание"
      Русская озвучка уже доступна для скачивания!
      ElikaStudio выражает огромную благодарность всем, кто принял участие в создании проекта! 
      Группе Mechanics VoiceOver R.G. MVO , в частности их руководителю Дмитрию за неоценимую помощь в выпуске эпизода.
      Скачать для PC Classic (2018):
      GDrive: - https://drive.google.com/file/d/19CL_L80Mz0sIxcb54Ss64byAkeZmV22r/view?usp=sharing
      Скачать для PC Remastered (2022):
      GDrive: - https://drive.google.com/file/d/13q58Lpvw5_aYPYeZ7OGYZlAKOoS1gEbL/view?usp=sharing
      Скачать для Свитч(2022):
      GDrive: - https://drive.google.com/file/d/15e—T1LQiGQCYIHeNnj_C2qJA16Gvh1i/view?usp=sharing
      Ручная установка PC(оба издания):
      https://drive.google.com/drive/folders/1MJPd8965m4XxxAuOBt8enSHtv8_yy5xh?usp=sharing
      Баг репорт в обсуждении:
      https://vk.com/topic-48153754_55571577
      ___________________________________________
      Финансовый аппарат:
      www.donationalerts.com/r/elikastudio
      Пожертвовать средства на наши проекты:
      Кошелек ЮMoney 4100 1188 6818 3009
      карта Сбер банк 2202 2018 6334 1042
      карта Альфа банк 5559 4937 0209 8584
      Спасибо за вашу поддержку!
      #elikastudio #русскаяозвучка
      · 0 ответов
    • fox222  »  Siberian GRemlin

      Здравствуйте, хочу купить персональный доступ к переводам, сколько стоит?
      · 1 ответ
    • vitkach  »  eaZy

      Извините за беспокойство. Хотел спросить, а русификатор ещё когда-нибудь будет обновляться? Дело в том, что после его выхода выходили ещё обновления, в частности обновление 1.1, вышедшее летом 2023 года, где была добавлена целая сюжетная глава в конце если проходишь на лучшую концовку золотого пути, это где-то ещё полчаса диалогов. Также в игре присутсвуют иногда кракозябры вместо русского языка, это в основном связано с тем, что кое-где текст был изменён, в основном в обучающих сообщениях.
      · 0 ответов
    • TerryBogard  »  Siberian GRemlin

      C&C: RA: Retaliation (ПК) не работает.
      · 0 ответов
  • Лучшие авторы


×