Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src-tauri/src/commands/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use tauri::{Emitter, State};

use crate::engine::{
audio_io, AudioDeviceInfo, ClipSchedule, ClipSource, CommandError, CountIn, Engine,
EngineConfig, EngineError, EngineStatus, LoopRegion, MetronomeConfig, PositionEmitter,
PunchRegion, TempoEvent, TempoMap, TimeSignatureEvent, TimeSignatureMap, TrackParams,
POSITION_EVENT_DEFAULT_INTERVAL,
EngineConfig, EngineError, EngineStatus, LoopRegion, MeterReading, MetronomeConfig,
PositionEmitter, PunchRegion, TempoEvent, TempoMap, TimeSignatureEvent, TimeSignatureMap,
TrackParams, POSITION_EVENT_DEFAULT_INTERVAL,
};
use crate::engine::slot::SlotHandle;

Expand Down Expand Up @@ -102,6 +102,14 @@ pub fn audio_start_engine(
.lock()
.map_err(|_| EngineError::Open("emitter mutex poisoned".into()))?;

let current_status = engine.status();
if current_status.is_running() {
// `Engine::start` returns AlreadyRunning, but the emitter is
// already valid in the normal running case. Return the current
// status without tearing it down.
return Ok(current_status);
}

// Stop any leftover emitter BEFORE starting the new engine — so
// we never have two emitters pushing to the same event name, and
// we don't leak a thread if `start` fails.
Expand Down Expand Up @@ -218,6 +226,48 @@ pub fn audio_set_master_volume(
engine.set_master_volume(volume)
}

#[tauri::command]
pub fn audio_get_track_meter(
handle: SlotHandle,
state: State<'_, EngineState>,
) -> Result<MeterReading, CommandError> {
let mut engine = state
.0
.lock()
.map_err(|_| CommandError::Disconnected)?;
Ok(engine.get_track_meter(handle))
}

#[tauri::command]
pub fn audio_get_master_meter(state: State<'_, EngineState>) -> Result<MeterReading, CommandError> {
let mut engine = state
.0
.lock()
.map_err(|_| CommandError::Disconnected)?;
Ok(engine.get_master_meter())
}

#[tauri::command]
pub fn audio_reset_track_clip(
handle: SlotHandle,
state: State<'_, EngineState>,
) -> Result<(), CommandError> {
let mut engine = state
.0
.lock()
.map_err(|_| CommandError::Disconnected)?;
engine.reset_track_clip(handle)
}

#[tauri::command]
pub fn audio_reset_master_clip(state: State<'_, EngineState>) -> Result<(), CommandError> {
let mut engine = state
.0
.lock()
.map_err(|_| CommandError::Disconnected)?;
engine.reset_master_clip()
}

// ── Transport (3A) ──────────────────────────────────────────────────

#[tauri::command]
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/engine/audio_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,14 @@ fn make_audio_callback(
bus.enabled = enabled;
}
}
EngineCommand::ResetTrackClip { handle } => {
if graph.handle_matches(handle) {
meters.track_meters[handle.index()].reset_clip();
}
}
EngineCommand::ResetMasterClip => {
meters.master_meter.reset_clip();
}
// Transport commands (3A) — route to the audio-thread
// `Transport` instance. Position advance happens at
// the tail of the callback, AFTER the drain, so that
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/src/engine/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ pub enum EngineCommand {
/// Enable or disable an aux return bus.
SetAuxBusEnabled { bus_index: u8, enabled: bool },

/// Clear the latched clip flag on a track meter.
/// Generation-checked like other track-targeted commands.
ResetTrackClip { handle: SlotHandle },

/// Clear the latched clip flag on the master meter.
ResetMasterClip,

// ── Transport (3A) ────────────────────────────────────────────────

/// Begin playback from the current position.
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/engine/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ impl AudioGraph {
| EngineCommand::SetTrackSendLevel { .. }
| EngineCommand::SetAuxBusVolume { .. }
| EngineCommand::SetAuxBusEnabled { .. }
| EngineCommand::ResetTrackClip { .. }
| EngineCommand::ResetMasterClip
| EngineCommand::TransportPlay
| EngineCommand::TransportStop
| EngineCommand::TransportPause
Expand Down
16 changes: 16 additions & 0 deletions src-tauri/src/engine/meter_bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ impl MeterConsumers {
drain_into(&mut self.master_consumer, &mut self.master_cache);
self.master_cache
}

/// Clear the latched clip flag from the main-thread cache for a
/// track slot. The audio-thread meter is reset by command queue;
/// this avoids one stale UI poll before that command drains.
pub fn reset_track_clip(&mut self, slot: usize) {
if let Some(cons) = self.track_consumers.get_mut(slot) {
drain_into(cons, &mut self.track_cache[slot]);
self.track_cache[slot].clipped = false;
}
}

/// Clear the latched clip flag from the main-thread master cache.
pub fn reset_master_clip(&mut self) {
drain_into(&mut self.master_consumer, &mut self.master_cache);
self.master_cache.clipped = false;
}
}

/// Drain the ring buffer into a cached value. If new entries exist,
Expand Down
14 changes: 14 additions & 0 deletions src-tauri/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,20 @@ impl Engine {
self.send_command(EngineCommand::SetMasterVolume { volume })
}

/// Clear a track meter's latched clip flag.
pub fn reset_track_clip(&mut self, handle: SlotHandle) -> Result<(), CommandError> {
let running = self.running.as_mut().ok_or(CommandError::NotRunning)?;
running.meter_consumers.reset_track_clip(handle.index());
self.send_command(EngineCommand::ResetTrackClip { handle })
}

/// Clear the master meter's latched clip flag.
pub fn reset_master_clip(&mut self) -> Result<(), CommandError> {
let running = self.running.as_mut().ok_or(CommandError::NotRunning)?;
running.meter_consumers.reset_master_clip();
self.send_command(EngineCommand::ResetMasterClip)
}

// ── Transport (3A) ───────────────────────────────────────────────

/// Begin playback from the current position.
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use tauri::Manager;

use crate::commands::audio::{
audio_add_track, audio_clip_get_schedule, audio_clip_set_schedule,
audio_get_default_device, audio_get_engine_status, audio_list_devices,
audio_get_default_device, audio_get_engine_status, audio_get_master_meter,
audio_get_track_meter, audio_list_devices, audio_reset_master_clip, audio_reset_track_clip,
audio_metronome_get_config, audio_metronome_set_config, audio_metronome_set_enabled,
audio_remove_track, audio_set_master_volume, audio_set_track_params,
audio_start_engine, audio_stop_engine, audio_transport_get_count_in,
Expand Down Expand Up @@ -55,6 +56,10 @@ pub fn run() {
audio_remove_track,
audio_set_track_params,
audio_set_master_volume,
audio_get_track_meter,
audio_get_master_meter,
audio_reset_track_clip,
audio_reset_master_clip,
audio_transport_play,
audio_transport_stop,
audio_transport_pause,
Expand Down
20 changes: 15 additions & 5 deletions src/components/mixer/LevelMeter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useRef, useCallback } from 'react';
import { getAudioEngine } from '../../hooks/useAudioEngine';
import { getAudioEngine, getTauriPlaybackClockOwner } from '../../hooks/useAudioEngine';
import { getAudioBridge } from '../../engine/bridge';
import { METER_CANVAS_STOPS, METER_DB_TICKS, METER_DB_TICKS_MINOR, METER_PADDING_PCT, dbToFill, levelToFill } from '../meter-colors';

const BAR_WIDTH = 4;
Expand Down Expand Up @@ -77,6 +78,7 @@ export function LevelMeter({ trackId, masterStage, returnTrackId, stereo, showSc
};

const engine = getAudioEngine();
const bridge = getAudioBridge(engine);

const tick = () => {
const dpr = window.devicePixelRatio || 1;
Expand All @@ -92,8 +94,12 @@ export function LevelMeter({ trackId, masterStage, returnTrackId, stereo, showSc
let rightLevel = 0;
let clipped = false;

const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;

if (masterStage) {
const meter = engine.getMasterMeter(masterStage);
const meter = meterSource.getMasterMeter(masterStage);
leftLevel = meter.level;
rightLevel = meter.level;
clipped = meter.clipped;
Expand All @@ -103,7 +109,7 @@ export function LevelMeter({ trackId, masterStage, returnTrackId, stereo, showSc
rightLevel = meter.level;
clipped = meter.clipped;
} else if (trackId) {
const meter = engine.getTrackMeter(trackId);
const meter = meterSource.getTrackMeter(trackId);
leftLevel = isStereo ? meter.leftLevel : meter.level;
rightLevel = isStereo ? meter.rightLevel : meter.level;
clipped = meter.clipped;
Expand Down Expand Up @@ -183,12 +189,16 @@ export function LevelMeter({ trackId, masterStage, returnTrackId, stereo, showSc

const resetClip = () => {
const engine = getAudioEngine();
const bridge = getAudioBridge(engine);
const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;
if (masterStage) {
engine.resetMasterClip(masterStage);
meterSource.resetMasterClip(masterStage);
} else if (returnTrackId) {
engine.resetReturnTrackClip(returnTrackId);
} else if (trackId) {
engine.resetTrackClip(trackId);
meterSource.resetTrackClip(trackId);
}
clippedRef.current = false;
clippedStateRef.current = false;
Expand Down
1 change: 1 addition & 0 deletions src/components/mixer/__tests__/LevelMeter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
import { LevelMeter } from '../LevelMeter';

vi.mock('../../../hooks/useAudioEngine', () => ({
getTauriPlaybackClockOwner: () => 'web-audio',
getAudioEngine: () => ({
getTrackMeter: () => ({ level: 0.5, leftLevel: 0.4, rightLevel: 0.6, clipped: false }),
getMasterMeter: () => ({ level: 0.3, clipped: false }),
Expand Down
9 changes: 7 additions & 2 deletions src/components/session/SessionMixerStrip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { getAudioEngine } from '../../hooks/useAudioEngine';
import { getAudioEngine, getTauriPlaybackClockOwner } from '../../hooks/useAudioEngine';
import { getAudioBridge } from '../../engine/bridge';
import { Knob } from '../ui/Knob';

/** Convert linear level (0..1+) to a 0..1 fill fraction mapping -60dB..0dB */
Expand Down Expand Up @@ -45,8 +46,12 @@ export function SessionMixerStrip({
// Animate meter levels
useEffect(() => {
const engine = getAudioEngine();
const bridge = getAudioBridge(engine);
const tick = () => {
const meter = engine.getTrackMeter(trackId);
const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;
const meter = meterSource.getTrackMeter(trackId);
setLeftFill(levelToFill(meter.leftLevel));
setRightFill(levelToFill(meter.rightLevel));
rafRef.current = requestAnimationFrame(tick);
Expand Down
1 change: 1 addition & 0 deletions src/components/session/__tests__/SessionMixer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ vi.mock('../../../services/projectStorage', () => ({
}));

vi.mock('../../../hooks/useAudioEngine', () => ({
getTauriPlaybackClockOwner: () => 'web-audio',
getAudioEngine: () => ({
getTrackMeter: () => ({ leftLevel: 0, rightLevel: 0, clipped: false }),
getTrackLevel: () => 0,
Expand Down
15 changes: 12 additions & 3 deletions src/components/tracks/FaderMeter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { getAudioEngine } from '../../hooks/useAudioEngine';
import { getAudioEngine, getTauriPlaybackClockOwner } from '../../hooks/useAudioEngine';
import { getAudioBridge } from '../../engine/bridge';
import { METER_GRADIENT_HORIZONTAL, levelToMeterFill } from '../meter-colors';

interface FaderMeterProps {
Expand Down Expand Up @@ -33,8 +34,12 @@ export function FaderMeter({ trackId, volume, onVolumeChange, trackName }: Fader
// Animate meter levels
useEffect(() => {
const engine = getAudioEngine();
const bridge = getAudioBridge(engine);
const tick = () => {
const meter = engine.getTrackMeter(trackId);
const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;
const meter = meterSource.getTrackMeter(trackId);
setLeftFill(levelToMeterFill(meter.leftLevel));
setRightFill(levelToMeterFill(meter.rightLevel));
setClipping((was) => was || meter.clipped);
Expand All @@ -46,7 +51,11 @@ export function FaderMeter({ trackId, volume, onVolumeChange, trackName }: Fader

const resetClip = useCallback(() => {
const engine = getAudioEngine();
engine.resetTrackClip(trackId);
const bridge = getAudioBridge(engine);
const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;
meterSource.resetTrackClip(trackId);
setClipping(false);
}, [trackId]);

Expand Down
15 changes: 12 additions & 3 deletions src/components/tracks/StereoMeter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { getAudioEngine } from '../../hooks/useAudioEngine';
import { getAudioEngine, getTauriPlaybackClockOwner } from '../../hooks/useAudioEngine';
import { getAudioBridge } from '../../engine/bridge';
import { METER_GRADIENT_HORIZONTAL, levelToMeterFill } from '../meter-colors';

interface StereoMeterProps {
Expand All @@ -14,9 +15,13 @@ export function StereoMeter({ trackId }: StereoMeterProps) {

useEffect(() => {
const engine = getAudioEngine();
const bridge = getAudioBridge(engine);

const tick = () => {
const meter = engine.getTrackMeter(trackId);
const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;
const meter = meterSource.getTrackMeter(trackId);
setLeftFill(levelToMeterFill(meter.leftLevel));
setRightFill(levelToMeterFill(meter.rightLevel));
setClipping((was) => was || meter.clipped);
Expand All @@ -29,7 +34,11 @@ export function StereoMeter({ trackId }: StereoMeterProps) {

const resetClip = () => {
const engine = getAudioEngine();
engine.resetTrackClip(trackId);
const bridge = getAudioBridge(engine);
const meterSource = bridge.backend === 'tauri' && getTauriPlaybackClockOwner() === 'native'
? bridge
: engine;
meterSource.resetTrackClip(trackId);
setClipping(false);
};

Expand Down
1 change: 1 addition & 0 deletions src/components/tracks/__tests__/StereoMeter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const engine = {

vi.mock('../../../hooks/useAudioEngine', () => ({
getAudioEngine: () => engine,
getTauriPlaybackClockOwner: () => 'web-audio',
}));

describe('StereoMeter', () => {
Expand Down
1 change: 1 addition & 0 deletions src/components/tracks/__tests__/TrackHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ vi.mock('../../../services/freezeTrack', () => ({
}));

vi.mock('../../../hooks/useAudioEngine', () => ({
getTauriPlaybackClockOwner: () => 'web-audio',
getAudioEngine: () => ({
getTrackLevel: () => 0,
getTrackMeter: () => ({ level: 0, clipped: false }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ vi.mock('../../../services/freezeTrack', () => ({
flattenTrackToAudio: vi.fn(),
}));
vi.mock('../../../hooks/useAudioEngine', () => ({
getTauriPlaybackClockOwner: () => 'web-audio',
getAudioEngine: () => ({
getTrackLevel: () => 0,
}),
Expand Down
1 change: 1 addition & 0 deletions src/components/tracks/__tests__/TrackHeaderLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ vi.mock('../../../services/freezeTrack', () => ({
flattenTrackToAudio: vi.fn(),
}));
vi.mock('../../../hooks/useAudioEngine', () => ({
getTauriPlaybackClockOwner: () => 'web-audio',
getAudioEngine: () => ({
getTrackLevel: () => 0,
getTrackMeter: () => ({ level: 0, clipped: false }),
Expand Down
1 change: 1 addition & 0 deletions src/components/tracks/__tests__/TrackHeaderMeter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const engine = {

vi.mock('../../../hooks/useAudioEngine', () => ({
getAudioEngine: () => engine,
getTauriPlaybackClockOwner: () => 'web-audio',
}));

describe('TrackHeaderMeter', () => {
Expand Down
Loading
Loading