Skip to content

Commit a40cb0c

Browse files
authored
Merge pull request #5 from mxggle/feature/current-changes-20260311
Release 0.9.0: AI settings overhaul, shadowing simplification, and waveform loading improvements
2 parents 8c08a04 + d311f93 commit a40cb0c

26 files changed

Lines changed: 2172 additions & 1127 deletions

.claude/settings.local.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
"allow": [
44
"Bash(find:*)",
55
"Bash(lsof:*)",
6-
"Bash(npm run build)"
6+
"Bash(npm run build)",
7+
"Bash(npx tsc --noEmit)",
8+
"Bash(grep -rn \"SharedArrayBuffer\\\\|crossOriginIsolated\\\\|COOP\\\\|COEP\" /Users/szy/CascadeProjects/modern-ab-loop/ --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.json\" 2>/dev/null | head -10)",
9+
"Bash(grep -rn \"audio/wav\\\\|audio/mp3\\\\|audio/mp4\\\\|audio/webm\\\\|audio/ogg\\\\|audio/flac\\\\|type.*audio\" /Users/szy/CascadeProjects/modern-ab-loop/src/ --include=\"*.ts\" --include=\"*.tsx\" 2>/dev/null | head -20)",
10+
"Bash(grep -rn \"file.*size\\\\|maxFileSize\\\\|25MB\\\\|25 MB\\\\|fileSize\\\\|MAX_SIZE\" /Users/szy/CascadeProjects/modern-ab-loop/src/ --include=\"*.ts\" --include=\"*.tsx\" 2>/dev/null | head -10)",
11+
"Bash(grep -rn \"local.*server\\\\|localhost\\\\|127\\\\.0\\\\.0\\\\.1\\\\|http://\\\\|proxy\\\\|API_URL\\\\|BASE_URL\\\\|VITE_\" /Users/szy/CascadeProjects/modern-ab-loop/src/ --include=\"*.ts\" --include=\"*.tsx\" 2>/dev/null | grep -v \"//.*http\\\\|node_modules\" | head -20)",
12+
"Bash(grep -rn \"worker\\\\|Worker\" /Users/szy/CascadeProjects/modern-ab-loop/src/ --include=\"*.ts\" --include=\"*.tsx\" 2>/dev/null | grep -v \"node_modules\\\\|WorkerController\\\\|service-worker\\\\|ServiceWorker\" | head -15)",
13+
"Bash(grep -rn \"sampleRate\\\\|48000\\\\|16000\\\\|44100\" /Users/szy/CascadeProjects/modern-ab-loop/src/ --include=\"*.ts\" --include=\"*.tsx\" 2>/dev/null | head -15)"
714
]
815
}
916
}

CHANGELOG.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.9.0] - 2026-03-12
11+
12+
### Added
13+
- **Playback controls**:
14+
- Added a dedicated "play from start" control in the player UI
15+
- Added safer pending-play handling so media can begin automatically once the element is ready
16+
- **Transcription workflow**:
17+
- Added loop-range transcription so the active A-B selection can be sent directly for transcription
18+
- Added an explicit full-range transcription action and cancel controls for long-running jobs
19+
- **Waveform processing**:
20+
- Added cached waveform analysis utilities for background generation and progressive loading
21+
- Added large-file waveform loading feedback with analysis progress states
22+
23+
### Changed
24+
- **AI settings overhaul**:
25+
- Rebuilt the AI settings page into clearer defaults, providers, and transcription sections
26+
- Improved provider selection, model selection, API key handling, and connection testing flows
27+
- Expanded translations for the new AI and transcription settings UI
28+
- **Shadowing workflow simplification**:
29+
- Simplified shadowing playback and recording to a single-track model
30+
- Refined recorder state handling, cleanup behavior, and waveform rendering for shadowing sessions
31+
- **Waveform experience**:
32+
- Reworked waveform loading and rendering to better handle very large local audio files
33+
- Improved waveform caching, zoom behavior, and loading transitions for stored media
34+
- **Player and media state management**:
35+
- Improved playback state synchronization between the media element and the global store
36+
- Reduced noisy current-time updates and tightened media cleanup during unmounts
37+
- **Transcript actions**:
38+
- Refined transcript action buttons and range-aware transcription entry points
39+
40+
### Fixed
41+
- **Playback reliability**:
42+
- Fixed cases where play requests could be dropped before media was ready
43+
- Fixed store playback state drifting out of sync with the actual media element
44+
- **Waveform stability**:
45+
- Fixed large audio waveform loading regressions and reduced failures during background analysis
46+
- Improved recovery and status handling when waveform analysis cannot complete normally
47+
- **Shadowing and storage cleanup**:
48+
- Improved cleanup around shadowing recordings and related persisted media state
49+
- Fixed several edge cases in local media storage and waveform cache updates
50+
1051
## [0.8.1] - 2026-02-17
1152

