Skip to content

Commit 43e1843

Browse files
committed
control room app vu meters following live music from logic pro
1 parent 020f6bd commit 43e1843

20 files changed

Lines changed: 144 additions & 2431 deletions

AIplayer/AIplayer/Source/PluginProcessor.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,13 @@ void AIplayerAudioProcessor::oscMessageReceived (const juce::OSCMessage& message
802802
{
803803
logMessage(LogLevel::Info, "Sent UUID assignment confirmation for track " + this->logicTrackUUID);
804804
}
805+
806+
// Start RMS telemetry timer if we have a port and it's not already running
807+
if (portState == PortState::Bound && !isTimerRunning())
808+
{
809+
logMessage(LogLevel::Info, "Starting RMS telemetry timer after UUID assignment");
810+
startTimerHz(24); // 24 Hz for smooth VU meter updates
811+
}
805812
}
806813
else if (!matched)
807814
{
@@ -1258,7 +1265,8 @@ void AIplayerAudioProcessor::sendRMSTelemetry()
12581265
{
12591266
// Ensure connected flag is set
12601267
senderConnected = true;
1261-
// Don't log successful sends - they happen 24 times per second!
1268+
// Log RMS sends for debugging (now only 1 Hz)
1269+
logMessage(LogLevel::Info, "Sent RMS: " + addressPatternString + " [" + idToSend + "] = " + juce::String(rmsValue));
12621270
}
12631271
}
12641272
catch (const std::bad_alloc& e)
@@ -1474,6 +1482,7 @@ bool AIplayerAudioProcessor::bindToAssignedPort(int port)
14741482
}
14751483

14761484
// Now we can start sending RMS telemetry
1485+
logMessage(LogLevel::Info, "Starting RMS telemetry timer at 24 Hz");
14771486
startTimerHz(24); // 24 Hz for smooth VU meter updates
14781487

14791488
return true;

ChattyChannels/ChattyChannels/Models/AudioLevel.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ public struct AudioLevel: Identifiable {
7575

7676
/// Convert current RMS value to dBFS.
7777
public var dbfsValue: Float {
78-
guard rmsValue > 0 else { return -Float.infinity } // or a very low dB value like -120.0
79-
return 20 * log10(rmsValue)
78+
guard rmsValue > 0 else { return -120.0 } // Return -120 dB for silence instead of -infinity
79+
let db = 20 * log10(rmsValue)
80+
// Clamp to reasonable range to avoid extreme values
81+
return max(-120.0, min(6.0, db))
8082
}
8183

8284
/// Convert raw value to dB (v0.6 compatibility)

ChattyChannels/ChattyChannels/OSCService.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,13 +417,29 @@ public final class OSCService: ObservableObject {
417417
return
418418
}
419419

420+
// Apply 10x amplification to the RMS value
421+
let amplifiedRMS = amplifyRMS(rmsValue, amplificationFactor: 10.0)
422+
423+
// Commented out verbose logging to reduce console spam at 24 Hz
424+
// print("[OSCService] Processing identified RMS: \(logicTrackUUID) = \(rmsValue) (amplified to \(amplifiedRMS))")
425+
420426
// Dispatch to MainActor as LevelMeterService.updateLevel is @MainActor isolated
421427
Task { @MainActor in
422-
levelMeterService.updateLevel(logicTrackUUID: logicTrackUUID, rmsValue: rmsValue)
423-
// Removed verbose logging - these messages come in continuously
428+
levelMeterService.updateLevel(logicTrackUUID: logicTrackUUID, rmsValue: amplifiedRMS)
429+
// print("[OSCService] Updated LevelMeterService for \(logicTrackUUID)")
424430
}
425431
}
426432

433+
/// Amplifies an RMS value by a given factor and clamps to 0.0-1.0 range
434+
/// - Parameters:
435+
/// - rmsValue: The original RMS value
436+
/// - amplificationFactor: The factor to multiply the RMS value by
437+
/// - Returns: The amplified and clamped RMS value
438+
private func amplifyRMS(_ rmsValue: Float, amplificationFactor: Float) -> Float {
439+
let amplified = rmsValue * amplificationFactor
440+
return min(1.0, max(0.0, amplified))
441+
}
442+
427443
/// Processes an incoming port-based RMS message.
428444
/// - Parameters:
429445
/// - tempID: The temporary instance ID from the plugin.

ChattyChannels/ChattyChannels/Services/LevelMeterService.swift

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public class LevelMeterService: ObservableObject {
1717
/// Published current track name for compatibility with v0.6 UI
1818
@Published public var currentTrack: String = "Master Bus"
1919

20+
/// Dedicated published property for TR1 (kick drum) to ensure VU meter updates
21+
@Published public var tr1Level = AudioLevel(id: "TR1", rmsValue: 0.0, peakRmsValue: 0.0, trackName: "Kick")
22+
2023
/// Compatibility properties for v0.6 UI - mapping between old channel-based and new track-based system
2124
@Published public var leftChannel = AudioLevel(id: "LEFT", rmsValue: 0.0, peakRmsValue: 0.0, trackName: "Master Bus")
2225
@Published public var rightChannel = AudioLevel(id: "RIGHT", rmsValue: 0.0, peakRmsValue: 0.0, trackName: "Master Bus")
@@ -36,6 +39,7 @@ public class LevelMeterService: ObservableObject {
3639
logger.info("LevelMeterService initialized")
3740
// Set up default master bus audio level
3841
audioLevels[masterBusUUID] = AudioLevel(id: masterBusUUID, rmsValue: 0.0, peakRmsValue: 0.0, trackName: "Master Bus")
42+
audioLevels["TR1"] = tr1Level // Initialize TR1 in the dictionary
3943
startPeakDecayTimer()
4044

4145
// Start a timer to update the v0.6 compatibility properties from the master bus data
@@ -49,7 +53,7 @@ public class LevelMeterService: ObservableObject {
4953
/// - rmsValue: The new RMS value (0.0 to 1.0).
5054
/// - peakRmsValueOverride: Optional peak value if provided directly by the source.
5155
public func updateLevel(logicTrackUUID: String, rmsValue: Float, peakRmsValueOverride: Float? = nil) {
52-
var level = audioLevels[logicTrackUUID] ?? AudioLevel(id: logicTrackUUID)
56+
var level = audioLevels[logicTrackUUID] ?? AudioLevel(id: logicTrackUUID, rmsValue: 0.0, peakRmsValue: 0.0, trackName: logicTrackUUID)
5357

5458
level.rmsValue = max(0.0, min(1.0, rmsValue)) // Clamp value
5559

@@ -62,6 +66,22 @@ public class LevelMeterService: ObservableObject {
6266

6367
audioLevels[logicTrackUUID] = level
6468

69+
// CRITICAL FIX: Update the dedicated TR1 property when TR1 data arrives
70+
if logicTrackUUID == "TR1" {
71+
tr1Level = level
72+
// Commented out to reduce console spam at 24 Hz
73+
// logger.info("Updated TR1 dedicated property: RMS \(level.rmsValue, privacy: .public), Peak \(level.peakRmsValue, privacy: .public)")
74+
}
75+
76+
// Force SwiftUI to detect the change by replacing the entire dictionary
77+
let newDict = audioLevels
78+
audioLevels = newDict
79+
80+
// Force objectWillChange to fire to ensure SwiftUI updates
81+
DispatchQueue.main.async {
82+
self.objectWillChange.send()
83+
}
84+
6585
// If this is the master bus, update the current track name for UI compatibility
6686
if logicTrackUUID == masterBusUUID {
6787
if let trackName = level.trackName {
@@ -72,8 +92,9 @@ public class LevelMeterService: ObservableObject {
7292
updateCompatibilityChannels(from: level)
7393
}
7494

75-
// Removed excessive debug logging - this was generating thousands of log entries per second
76-
// logger.debug("Updated level for \(logicTrackUUID, privacy: .public): RMS \(level.rmsValue, privacy: .public), Peak \(level.peakRmsValue, privacy: .public)")
95+
// Commented out verbose logging to reduce console spam at 24 Hz
96+
// logger.info("Updated level for \(logicTrackUUID, privacy: .public): RMS \(level.rmsValue, privacy: .public), Peak \(level.peakRmsValue, privacy: .public)")
97+
7798
}
7899

79100
/// Updates the v0.6 compatibility channel properties from the master bus data
@@ -82,9 +103,44 @@ public class LevelMeterService: ObservableObject {
82103
rightChannel = AudioLevel(id: "RIGHT", rmsValue: masterLevel.rmsValue, peakRmsValue: masterLevel.peakRmsValue, trackName: masterLevel.trackName)
83104
}
84105

106+
/// Updates the master bus level with the sum of all track levels
107+
private func updateMasterBusLevel() {
108+
// Calculate RMS sum of all tracks (excluding master bus itself)
109+
var sumOfSquares: Float = 0.0
110+
var peakValue: Float = 0.0
111+
var trackCount = 0
112+
113+
for (uuid, level) in audioLevels {
114+
if uuid != masterBusUUID {
115+
// RMS values are summed as power (squared values)
116+
sumOfSquares += level.rmsValue * level.rmsValue
117+
peakValue = max(peakValue, level.peakRmsValue)
118+
trackCount += 1
119+
}
120+
}
121+
122+
// Calculate combined RMS (square root of sum of squares)
123+
let combinedRMS = trackCount > 0 ? sqrt(sumOfSquares) : 0.0
124+
125+
// Apply 10x amplification and clamp to valid range
126+
let amplifiedRMS = min(1.0, combinedRMS * 10.0)
127+
let amplifiedPeak = min(1.0, peakValue * 10.0)
128+
129+
// Update master bus level
130+
var masterLevel = audioLevels[masterBusUUID] ?? AudioLevel(id: masterBusUUID, rmsValue: 0.0, peakRmsValue: 0.0, trackName: "Master Bus")
131+
masterLevel.rmsValue = amplifiedRMS
132+
masterLevel.peakRmsValue = max(masterLevel.peakRmsValue, amplifiedPeak)
133+
masterLevel.lastUpdateTime = Date()
134+
135+
audioLevels[masterBusUUID] = masterLevel
136+
137+
// Update compatibility channels
138+
updateCompatibilityChannels(from: masterLevel)
139+
}
140+
85141
/// Starts a timer to update the v0.6 compatibility properties from the master bus data
86142
private func startCompatibilityTimer() {
87-
Timer.scheduledTimer(withTimeInterval: 1.0/30.0, repeats: true) { [weak self] _ in
143+
Timer.scheduledTimer(withTimeInterval: 1.0/24.0, repeats: true) { [weak self] _ in
88144
guard let self = self else { return }
89145
Task { @MainActor in
90146
if let masterLevel = self.audioLevels[self.masterBusUUID] {
@@ -121,6 +177,11 @@ public class LevelMeterService: ObservableObject {
121177
audioLevels[logicTrackUUID]?.peakRmsValue = audioLevels[logicTrackUUID]?.rmsValue ?? 0.0
122178
logger.info("Peak value reset for track: \(logicTrackUUID, privacy: .public)")
123179

180+
// If this is TR1, also update the dedicated property
181+
if logicTrackUUID == "TR1", let level = audioLevels[logicTrackUUID] {
182+
tr1Level = level
183+
}
184+
124185
// If this is the master bus, also update the compatibility channels
125186
if logicTrackUUID == masterBusUUID {
126187
leftChannel.peakValue = leftChannel.value
@@ -140,7 +201,7 @@ public class LevelMeterService: ObservableObject {
140201
/// Starts a timer to handle peak value decay.
141202
private func startPeakDecayTimer() {
142203
peakDecayTimer?.invalidate() // Invalidate existing timer if any
143-
peakDecayTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { [weak self] _ in
204+
peakDecayTimer = Timer.scheduledTimer(withTimeInterval: 1.0/24.0, repeats: true) { [weak self] _ in
144205
guard let strongSelf = self else { return }
145206

146207
Task { @MainActor in
@@ -155,16 +216,14 @@ public class LevelMeterService: ObservableObject {
155216
}
156217
strongSelf.audioLevels[uuid] = level
157218

158-
// If this is the master bus, also update compatibility channels
159-
if uuid == strongSelf.masterBusUUID {
160-
strongSelf.leftChannel.peakValue *= strongSelf.peakDecayRate
161-
strongSelf.rightChannel.peakValue *= strongSelf.peakDecayRate
162-
if strongSelf.leftChannel.peakValue < 0.001 {
163-
strongSelf.leftChannel.peakValue = 0.0
164-
}
165-
if strongSelf.rightChannel.peakValue < 0.001 {
166-
strongSelf.rightChannel.peakValue = 0.0
167-
}
219+
// Update TR1 dedicated property if needed
220+
if uuid == "TR1" {
221+
strongSelf.tr1Level = level
222+
}
223+
224+
// Update master bus level after individual track decay
225+
if uuid != strongSelf.masterBusUUID {
226+
strongSelf.updateMasterBusLevel()
168227
}
169228
}
170229
}

ChattyChannels/ChattyChannels/Services/OSCListener.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ public class OSCListener: ObservableObject, @unchecked Sendable {
202202
return
203203
}
204204

205+
// Commented out to reduce console spam at 24 Hz
206+
// logger.info("Received identified RMS: \(logicTrackUUID) = \(rmsValue)")
207+
205208
oscService.processIdentifiedRMS(logicTrackUUID: logicTrackUUID, rmsValue: rmsValue)
206209
}
207210

ChattyChannels/ChattyChannels/Views/VUMeter/SingleMeterView.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,23 @@ struct SingleMeterView: View {
6060
activeColor: .red,
6161
inactiveColor: .red.opacity(0.15)
6262
)
63-
.position(x: geometry.size.width * 0.77, // Use geometry
64-
y: geometry.size.height * 0.3) // Use geometry
63+
.position(x: geometry.size.width * 0.88, // Moved slightly left
64+
y: geometry.size.height * 0.55) // Moved down considerably
6565
}
6666
// Removed .clipped()
6767
.onReceive(timer) { _ in
6868
updateMeter()
6969
}
70+
.onChange(of: audioLevel.rmsValue) { _, newValue in
71+
// Force update when RMS value changes
72+
// Commented out to reduce console spam at 24 Hz
73+
// print("[VU Meter] RMS value changed to: \(newValue) for \(audioLevel.id)")
74+
updateMeter()
75+
}
76+
.onAppear {
77+
// Initialize needle position on appear
78+
updateMeter()
79+
}
7080
}
7181
}
7282

@@ -80,9 +90,19 @@ struct SingleMeterView: View {
8090
let dbValue = audioLevel.dbfsValue
8191
let constrainedDb = min(max(dbValue, -20), 3) // Standard VU range -20dB to +3dB
8292

93+
// Debug logging - commented out to reduce console spam at 24 Hz
94+
// if audioLevel.id == "TR1" {
95+
// print("[VU Meter TR1] RMS: \(audioLevel.rmsValue), dB: \(dbValue), constrained: \(constrainedDb)")
96+
// }
97+
8398
// Map dB to rotation angle
8499
needleTarget = mapDbToRotation(db: constrainedDb)
85100

101+
// Debug needle position - commented out to reduce console spam at 24 Hz
102+
// if audioLevel.id == "TR1" {
103+
// print("[VU Meter TR1] Needle target: \(needleTarget)°, current: \(needleRotation)°")
104+
// }
105+
86106
// Apply VU meter ballistics (300ms integration time)
87107
// This coefficient adjusted for 24 Hz timing (was 0.15 for 60 Hz)
88108
needleRotation += (needleTarget - needleRotation) * 0.35

ChattyChannels/ChattyChannels/Views/VUMeter/VUMeterView.swift

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,49 +14,9 @@ struct VUMeterView: View {
1414
/// The service that provides audio level data
1515
@ObservedObject var levelService: LevelMeterService
1616

17-
// Get the most recently active track (for v0.7 OSC integration)
18-
// This allows VU meters to automatically follow whichever track is sending RMS data
19-
private var activeTrackUUID: String {
20-
// Find the track with the most recent update time
21-
let sortedTracks = levelService.audioLevels.sorted { lhs, rhs in
22-
let lhsTime = lhs.value.lastUpdateTime
23-
let rhsTime = rhs.value.lastUpdateTime
24-
return lhsTime > rhsTime
25-
}
26-
27-
// Return the most recently updated track UUID, or a default
28-
return sortedTracks.first?.key ?? "NO_ACTIVE_TRACK"
29-
}
30-
31-
// Computed properties for compatibility with v0.7 data model
32-
private var leftChannelBinding: Binding<AudioLevel> {
33-
Binding<AudioLevel>(
34-
get: {
35-
// Get the AudioLevel for the most active track or create a default one
36-
let activeLevel = levelService.audioLevels[activeTrackUUID] ??
37-
AudioLevel(id: activeTrackUUID, rmsValue: 0.0, peakRmsValue: 0.0, trackName: "No Active Track")
38-
39-
// For demo and backward compatibility - we'll treat the same level as both L and R channels
40-
return activeLevel
41-
},
42-
set: { newLevel in
43-
// We don't actually modify the level here as it's managed by LevelMeterService
44-
}
45-
)
46-
}
47-
48-
private var rightChannelBinding: Binding<AudioLevel> {
49-
// We're using the same binding for both channels for now
50-
// In a future version, we could map to different tracks for L/R channels
51-
return leftChannelBinding
52-
}
53-
54-
// Current track name - defaults to "No Active Track" if not available
17+
// Current track name - always shows "Master Bus"
5518
private var currentTrackName: String {
56-
if let track = levelService.audioLevels[activeTrackUUID]?.trackName {
57-
return track
58-
}
59-
return "No Active Track"
19+
return "Master Bus"
6020
}
6121

6222
var body: some View {
@@ -75,12 +35,12 @@ struct VUMeterView: View {
7535

7636
// Meters with spacing between them matching the right padding
7737
HStack(spacing: 26) { // Increased spacing to match right padding
78-
// Left channel meter
79-
SingleMeterView(audioLevel: leftChannelBinding)
38+
// Left channel meter - now using direct binding to tr1Level
39+
SingleMeterView(audioLevel: $levelService.tr1Level)
8040
.frame(width: 112, height: 69) // Re-applying frame, 112, 69
8141

82-
// Right channel meter
83-
SingleMeterView(audioLevel: rightChannelBinding)
42+
// Right channel meter - also using tr1Level (both meters show kick drum)
43+
SingleMeterView(audioLevel: $levelService.tr1Level)
8444
.frame(width: 112, height: 69) // Re-applying frame, 112, 69
8545
}
8646
.frame(width: 250) // Adjusted width for new meter size (112 + 26 + 112)
@@ -127,20 +87,19 @@ struct VUMeterView: View {
12787
struct VUMeterPreviewWrapper: View {
12888
@StateObject var levelServiceInstance = LevelMeterService()
12989

130-
init(populateData: Bool = true) {
131-
if populateData {
132-
let kickTrackUUID = "KICK_TRACK_UUID"
133-
levelServiceInstance.audioLevels[kickTrackUUID] = AudioLevel(id: kickTrackUUID, rmsValue: 0.6, peakRmsValue: 0.9, trackName: "Kick")
134-
}
135-
}
136-
13790
var body: some View {
13891
VUMeterView(levelService: levelServiceInstance)
92+
.onAppear {
93+
// Set up TR1 with test data
94+
Task { @MainActor in
95+
levelServiceInstance.updateLevel(logicTrackUUID: "TR1", rmsValue: 0.6, peakRmsValueOverride: 0.9)
96+
}
97+
}
13998
}
14099
}
141100

142101
#Preview("VU Meter View - Kick Track", traits: .sizeThatFitsLayout) {
143-
VUMeterPreviewWrapper(populateData: true)
102+
VUMeterPreviewWrapper()
144103
.frame(width: 300, height: 150)
145104
.padding()
146105
.background(Color.gray.opacity(0.2))

0 commit comments

Comments
 (0)