diff --git a/changelog/v0.1.0-beta3.txt b/changelog/v0.1.0-beta3.txt index 2b7257a1..2cec3b8b 100644 --- a/changelog/v0.1.0-beta3.txt +++ b/changelog/v0.1.0-beta3.txt @@ -71,6 +71,8 @@ MACOS - Flow uses the native macOS window controls and titlebar again. Closing the window hides it and keeps playback and downloads running until you quit, and clicking the Dock icon brings it back. - The window background now follows your theme. A Tauri bug was clearing it, which showed a mismatched color around the app while resizing. - On 120 Hz ProMotion screens Flow now draws at 120 frames per second. WebKit was holding every animation, scroll and lyric line to 60. +- The music equalizer and crossfade are now heard on macOS. WebKit cannot pass Opus songs through audio processing, so once either is on, songs stream as AAC and the song playing reloads once from where it is. +- The volume slider keeps working on a downloaded Opus song while the equalizer is on. LINUX - The AppImage is built on Ubuntu 24.04 with a newer WebKitGTK that no longer crashes on color emoji fonts, the cause of the freeze on Fedora 44 and Bazzite. It needs glibc 2.39 or newer, so on Ubuntu 22.04, Mint 21 or Debian 12 use the .deb instead. diff --git a/src/lib/audio/musicAudioEngine.eq.test.ts b/src/lib/audio/musicAudioEngine.eq.test.ts index 075d27a6..c24ab307 100644 --- a/src/lib/audio/musicAudioEngine.eq.test.ts +++ b/src/lib/audio/musicAudioEngine.eq.test.ts @@ -4,6 +4,14 @@ import { graphSpec, EQ_GRAPH_OFF } from "./eq/graphPlan"; import { defaultEqState, selectPreset } from "./eq/stateOps"; import { musicAudioEngine } from "./musicAudioEngine"; +const platform = vi.hoisted(() => ({ macos: false })); +vi.mock("../platform", () => ({ + IS_LINUX_RUNTIME: false, + get IS_MACOS_RUNTIME() { + return platform.macos; + }, +})); + // A minimal WebAudio stand-in: enough to see which nodes exist and what they were told. class FakeParam { constructor(public value = 0) {} @@ -92,6 +100,7 @@ beforeEach(() => { }); afterEach(() => { + platform.macos = false; vi.useRealTimers(); vi.unstubAllGlobals(); }); @@ -124,8 +133,8 @@ describe("music EQ graph", () => { expect(ctx.biquads[0]!.gain.value).toBe(rock.bands[0]!.gain); expect(ctx.biquads[5]!.gain.value).toBe(0); - await musicAudioEngine.load("http://127.0.0.1/stream/a"); - musicAudioEngine.preload("b", "http://127.0.0.1/stream/b", null); + await musicAudioEngine.load("http://127.0.0.1/stream/a", null); + musicAudioEngine.preload("b", "http://127.0.0.1/stream/b", null, null); Object.defineProperty(pair[1], "readyState", { value: HTMLMediaElement.HAVE_FUTURE_DATA, configurable: true }); expect(musicAudioEngine.activatePreloaded("b")).toBe(true); @@ -138,6 +147,27 @@ describe("music EQ graph", () => { expect(ctx.biquads.every((f) => f.gain.value === 0)).toBe(true); }); + it("asks for AAC on macOS, and keeps a WebM song it cannot hear on the element's volume", async () => { + platform.macos = true; + const pair = elements(); + musicAudioEngine.attach(pair); + musicAudioEngine.setEq(EQ_GRAPH_OFF, false); + expect(musicAudioEngine.wantsAac()).toBe(false); + musicAudioEngine.setEq(rock, true); + expect(musicAudioEngine.wantsAac()).toBe(true); + + await musicAudioEngine.load("http://127.0.0.1/stream/opus", 'audio/webm; codecs="opus"'); + await musicAudioEngine.play(); + musicAudioEngine.setLoudness(null, true); + musicAudioEngine.setVolume(0.5); + expect(musicAudioEngine.activeBypassesGraph()).toBe(true); + expect(pair[0].volume).toBeCloseTo(0.25, 2); + + await musicAudioEngine.load("http://127.0.0.1/stream/aac", 'audio/mp4; codecs="mp4a.40.2"'); + expect(musicAudioEngine.activeBypassesGraph()).toBe(false); + expect(pair[0].volume).toBe(1); + }); + it("starts the element even when the context will not resume", async () => { vi.useFakeTimers(); const pair = elements(); @@ -175,8 +205,8 @@ describe("music EQ graph", () => { Object.defineProperty(pair[0], "paused", { value: false, configurable: true }); musicAudioEngine.setCrossfadeEnabled(true); const ctx = lastContext(); - await musicAudioEngine.load("http://127.0.0.1/stream/a"); - musicAudioEngine.preload("b", "http://127.0.0.1/stream/b", null); + await musicAudioEngine.load("http://127.0.0.1/stream/a", null); + musicAudioEngine.preload("b", "http://127.0.0.1/stream/b", null, null); Object.defineProperty(pair[1], "readyState", { value: HTMLMediaElement.HAVE_FUTURE_DATA, configurable: true }); musicAudioEngine.armCrossfade(4); @@ -205,8 +235,8 @@ describe("crossfade without WebAudio", () => { engine.setCrossfadeEnabled(true); expect(engine.isEqAvailable()).toBe(false); - await engine.load("http://127.0.0.1/stream/a"); - engine.preload("b", "http://127.0.0.1/stream/b", null); + await engine.load("http://127.0.0.1/stream/a", null); + engine.preload("b", "http://127.0.0.1/stream/b", null, null); Object.defineProperty(pair[1], "readyState", { value: HTMLMediaElement.HAVE_FUTURE_DATA, configurable: true }); Object.defineProperty(pair[0], "paused", { value: false, configurable: true }); engine.armCrossfade(2); diff --git a/src/lib/audio/musicAudioEngine.test.ts b/src/lib/audio/musicAudioEngine.test.ts index 085178c3..fcd879a4 100644 --- a/src/lib/audio/musicAudioEngine.test.ts +++ b/src/lib/audio/musicAudioEngine.test.ts @@ -28,8 +28,8 @@ beforeEach(() => { describe("gapless standby element", () => { it("buffers the next track on the element that is not playing", async () => { - await musicAudioEngine.load("http://127.0.0.1/stream/current"); - musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", -4); + await musicAudioEngine.load("http://127.0.0.1/stream/current", null); + musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", -4, null); expect(musicAudioEngine.getActiveElement()).toBe(pair[0]); expect(pair[1].getAttribute("src")).toBe("http://127.0.0.1/stream/next"); @@ -38,8 +38,8 @@ describe("gapless standby element", () => { }); it("swaps only for the track it holds, and empties the one that played", async () => { - await musicAudioEngine.load("http://127.0.0.1/stream/current"); - musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", -4); + await musicAudioEngine.load("http://127.0.0.1/stream/current", null); + musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", -4, null); bufferedEnough(pair[1]); expect(musicAudioEngine.activatePreloaded("other")).toBe(false); @@ -53,15 +53,15 @@ describe("gapless standby element", () => { }); it("never swaps in a preload that has no audio to play yet", async () => { - await musicAudioEngine.load("http://127.0.0.1/stream/current"); - musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", null); + await musicAudioEngine.load("http://127.0.0.1/stream/current", null); + musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", null, null); expect(musicAudioEngine.activatePreloaded("next")).toBe(false); expect(musicAudioEngine.getActiveElement()).toBe(pair[0]); }); it("reports and drops what the standby element held", () => { - musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", null); + musicAudioEngine.preload("next", "http://127.0.0.1/stream/next", null, null); expect(musicAudioEngine.clearPreload()).toBe("next"); expect(pair[1].getAttribute("src")).toBeNull(); expect(musicAudioEngine.clearPreload()).toBeNull(); diff --git a/src/lib/audio/musicAudioEngine.ts b/src/lib/audio/musicAudioEngine.ts index 34ad9921..60d8f523 100644 --- a/src/lib/audio/musicAudioEngine.ts +++ b/src/lib/audio/musicAudioEngine.ts @@ -1,10 +1,11 @@ import { loudnessFactor, MUSIC_MAX_CUT_DB } from "../loudness"; -import { IS_LINUX_RUNTIME } from "../platform"; +import { IS_LINUX_RUNTIME, IS_MACOS_RUNTIME } from "../platform"; import { createAudioContext, keepRunning, resumeWithin, webAudioSupported } from "./audioContext"; import { fadeInGain, fadeOutGain, resetFade, scheduleFade, steppedFade } from "./crossfadeRamp"; import { EQ_GRAPH_OFF, type EqGraphSpec } from "./eq/graphPlan"; import { EqChain, RAMP_TIME_CONSTANT } from "./eqChain"; import { volumeGain } from "./volumeCurve"; +import { webAudioHears } from "./webAudioFormats"; /** * One of the two