1253
### Added
@@ -214,7 +255,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
214255
- Layout optimizations for desktop view
215256
- TypeScript and React component structure
216257

217-
[Unreleased]: https://github.com/USERNAME/loopmate/compare/v0.8.1...HEAD
258+
[Unreleased]: https://github.com/USERNAME/loopmate/compare/v0.9.0...HEAD
259+
[0.9.0]: https://github.com/USERNAME/loopmate/compare/v0.8.1...v0.9.0
218260
[0.8.1]: https://github.com/USERNAME/loopmate/compare/v0.8.0...v0.8.1
219261
[0.8.0]: https://github.com/USERNAME/loopmate/compare/v0.7.0...v0.8.0
220262
[0.7.0]: https://github.com/USERNAME/loopmate/compare/v0.6.1...v0.7.0

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"loop-station",
2020
"media-player"
2121
],
22-
"version": "0.8.1",
22+
"version": "0.9.0",
2323
"type": "module",
2424
"scripts": {
2525
"dev": "vite",
@@ -79,4 +79,4 @@
7979
"typescript": "^5.3.0",
8080
"vite": "^5.0.0"
8181
}
82-
}
82+
}

src/components/controls/CombinedControls.tsx

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { useState, useEffect, useMemo } from "react";
1+
import { useState, useEffect } from "react";
22
import { usePlayerStore } from "../../stores/playerStore";
33
import { useTranslation } from "react-i18next";
44
import { formatTime } from "../../utils/formatTime";
5-
import { checkAudioRecordingSupport, getRecordingUnsupportedMessage } from "../../utils/browserCheck";
65
import {
76
Play,
87
Pause,
@@ -20,11 +19,8 @@ import {
2019
Bookmark,
2120
ListMusic,
2221
X,
23-
Mic,
2422
RotateCcw,
2523
} from "lucide-react";
26-
import { useShadowingStore } from "../../stores/shadowingStore";
27-
import { useShadowingRecorder } from "../../hooks/useShadowingRecorder";
2824
import { Slider } from "../ui/slider";
2925
import { Button } from "../ui/button";
3026
import { Plus } from "lucide-react";
@@ -69,18 +65,8 @@ export const CombinedControls = () => {
6965
selectedBookmarkId,
7066
} = usePlayerStore();
7167

72-
const {
73-
isShadowingMode,
74-
setShadowingMode,
75-
isRecording,
76-
} = useShadowingStore();
77-
78-
// Initialize shadowing recorder
79-
useShadowingRecorder();
80-
8168
const [rangeValues, setRangeValues] = useState<[number, number]>([0, 100]);
8269
const [showABControls, setShowABControls] = useState(false);
83-
8470
// Get current media bookmarks for the bookmark button
8571
const bookmarks = getCurrentMediaBookmarks();
8672

@@ -94,20 +80,6 @@ export const CombinedControls = () => {
9480
setRangeValues([(start / duration) * 100, (end / duration) * 100]);
9581
}, [loopStart, loopEnd, duration]);
9682

97-
// Check if audio recording is supported in this browser
98-
const recordingCapabilities = useMemo(() => checkAudioRecordingSupport(), []);
99-
const canRecord = recordingCapabilities.supportsAudioRecording;
100-
101-
// Handle shadowing mode toggle
102-
const handleShadowingToggle = () => {
103-
if (!isShadowingMode && !canRecord) {
104-
const errorMessage = getRecordingUnsupportedMessage(recordingCapabilities);
105-
toast.error(errorMessage, { duration: 5000 });
106-
return;
107-
}
108-
setShadowingMode(!isShadowingMode);
109-
};
110-
11183
// Toggle play/pause
11284
const togglePlayPause = () => {
11385
setIsPlaying(!isPlaying);
@@ -414,23 +386,6 @@ export const CombinedControls = () => {
414386
)}
415387
</button>
416388

417-
{/* Shadowing toggle button */}
418-
<button
419-
onClick={handleShadowingToggle}
420-
className={`p-2.5 rounded-full transition-all duration-150 ${isRecording
421-
? "bg-red-600 text-white animate-pulse"
422-
: isShadowingMode
423-
? "bg-orange-600 text-white hover:bg-orange-700"
424-
: canRecord
425-
? "bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
426-
: "bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-50"
427-
}`}
428-
aria-label={isShadowingMode ? t("shadowing.disable") : t("shadowing.enable")}
429-
title={!canRecord ? "Audio recording is not supported on this device/browser" : (isShadowingMode ? t("shadowing.disable") : t("shadowing.enable"))}
430-
>
431-
<Mic size={18} className="sm:w-[20px] sm:h-[20px]" />
432-
</button>
433-
434389
<button
435390
onClick={seekForward}
436391
className="p-2.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300 transition-colors"

