@@ -4,13 +4,24 @@ import Accelerate
44
55@_exported import SnapshotTesting
66
7+ /// Specifies the amplitude scaling method for spectrograms.
8+ public enum AmplitudeScale {
9+ /// Linear amplitude scale (raw FFT magnitudes normalized to 0...1)
10+ case linear
11+ /// Logarithmic amplitude scale in decibels with specified dynamic range
12+ /// - Parameter range: The dB range from minimum to maximum (e.g., 120 means -120dB to 0dB)
13+ case logarithmic( range: Float )
14+ }
15+
716#if os(macOS)
817public typealias PlatformImage = NSImage
918typealias PlatformView = NSView
19+ typealias PlatformColor = NSColor
1020typealias PlatformHostingView = NSHostingView
1121#elseif os(iOS)
1222public typealias PlatformImage = UIImage
1323typealias PlatformView = UIView
24+ typealias PlatformColor = UIColor
1425typealias PlatformHostingView = _UIHostingView
1526#endif
1627
@@ -90,17 +101,66 @@ public extension Snapshotting where Format == PlatformImage, Value == AVAudioPCM
90101 ) -> Snapshotting {
91102 Snapshotting < PlatformView , PlatformImage > . image ( size: . init( width: width, height: height) )
92103 . pullback { buffer in
93- let fallbackWindow = vDSP. window (
94- ofType: Float . self,
95- usingSequence: . hanningNormalized,
96- count: Int ( buffer. frameLength) ,
97- isHalfWindow: false
98- )
104+ let effectiveWindow = window ?? createHannWindow ( size: Int ( buffer. frameLength) )
99105 let data = buffer
100- . spectrum ( window: window ?? fallbackWindow )
106+ . spectrum ( window: effectiveWindow )
101107 . filter { $0. amplitude > threshold }
102108 let spectrum = SpectrumView ( data: data, height: CGFloat ( height) )
103109 return PlatformHostingView ( rootView: spectrum. environment ( \. colorScheme, . light) )
104110 }
105111 }
112+
113+ /// Generates a spectrogram of the given `AVAudioPCMBuffer`.
114+ /// - Parameters:
115+ /// - hopSize: The number of audio frames between successive spectral frames.
116+ /// - frequencyCount: The number of frequency bins to include in each spectral frame.
117+ /// - window: An optional array of floats representing the window function to apply before computing the FFT. If not provided, a Hann window will be used by default.
118+ /// - amplitudeScale: The amplitude scaling method. Defaults to `.logarithmic(range: 120)` for standard 120 dB dynamic range. Use `.linear` for raw FFT magnitudes.
119+ /// - imageWidth: The width of the resulting snapshot image in pixels. Defaults to 1000.
120+ @available ( iOS 16 , macOS 13 , * )
121+ static func spectrogram(
122+ hopSize: Int ,
123+ frequencyCount: Int ,
124+ window: [ Float ] ? = nil ,
125+ amplitudeScale: AmplitudeScale = . logarithmic( range: 120 ) ,
126+ imageWidth: Int = 1000
127+ ) -> Snapshotting {
128+ let height = frequencyCount
129+ return Snapshotting < PlatformView , PlatformImage > . image ( size: . init( width: imageWidth, height: height) )
130+ . pullback { buffer in
131+ let fftSize = frequencyCount * 2
132+ let lastBucketStart = Int ( buffer. frameLength) - fftSize
133+
134+ // Calculate number of FFT windows that fit in the buffer
135+ // We need at least fftSize samples for each window, starting at position 0
136+ // The last window starts at (frameLength - fftSize), and we hop by hopSize
137+ // +1 accounts for the initial window at position 0
138+ let width = 1 + ( lastBucketStart / hopSize)
139+
140+ let effectiveWindow = window ?? createHannWindow ( size: fftSize)
141+ let data = buffer. spectrogram ( fftSize: fftSize, hopSize: hopSize, width: width, window: effectiveWindow, amplitudeScale: amplitudeScale)
142+
143+ // Calculate bin size and max frequency based on sample rate and frequency count
144+ let binSize = buffer. format. sampleRate / Double( fftSize)
145+ let maxFrequency = Int ( binSize * Double( frequencyCount) )
146+
147+ // Calculate duration from buffer length and sample rate
148+ let duration = Double ( buffer. frameLength) / buffer. format. sampleRate
149+
150+ let spectrogram = SpectrogramView ( data: data, width: width, height: height, maxFrequency: maxFrequency, duration: duration)
151+ return PlatformHostingView ( rootView: spectrogram. environment ( \. colorScheme, . light) )
152+ }
153+ }
154+ }
155+
156+ /// Creates a normalized Hann window of the specified size
157+ /// - Parameter size: The number of samples in the window
158+ /// - Returns: An array of Float values representing the Hann window function
159+ private func createHannWindow( size: Int ) -> [ Float ] {
160+ vDSP. window (
161+ ofType: Float . self,
162+ usingSequence: . hanningNormalized,
163+ count: size,
164+ isHalfWindow: false
165+ )
106166}
0 commit comments