LINUX.ORG.RU
ФорумTalks

Почему pipewire такой сложный?

 , ,


1

3

Захотел написать простейшую программу, которая будет считывать звук с микрофона. Как-то очень сложно получилось, это тупо абсолютный минимум, чтобы просто печатать сэмплы в stdout:

#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
#include <stdio.h>

struct stream_data {
  struct pw_stream *stream;

  enum spa_audio_format audio_format;
};

static void main_loop_signal(void *data, int signal_number);

static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param);

static void stream_param_format_changed(void *data, const struct spa_pod *param);

static void stream_process(void *data);

int main(int argc, char *argv[]) {
  pw_init(&argc, &argv);

  struct pw_main_loop *main_loop = pw_main_loop_new(NULL);

  struct pw_loop *loop = pw_main_loop_get_loop(main_loop);

  pw_loop_add_signal(loop, SIGINT, main_loop_signal, main_loop);
  pw_loop_add_signal(loop, SIGTERM, main_loop_signal, main_loop);

  struct pw_properties *stream_properties = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY,
                                                              "Capture", PW_KEY_MEDIA_ROLE, "Accessibility", NULL);

  struct pw_stream_events stream_events = {
      .version = PW_VERSION_STREAM_EVENTS,
      .param_changed = stream_param_changed,
      .process = stream_process,
  };

  struct stream_data stream_data;

  struct pw_stream *stream = pw_stream_new_simple(loop, "tuktuk", stream_properties, &stream_events, &stream_data);
  stream_data.stream = stream;

  const struct spa_pod *stream_connect_params[1];

  uint8_t spa_pod_builder_buffer[1024];
  struct spa_pod_builder spa_pod_builder = SPA_POD_BUILDER_INIT(spa_pod_builder_buffer, sizeof(spa_pod_builder_buffer));
  struct spa_audio_info_raw spa_audio_info_raw = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN);
  stream_connect_params[0] = spa_format_audio_raw_build(&spa_pod_builder, SPA_PARAM_EnumFormat, &spa_audio_info_raw);

  pw_stream_connect(stream, PW_DIRECTION_INPUT, PW_ID_ANY, PW_STREAM_FLAG_AUTOCONNECT, stream_connect_params,
                    sizeof(stream_connect_params) / sizeof(stream_connect_params[0]));

  pw_main_loop_run(main_loop);

  pw_main_loop_destroy(main_loop);

  pw_deinit();
}

static void main_loop_signal(void *data, int signal_number) {
  struct pw_main_loop *main_loop = data;
  pw_main_loop_quit(main_loop);
}

static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param) {
  switch (id) {
    case SPA_PARAM_Format:
      stream_param_format_changed(data, param);
      break;
  }
}

static void stream_param_format_changed(void *data, const struct spa_pod *param) {
  uint32_t media_type;
  uint32_t media_subtype;
  if (spa_format_parse(param, &media_type, &media_subtype) < 0) {
    fprintf(stderr, "Failed to parse format");
    return;
  }

  if (media_type != SPA_MEDIA_TYPE_audio || media_subtype != SPA_MEDIA_SUBTYPE_raw) {
    fprintf(stderr, "stream_param_format_changed media_type=%u media_subtype=%u\n", media_type, media_subtype);
    return;
  }

  struct spa_audio_info_raw audio_info_raw;
  spa_format_audio_raw_parse(param, &audio_info_raw);

  enum spa_audio_format audio_format = audio_info_raw.format;

  if (audio_format != SPA_AUDIO_FORMAT_DSP_F32) {
    fprintf(stderr, "stream_param_format_changed audio_format=%d\n", audio_format);
  }
}

