Skip to content

Commit

Permalink
feat: loudness normalization
Browse files Browse the repository at this point in the history
  • Loading branch information
lideming committed Dec 22, 2024
1 parent 2c5da41 commit 986637a
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/I18n/i18n-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
["UI color:", "界面色彩:", "UIの色:"],
["UI style:", "界面样式:", "UIのスタイル:"],
["Preferred bitrate:", "首选码率:", "ビットレート:"],
["Loudness Normalization:", "音量标准化:", "音量正規化:"],
["Custom server URL", "自定义服务器 URL", "カスタムサーバーURL"],
["Notification:", "通知:", "通知:"],
["enabled", "启用", "有効"],
Expand Down
21 changes: 5 additions & 16 deletions src/Infra/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,23 +294,12 @@ class LoudnessMap extends View<HTMLCanvasElement> {

async updateLoudnessMap(player: typeof playerCore) {
const track = player.track;
var louds = await track?._loudmap;
if (track && !louds) {
if (this.dom) {
const ctx = this.dom.getContext("2d")!;
const { width, height } = this.dom;
ctx.clearRect(0, 0, width, height);
}
track._loudmap = (async () => {
var resp = (await api.get(
`tracks/${track.id}/loudnessmap`
)) as Response;
if (!resp.ok) return null;
var ab = await resp.arrayBuffer();
return (track._loudmap = new Uint8Array(ab));
})();
louds = await track._loudmap;
if (track && !track._loudmap && this.dom) {
const ctx = this.dom.getContext("2d")!;
const { width, height } = this.dom;
ctx.clearRect(0, 0, width, height);
}
const louds = await track?.getLoudnessMap();
if (player.track !== track) return;
if (louds && louds.length > 20) {
const [width, height] = [Math.min(1024, louds.length / 4), 32];
Expand Down
49 changes: 44 additions & 5 deletions src/Player/PlayerCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const playerCore = new (class PlayerCore {
loopMode: "list-loop" as PlayingLoopMode,
volume: 1,
preferBitrate: 256,
loudnessNormalization: true,
});
get loopMode() {
return this.siPlayer.data.loopMode;
Expand Down Expand Up @@ -76,18 +77,29 @@ export const playerCore = new (class PlayerCore {
return this.track?.type == "video";
}

private _volume = 1;
get volume() {
return this.audio?.volume ?? 1;
return this._volume;
}
set volume(val) {
this.audio.volume = val;
this._volume = val;
this.audio.volume = Math.pow(val, 2) * this.normalizingGain;
if (val !== this.siPlayer.data.volume) {
this.siPlayer.data.volume = val;
this.siPlayer.save();
}
}
onVolumeChanged = new Callbacks<Action>();

private _normalizingGain = 1;
public get normalizingGain() {
return this._normalizingGain;
}
public set normalizingGain(value) {
this._normalizingGain = value;
this.audio.volume = this.volume * this.normalizingGain;
}

get playbackRate() {
return this.audio.playbackRate;
}
Expand Down Expand Up @@ -170,7 +182,7 @@ export const playerCore = new (class PlayerCore {
this.audio.addEventListener("volumechange", () =>
this.onVolumeChanged.invoke()
);
this.audio.volume = this.siPlayer.data.volume;
this.volume = this.siPlayer.data.volume;
this.onAudioCreated.invoke();
}
prev() {
Expand Down Expand Up @@ -287,8 +299,35 @@ export const playerCore = new (class PlayerCore {
}
}
async ensureLoaded() {
var track = this.track;
if (track && !this.audioLoaded) await this.loadTrack(track!);
if (this.track) {
await Promise.all([
this.computeNormalizingGain(),
!this.audioLoaded && this.loadTrack(this.track!),
]);
}
}
async computeNormalizingGain() {
if (this.siPlayer.data.loudnessNormalization === false) {
this.normalizingGain = 1;
return;
}
const data = await this.track?.getLoudnessMap();
if (!data) {
this.normalizingGain = 1;
return;
}
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += data[i];
}
const avg = sum / data.length;
const sorted = data.slice().sort();
const trackGain = sorted[Math.floor(sorted.length * (90 / 100))];
const trackDb = Math.log10(trackGain / 128) * 10
const targetDb = -7;
const gain = Math.pow(10, (targetDb - trackDb) / 10)
console.info(this.track?.name, { avg, trackGain, trackDb, gain });
this.normalizingGain = Math.min(1, gain);
}
pause() {
this.audio.pause();
Expand Down
19 changes: 19 additions & 0 deletions src/Settings/SettingsUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,25 @@ class SettingsDialog extends Dialog {
</SettingItem>,
);

this.addContent(
<SettingItem label={() => I`Loudness Normalization:`}>
<RadioContainer
currentValue={playerCore.siPlayer.data.loudnessNormalization ?? true}
onCurrentChange={(option) => {
playerCore.siPlayer.data.loudnessNormalization = option.value;
playerCore.siPlayer.save();
playerCore.computeNormalizingGain();
}}
>
{[false, true].map((option) => (
<RadioOption value={option}>
{() => (option ? I`enabled` : I`disabled`)}
</RadioOption>
))}
</RadioContainer>
</SettingItem>,
);

this.addContent(
<SettingItem label={() => I`Notification:`}>
<RadioContainer
Expand Down
14 changes: 14 additions & 0 deletions src/Track/Track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ export class Track {
getGroup(): Promise<{ tracks: Api.Track[] }> {
return api.get(`tracks/group/${this.groupId ?? this.id}`);
}

async getLoudnessMap() {
if (!this._loudmap) {
this._loudmap = (async () => {
var resp = (await api.get(
`tracks/${this.id}/loudnessmap`
)) as Response;
if (!resp.ok) return null;
var ab = await resp.arrayBuffer();
return (this._loudmap = new Uint8Array(ab));
})();
}
return this._loudmap;
}
}

export class TrackDialog extends Dialog {
Expand Down

0 comments on commit 986637a

Please sign in to comment.