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,
|
||||
file: String,
|
||||
type_: String,
|
||||
replay_gain: String,
|
||||
replay_gain: Float,
|
||||
uploaded_time: String,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue