Add support for replay gain

This commit is contained in:
Mikko Ahlroth 2024-02-19 23:00:02 +02:00
parent 4e6cc39a27
commit 3144af69e4
6 changed files with 98 additions and 2 deletions

View file

@ -25,7 +25,7 @@ pub type Track {
plays: Int,
file: String,
type_: String,
replay_gain: String,
replay_gain: Float,
uploaded_time: String,
)
}

View file

@ -1,5 +1,7 @@
import gleam/string
import gleam/option
import gleam/float
import gleam/result
import ibroadcast/library/library.{type Track as APITrack}
import elekf/library/track.{Track}
@ -30,7 +32,7 @@ pub fn from(track: APITrack) {
plays: track.plays,
file: track.file,
type_: track.type_,
replay_gain: track.replay_gain,
replay_gain: result.unwrap(float.parse(track.replay_gain), 1.0),
uploaded_time: track.uploaded_time,
)
}

View file

@ -15,12 +15,14 @@ import plinth/browser/media/session
import plinth/browser/media/action
import plinth/browser/media/position
import elektrofoni
import elekf/utils/lustre
import elekf/web/common
import elekf/web/components/icon.{Alt, icon}
import elekf/web/components/track_length.{track_length}
import elekf/web/components/button_group
import elekf/web/components/button
import elekf/web/components/thumbnail
import elekf/web/volume
import elekf/library.{type Library}
import elekf/library/track.{type Track}
import elekf/library/artist.{type Artist}
@ -32,6 +34,12 @@ import ibroadcast/artwork
const default_seek_offset = 10.0
pub type AudioContext
pub type MediaElementAudioSourceNode
pub type GainNode
pub type PlayState {
Playing
Paused
@ -51,6 +59,9 @@ pub type Model {
loading_stream: Bool,
request_config: RequestConfig,
user_is_skipping: Bool,
audio_context: AudioContext,
audio_source: option.Option(MediaElementAudioSourceNode),
gain_node: GainNode,
)
}
@ -70,6 +81,7 @@ pub type Msg {
PositionChanged(Int)
SeekBackward(Float)
SeekForward(Float)
CreateMediaElementAudioSourceNode
}
pub fn init(
@ -100,10 +112,15 @@ pub fn init(
let seek_to = option.unwrap(data.seek_time, 0.0)
dispatch(PositionSelected(float.round(seek_to)))
})
lustre.after_next_render(fn() {
dispatch(CreateMediaElementAudioSourceNode)
})
})
start_play(settings, artist, album, track)
let audio_context = create_audio_context()
#(
Model(
settings,
@ -118,6 +135,9 @@ pub fn init(
False,
request_config,
False,
audio_context,
option.None,
create_gain(audio_context),
),
action_effect,
)
@ -130,6 +150,12 @@ pub fn update(model: Model, msg) {
let album = library.assert_album(model.library, track.album_id)
start_play(model.settings, artist, album, track)
linear_ramp_to_value(
model.gain_node,
volume.calculate(1.0, track.replay_gain),
0.5,
)
let url = form_url(track_id, track, model.settings, model.request_config)
#(
Model(
@ -207,6 +233,12 @@ pub fn update(model: Model, msg) {
let new_pos = float.round(current_time() +. amount)
update(model, PositionSelected(new_pos))
}
CreateMediaElementAudioSourceNode -> {
let source = create_media_element_source(model.audio_context)
connect_gain(model.gain_node, model.audio_context, source)
#(Model(..model, audio_source: option.Some(source)), effect.none())
}
}
}
@ -289,6 +321,7 @@ pub fn view(model: Model) {
attribute.src(src),
attribute.controls(False),
attribute.autoplay(True),
attribute.attribute("crossorigin", "anonymous"),
event.on("play", event_play),
event.on("pause", event_pause),
event.on("ended", event_ended),
@ -396,3 +429,22 @@ fn current_track_position() -> Int
@external(javascript, "../../../player_ffi.mjs", "setTrackTo")
fn set_track_to(pos: Int) -> Nil
@external(javascript, "../../../player_ffi.mjs", "createAudioContext")
fn create_audio_context() -> AudioContext
@external(javascript, "../../../player_ffi.mjs", "createMediaElementSource")
fn create_media_element_source(ctx: AudioContext) -> MediaElementAudioSourceNode
@external(javascript, "../../../player_ffi.mjs", "createGain")
fn create_gain(ctx: AudioContext) -> GainNode
@external(javascript, "../../../player_ffi.mjs", "connectGain")
fn connect_gain(
node: GainNode,
ctx: AudioContext,
source: MediaElementAudioSourceNode,
) -> Nil
@external(javascript, "../../../player_ffi.mjs", "linearRampToValue")
fn linear_ramp_to_value(node: GainNode, gain: Float, at: Float) -> Nil

View file

@ -0,0 +1,10 @@
import gleam/float
import ibroadcast/replay_gain
/// Calculate the gain to use for the audio pipeline's GainNode.
///
/// Returns a value from 0.0 to 1.0.
pub fn calculate(main_volume: Float, gain: Float) -> Float {
let vol = main_volume *. replay_gain.factor(gain)
float.min(vol, 1.0)
}

View file

@ -0,0 +1,10 @@
import gleam/float
/// Calculate the replay gain factor based on the value stored for a track.
///
/// The resulting factor ranges from 0 to a positive number. It should be scaled
/// down using the master volume and then clamped to a range from 0 to 1.
pub fn factor(replay_gain: Float) -> Float {
let assert Ok(factor) = float.power(10.0, replay_gain /. 20.0)
factor
}

View file

@ -46,6 +46,28 @@ export function setTrackTo(pos) {
track.value = String(pos);
}
export function createAudioContext() {
return new globalThis.AudioContext();
}
export function createMediaElementSource(ctx) {
getPlayer();
return ctx.createMediaElementSource(player);
}
export function createGain(ctx) {
return ctx.createGain();
}
export function connectGain(node, ctx, source) {
source.connect(node);
node.connect(ctx.destination);
}
export function linearRampToValue(node, gain, at) {
node.gain.linearRampToValueAtTime(gain, at);
}
function getPlayer() {
if (player === undefined) {
player = document.getElementById(player_id);