static void stream_process(void *data) {
  struct stream_data *stream_data = data;
  struct pw_stream *stream = stream_data->stream;

  struct pw_buffer *pw_buffer = pw_stream_dequeue_buffer(stream);

  if (pw_buffer == NULL) {
    pw_log_warn("out of buffers: %m");
    return;
  }

  struct spa_buffer *spa_buffer = pw_buffer->buffer;

  struct spa_data *spa_data = &spa_buffer->datas[0];

  struct spa_chunk *spa_chunk = spa_data->chunk;

  float *samples = spa_data->data;
  uint32_t sample_count = spa_chunk->size / sizeof(float);

  for (uint32_t i = 0; i < sample_count; i++) {
    printf("%f\n", samples[i]);
  }

  pw_stream_queue_buffer(stream, pw_buffer);
}

Почему всё так сложно? Должно же быть проще. Зачем аудио-библиотеке выдумывать какие-то main loop-ы, это же не её дело. Почему такие хитрые API. Почему конфигурация через строки. Сложно, очень сложно, надо проще. И все хвалят этот pipewire. А я вот не хвалю, мне не понравилось.

Ещё добавлю, что документации там вообще около нуля. Вот наглядный пример: pw_main_loop_new

Что делает функция pw_main_loop_new? «Create a new main loop.». Да что вы говорите. Что она возвращает? «a newly allocated Main Loop». Да что вы говорите. А бывают ли ошибки? Ой, не написано. А что передаётся в параметре props? Ой, не написано. И это ещё хорошо документированная функция, в куче других и того нет.

★★★★★

Мы же уже выяснили, почему.

Советую перейти на кухню, может там будет проще.

XMPP
()

Если не очень нужно обращаться к звуку на достаточно низком уровне, мб имеет смысл в твоём юзкейсе использовать медиабиблиотеку типа SDL?

PW шляпа, но все аудиоапи шляпные своим особым образом.

Bfgeshka ★★★★★
()

Банальнейший event-loop, чего необычного-то?

Зачем аудио-библиотеке выдумывать какие-то main loop-ы, это же не её дело

Ну, бери core и делай свой MainLoop, проблема-то в чём?

Почему конфигурация через строки

Потому что это самое простое, что можно придумать.

А бывают ли ошибки? Ой, не написано

В таких API в 99,9% в случае ошибки возвращается NULL. И это, как-бы, чертовски логично.

А что передаётся в параметре props?

А ссылка на struct spa_dict * там для кого?

И все хвалят этот pipewire.

Потому что работает. И хорошо работает.

А я вот не хвалю, мне не понравилось.

Пацаны, расходимся, выкидываем. Юзеру с ЛОР-а не понравилось :)

SkyMaverick ★★★★★
()
Последнее исправление: SkyMaverick (всего исправлений: 1)
Ответ на: комментарий от SkyMaverick

Банальнейший event-loop, чего необычного-то?

Необычно то, что в аудио-библиотеке нет никакого места никаким event loop-ам. Это не графический тулкит. Это не её дело. Т.е. авторы вместо борьбы с шипением пишут евент лупы. Странно.

Почему конфигурация через строки

Потому что это самое простое, что можно придумать.

Нет, самое простое, что можно придумать, это конфигурация через структуры.

В таких API в 99,9% в случае ошибки возвращается NULL. И это, как-бы, чертовски логично.

А вот в examples ничего не проверяется на NULL. При этом если залезть таки в код pipewire, то NULL может возвращаться. В адекватной (я даже не пишу «хорошей») документации пишут, в каких случаях может произойти ошибка, и как эту ошибку распознать (кроме просто проверки на NULL).

А что передаётся в параметре props?

А ссылка на struct spa_dict * там для кого?

А ты понимаешь, что такое dict? Я объясню. Это словарь. Который отображает что-то на что-то. Что на что? Не написали.

Но если полазить по исходникам, можно найти свойства вроде "library.name.system" "library.name.loop" и др, которые таки используются. Но, видимо, кроме разработчиков pipewire про это никто не знает.

Потому что работает. И хорошо работает.

Всё хорошо работает. Я линуксом пользуюсь года с 2000-го, тогда вроде ALSA была, тоже хорошо работала.

vbr ★★★★★
() автор топика
Ответ на: комментарий от vbr

Вот попросил ChatGPT написать программу для ALSA, сам не проверял, но скорей всего плюс-минус правильно, просто для сравнения:

/* capture.c – minimal ALSA recorder that writes raw PCM to stdout */
#include <alsa/asoundlib.h>
#include <unistd.h>     /* for write() */

int main(void)
{
    const char *dev = "default";           /* capture device name */
    snd_pcm_t *pcm   = NULL;

    /* 1. Open PCM device for recording */
    if (snd_pcm_open(&pcm, dev, SND_PCM_STREAM_CAPTURE, 0) < 0) {
        perror("snd_pcm_open");
        return 1;
    }

    /* 2. Set hardware parameters
       Format   : 16‑bit little‑endian
       Access   : interleaved (RW)
       Channels : 1 (mono)
       Rate     : 44100 Hz
       Latency  : 0.5 s (500 000 µs) – safe default for most cards          */
    if (snd_pcm_set_params(pcm,
                           SND_PCM_FORMAT_S16_LE,
                           SND_PCM_ACCESS_RW_INTERLEAVED,
                           1,               /* channels */
                           44100,           /* sample rate */
                           1,               /* allow ALSA resample */
                           500000) < 0) {   /* latency (µs) */
        perror("snd_pcm_set_params");
        snd_pcm_close(pcm);
        return 1;
    }

    /* 3. Capture loop */
    const unsigned int FRAMES = 1024;       /* ALSA “frames”, not bytes */
    int16_t buffer[FRAMES];                 /* 2 bytes × 1 channel × FRAMES */

    for (;;) {
        int got = snd_pcm_readi(pcm, buffer, FRAMES);
        if (got == -EPIPE) {                /* overrun — recover */
            snd_pcm_prepare(pcm);
            continue;
        }
        if (got < 0) {                      /* fatal error */
            perror("snd_pcm_readi");
            break;
        }
        /* write() wants bytes, not frames */
        ssize_t len = got * sizeof(buffer[0]);
        if (write(STDOUT_FILENO, buffer, len) < 0) {
            perror("write");
            break;
        }
    }

    snd_pcm_close(pcm);
    return 0;
}

Вот так выглядит адекватная библиотека. Тут вообще вопросов нет, всё сделано как положено.

vbr ★★★★★
() автор топика
Ответ на: комментарий от wandrien

Ну мне лень всё это сейчас ставить, компилировать и вообще я не уверен, что ALSA сейчас будет работать с этими пайпварями. Простые программки ChatGPT пишет хорошо, тут у меня сомнений особых нет, что как-то так оно и будет выглядеть.

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

vbr ★★★★★
() автор топика

У пайпвари даже клиентская документация - полное говно. А уж как они умудрились достичь того, что в мобильном хромом на сайте с доками в 2025 году не работает прокрутка - вообще загадка из загадок.

thesis ★★★★★
()
Последнее исправление: thesis (всего исправлений: 1)

Это практически то же самое, о чем я иногда пишу в комментариях - своеобразный вендорлок по-красношапски: мы сделаем полусырого монстра с всратой документацией и кучей вызовов и насадим его административным путем сначала в наши дистры, потом в зависимые; а затем в определенный момент вы будете либо пользоваться нашими продуктами, либо говном мамонта.

windows10 ★★★★★
()
Ответ на: комментарий от vbr

Необычно то, что в аудио-библиотеке нет никакого места никаким event loop-ам

libuv(ev) смотрит с недоумением. Но, вообще, pw позиционируется как фреймворк, а не как «просто библиотека».

Т.е. авторы вместо борьбы с шипением пишут евент лупы.

Это упрощённый API, чтобы пользователь, который пишет очередной плеер не сношал себе мозг внутренностями core, а использовал привычный интерфейс event-loop, который он видел уже в хренаста других API.

Нет, самое простое, что можно придумать, это конфигурация через структуры.

И тут тебе нужно что-то изменить в конфигурации. Ломаем API? Переписываем все «плееры»? Мы же это так любим делать.

А ты понимаешь, что такое dict?

key-value хранилище свойств. Тут, соглашусь, можно-бы было расписать и поподробней, а не курить example-ы.

тогда вроде ALSA была, тоже хорошо работала

Так она и сейчас работает (как-бы, базовая подсистема). С тех пор требования к сущности «персональный компьютер общего назначения» несколько подросли.

SkyMaverick ★★★★★
()

А бывают ли ошибки? Ой, не написано. А что передаётся в параметре props? Ой, не написано. И это ещё хорошо документированная функция, в куче других и того нет.

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

seiken ★★★★★
()
Ответ на: комментарий от seiken

Это не просто продукт - это базовая функциональность современного десктопного линукса. Фактически часть системы.

А ведь исторически Unix/Linux как раз были хорошо документированы. man, info /usr/doc и др.

praseodim ★★★★★
()
Ответ на: комментарий от thesis

Надо им написать, чтобы приняли таблетки, забыли наверное.

James_Holden ★★★★★
()

По сабжу - да какая разница какой там API, это все берется из готового файла примера и все.

Любят его не за это, а за то, что он даёт возможности, которых даже рядом, нет вообще нигде.

James_Holden ★★★★★
()
Ответ на: комментарий от vbr

А чем for(;;) от ивентлупа отличается?! Наоборот потом легче с ним будет, чем вручную буферы с альсой кидать. Да и вообще, ООП в небольших дозах облегчает написание софта

Shadow ★★★★★
()
Последнее исправление: Shadow (всего исправлений: 1)

Сложно, очень сложно, надо проще.

Да многие либы на сишечке сложные, за редким исключением, сишники даже такое не считают сложным. Это ты просто привык к нормальному подходу жс, где хелловорлд делается одним импортом и тремя строчками кода, вместо простыни бойлерплейта.

goingUp ★★★★★
()

Вроде не сложнее чем jack или alsa. Звук это всегда сложно.
Да и никто не запрещает работать с pw посредством api jack, pulse или alsa
Если ты не хочешь, чтобы loop блокировал твоё приложение - используй iterate
http://maemo.org/api_refs/5.0/5.0-final/pulseaudio/mainloop.html
В целом pa похож на OpenSLES, но у него апишка намного проще.
Касательно же примера выше от ии-слопа - это синхронный API, асинхронный вариант будет примерно таким же по сложности

mittorn ★★★★★
()
Последнее исправление: mittorn (всего исправлений: 2)

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

можно прямо в ком.строке написать конвейер gst-launch, всё проверить и глядя на него повторить на C. Вкурить документацию Qt и написать на С++ тоже вариант. Или брать игродельские библиотеки

потому что если нужна «простейшая программа» то берут верхние прикладные high-level интерфейсы. Непосредственно в алзу и pipewire лезут если действительно что-то уникально-особенное или ресурсы крайне ограниченны (например выжимать из крохотного старого-старого RPi)

MKuznetsov ★★★★★
()

В своё время когда мне такое надо было я нагуглил portaudio там всё очень просто, понятно и по человечески было. Проект до сих пор вроде как больше жив https://files.portaudio.com/download.html .

vtVitus ★★★★★
()
Последнее исправление: vtVitus (всего исправлений: 1)

Никто не говорил, что будет легко, ибо это непортабельная, низкоуровневая либа.

ac130kz ★★
()
Ответ на: комментарий от praseodim

современного десктопного линукс

ну, во-первых, десктопный Линукс - это экзотика, а во-вторых среди этой экзотики ещё меньшая доля программирует звук. Я вот вообще даже не знаю, какая на моем ноуте звуковая система. «Что-то, что идёт с убунтой вместе».

А ведь исторически Unix/Linux как раз были хорошо документированы

Маны для API мумедии были хорошо составлены «исторически»? Это в каком ЮНИКСе?

seiken ★★★★★
()
Ответ на: комментарий от James_Holden

Причем, горизонтальная работает, а вертикальная - хрен моржовый!

Это какой-то из высеров макак, более чем на одном сайте такое встречал.

anc ★★★★★
()
Ответ на: комментарий от vbr

я не уверен, что ALSA сейчас будет работать с этими пайпварями.

