Skip to content

feat: add crossfade playback with equal-power envelope (#89)#104

Closed
thedavidweng wants to merge 31 commits into
feat/next-track-gapless-88from
feat/crossfade-playback-89
Closed

feat: add crossfade playback with equal-power envelope (#89)#104
thedavidweng wants to merge 31 commits into
feat/next-track-gapless-88from
feat/crossfade-playback-89

Conversation

@thedavidweng

@thedavidweng thedavidweng commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add optional crossfade playback with an equal-power (cos/sin) envelope that overlaps the tail of the outgoing track with the start of the prepared incoming track
  • Build on the feat: add NextTrack pipeline with gapless playback #88 prepared-track state machine: the realtime audio callback starts the overlap when remaining frames fall within the configured duration, mixes both tracks frame-by-frame, and promotes the incoming track when the overlap completes
  • New IPC commands set_crossfade_enabled / set_crossfade_duration_ms return AppSettings and use the coordinator for live updates with best-effort rollback on persistence failure
  • Settings persist to config.json (crossfade_enabled, crossfade_duration_ms, range 500-10000 ms) and hydrate before audio startup
  • New Settings UI section (SettingsCrossfadeSection) with an enable checkbox and a duration slider with 75 ms debounced persistence and en/zh-CN localization
  • Eligibility: only fully-decoded local tracks with no stems/CDG and known durations; effective overlap is clamped to half of each track total frames and the remaining frames, falling back to feat: add NextTrack pipeline with gapless playback #88 gapless when below 500 ms
  • Seek aborts an active crossfade (restoring the incoming payload to prepared_track at frame 0); manual play, stem attachment, and output-device recreation cancel both active crossfade and prepared track

Test plan

  • Backend: 485 Rust tests pass (including 11 new crossfade DSP tests)
  • Frontend: 1372 vitest tests pass (including 12 new SettingsCrossfadeSection tests, 6 new settings-store crossfade action tests, 6 new SettingsOverlay.state crossfade action tests)
  • Lint: 0 warnings, 0 errors
  • Typecheck: tsc + vite build passes
  • i18n: all locales match en.json reference
  • Patch coverage: 98.1% (211/215 instrumented lines), above 80% target
  • Clippy: 0 warnings
  • IPC contract docs updated (docs/references/contracts/playback.md)
  • CHANGELOG.md updated

Open in Devin Review

@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 78.16% (🎯 65%) 3870 / 4951
🔵 Statements 77.74% (🎯 65%) 4115 / 5293
🔵 Functions 68.86% (🎯 60%) 1053 / 1529
🔵 Branches 70.82% (🎯 60%) 2182 / 3081
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Settings/SettingsCrossfadeSection.tsx 100% 87.5% 100% 100%
src/components/Settings/SettingsOverlay.context.ts 100% 100% 22.22% 100%
src/components/Settings/SettingsOverlay.state.ts 89.05% 77.14% 88% 88.72% 178-179, 184-197, 202-208, 219, 287
src/components/Settings/SettingsOverlay.tsx 100% 100% 100% 100%
src/components/Settings/settings-overlay.library-actions.ts 94.89% 82.22% 100% 94.77% 43, 85-95, 156, 161-163, 172-174
src/hooks/use-playback-runtime.ts 67.28% 64.28% 61.76% 67.13% 41-54, 139, 143, 159-161, 187-218, 233-235, 250-252, 325-338, 368-387, 410-430
src/locales/en.json 100% 100% 100% 100%
src/locales/zh-CN.json 100% 100% 100% 100%
src/playback/session.ts 96.36% 89.58% 95.83% 96.15% 259-265
src/stores/player-store.ts 87.87% 82.14% 90.47% 90.62% 220, 228, 286, 294, 302, 326, 332, 340, 348, 403-404
src/stores/queue-store.ts 98.8% 97.05% 100% 98.68% 53
src/stores/settings-store.ts 100% 77.77% 100% 100%
Generated in workflow #879 for commit cb8fa6e by the Vitest Coverage Report Action

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.2%. Comparing base (4d22f8c) to head (cb8fa6e).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                     Coverage Diff                      @@
##           feat/next-track-gapless-88    #104     +/-   ##
============================================================
+ Coverage                        77.8%   78.2%   +0.3%     
============================================================
  Files                             130     131      +1     
  Lines                            6688    6804    +116     
  Branches                         2043    2112     +69     
============================================================
+ Hits                             5209    5325    +116     
  Misses                           1435    1435             
  Partials                           44      44             
Flag Coverage Δ
frontend 78.2% <100.0%> (+0.3%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
frontend 78.2% <100.0%> (+0.3%) ⬆️
rust ∅ <ø> (∅)
Files with missing lines Coverage Δ
...c/components/Settings/SettingsCrossfadeSection.tsx 100.0% <100.0%> (ø)
src/components/Settings/SettingsOverlay.context.ts 39.1% <100.0%> (+2.7%) ⬆️
src/components/Settings/SettingsOverlay.state.ts 89.1% <100.0%> (+8.4%) ⬆️
src/components/Settings/SettingsOverlay.tsx 100.0% <ø> (ø)
...nents/Settings/settings-overlay.library-actions.ts 94.5% <ø> (-1.0%) ⬇️
src/hooks/use-playback-runtime.ts 66.8% <100.0%> (ø)
src/locales/en.json 100.0% <ø> (ø)
src/locales/zh-CN.json 100.0% <ø> (ø)
src/playback/session.ts 96.8% <100.0%> (+0.1%) ⬆️
src/stores/player-store.ts 92.3% <100.0%> (+<0.1%) ⬆️
... and 2 more
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

devin-ai-integration[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds optional equal-power crossfade playback on top of prepared next-track preloading. The main changes are:

  • Crossfade overlap rendering in the realtime audio callback with cos/sin equal-power mixing.
  • Crossfade eligibility, cancellation, promotion, and gapless fallback handling in backend playback state.
  • New persisted crossfade settings with IPC commands and coordinator-backed live updates.
  • A settings UI section with enable and duration controls plus localization.
  • Updated track-transitioned payload handling so frontend state, queue, history, and preload candidates reconcile after backend-completed transitions.
  • Added backend and frontend tests covering the new playback, settings, and transition behavior.

Confidence Score: 5/5

This PR appears safe to merge based on the reviewed diff.

The crossfade render path handles the active, paused, stale-state, resampling, and fallback cases covered during review. Settings responses are guarded against stale writes, and IPC, docs, types, and tests are aligned across backend and frontend. No accepted issues remain.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Captured the pre-change UI state, showing the Crossfade section rendered disabled at 3.0 s.
  • Captured the post-change UI state, showing Enable crossfade checked and the duration updated.
  • Ran the narrow test suite; 4 test files and 133 tests completed successfully.
  • Collected and reviewed artifacts (videos, posters, logs, scripts, and config) that document the before/after states and test results.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src-tauri/src/audio/output.rs Implements realtime crossfade overlap rendering, promotion, fallback, limiter, and resampler-cache handling.
src-tauri/src/audio/playback.rs Adds crossfade controller state, config setters, cancellation, transition stamping, and promotion helpers.
src-tauri/src/audio/crossfade.rs Introduces tested equal-power gain and effective-overlap frame calculations.
src-tauri/src/audio/coordinator.rs Adds serialized coordinator commands for live crossfade configuration updates.
src-tauri/src/commands/settings.rs Adds failure-atomic crossfade settings commands with validation and persistence rollback.
src/stores/settings-store.ts Adds crossfade settings state, optimistic updates, stale response guards, and sync propagation.
src/components/Settings/SettingsCrossfadeSection.tsx Adds the settings UI section for enabling crossfade and adjusting duration.
src/playback/session.ts Applies authoritative transition snapshots before queue reconciliation and updates preload candidates.
src/types/ipc.ts Extends TypeScript IPC types for crossfade settings and track-transition state payloads.
docs/references/contracts/playback.md Updates playback contract docs for crossfade settings and expanded track-transition payload semantics.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as Settings UI
participant Store as settings-store
participant IPC as Tauri settings command
participant Coord as PlaybackCoordinator
participant Audio as CPAL output callback
participant Queue as PlaybackSession/Queue

UI->>Store: toggle crossfade / adjust duration
Store->>IPC: set_crossfade_enabled / set_crossfade_duration_ms
IPC->>Coord: SetCrossfadeEnabled / SetCrossfadeDuration
Coord->>Audio: update PlaybackController.crossfade_config
IPC-->>Store: AppSettings snapshot
Audio->>Audio: when prepared track enters overlap window
Audio->>Audio: equal-power mix outgoing tail + incoming head
Audio->>Audio: promote incoming track on overlap completion
Audio-->>Queue: track-transitioned(state, serial, fromSongId, toSongId)
Queue->>Queue: apply authoritative state and reconcile queue/history
Queue->>Coord: setPreloadCandidate(new queue head)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as Settings UI
participant Store as settings-store
participant IPC as Tauri settings command
participant Coord as PlaybackCoordinator
participant Audio as CPAL output callback
participant Queue as PlaybackSession/Queue

UI->>Store: toggle crossfade / adjust duration
Store->>IPC: set_crossfade_enabled / set_crossfade_duration_ms
IPC->>Coord: SetCrossfadeEnabled / SetCrossfadeDuration
Coord->>Audio: update PlaybackController.crossfade_config
IPC-->>Store: AppSettings snapshot
Audio->>Audio: when prepared track enters overlap window
Audio->>Audio: equal-power mix outgoing tail + incoming head
Audio->>Audio: promote incoming track on overlap completion
Audio-->>Queue: track-transitioned(state, serial, fromSongId, toSongId)
Queue->>Queue: apply authoritative state and reconcile queue/history
Queue->>Coord: setPreloadCandidate(new queue head)
Loading

Reviews (7): Last reviewed commit: "fix(crossfade): guard stale enable respo..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Owner Author

Maintainer acceptance — merge slot 9, strictly after repaired #103

BLOCKED — changes required.

Blocking crossfade correctness issues

  1. render_crossfade_overlap compares track.render_frame and original_audio.samples / channels (source-rate frames) with effective_overlap_frames(..., device_sample_rate, ...) (device/output-rate frames). Any source sample rate different from the output device therefore starts/clamps the overlap in mismatched units, producing an early/late duration and possible premature source exhaustion.

  2. The outgoing and incoming mix_stem_resampled calls share the same stateful ResamplerCache entries, whose key contains only rate pair/channel/chunk. When both tracks need the same sample-rate conversion, one stream mutates the other's sinc history inside each overlap chunk. Crossfade needs separate resampler lanes/state for outgoing and incoming audio.

Normalize both duration/remaining calculations into one frame domain and give each stream independent resampler state. Add a non-native-rate outgoing+incoming test that verifies overlap timing, cursor advancement, and no cross-stream state contamination.

Also rebase onto final #103 and rerun CI; this current head is not mergeable.

devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch 2 times, most recently from 23f8de8 to 88891a0 Compare July 14, 2026 01:14

Copy link
Copy Markdown
Owner Author

Re-review: two blocking crossfade audio-correctness fixes required

Keep this PR stacked after the final #103. The current crossfade path has two correctness defects that must be fixed before any UI acceptance:

  1. Use one frame domain for overlap timing. render_crossfade_overlap currently mixes source-track frame counts (for example track.render_frame and source total frames) with device_sample_rate. This is incorrect whenever source and output rates differ. Define the requested overlap as output/device frames: duration_ms * device_sample_rate / 1000. Clamp it only with quantities expressed in that same output-frame domain—available converted outgoing/incoming frames and remaining output frames. Do not compare 44.1 kHz source frames directly to 48 kHz device frames.
  2. Separate resampler state by stream. The outgoing and incoming tracks can currently share one ResamplerCache entry when their conversion parameters match. A sinc/resampler history is stream state; sharing it contaminates one track with the other. Give outgoing and incoming playback distinct resampler lanes/cache instances (or include an explicit stream role in the key), and ensure those lanes are reset/released on every completion, cancellation, seek, manual play, and device recreation.

Add focused DSP regressions:

  • crossfade a 44.1 kHz source into a 48 kHz device and the inverse; assert overlap duration in output frames is within one output frame of the requested duration and no tail/head frame is skipped or duplicated;
  • use outgoing and incoming streams with identical conversion parameters but distinguishable signals; assert their resampler histories never bleed into one another;
  • retain cancellation/seek/device-recreate tests and prove they clear both resampler lanes.

Then rebase onto the final #103/main chain and run all Rust/frontend/CI checks. Complete a packaged audio manual pass with eligible tracks at mismatched sample rates. Do not merge until the PR reply includes the new test names, output-rate scenarios, and evidence.

@thedavidweng
thedavidweng force-pushed the feat/next-track-gapless-88 branch from bdabe45 to b100cab Compare July 15, 2026 09:22
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 88891a0 to c32a92f Compare July 15, 2026 09:32
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/next-track-gapless-88 branch from 3251ba7 to 30fe2b9 Compare July 15, 2026 20:00
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 1f6007d to d1d5abf Compare July 15, 2026 20:00
@thedavidweng
thedavidweng force-pushed the feat/next-track-gapless-88 branch from 30fe2b9 to 849a83b Compare July 15, 2026 20:43
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from d1d5abf to cf0947a Compare July 15, 2026 20:43
devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/next-track-gapless-88 branch from 849a83b to 9973512 Compare July 15, 2026 21:13
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from dbe353c to 049d783 Compare July 15, 2026 21:18

Copy link
Copy Markdown
Owner Author

Re-review — still blocked: crossfade mixes source and device frame domains, and loses the incoming resampler state

Reviewed at 049d783.

1. Overlap timing still mixes frame domains

render_crossfade_overlap passes outgoing_total_frames, incoming_total_frames, and outgoing_frames_remaining directly from decoded source media into effective_overlap_frames(..., device_sample_rate, ...). The helper therefore compares source-rate counts with a configured duration expressed in output/device frames. The start point and clamp are wrong whenever either track is not at the device rate.

Convert every duration/remaining limit to output frames before calling the helper. Keep ActiveCrossfade::total_frames and equal-power progress in device frames; retain separate source cursors only for source reads.

2. Promotion uses the wrong incoming cursor and drops its resampler history

During overlap, active.incoming_source_frame correctly advances by inc_consumed, but completion calls promote_crossfade_track(..., active.rendered_frames). rendered_frames is device frames, not incoming source frames. In addition, the incoming side is rendered through incoming_resampler_cache, while the post-promotion remainder and later normal callbacks use outgoing_resampler_cache. That discards/switches the incoming sinc history at the seam.

Implement this exact handoff:

  1. Promote using active.incoming_source_frame, not active.rendered_frames.
  2. On successful promotion, swap/transfer the incoming cache into the primary current-track lane before rendering the callback remainder; clear/reset the now-spare incoming lane before a later crossfade. Cancellation, seek, manual play, and device recreation must clear both lanes.
  3. Keep outgoing and incoming lanes distinct during the overlap.
  4. Add focused 44.1→48 kHz and 48→44.1 kHz tests that assert the overlap length in output frames, no skipped/duplicated source cursor positions, and a continuous post-promotion signal. Include distinguishable same-conversion outgoing/incoming signals to prove lane isolation and handoff.

Do not merge based only on same-rate coverage; these defects are in the mismatched-rate path required by the issue.

@thedavidweng
thedavidweng force-pushed the feat/next-track-gapless-88 branch from 9973512 to 50f35f9 Compare July 16, 2026 03:50
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 049d783 to a96c2f7 Compare July 16, 2026 03:50
…n paths

The crossfade render path and both gapless-swap fallback paths applied EQ
but skipped the soft limiter before the fade envelope and peak accumulation,
so EQ-boosted samples could exceed 1.0 and clip the DAC during transitions.
Mirrors the normal render path (EQ → soft limiter → fade → peaks) in all
three EQ process sites.
Validate song ownership before canceling an active crossfade on stem
attach, and complete eq/crossfade/peaks fields in the Playwright mock so
Settings opens without crashing.
Wrong-song attach must reject without canceling active crossfade or
prepared track; same-song attach still cancels and installs stems.
Advance the incoming stem with a dedicated source-frame cursor and give
it its own ResamplerCache so device-frame progress and shared rubato
state cannot corrupt rate conversion during overlap.
render_output_buffer gained a dedicated incoming ResamplerCache; update
phase2/phase3 integration call sites so the full crate test build compiles.
… promotion

Convert source-frame quantities to device (output) frames before calling
effective_overlap_frames so the overlap timing clamp operates in a single
frame domain. Use incoming_source_frame (not rendered_frames) for
promote_crossfade_track so the promoted track render_frame matches the
incoming source cursor. Swap the incoming resampler cache into the primary
lane on promotion to preserve sinc delay history, and clear both lanes on
cancellation/seek/device-recreation.
…n tail

The gapless swap path in the realtime callback applied EQ to the
post-swap remaining buffer but skipped the soft_limit pass that the
normal render path and the crossfade fallback gapless path both apply.
A positive EQ band gain on the transition tail could exceed full-scale
and clip. Add the limiter pass after EQ, matching the other two paths,
with a regression test that feeds above-threshold samples through the
gapless swap and asserts the tail is bounded.
Verify the PeakRing captures post-limiter values (after soft_limit),
not pre-limiter values clamped by sanitize_peak. A track with samples
at 1.0 (above the 0.95 threshold) is rendered; the output and peak ring
must both show the compressed value (< 1.0, > threshold), proving the
peak accumulator runs after the soft limiter in the render pipeline.
promote_crossfade_track replaced the current track but did not bump
transport_generation, unlike perform_gapless_swap. Without the bump, a
delayed playback-position event from the old song (same generation)
could arrive after the new-song snapshot and be accepted by the
frontend generation filter, reverting the clock and queue
reconciliation back to the old song.

Add the bump and a regression test mirroring the gapless swap test.
Update the playback contract doc to list crossfade promotion alongside
gapless swap as a transport_generation-bumping event, and correct the
race-protection note that previously claimed gapless swap does not bump
the generation.
Finding 1 (EQ action shadow): Remove duplicate setEqEnabled/setEqGains/
resetEqGains from settings-overlay.library-actions.ts that shadowed the
state.ts versions. Re-add the Math.max(-12, Math.min(12, g)) clamp and
current.every() no-op guard to setEqGains in SettingsOverlay.state.ts,
plus the optimistic settingsStore.patchAppSettings update with rollback
on failure for all three EQ actions. Move EQ tests from
library-actions.test.ts to state.test.ts with assertions for the new
clamping, no-op guard, and patchAppSettings behavior.

Finding 2 (duplicate test): Remove the duplicate 'cleans up timer and
cancels polling on unmount' test block from PeakMeter.test.tsx so the
test appears only once.

Findings 3 and 4 (output.rs frame/sample mismatch and soft limiter
regression) were already fixed in prior commits on this branch.

Update CHANGELOG.md with the fixes.
The crossfade branch added a duplicate "eq" section in both en.json and
zh-CN.json (at line 360) while the same key already existed later in each
file (at line 430) with more complete content including band labels.
JSON parsers keep only the last occurrence, so the earlier duplicate was
silently ignored. Removing the duplicate ensures the complete EQ locale
section is the only one present.
cancel_crossfade_and_prepared now also clears pending_transition_out so
that a stale gapless/crossfade transition (A->B) is never emitted to the
frontend after the user manually loads an unrelated song C. Previously,
start_track_loading and clear_track cancelled the active crossfade and
prepared track but left the pending transition in place, allowing the
position emitter to drain it with a snapshot of the newly-loaded song
at the new transport_generation — bypassing the frontend staleness
guard and corrupting the queue/history reconciliation.

Also corrects the inaccurate rationale comment in session.ts that
claimed the gapless swap does not bump transport_generation.
The soft limiter is now gated on EQ activity (is_fully_bypassed). The
peak_ring_captures_post_limiter_values test used a default-disabled
EqProcessor, so the limiter was skipped and the 1.0 samples were not
clipped. Enable the EQ in the test so the limiter is active.
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 5046ec5 to ce496d5 Compare July 17, 2026 11:04
… feat/crossfade-playback-89

# Conflicts:
#	src-tauri/src/audio/output.rs
Merge brought in the gapless branch's test (snake_case fields, two-arg
onTrackTransitioned) but the crossfade branch's implementation uses
camelCase (via serde rename_all) and passes the whole event object.
Update the test to match the crossfade event shape.
The test used `instrumental` which doesn't exist on the StemVolumes
interface (vocals/drums/bass/other). This caused a tsc build failure in CI.
The soft limiter is gated on EQ activity (is_fully_bypassed). The
gapless_swap_tail_applies_soft_limiter test used a default-disabled
EqProcessor, so the limiter was skipped and the 1.0 tail samples were
not compressed. Enable the EQ in the test so the limiter is active,
matching the fix applied to peak_ring_captures_post_limiter_values in
ce496d5.
@thedavidweng
thedavidweng deleted the branch feat/next-track-gapless-88 July 18, 2026 08:02

Copy link
Copy Markdown
Owner Author

Status review — closed without merge; no LGTM possible

This PR was closed unmerged at head cb8fa6eb7d9964d4580c3bfece415452f0f59fd1, while #103 was merged separately.

If crossfade remains desired, reopen only after rebuilding the work on the repaired current main (or create a clean replacement PR). Do not continue from the old #103 base: the repository now contains a different merged EQ/Peak/gapless combination.

The replacement must retain the reviewed crossfade fixes, present a crossfade-only diff, run CI/Packaging, and complete manual audible transition/pause/seek/device-change acceptance before LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant