Coin web de Frédéric Péters

Tranches musicales automatiques moins aléatoires

Dans les travaux qui s’annonçaient après la mise à l’antenne de Stamina il y avait à s’intéresser aux morceaux diffusés lors des tranches automatiques : le dispositif avait à peine évolué, c’était encore juste piocher au hasard dans les morceaux assignés à l’horaire en cours, le seul mini-changement étant qu’il y avait en plus une requête pour éviter au même morceau d’être rejoué trop rapidement.

Derrière ce travail sur la sélection musicale il y a différentes préoccupations, les principales étant qu’on voudrait donner plus de chances aux nouveaux morceaux d’être diffusés et qu’il y a des obligations légales de pourcentages de titres francophones, ou d’artistes locaux.

De là j’ai un peu regardé ce qui se faisait ailleurs, il y a les "Scheduler rules" dans Rivendell, qui permettent des choses comme :

(...) I don’t want more than 5 songs for 00’s to play in a row. Yet I don’t want a 70’s track to play after an 80’s or 60’s track. And to be sure, I wan’t there to be at least a 7 min before it thinks about possibly playing another one.  (tiré d’un vieux post "Rivendell - how to schedule music")

Et sur cette base on pourrait imaginer dire qu’on veut qu’un morceau sur trois soit en français, et pareil définir qu’on veut qu’un morceau sur cinq soit "récent", etc. mais ça me semble pouvoir devenir compliqué quand on veut gérer les combinaisons, et peut-être devenir quelque chose de trop programmé ou systématique.

Pour Airtime/Libretime il y a un système de "Smart blocks", expliqué avec cet exemple :

Click the plus button on the left to add OR criteria, such as Creator containing beck OR jimi. To add AND criteria, such as Creator containing jimi AND BPM in the range 120 to 130, click the plus button on the right. (extrait de la documentation de Libretime)

Mais ça me semble créer quelque chose de trop restrictif et pas adapté à ce qu’on veut faire.

Il y a les "Dynamic Segments" dans Openbroadcaster mais leur documentation n’explique pas vraiment donc pas d’idée à tirer ici.

À regarder au-delà des systèmes de diffusion, il y a le lecteur audio Amarok avec des "Dynamic playlists" et plus particulièrement une partie "partition" :

This group bias matches tracks from the sub-biases in proportion. The edit window for this bias has sliders for each sub-bias to adjust the proportions. For example, with two sub-biases with their proportion sliders set equal, half of the playlist will match one bias and half the other. (extrait du manuel d’Amarok)

Et c’est plutôt vers ça qu’on veut aller, i.e. pousser des morceaux à être diffusés au-delà (ou en-deça) de leurs chances "neutres"; et de là rapidement une maquette ASCII de l’idée :

 Langue: fr    |<-----------x---->|
 Langue: en    |<-------x-------->|
 Langue: nl    |<-------x-------->|
 ...           |<---------------->|
 CFWB          |<-------------x-->|
 Instru        |<----x----------->|
 Récente       |<------------x--->|

Et puis des calculs (et honnêtement aussi pas mal d’empirisme) pour déterminer la manière dont les biais vont s’appliquer (de manière linéaire selon la position sur l’axe, ou exponentielle, ou façon courbe de Bézier comme utilisé dans les transitions CSS, façon ease-in-out, ou autre) pour finalement aller au presque plus simple et plutôt étendre l’interface pour indiquer directement ce qu’une position représenterait en terme de proportion de morceaux diffusés, ce qui donne :

(avec la proportion particulière pour les pistes en français, où le quota légal se base évidemment sur les pistes parlées, i.e. ce qui compte c’est le pourcentage hors morceaux instrumentaux).

Côté code j’ai un peu approché l’idée de gérer ça au niveau de la base de données (dans mes recherches, cet article, Weighted Random Sampling with PostgreSQL) mais on n’a pas tant de données et depuis Python 3.6 il y a une gestion d’un aléatoire pondéré directement dans random.choices de la bibliothèque standard donc autant utiliser ça. Le cœur de l’affaire donne finalement ceci :

def compute_weight(track):
    weight = 0   
    for weight_key, weight_value in weights.items():
        if track.match_criteria(weight_key):
            weight += weight_value 
    if weight < 0:
        weight = 1 + (weight / 20)
    else:
        weight = 1 + (weight / 2) 
    return weight

track_weights = [compute_weight(x) for x in tracks] 
tracks = random.choices(tracks, weights=track_weights, k=k)

Et testé/déployé, c’est utilisé depuis le début du mois de manière expérimentale, et ça a déjà donné l’occasion de pas mal de discussions, à continuer à la rentrée, où on aura peut-être d’autres idées de biais à vouloir appliquer…

23 août 2020, 15:11