Это продолжение предыдущей статьи об умном радио, не умирающем при потере Интернета. Похоже, что первый блин был скорее комом: большинству пользователей приложение не понравилось. Критика в основном разделилась на два фронта:
- Одни и те же треки очень часто повторяются, а новые появляются очень редко.
- Нету возможности ни выбрать любимые жанры, ни минусовать негодные треки, чтобы не приходилось их мучительно пропускать.
Вторая проблема сильно усугублялась первой, поскольку пропуски очень часто приводили к повторам всё тех же треков, пусть и в другой последовательности.
Рад сообщить, что мне удалось решить первую проблему (обновление уже в Play Store). Под катом будет описание выбранного алгоритма выбора и ротации треков, а также сути исправления, которое, как я ожидаю, должно кардинально улучшить пользовательский опыт.
Базовая идея выбора треков появилась практически сразу: вероятность выбора композиции для воспроизведения пропорциональна её "оценке предпочтения". Последняя рассчитывается как отношение среднего времени прослушивания трека к его продолжительности. Такой способ выбора приводит к частому воспроизведению тех композиций, которые более предпочтительны для слушателя. К сожалению, эта оценка не всегда будет соответствовать реальному предпочтению, но должна хорошо с ним коррелировать.
По первоначальной задумке ротация треков должна происходить в момент, когда все треки прослушаны не менее двух раз (одного раза казалось недостаточным для достоверной оценки трека). В момент ротации удалялись треки, чья оценка предпочтений была ниже определённого уровня, а также загружалось новая порция треков.
Этот алгоритм на практике приводил к очень редкой ротации. В самом деле треки, которые были быстро пропущены, надолго теряли возможность быть прослушанными ещё раз. Поэтому мне пришлось искусственно завышать оценку предпочтения для треков, прослушанных менее двух раз, чтобы поднять вероятность их воспроизведения.
Кроме того, чтобы исключить повторение только что воспроизведённых треков, было принято решение искусственно занижать оценку предпочтения для десяти последних проигранных треков.
На практике это довольно неплохо работало, но лишь при условии более-менее сформированного профиля предпочтений, когда из первых двадцати загруженных треков подавляющее большинство не вызывают отторжения. Новый же пользователь получал набор совершенно случайных треков, которые был вынужден бесконечно пропускать. В первом случае пропуски были довольно редкими, и не вызывали раздражение. Во втором — слушатель просто не мог дождаться следующей итерации ротации.
Так было в первоначальной версии приложения, которая была опубликована на момент написания предыдущей статьи. Теперь о том, что было сделано, чтобы улучшить пользовательский опыт:
- Для следующей итерации ротации теперь достаточно, чтобы все треки были прослушаны только раз (раньше было два раза).
- При наличии непрослушанных композиций их оценка предпочтения резко увеличивается, для всех остальных — резко уменьшается (и то и другое — на порядок).
Эти изменения привели к тому, что при наличии Интернет-соединения треки теперь проигрываются только по одному разу (как и в обычном радио), но понравившиеся треки оседают в кэше, вытесняя остальные. Как только Интернет-соединение пропадает, включается старый механизм, воспроизводящий отобранные треки пропорционально их оценке предпочтения.
P.S. Был также исправлен баг, связанный с отсутствием распознавания появившегося WiFi.
P.P.S. Релиз 1.0.2 оказался поломанным. Включил минификацию и сжатие ресурсов, в результате приложение валится, на что мне указал nikita_dol. Приношу свои извинения, впредь буду заливать обновления только через бета-версии. Очень надеюсь, версия 1.0.3 будет рабочей.
Комментарии (16)
nikita_dol
18.08.2019 16:32Падает на 7.1.2
ababo Автор
18.08.2019 16:32А можно подробнее?
nikita_dol
18.08.2019 18:10logcat2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] No pending exception expected: java.lang.NoSuchMethodError: no non-static method "Lcom/silindo/nota/Client;.handlePlayerState(Lcom/silindo/nota/PlayerState;)V"
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at c.d.a.j com.silindo.nota.Client.initialize(java.lang.String, java.lang.String) (:-2)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void com.silindo.nota.Client.() (:8)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void com.silindo.nota.MainActivity.onResume() (:-1)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.Instrumentation.callActivityOnResume(android.app.Activity) (Instrumentation.java:1270)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.Activity.performResume() (Activity.java:6788)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at android.app.ActivityThread$ActivityClientRecord android.app.ActivityThread.performResumeActivity(android.os.IBinder, boolean, java.lang.String) (ActivityThread.java:3431)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.ActivityThread.handleResumeActivity(android.os.IBinder, boolean, boolean, boolean, int, java.lang.String) (ActivityThread.java:3494)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.ActivityThread.handleLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent, java.lang.String) (ActivityThread.java:2757)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.ActivityThread.-wrap12(android.app.ActivityThread, android.app.ActivityThread$ActivityClientRecord, android.content.Intent, java.lang.String) (ActivityThread.java:-1)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.ActivityThread$H.handleMessage(android.os.Message) (ActivityThread.java:1496)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.os.Handler.dispatchMessage(android.os.Message) (Handler.java:102)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.os.Looper.loop() (Looper.java:154)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6186)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at java.lang.Object java.lang.reflect.Method.invoke!(java.lang.Object, java.lang.Object[]) (Method.java:-2)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run() (ZygoteInit.java:889)
2019-08-18 18:05:19.264 18132-18132/com.silindo.nota A/art: art/runtime/thread.cc:1661] at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:779)
ababo Автор
18.08.2019 18:43Стабильно валится? После апдейта или чистая инсталляция?
nikita_dol
18.08.2019 18:48Стабильно валится?
При каждом запуске
После апдейта или чистая инсталляция?
Чистая
ababo Автор
18.08.2019 19:07Я в шоке, похоже последний апдейт поломанный из-за включенной обфускации и сжатия ресурсов.
ababo Автор
19.08.2019 07:56Попробуйте версию 1.0.3.
nikita_dol
19.08.2019 09:08Запускается.
Выключил загрузку только через Wi-Fi и не понимаю что происходит, ибо нет никаких индикаторов.
Если зайти в приложение, то появляется уведомление. Потом сам закрываешь приложение и уведомление сколько не убирай появляется обратно.ababo Автор
19.08.2019 09:12Я скоро это исправлю. Проведите (swipe out) по уведомлению в состоянии паузы.
ovsale
как вы определяете похожесть музыки?
например если я прослушал один трек (т.е. лайкнул) и скипнул другой (т.е. дизлайкнул) чтобы рекомендовать дальше вам нужны алгоритмы находящие треки похожие на первый и не похожие на второй. как вы это делаете?
ababo Автор
На сервере в базе данных каждому треку соответствует один или несколько жанров. Таким образом оценка предпочтения жанра вычисляется как среднее значение оценок предпочтения всех соответстаующих треков, прослушанных пользователем. Для каждого трека в процессе обновления вначале выбирается жанр, вероятность выбора которого пропорциональна оценке предпочтения. Далее внутри жанра выбирается случайный трек.
ovsale
а жанр получаете из Free Music Archive?
сколько всего жанров?
вы сами пользуетесь своей программой?
ababo Автор
Сейчас 160.
Ежедневно.
ovsale
с какими аналогами сравнивали? ваш лучше рекомендует?
много нужно прослушать чтобы настроить?
ababo Автор
Я аналогов не нашёл (впрочем, это не значит, что их нет). Сложно судить насчёт «времени обучения». Я его слушаю ежедневно, но тем не менее иногда встречаю треки, которые мне резко не нравятся. Так что я бы не стал ожидать чудес. С другой стороны, после некоторого времени использования воспроизводимая музыка чаще всего относится к любимым жанрам.
ovsale
первым был lastfm наверно, а сейчас их уже десятки.
я написал подобную программу несколько лет назад. за основу взял плейлисты вконтакте. в моем случае систему не нужно настраивать при наличии существующего плейлиста. т.е. она сразу играет «похожую» музыку. ну и у меня изначально обрабатывается 160 тысяч плейлистов (10 гигабайт в текстовом формате). что конечно дает намного больше информации о «похожести» музыки чем 160 жанров. я жанры не использовал вообще.
ну и она работает намного лучше яндекс музыки например (который тоже использует плейлисты вконтакте в качестве слепка музыкального вкуса) но почему-то в основном этот плейлист потом и рекомендует)
до релиза не дописал(