Перейти к содержимому

Написание скриптов FMOD-Unity: Основы

Целевой стек: Unity 6000.3.10f1 (Unity 6.3) · FMOD for Unity 2.03.13

Существует довольно много документации по интеграции FMOD-Unity и написанию сценариев, но большая часть её разбросана, дезорганизована и похоронена в сообщениях на форуме или видео. Этот документ представляет собой краткое изложение этих «основ» с простыми, проверенными примерами сценариев под актуальную связку Unity 6.3 + FMOD 2.03.13.

Оглавление:
1. Cheat Sheet
2. Воспроизведение звука
3. Размещение звука в пространстве
4. Отслеживание инстансов ивентов (Event Instances)
5. Циклические звуки
6. Остановка звуков
7. Вызов keyOff (отключение точек сустейна)
8. Настройка параметров (Parameters)
9. Ссылки на источник звука — эмиттеров (Emitter)
10. Глобальные Параметры (Global Parameters)
11. Снепшоты для шин и групп (Mixer/Group Snapshots)
12. Обратные вызовы событий — колбеки ивентов (Event Callbacks)

Cheat Sheet

Краткий список общих функций FMOD Unity 2.03.13:

using FMODUnity;
using FMOD.Studio;
// Типы включены для справки и не должны явно включаться в функциональный код

// воспроизводит звуковое событие как 2D-звук
RuntimeManager.PlayOneShot(EventReference soundEvent);

// воспроизводит звуковое событие в фиксированной точке
RuntimeManager.PlayOneShot(EventReference soundEvent, Vector3 startLocation);

// воспроизводит звуковое событие с привязкой к объекту
RuntimeManager.PlayOneShotAttached(EventReference soundEvent, GameObject targetObject);

// создаёт и сохраняет экземпляр звукового события
EventInstance soundInstance = RuntimeManager.CreateInstance(EventReference soundEvent);

// запускает воспроизведение указанного экземпляра звукового события
soundInstance.start();

// останавливает воспроизведение с затуханием или без него
soundInstance.stop(STOP_MODE.ALLOWFADEOUT);
soundInstance.stop(STOP_MODE.IMMEDIATE);

// прикрепляет экземпляр к объекту для трёхмерного позиционирования
RuntimeManager.AttachInstanceToGameObject(soundInstance, gameObject);
RuntimeManager.AttachInstanceToGameObject(soundInstance, transform, rigidbody);

// вручную настраивает 3D-атрибуты экземпляра
soundInstance.set3DAttributes(RuntimeUtils.To3DAttributes(transform));

// получает звуковую шину (lowercase 'bus:/'!)
Bus aBus = RuntimeManager.GetBus("bus:/SFX");

// останавливает все события на шине
aBus.stopAllEvents(STOP_MODE.ALLOWFADEOUT);

// снимает ближайшую точку сустейна (Key Off)
// ВАЖНО: в FMOD 2.02+ метод triggerCue() переименован в keyOff()
soundInstance.keyOff();

// устанавливает значение параметра по имени
soundInstance.setParameterByName(string parameterName, float parameterValue);

// устанавливает значение параметра по кэшированному идентификатору (быстрее)
soundInstance.setParameterByID(PARAMETER_ID parameterID, float parameterValue);

// устанавливает несколько параметров за один вызов
soundInstance.setParametersByIDs(PARAMETER_ID[] ids, float[] values, int count);

// освобождает инстанс из памяти (обязательно к каждому CreateInstance)
soundInstance.release();

// устанавливает глобальный параметр (не привязан к event)
RuntimeManager.StudioSystem.setParameterByName("TimeOfDay", 0.5f);

Воспроизведение звука:

Следующий скрипт будет воспроизводить одиночное звуковое событие (далее ивент) (event) как 2D-звук (стерео/непозиционируемый) всякий раз, когда вызывается метод PlayTheSound.

using UnityEngine;
using FMODUnity;

public class FMODUnityPlayingASound : MonoBehaviour
{
    [SerializeField] private EventReference aSound;

    public void PlayTheSound()
    {
        RuntimeManager.PlayOneShot(aSound);
    }
}

Кроме того, размещение PlayOneShot() в void Awake(), void Start() или void OnEnable() — простые способы начать воспроизведение звукового ивента при создании или включении объекта. Примером использования являются звуковые эффекты стрельбы снарядами. В случаях, когда снаряды создаются и уничтожаются, void Start() обычно является правильным местом для того чтобы проиграть ивент. А в ситуации, когда они предварительно создаются (объединяются в пул) и включаются при использовании, наилучшей альтернативой станет OnEnable().

Важно для Unity 6.3: атрибут [FMODUnity.EventRef] string, который использовался в FMOD 2.01 и ранее, в 2.02+ помечен как [Obsolete] — используйте структуру EventReference, она даёт drag-and-drop поле в инспекторе.

Размещение звука в пространстве:

Первый и самый простой способ воспроизвести звук в указанном (фиксированном) месте — использовать перегрузку с параметром position (Vector3) функции PlayOneShot.

using UnityEngine;
using FMODUnity;

public class FMODUnityPositioningASound : MonoBehaviour
{
    [SerializeField] private EventReference aSound;
    [SerializeField] private Vector3 aLocation;

    public void PlayASoundAtALocation()
    {
        RuntimeManager.PlayOneShot(aSound, aLocation);
    }
}

Примечание. В этом примере я напрямую указываю местоположение, но вы также можете получить местоположение объекта с помощью transform.position. Переменная transform.position также является Vector3.

Кроме того, PlayOneShot не будет динамически отслеживать положение объектов. Он будет привязан к месту, указанному при вызове функции. Для динамического позиционирования нужно будет рассматривать некоторые из следующих (более сложных) вариантов.

Для многих целей PlayOneShotAttached — лучшее решение для воспроизведения звука, который следует за объектом. Он работает так же, как PlayOneShot, но вместо воспроизведения звука в фиксированном месте он будет отслеживать положение объекта, к которому он прикреплён, в течение всего времени существования звукового ивента.

using UnityEngine;
using FMODUnity;

public class FMODUnityPositioningAnotherSound : MonoBehaviour
{
    [SerializeField] private EventReference aSound;
    [SerializeField] private GameObject anObject;

    public void PlayASoundOnAnObject()
    {
        RuntimeManager.PlayOneShotAttached(aSound, anObject);
    }
}

PlayOneShot и PlayOneShotAttached обычно охватывают большую часть простого однократного воспроизведения звуковых ивентов, однако ни в одном из этих методов нет простого контроля звуковых ивентов после их начала. Многие функции FMOD требуют или значительно упрощаются за счёт доступа к инстансу ивента (instance) после его запуска. Вот несколько примеров сценариев, для которых нужны instance: остановка воспроизведения звукового лупа, отключение точек сустейна и установка параметров FMOD для определённых звуковых ивентов. Я расскажу о каждом из них ниже.

Отслеживание инстансов ивентов (Event Instances)

Во многих случаях (loops, cues, parameters и т. д.) нам нужен доступ к звуковому ивенту после начала воспроизведения. В этих случаях вместо использования функций PlayOneShot мы сохраним ссылку на EventInstance, вызвав CreateInstance для имени ивента. Чтобы начать воспроизведение ивента, нам просто нужно вызвать start() для EventInstance. Обратите внимание, что всякий раз, когда звук создаётся с помощью CreateInstance, он также должен быть выгружен из памяти в какой-то момент, чтобы избежать утечки памяти.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnityTrackingEventInstances : MonoBehaviour
{
    [SerializeField] private EventReference aSound;
    private EventInstance aSoundInstance;

    private void Awake()
    {
        aSoundInstance = RuntimeManager.CreateInstance(aSound);
    }

    public void PlayASoundEventInstance()
    {
        aSoundInstance.start();
    }

    private void OnDestroy()
    {
        aSoundInstance.stop(STOP_MODE.IMMEDIATE);
        aSoundInstance.release();
    }
}

