Add support for replay gain
This commit is contained in:
parent
4e6cc39a27
commit
3144af69e4
6 changed files with 98 additions and 2 deletions
|
@ -25,7 +25,7 @@ pub type Track {
|
||||||
plays: Int,
|
plays: Int,
|
||||||
file: String,
|
file: String,
|
||||||
type_: String,
|
type_: String,
|
||||||
replay_gain: String,
|
replay_gain: Float,
|
||||||
uploaded_time: String,
|
uploaded_time: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import gleam/option
|
import gleam/option
|
||||||
|
import gleam/float
|
||||||
|
import gleam/result
|
||||||
import ibroadcast/library/library.{type Track as APITrack}
|
import ibroadcast/library/library.{type Track as APITrack}
|
||||||
import elekf/library/track.{Track}
|
import elekf/library/track.{Track}
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ pub fn from(track: APITrack) {
|
||||||
plays: track.plays,
|
plays: track.plays,
|
||||||
file: track.file,
|
file: track.file,
|
||||||
type_: track.type_,
|
type_: track.type_,
|
||||||
replay_gain: track.replay_gain,
|
replay_gain: result.unwrap(float.parse(track.replay_gain), 1.0),
|
||||||
uploaded_time: track.uploaded_time,
|
uploaded_time: track.uploaded_time,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,14 @@ import plinth/browser/media/session
|
||||||
import plinth/browser/media/action
|
import plinth/browser/media/action
|
||||||
import plinth/browser/media/position
|
import plinth/browser/media/position
|
||||||
import elektrofoni
|
import elektrofoni
|
||||||
|
import elekf/utils/lustre
|
||||||
import elekf/web/common
|
import elekf/web/common
|
||||||
import elekf/web/components/icon.{Alt, icon}
|
import elekf/web/components/icon.{Alt, icon}
|
||||||
import elekf/web/components/track_length.{track_length}
|
import elekf/web/components/track_length.{track_length}
|
||||||
import elekf/web/components/button_group
|
import elekf/web/components/button_group
|
||||||
import elekf/web/components/button
|
import elekf/web/components/button
|
||||||
import elekf/web/components/thumbnail
|
import elekf/web/components/thumbnail
|
||||||
|
import elekf/web/volume
|
||||||
import elekf/library.{type Library}
|
import elekf/library.{type Library}
|
||||||
import elekf/library/track.{type Track}
|
import elekf/library/track.{type Track}
|
||||||
import elekf/library/artist.{type Artist}
|
import elekf/library/artist.{type Artist}
|
||||||
|
@ -32,6 +34,12 @@ import ibroadcast/artwork
|
||||||
|
|
||||||
const default_seek_offset = 10.0
|
const default_seek_offset = 10.0
|
||||||
|
|
||||||
|
pub type AudioContext
|
||||||
|
|
||||||
|
pub type MediaElementAudioSourceNode
|
||||||
|
|
||||||
|
pub type GainNode
|
||||||
|
|
||||||
pub type PlayState {
|
pub type PlayState {
|
||||||
Playing
|
Playing
|
||||||
Paused
|
Paused
|
||||||
|
@ -51,6 +59,9 @@ pub type Model {
|
||||||
loading_stream: Bool,
|
loading_stream: Bool,
|
||||||
request_config: RequestConfig,
|
request_config: RequestConfig,
|
||||||
user_is_skipping: Bool,
|
user_is_skipping: Bool,
|
||||||
|
audio_context: AudioContext,
|
||||||
|
audio_source: option.Option(MediaElementAudioSourceNode),
|
||||||
|
gain_node: GainNode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +81,7 @@ pub type Msg {
|
||||||
PositionChanged(Int)
|
PositionChanged(Int)
|
||||||
SeekBackward(Float)
|
SeekBackward(Float)
|
||||||
SeekForward(Float)
|
SeekForward(Float)
|
||||||
|
CreateMediaElementAudioSourceNode
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
|
@ -100,10 +112,15 @@ pub fn init(
|
||||||
let seek_to = option.unwrap(data.seek_time, 0.0)
|
let seek_to = option.unwrap(data.seek_time, 0.0)
|
||||||
dispatch(PositionSelected(float.round(seek_to)))
|
dispatch(PositionSelected(float.round(seek_to)))
|
||||||
})
|
})
|
||||||
|
lustre.after_next_render(fn() {
|
||||||
|
dispatch(CreateMediaElementAudioSourceNode)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
start_play(settings, artist, album, track)
|
start_play(settings, artist, album, track)
|
||||||
|
|
||||||
|
let audio_context = create_audio_context()
|
||||||
|
|
||||||
#(
|
#(
|
||||||
Model(
|
Model(
|
||||||
settings,
|
settings,
|
||||||
|
@ -118,6 +135,9 @@ pub fn init(
|
||||||
False,
|
False,
|
||||||
request_config,
|
request_config,
|
||||||
False,
|
False,
|
||||||
|
audio_context,
|
||||||
|
option.None,
|
||||||
|
create_gain(audio_context),
|
||||||
),
|
),
|
||||||
action_effect,
|
action_effect,
|
||||||
)
|
)
|
||||||
|
@ -130,6 +150,12 @@ pub fn update(model: Model, msg) {
|
||||||
let album = library.assert_album(model.library, track.album_id)
|
let album = library.assert_album(model.library, track.album_id)
|
||||||
start_play(model.settings, artist, album, track)
|
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)
|
let url = form_url(track_id, track, model.settings, model.request_config)
|
||||||
#(
|
#(
|
||||||
Model(
|
Model(
|
||||||
|
@ -207,6 +233,12 @@ pub fn update(model: Model, msg) {
|
||||||
let new_pos = float.round(current_time() +. amount)
|
let new_pos = float.round(current_time() +. amount)
|
||||||
update(model, PositionSelected(new_pos))
|
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.src(src),
|
||||||
attribute.controls(False),
|
attribute.controls(False),
|
||||||
attribute.autoplay(True),
|
attribute.autoplay(True),
|
||||||
|
attribute.attribute("crossorigin", "anonymous"),
|
||||||
event.on("play", event_play),
|
event.on("play", event_play),
|
||||||
event.on("pause", event_pause),
|
event.on("pause", event_pause),
|
||||||
event.on("ended", event_ended),
|
event.on("ended", event_ended),
|
||||||
|
@ -396,3 +429,22 @@ fn current_track_position() -> Int
|
||||||
|
|
||||||
@external(javascript, "../../../player_ffi.mjs", "setTrackTo")
|
@external(javascript, "../../../player_ffi.mjs", "setTrackTo")
|
||||||
fn set_track_to(pos: Int) -> Nil
|
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
|
||||||
|
|
10
src/elekf/web/volume.gleam
Normal file
10
src/elekf/web/volume.gleam
Normal 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)
|
||||||
|
}
|
10
src/ibroadcast/replay_gain.gleam
Normal file
10
src/ibroadcast/replay_gain.gleam
Normal 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
|
||||||
|
}
|
|
@ -46,6 +46,28 @@ export function setTrackTo(pos) {
|
||||||
track.value = String(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() {
|
function getPlayer() {
|
||||||
if (player === undefined) {
|
if (player === undefined) {
|
||||||
player = document.getElementById(player_id);
|
player = document.getElementById(player_id);
|
||||||
|
|
Loading…
Reference in a new issue