ALSA будет работать на любом линуксе, наличие или отсутствие пульсо-прокладок на это не влияет. Прокладки будут просто сбоку ничего не делать (если они есть).

firkax ★★★★★
()

Почему всё так сложно? Должно же быть проще.

Потому что проще - это ALSA.

Ещё добавлю, что документации там вообще около нуля.

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

firkax ★★★★★
()
Ответ на: комментарий от SkyMaverick

Нашел первую попавшуюся страницу с этой же строкой - нормально скроллится. Вероятно, пайпваревцы дополнительно влезли косыми лапками.

thesis ★★★★★
()
Ответ на: комментарий от goingUp

Нормальные библиотеки не сложные в использовании. По крайней мере той простыни что привёл автор я не помню чтобы где-то было нужно.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Нормальные библиотеки

…в меньшинстве. Недавно нужна была библиотека для протокола ccnet для купюроприемников (там просто байты посылать через ком порт), нашел монстрообразную библиотеку на Си, при том что сам протокол - пару листочков А4. Не захотел разбираться с либой, сделал все сам намного быстрее.

goingUp ★★★★★
()

Почему pipewire такой сложный?

потому что его делали на смену невменяемому pulseaudio. В нынешнем clown world каждое новшество непременно обязано быть сложнее во всём чем то, что оно заменяет. Сложнее использовать, сложнее настраивать, сложнее разобраться и т.п.

Сложность - это такой вендорлок для опенсурса всякие подонки изобрели. На браузеры посмотри. При достаточном уровне сложности становится невозможно использовать опенсурс как опенсурс, что подонкам и требуется.

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

Stanson ★★★★★
()

Причём, что противно, зачастую даже до реализации добраться проблема.

Вот простой пример. Очередная недокументированная функция pw_loop_add_signal. Хочу посмотреть её реализацию, чтобы хоть что-то понять. Ниже цепочка кода, в который она разворачивается:

  pw_loop_add_signal(loop, SIGINT, main_loop_signal, main_loop);
PW_API_LOOP_IMPL struct spa_source *
pw_loop_add_signal(struct pw_loop *object, int signal_number,
                spa_source_signal_func_t func, void *data)
{
	return spa_loop_utils_add_signal(object->utils, signal_number, func, data);
}
SPA_API_LOOP struct spa_source *
spa_loop_utils_add_signal(struct spa_loop_utils *object, int signal_number,
		spa_source_signal_func_t func, void *data)
{
	return spa_api_method_r(struct spa_source *, NULL,
			spa_loop_utils, &object->iface, add_signal, 0,
			signal_number, func, data);
}
#define spa_api_method_r(rtype,def,type,o,method,version,...)		\
({									\
	rtype _res = def;						\
	struct spa_interface *_i = o;			\
	spa_interface_call_res(_i, struct type ##_methods,		\
			_res, method, version, ##__VA_ARGS__);		\
	_res;								\
})
#define spa_interface_call_res(iface,method_type,res,method,vers,...)			\
	spa_callbacks_call_res(&(iface)->cb,method_type,res,method,vers,##__VA_ARGS__)
#define spa_callbacks_call_res(callbacks,type,res,method,vers,...)		\
({										\
	const type *_f = (const type *) (callbacks)->funcs;			\
	if (SPA_LIKELY(SPA_CALLBACK_CHECK(_f,method,vers)))			\
		res = (_f->method)((callbacks)->data, ## __VA_ARGS__);		\
	res;									\
})

В общем пришли к доморощенному ООП и вызову функции по указателю. Какой функции? А кто его знает.

Последние несколько макросов в итоге разворачиваются в

({ struct spa_source * _res = ((void *)0); struct spa_interface *_i = &object->iface; ({ const struct spa_loop_utils_methods *_f = (const struct spa_loop_utils_methods *) (&(_i)->cb)->funcs; if ((__builtin_expect(!!((((_f) && ((0) == 0 || (_f)->version > (0)-1)) && (_f)->add_signal)),1))) _res = (_f->add_signal)((&(_i)->cb)->data,signal_number, func, data); _res; }); _res; })

В общем на этом мне наскучило это занятие, дальше копать не стал, этот указатель инициализируется бог знает где.

А ведь можно было просто взять и написать реализацию этой функции без всех этих усложнений. Это мне напоминало enterprise hello world.

vbr ★★★★★
() автор топика
Последнее исправление: vbr (всего исправлений: 1)

Я бы сказал не сложно, а страшно. Очень страшно. Код с примером для альсы от ии хорошая предпосылка забыть pipewire вообще.

nionio35
()
Ответ на: комментарий от nionio35

Печалька, как сложно вам жить, каждые 5 минут 24/7 приходится писать с нуля код инициализации звуковой системы. Примите соболезнования

James_Holden ★★★★★
()
Ответ на: комментарий от nionio35

Также крайне смешно, как неосиляторы программирования пытаются писать на сишке, и выпадают в осадок.

James_Holden ★★★★★
()
Ответ на: комментарий от vbr

Это что же, ты хочешь как у дидов всё было просто и понятно? Да тебя сейчас в луддиты пропишут 😀. Сейчас век прослоек над прослойками, что, собственно, сея монстра и реализует, так что всё модно и молодёжно. И неча тут, а так дойдём зачем нам этот вяленный, груб и прочие монстры заменившие простые подходы на современные.

vtVitus ★★★★★
()
Ответ на: комментарий от SkyMaverick

Нет, самое простое, что можно придумать, это конфигурация через структуры.

И тут тебе нужно что-то изменить в конфигурации. Ломаем API? Переписываем все «плееры»? Мы же это так любим делать.

Делаем новую структуру или добавляем поля в старую. Если неймётся, то можно и «плееры переписать» для поддержки новых возможностей, причем это не зависит от способа передачи параметров.

u-235
()
Ответ на: комментарий от vbr

В общем на этом мне наскучило это занятие, дальше копать не стал, этот указатель инициализируется бог знает где.

Классика ООП, абстрактные классы и все такое. Правой кнопкой мыши в IDE - «найти запись в переменную», ты же на Java писал, знаешь наверное. Такое без IDE не редактируется и не читается.

Посмотрел на набор абстракций, в Wayland похожий используется, видимо новый стиль в RH.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 5)
Ответ на: комментарий от James_Holden

Хотите вкорячиваться раком писать такие инициализации - вообще нет проблем. Я бы выбрал конечно альсу. Но иногда у людей бывают специфичные потребности.

nionio35
()
Ответ на: комментарий от nionio35

Бывают потребности работать со звуком, а не просто озвучить щелчок по кнопке. Тогда придётся использовать то, что позволяет это делать.

James_Holden ★★★★★
()
Ответ на: комментарий от nionio35

Я бы сказал не сложно, а страшно. Очень страшно.

Если бы мы знали, что это такое?! Но мы не знаем, что это такое…

LamerOk ★★★★★
()

У них другие цели. Клиентский софт обычно всё равно работает с аудио не напрямую, а через high-level медиа прослойку вроде SDL, miniaudio и т.п. Т.е. их клиент – не разработчик приложений, а разботчик фреймворка, которому нужна не простата, а хорошая кастомизируемость и поменьше накладных расходов. А поскольку фреймворков мало – они могут не писать публичные доки, а договориться напрямую.

snizovtsev ★★★★★
()
Ответ на: комментарий от snizovtsev

которому нужна не простата

Ну, простАта, ему, наверное, пригодилась-бы, всё-таки :)

SkyMaverick ★★★★★
()
Ответ на: комментарий от u-235

Делаем новую структуру или добавляем поля в старую

И размер структуры первым полем :3

utf8nowhere ★★★★
()
Ответ на: комментарий от praseodim

десктопного линукса

Я слышал его не существует

utf8nowhere ★★★★
()

«До этого все было плохое, мы сделаем лучше!»

Просто используй OSS. С ним очень просто работать и все варианты работы со звуком совместимы с OSS.

zx_gamer ★★★
()
Закрыто добавление комментариев для недавно зарегистрированных пользователей (со score < 50)