Возвращаясь на время к нашей предыдущей теме, при создании и запуске воспроизведения EventInstance по-прежнему отсутствуют данные о позиции объекта. Если мы хотим отслеживать положение объекта, мы можем либо прикрепить экземпляр к объекту с помощью AttachInstanceToGameObject(), либо вручную привязать объект и твердое тело (rigidbody) с помощью set3DAttributes(). Как правило, всё что нужно — это первый вариант.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnityTrackingMoreEventInstances : MonoBehaviour
{
    [SerializeField] private EventReference aSound;
    [SerializeField] private GameObject anObject;
    private EventInstance aSoundInstance;

    private void Start()
    {
        aSoundInstance = RuntimeManager.CreateInstance(aSound);

        // Вариант A (рекомендуется): авто-pickup Rigidbody, если он есть
        RuntimeManager.AttachInstanceToGameObject(aSoundInstance, anObject);

        // Вариант B (вручную с конкретным Rigidbody — для Doppler-эффекта)
        // Rigidbody rb = anObject.GetComponent<Rigidbody>();
        // RuntimeManager.AttachInstanceToGameObject(aSoundInstance, anObject.transform, rb);

        aSoundInstance.start();
    }

    private void OnDestroy()
    {
        aSoundInstance.stop(STOP_MODE.IMMEDIATE);
        aSoundInstance.release();
    }
}

Примечание. Сохранение инстансов ивента на объектах, которые могут существовать в сцене в большом количестве, может привести к значительным утечкам памяти. При этом для часто используемых звуков сохранение инстанса ивента может снизить влияние на производительность.

Циклические звуки:

Другая необходимость в использовании инстансов ивентов возникает при зацикливании воспроизведения звуков. FMOD обрабатывает циклические звуковые ивенты точно так же, как и однократные ивенты. С учётом сказанного, мы почти всегда захотим в какой-то момент остановить цикл.

Для этого нам нужно:

· вызвать команду stop на шине/группе, через которую воспроизводится звук (подробнее об этом позже),

· уничтожить объект, к которому прикреплён звук,

· перезагрузить сцену или часть сцены, в которой объект находится (не самый лучший способ),

· перейти из сегмента цикла,

· вызвать событие stop() в самом инстансе ивента.

Последние два варианта, как правило, обеспечивают максимальную гибкость и в обоих случаях (обычно) требуются ссылки на инстансы ивента для вызова необходимых функций.

Остановка звуков

Как упоминалось выше, циклические звуки и однократные звуки, а также снепшоты микшера (snapshots) обрабатываются как ивенты и могут быть остановлены аналогичным образом. Для этого метода требуется ссылка на EventInstance, и он может останавливаться в одном из двух режимов: режим немедленной остановки или режим постепенного затухания, в котором кривая затухания соответствует кривой «затухания» громкости AHDSR для звукового ивента в FMOD.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnityStoppingSounds : MonoBehaviour
{
    [SerializeField] private EventReference aSound;
    private EventInstance aSoundInstance;

    private void Awake()
    {
        aSoundInstance = RuntimeManager.CreateInstance(aSound);
        aSoundInstance.start();
    }

    public void StopASoundWithFade()
    {
        aSoundInstance.stop(STOP_MODE.ALLOWFADEOUT);
    }

    public void StopASoundWithoutFade()
    {
        aSoundInstance.stop(STOP_MODE.IMMEDIATE);
    }

    private void OnDestroy()
    {
        aSoundInstance.release();
    }
}

Стоит отметить, что если звук или группу звуков необходимо остановить сразу, но у вас нет доступа к ссылкам для любого или всех из них, стоп-вызов может быть вызван для всех ивентов на данной шине/микшерной группе (bus/mix group). Я считаю, что это требует значительных вычислительных затрат, но это очень полезно для нечастых вызовов, таких как изменение уровня или карты, или переходы между различными игровыми состояниями.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnityStoppingBusses : MonoBehaviour
{
    // ВАЖНО: пути к bus всегда в нижнем регистре: "bus:/SFX", "bus:/Music/Combat"
    [SerializeField] private string busPath = "bus:/SFX";
    private Bus theBusASoundPassesThrough;

    private void Awake()
    {
        theBusASoundPassesThrough = RuntimeManager.GetBus(busPath);
    }

    public void StopEntireBus()
    {
        theBusASoundPassesThrough.stopAllEvents(STOP_MODE.ALLOWFADEOUT);
    }
}

Вызов keyOff (отключение точек сустейна)

Одним из распространённых способов использования точек сустейна в FMOD является обеспечение зацикливания асинхронных аудиоклипов, хотя их использование может выходить далеко за рамки этого. Независимо от варианта использования, точка сустейна должна быть отключена (key off), чтобы по таймлайну пройти после неё в FMOD. Чтобы сделать это, мы просто вызываем keyOff() для ивента с меткой (cue). Опять же, для этого действия требуется ссылка EventInstance.

Важно для апгрейда со старых версий. Метод EventInstance.triggerCue() был переименован в EventInstance.keyOff() начиная с FMOD 2.02. В FMOD 2.03.13 старого имени больше нет — старый код выдаст ошибку компиляции CS1061: EventInstance does not contain a definition for triggerCue.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnityCallingCues : MonoBehaviour
{
    [SerializeField] private EventReference someMusic;
    private EventInstance someMusicInstance;

    private void Awake()
    {
        someMusicInstance = RuntimeManager.CreateInstance(someMusic);
        someMusicInstance.start();
    }

    // Старый API:  someMusicInstance.triggerCue();  // больше не компилируется в 2.02+
    // Новый API:  someMusicInstance.keyOff();       // правильно
    public void TriggerSomeCue()
    {
        someMusicInstance.keyOff();
    }

    private void OnDestroy()
    {
        someMusicInstance.stop(STOP_MODE.IMMEDIATE);
        someMusicInstance.release();
    }
}

Примечание. При запуске сигналов до того, как указатель воспроизведения достиг следующей точки сустейна, эта точка сустейна будет проигнорирована. Каждый вызов keyOff() будет отключать только следующую точку сустейна в последовательности, однако несколько вызовов keyOff() могут быть помещены в буфер. Из-за этого взаимодействия иногда полезно использовать маркеры назначения (destination markers) и области переходов (transition regions), когда требуется более высокая сложность или точность. Области переходов реагируют на назначения параметров FMOD (parameters) и ждут выполнения определённых условий в рамках этих параметров (parameters).

Настройка параметров (Parameters)

Параметры (Parameters) являются основой большинства адаптивных звуковых возможностей FMOD и могут легко потребовать отдельной статьи. В этом разделе предполагается, что вы имеете некоторое представление о работе параметров FMOD (Parameters), и особое внимание будет уделено способам их имплементации через скрипты.

Несмотря на комплексность параметров FMOD, написание скрипта для них оказывается относительно простым. Для доступа к локальным параметрам отдельных ивентов FMOD, как и в случае с метками (cue), требуется ссылка EventInstance. Кроме этого, ивент необходимо просто запустить.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnitySettingParameters : MonoBehaviour
{
    [SerializeField] private EventReference aSound;
    [SerializeField] [ParamRef] private string caseSensitiveParameterName;
    [SerializeField] private float parameterValue;

    private EventInstance instance;
    private PARAMETER_ID parameterID;

    private void Start()
    {
        instance = RuntimeManager.CreateInstance(aSound);

        // Кэшируем PARAMETER_ID один раз — дальше работаем только с ним (быстрее)
        instance.getDescription(out EventDescription desc);
        desc.getParameterDescriptionByName(caseSensitiveParameterName, out PARAMETER_DESCRIPTION pd);
        parameterID = pd.id;

        instance.setParameterByID(parameterID, parameterValue);
        instance.start();
    }

    public void UpdateParameter(float newValue)
    {
        instance.setParameterByID(parameterID, newValue);
    }

    private void OnDestroy()
    {
        instance.stop(STOP_MODE.IMMEDIATE);
        instance.release();
    }
}

Примечание. setParameterByID() можно использовать вместо setParameterByName(), однако получение идентификатора параметра (parameter ID) часто требует больше работы, чем простой запрос по имени, а также требует строкового запроса. При этом, в случае захвата идентификатора, его повторное использование может обеспечить более высокую производительность, чем запрос строки в каждом вызове функции. Кроме того, существует множественная функция setParametersByIDs(), которая может одновременно работать с массивом идентификаторов параметров.

Ссылки на источник звука — эмиттер (Studio Event Emitter)

Давайте вместо того, чтобы запустить звуки из компонента Studio Event Emitter, захватим их с помощью переменной типа StudioEventEmitter. Затем мы можем начать воспроизведение с помощью функции эмиттера Play() и можем установить параметры, доступные для редактирования, с помощью SetParameter().

using UnityEngine;
using FMODUnity;

public class FMODUnityEmitterReferences : MonoBehaviour
{
    [SerializeField] private StudioEventEmitter anEmitter;
    [SerializeField] private string parameterName = "Intensity";
    [SerializeField] private float parameterValue = 1f;

    public void PlayEmitterAndSetParameter()
    {
        anEmitter.Play();
        anEmitter.SetParameter(parameterName, parameterValue);
    }

    public void StopEmitter()
    {
        anEmitter.Stop();
    }
}

Примечание. Прямая ссылка через [SerializeField] StudioEventEmitter проще и быстрее, чем устаревший EmitterRef-паттерн, который требовал ручного ввода строк с учётом регистра и индексных значений массива Params[] для параметра, в который вы хотите внести изменения.

Глобальные параметры (Global Parameters)

Глобальные параметры являются более поздним дополнением в FMOD и, в отличие от локальных, не связаны с каким-либо конкретным ивентом. Благодаря этому они также не требуют ссылок на инстансы ивентов и могут эффективно изменять свойства ивентов FMOD практически из любой строчки вашего кода. На мой взгляд, любые параметры, которые не нужно применять по-разному к отдельным копиям инстансов, должны быть глобальными. Полные изменения состояния проекта почти всегда должны передаваться как глобальные параметры, однако параметры, ограниченные одним экземпляром ивента, также могут эффективно использоваться в качестве глобальных.

using UnityEngine;
using FMODUnity;

public class FMODUnityGlobalParameters : MonoBehaviour
{
    [SerializeField] [ParamRef] private string aGlobalParameter;
    [SerializeField] private float aGlobalValue;

    // ИСПРАВЛЕНИЕ: в оригинале метод объявлялся как `private void SetGlobalParameter`
    // без круглых скобок — это синтаксическая ошибка (CS1519). Добавлены ().
    // Также метод сделан public, чтобы его можно было вызвать из UnityEvent.
    public void SetGlobalParameter()
    {
        RuntimeManager.StudioSystem.setParameterByName(aGlobalParameter, aGlobalValue);
    }
}

В отличие от локальных параметров, к глобальным параметрам можно получить доступ в редакторе с помощью атрибута [ParamRef]. Кроме того, как и в случае с локальными функциями setParameter, глобальные параметры также могут быть установлены по идентификатору и по массиву идентификаторов.

Снепшоты для шин и групп (Mixer/Group Snapshots)

Снепшоты для шин, или групповые снепшоты (Group Snapshots) (как их называет FMOD) функционируют по существу идентично звуковым ивентам и могут рассматриваться как своего рода глобальный звуковой ивент. С учётом сказанного, они ведут себя больше похоже на циклические звуки, чем на однократные звуки, в том смысле, что они будут сохраняться до тех пор, пока не будут остановлены вручную или принудительно. Затухание может быть применено к снимкам микшера через время атаки и восстановления (AHDSR) на различных «макросах снимков», «измерителе интенсивности», а также на любых изменённых параметрах снимка. Как и в случае стандартных ивентов остановки, релиз фейда также можно обойти с помощью режима немедленной остановки.

using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnitySnapshots : MonoBehaviour
{
    // Путь вида "snapshot:/Underwater", "snapshot:/Slowmo" и т.д.
    [SerializeField] private EventReference aGroupSnapshot;
    private EventInstance snapshotInstance;
    private bool isActive;

    private void Awake()
    {
        snapshotInstance = RuntimeManager.CreateInstance(aGroupSnapshot);
    }

    public void StartSnapshot()
    {
        if (isActive) return;
        snapshotInstance.start();
        isActive = true;
    }

    public void StopSnapshot()
    {
        if (!isActive) return;
        // ALLOWFADEOUT уважает snapshot AHDSR (плавный переход обратно)
        snapshotInstance.stop(STOP_MODE.ALLOWFADEOUT);
        isActive = false;
    }

    private void OnDestroy()
    {
        snapshotInstance.release();
    }
}

Обратные вызовы событий — колбеки ивентов (Event Callbacks)