src/components/controls/MobileControls.tsx

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { useState, useMemo } from "react";
1+
import { useState } from "react";
22
import { usePlayerStore } from "../../stores/playerStore";
33
import { useTranslation } from "react-i18next";
4-
import { checkAudioRecordingSupport, getRecordingUnsupportedMessage } from "../../utils/browserCheck";
54

65
import { toast } from "react-hot-toast";
76
import {
@@ -21,18 +20,12 @@ import {
2120
ChevronLeft,
2221
ChevronRight,
2322
ChevronsRight,
24-
Mic,
2523
} from "lucide-react";
2624
import { Button } from "../ui/button";
27-
import { useShadowingStore } from "../../stores/shadowingStore";
28-
import { useShadowingRecorder } from "../../hooks/useShadowingRecorder";
2925

3026
export const MobileControls = () => {
3127
const { t } = useTranslation();
3228

33-
// Initialize shadowing recorder
34-
useShadowingRecorder();
35-
3629
const {
3730
isPlaying,
3831
duration,
@@ -69,20 +62,10 @@ export const MobileControls = () => {
6962
toggleLooping,
7063
} = usePlayerStore();
7164

72-
const {
73-
isShadowingMode,
74-
setShadowingMode,
75-
isRecording,
76-
} = useShadowingStore();
77-
7865
const [showSpeedControls, setShowSpeedControls] = useState(false);
7966
const [showVolumeDrawer, setShowVolumeDrawer] = useState(false);
8067
const [showPlaylistDrawer, setShowPlaylistDrawer] = useState(false);
8168

82-
// Check if audio recording is supported in this browser
83-
const recordingCapabilities = useMemo(() => checkAudioRecordingSupport(), []);
84-
const canRecord = recordingCapabilities.supportsAudioRecording;
85-
8669
// Get current media bookmarks for the bookmark button
8770
const bookmarks = getCurrentMediaBookmarks();
8871

@@ -236,19 +219,6 @@ export const MobileControls = () => {
236219

237220
// Clear loop points function moved to waveform footer
238221

239-
// Handle shadowing mode toggle
240-
const handleShadowingToggle = () => {
241-
// If trying to enable shadowing mode, check browser support first
242-
if (!isShadowingMode && !canRecord) {
243-
const errorMessage = getRecordingUnsupportedMessage(recordingCapabilities);
244-
toast.error(errorMessage, { duration: 5000 });
245-
return;
246-
}
247-
248-
// Toggle shadowing mode
249-
setShadowingMode(!isShadowingMode);
250-
};
251-
252222
// Note: Loop jump functions removed as they're not used in the mobile interface
253223

254224
return (
@@ -293,23 +263,6 @@ export const MobileControls = () => {
293263
)}
294264
</button>
295265

296-
{/* Shadowing toggle button */}
297-
<button
298-
onClick={handleShadowingToggle}
299-
className={`p-3 rounded-full transition-all duration-150 ${isRecording
300-
? "bg-red-600 text-white animate-pulse"
301-
: isShadowingMode
302-
? "bg-orange-600 text-white hover:bg-orange-700"
303-
: canRecord
304-
? "bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
305-
: "bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-50"
306-
}`}
307-
aria-label={isShadowingMode ? t("shadowing.disable") : t("shadowing.enable")}
308-
title={!canRecord ? "Audio recording is not supported on this device/browser" : undefined}
309-
>
310-
<Mic size={24} />
311-
</button>
312-
313266
<button
314267
onClick={seekForward}
315268
className="p-3 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"

src/components/layout/AppLayout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, Dispatch, SetStateAction } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { useTranslation } from "react-i18next";
44
import { usePlayerStore } from "../../stores/playerStore";
5+
import { useShallow } from "zustand/react/shallow";
56
import { SettingsDrawer } from "./SettingsDrawer";
67
import { Moon, Sun, Info, Settings, Layout, Eye, EyeOff, Music, Video, Youtube } from "lucide-react";
78
import * as Dialog from "@radix-ui/react-dialog";
@@ -31,7 +32,23 @@ export const AppLayout = ({
3132
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState(false);
3233
const [isLayoutPopoverOpen, setIsLayoutPopoverOpen] = useState(false);
3334

34-
const { currentFile, currentYouTube, theme, setTheme, seekStepSeconds, seekSmallStepSeconds } = usePlayerStore();
35+
const {
36+
currentFile,
37+
currentYouTube,
38+
theme,
39+
setTheme,
40+
seekStepSeconds,
41+
seekSmallStepSeconds,
42+
} = usePlayerStore(
43+
useShallow((state) => ({
44+
currentFile: state.currentFile,
45+
currentYouTube: state.currentYouTube,
46+
theme: state.theme,
47+
setTheme: state.setTheme,
48+
seekStepSeconds: state.seekStepSeconds,
49+
seekSmallStepSeconds: state.seekSmallStepSeconds,
50+
}))
51+
);
3552

3653
// Toggle theme
3754
const toggleTheme = () => {

0 commit comments

Comments
 (0)