Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 95 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: CI
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
XCODE_PROJECT: VoiceAssistant.xcodeproj
XCODE_SCHEME: VoiceAssistant
jobs:
build-and-test:
name: Build & Test
strategy:
fail-fast: false
matrix:
include:
# For information about the runner images and the software each image contains,
# refer to: https://github.com/actions/runner-images
- os: macos-15
xcode: 16.2
sdk: xrsimulator2.2
- os: macos-15
xcode: 16.2
sdk: macosx15.2
- os: macos-15
xcode: 16.2
sdk: iphonesimulator18.2
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4

## Currently there are no tests that require the server to be running
## Uncomment this when adding tests that require the server to be running
## ------------------------------------------------------------------------
# - name: Install LiveKit Server
# run: brew install livekit
# - name: Run LiveKit Server
# run: livekit-server --dev &

- name: Setup Xcode ${{ matrix.xcode }}
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}

- name: Print Versions
run: |
xcodebuild -version
xcrun swift --version

- name: Cache
uses: irgaly/xcode-cache@v1
with:
key: xcode-${{ matrix.xcode }}-${{ matrix.sdk }}-${{ github.sha }}
restore-keys: xcode-${{ matrix.xcode }}-${{ matrix.sdk }}-

- name: Write Default Config
run: cd VoiceAssistant && cat .env.example.xcconfig > .env.xcconfig

- name: Build for Testing
run: |
set -o pipefail && xcodebuild build-for-testing \
-project ${{ env.XCODE_PROJECT }} \
-scheme ${{ env.XCODE_SCHEME }} \
-sdk ${{ matrix.sdk }} \
CODE_SIGN_IDENTITY="" | xcbeautify --renderer github-actions

## This project does not currently have tests
## Uncomment this once they are added
## ------------------------------------------------------------------------
# - name: Run Tests
# run: |
# set -o pipefail && xcodebuild test-without-building \
# -project ${{ env.XCODE_PROJECT }} \
# -scheme ${{ env.XCODE_SCHEME }} \
# --sdk ${{ matrix.sdk }} \
# -parallel-testing-enabled YES \
# -parallel-testing-worker-count 4 | xcbeautify --renderer github-actions

lint:
name: Lint
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: SwiftFormat Lint
run: swiftformat --lint . --reporter github-actions-log
# Comes pre-installed on macOS runners
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6.0
1 change: 1 addition & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--ifdef no-indent
22 changes: 11 additions & 11 deletions VoiceAssistant/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,32 @@ import LiveKitKrispNoiseFilter
struct ContentView: View {
@StateObject private var room = Room()

// Krisp is available only on iOS and macOS right now
// Krisp is also a feature of LiveKit Cloud, so if you're using open-source / self-hosted you should remove this
#if os(iOS) || os(macOS)
// Krisp is available only on iOS and macOS right now
// Krisp is also a feature of LiveKit Cloud, so if you're using open-source / self-hosted you should remove this
#if os(iOS) || os(macOS)
private let krispProcessor = LiveKitKrispNoiseFilter()
#endif
#endif

init() {
#if os(iOS) || os(macOS)
#if os(iOS) || os(macOS)
AudioManager.shared.capturePostProcessingDelegate = krispProcessor
#endif
#endif
}

var body: some View {
VStack(spacing: 24) {
StatusView()
.frame(height: 256)
.frame(maxWidth: 512)

ControlBar()
}
.padding()
.environmentObject(room)
.onAppear {
#if os(iOS) || os(macOS)
#if os(iOS) || os(macOS)
room.add(delegate: krispProcessor)
#endif
#endif
}
}
}
35 changes: 18 additions & 17 deletions VoiceAssistant/ControlBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import SwiftUI
/// The ControlBar component handles connection, disconnection, and audio controls
/// You can customize this component to fit your app's needs
struct ControlBar: View {

// We injected these into the environment in VoiceAssistantApp.swift and ContentView.swift
@EnvironmentObject private var tokenService: TokenService
@EnvironmentObject private var room: Room
Expand All @@ -18,19 +17,18 @@ struct ControlBar: View {
// Namespace for view transitions
@Namespace private var animation


// These are the overall configurations for this component, based on current app state
private enum Configuration {
case disconnected, connected, transitioning
}

private var currentConfiguration: Configuration {
if isConnecting || isDisconnecting {
return .transitioning
.transitioning
} else if room.connectionState == .disconnected {
return .disconnected
.disconnected
} else {
return .connected
.connected
}
}

Expand All @@ -56,7 +54,7 @@ struct ControlBar: View {
} icon: {
Image(
systemName: room.localParticipant.isMicrophoneEnabled()
? "mic" : "mic.slash")
? "mic" : "mic.slash")
}
.labelStyle(.iconOnly)
.frame(width: 44, height: 44)
Expand All @@ -66,17 +64,17 @@ struct ControlBar: View {

LocalAudioVisualizer(track: room.localParticipant.firstAudioTrack)
.frame(height: 44)
.id(room.localParticipant.firstAudioTrack?.id ?? "no-track") // Force the component to re-render when the track changes
#if !os(macOS)
// Add extra padding to the visualizer if there's no third button
.id(room.localParticipant.firstAudioTrack?.id ?? "no-track") // Force the component to re-render when the track changes
#if !os(macOS)
// Add extra padding to the visualizer if there's no third button
.padding(.trailing, 8)
#endif
#endif

#if os(macOS)
#if os(macOS)
// Only on macOS, show the audio device selector
// iOS/visionOS users need to use their control center to change the audio input device
AudioDeviceSelector()
#endif
#endif
}
.background(.primary.opacity(0.1))
.cornerRadius(8)
Expand Down Expand Up @@ -114,7 +112,8 @@ struct ControlBar: View {
) {
// Connect to the room and enable the microphone
try await room.connect(
url: connectionDetails.serverUrl, token: connectionDetails.participantToken)
url: connectionDetails.serverUrl, token: connectionDetails.participantToken
)
try await room.localParticipant.setMicrophone(enabled: true)
} else {
print("Failed to fetch connection details")
Expand Down Expand Up @@ -149,18 +148,20 @@ private struct LocalAudioVisualizer: View {
wrappedValue: AudioProcessor(
track: track,
bandCount: 9,
isCentered: false))
isCentered: false
))
}

public var body: some View {
HStack(spacing: 3) {
ForEach(0..<9, id: \.self) { index in
ForEach(0 ..< 9, id: \.self) { index in
Rectangle()
.fill(.primary)
.frame(width: 2)
.frame(maxHeight: .infinity)
.scaleEffect(
y: max(0.05, CGFloat(audioProcessor.bands[index])), anchor: .center)
y: max(0.05, CGFloat(audioProcessor.bands[index])), anchor: .center
)
}
}
.padding(.vertical, 8)
Expand Down Expand Up @@ -269,7 +270,7 @@ private struct AudioDeviceSelector: View {
// Listen for audio device changes
// Note that this listener is global so can only override it from one spot
// In a more complex app, you may need a different approach
AudioManager.shared.onDeviceUpdate = { manager in
AudioManager.shared.onDeviceUpdate = { _ in
Task { @MainActor in
updateDevices()
}
Expand Down
Loading