Обратные вызовы ивентов (Event Callbacks) — это очень "необычный зверь". Их можно использовать для отслеживания и обратной реакции игрового мира практически на любое изменение состояния звуковой системы в рамках ивента. Однако, самое главное для меня, это их способность отслеживать маркеры шкалы времени и темп. Если вы хотите создавать системы которые реагируют на музыку, это хорошая отправная точка.

Критично для Unity 6.3 + IL2CPP. В Unity 6 для player-сборок IL2CPP включён по умолчанию. Callback-методы ОБЯЗАНЫ быть static и помечены атрибутом [AOT.MonoPInvokeCallback(typeof(EVENT_CALLBACK))]. Без этого атрибута IL2CPP при сборке отбросит метод как unused, и в рантайме произойдёт crash при первом вызове колбека.

using System;
using System.Runtime.InteropServices;
using UnityEngine;
using FMOD.Studio;
using FMODUnity;

public class FMODUnityBeatTracker : MonoBehaviour
{
    [SerializeField] private EventReference musicEvent;

    private EventInstance musicInstance;
    private EVENT_CALLBACK beatCallback;
    private TimelineInfo timelineInfo;
    private GCHandle timelineHandle;

    [StructLayout(LayoutKind.Sequential)]
    private class TimelineInfo
    {
        public int currentBar = 0;
        public FMOD.StringWrapper lastMarker = new FMOD.StringWrapper();
    }

    private void Start()
    {
        timelineInfo = new TimelineInfo();
        timelineHandle = GCHandle.Alloc(timelineInfo, GCHandleType.Pinned);
        beatCallback = BeatEventCallback;

        musicInstance = RuntimeManager.CreateInstance(musicEvent);
        musicInstance.setUserData(GCHandle.ToIntPtr(timelineHandle));
        musicInstance.setCallback(beatCallback,
            EVENT_CALLBACK_TYPE.TIMELINE_BEAT | EVENT_CALLBACK_TYPE.TIMELINE_MARKER);
        musicInstance.start();
    }

    private void OnDestroy()
    {
        musicInstance.setUserData(IntPtr.Zero);
        musicInstance.stop(STOP_MODE.IMMEDIATE);
        musicInstance.release();
        timelineHandle.Free();
    }

    // КРИТИЧНО: static + [AOT.MonoPInvokeCallback] для Unity 6 IL2CPP
    [AOT.MonoPInvokeCallback(typeof(EVENT_CALLBACK))]
    private static FMOD.RESULT BeatEventCallback(EVENT_CALLBACK_TYPE type,
                                                  IntPtr instancePtr, IntPtr paramPtr)
    {
        EventInstance instance = new EventInstance(instancePtr);
        instance.getUserData(out IntPtr userDataPtr);
        if (userDataPtr == IntPtr.Zero) return FMOD.RESULT.OK;

        GCHandle handle = GCHandle.FromIntPtr(userDataPtr);
        TimelineInfo info = (TimelineInfo)handle.Target;

        switch (type)
        {
            case EVENT_CALLBACK_TYPE.TIMELINE_BEAT:
                var beat = (TIMELINE_BEAT_PROPERTIES)Marshal.PtrToStructure(paramPtr,
                    typeof(TIMELINE_BEAT_PROPERTIES));
                info.currentBar = beat.bar;
                break;

            case EVENT_CALLBACK_TYPE.TIMELINE_MARKER:
                var marker = (TIMELINE_MARKER_PROPERTIES)Marshal.PtrToStructure(paramPtr,
                    typeof(TIMELINE_MARKER_PROPERTIES));
                info.lastMarker = marker.name;
                break;
        }
        return FMOD.RESULT.OK;
    }
}

Вся информация актуальна для FMOD for Unity 2.03.13 и Unity 6000.3.10f1 (Unity 6.3).

Ключевые отличия от старого руководства (FMOD 2.00.05):
[FMODUnity.EventRef] stringEventReference struct (с 2.02)
EventInstance.triggerCue()EventInstance.keyOff() (с 2.02)
— Для Event Callbacks в Unity 6 IL2CPP обязателен [AOT.MonoPInvokeCallback]
— Bus-пути в нижнем регистре (bus:/...), не Bus:/...