From 854b94fca6a8a00735a81fee06d6e84d33a3910a Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Sat, 8 Mar 2025 05:13:28 +0100 Subject: [PATCH 1/9] Implement filterMatches to remove matches with insufficient target zones --- shazam/shazam.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/shazam/shazam.go b/shazam/shazam.go index a3e5f68..09371cf 100644 --- a/shazam/shazam.go +++ b/shazam/shazam.go @@ -49,17 +49,26 @@ func FindMatches(audioSamples []float64, audioDuration float64, sampleRate int) matches := map[uint32][][2]uint32{} // songID -> [(sampleTime, dbTime)] timestamps := map[uint32][]uint32{} + targetZones := map[uint32]map[uint32]int{} // songID -> timestamp -> count for address, couples := range m { for _, couple := range couples { matches[couple.SongID] = append(matches[couple.SongID], [2]uint32{fingerprints[address].AnchorTimeMs, couple.AnchorTimeMs}) timestamps[couple.SongID] = append(timestamps[couple.SongID], couple.AnchorTimeMs) + + if _, ok := targetZones[couple.SongID]; !ok { + targetZones[couple.SongID] = make(map[uint32]int) + } + targetZones[couple.SongID][couple.AnchorTimeMs]++ } } + // matches = filterMatches(10, matches, targetZones) + scores := analyzeRelativeTiming(matches) var matchList []Match + for songID, points := range scores { song, songExists, err := db.GetSongByID(songID) if !songExists { @@ -86,6 +95,33 @@ func FindMatches(audioSamples []float64, audioDuration float64, sampleRate int) return matchList, time.Since(startTime), nil } +// filterMatches filters out matches that don't have enough +// target zones to meet the specified threshold +func filterMatches( + threshold int, + matches map[uint32][][2]uint32, + targetZones map[uint32]map[uint32]int) map[uint32][][2]uint32 { + + // Filter out non target zones. + // When a target zone has less than `targetZoneSize` anchor times, it is not considered a target zone. + for songID, anchorTimes := range targetZones { + for anchorTime, count := range anchorTimes { + if count < targetZoneSize { + delete(targetZones[songID], anchorTime) + } + } + } + + filteredMatches := map[uint32][][2]uint32{} + for songID, zones := range targetZones { + if len(zones) >= threshold { + filteredMatches[songID] = matches[songID] + } + } + + return filteredMatches +} + // AnalyzeRelativeTiming checks for consistent relative timing and returns a score func analyzeRelativeTiming(matches map[uint32][][2]uint32) map[uint32]float64 { scores := make(map[uint32]float64) From f1430ffb8b2ea17fc5646f71369076efdf70fe6e Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Sat, 8 Mar 2025 05:47:20 +0100 Subject: [PATCH 2/9] refactor: improve variable names and update comments for clarity --- shazam/shazam.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/shazam/shazam.go b/shazam/shazam.go index 09371cf..2358d06 100644 --- a/shazam/shazam.go +++ b/shazam/shazam.go @@ -18,21 +18,21 @@ type Match struct { Score float64 } -// FindMatches processes the audio samples and finds matches in the database -func FindMatches(audioSamples []float64, audioDuration float64, sampleRate int) ([]Match, time.Duration, error) { +// FindMatches analyzes the audio sample to find matching songs in the database. +func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ([]Match, time.Duration, error) { startTime := time.Now() logger := utils.GetLogger() - spectrogram, err := Spectrogram(audioSamples, sampleRate) + spectrogram, err := Spectrogram(audioSample, sampleRate) if err != nil { return nil, time.Since(startTime), fmt.Errorf("failed to get spectrogram of samples: %v", err) } peaks := ExtractPeaks(spectrogram, audioDuration) - fingerprints := Fingerprint(peaks, utils.GenerateUniqueID()) + sampleFingerprint := Fingerprint(peaks, utils.GenerateUniqueID()) - addresses := make([]uint32, 0, len(fingerprints)) - for address := range fingerprints { + addresses := make([]uint32, 0, len(sampleFingerprint)) + for address := range sampleFingerprint { addresses = append(addresses, address) } @@ -53,7 +53,10 @@ func FindMatches(audioSamples []float64, audioDuration float64, sampleRate int) for address, couples := range m { for _, couple := range couples { - matches[couple.SongID] = append(matches[couple.SongID], [2]uint32{fingerprints[address].AnchorTimeMs, couple.AnchorTimeMs}) + matches[couple.SongID] = append( + matches[couple.SongID], + [2]uint32{sampleFingerprint[address].AnchorTimeMs, couple.AnchorTimeMs}, + ) timestamps[couple.SongID] = append(timestamps[couple.SongID], couple.AnchorTimeMs) if _, ok := targetZones[couple.SongID]; !ok { @@ -122,7 +125,8 @@ func filterMatches( return filteredMatches } -// AnalyzeRelativeTiming checks for consistent relative timing and returns a score +// analyzeRelativeTiming calculates a score for each song based on the +// relative timing between the song and the sample's anchor times. func analyzeRelativeTiming(matches map[uint32][][2]uint32) map[uint32]float64 { scores := make(map[uint32]float64) for songID, times := range matches { From 3f8f1b0514bfc983a76c4af26fa2177067323358 Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Sat, 8 Mar 2025 06:01:16 +0100 Subject: [PATCH 3/9] refactor: optimize timestamp handling in FindMatches function --- shazam/shazam.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/shazam/shazam.go b/shazam/shazam.go index 2358d06..503b605 100644 --- a/shazam/shazam.go +++ b/shazam/shazam.go @@ -47,8 +47,8 @@ func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ( return nil, time.Since(startTime), err } - matches := map[uint32][][2]uint32{} // songID -> [(sampleTime, dbTime)] - timestamps := map[uint32][]uint32{} + matches := map[uint32][][2]uint32{} // songID -> [(sampleTime, dbTime)] + timestamps := map[uint32]uint32{} // songID -> earliest timestamp targetZones := map[uint32]map[uint32]int{} // songID -> timestamp -> count for address, couples := range m { @@ -57,7 +57,10 @@ func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ( matches[couple.SongID], [2]uint32{sampleFingerprint[address].AnchorTimeMs, couple.AnchorTimeMs}, ) - timestamps[couple.SongID] = append(timestamps[couple.SongID], couple.AnchorTimeMs) + + if existingTime, ok := timestamps[couple.SongID]; !ok || couple.AnchorTimeMs < existingTime { + timestamps[couple.SongID] = couple.AnchorTimeMs + } if _, ok := targetZones[couple.SongID]; !ok { targetZones[couple.SongID] = make(map[uint32]int) @@ -83,11 +86,7 @@ func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ( continue } - sort.Slice(timestamps[songID], func(i, j int) bool { - return timestamps[songID][i] < timestamps[songID][j] - }) - - match := Match{songID, song.Title, song.Artist, song.YouTubeID, timestamps[songID][0], points} + match := Match{songID, song.Title, song.Artist, song.YouTubeID, timestamps[songID], points} matchList = append(matchList, match) } From 1e5e42e1dc74e97996dcb8f6a0699763d76fe98e Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Sat, 8 Mar 2025 06:02:31 +0100 Subject: [PATCH 4/9] refactor: remove shazamInit.go --- shazam/shazamInit.go | 136 ------------------------------------------- 1 file changed, 136 deletions(-) delete mode 100644 shazam/shazamInit.go diff --git a/shazam/shazamInit.go b/shazam/shazamInit.go deleted file mode 100644 index f162663..0000000 --- a/shazam/shazamInit.go +++ /dev/null @@ -1,136 +0,0 @@ -package shazam - -import ( - "fmt" - "song-recognition/db" - "song-recognition/models" - "song-recognition/utils" - "sort" -) - -type Match1 struct { - SongID uint32 - SongTitle string - SongArtist string - YouTubeID string - Timestamp uint32 - Coherency float64 -} - -func Search(audioSamples []float64, audioDuration float64, sampleRate int) ([]Match1, error) { - spectrogram, err := Spectrogram(audioSamples, sampleRate) - if err != nil { - return nil, fmt.Errorf("failed to get spectrogram of samples: %v", err) - } - - peaks := ExtractPeaks(spectrogram, audioDuration) - fingerprints := Fingerprint(peaks, utils.GenerateUniqueID()) - - addresses := make([]uint32, 0, len(fingerprints)) - for address, _ := range fingerprints { - addresses = append(addresses, address) - } - - db, err := db.NewDBClient() - if err != nil { - return nil, err - } - defer db.Close() - - couples, err := db.GetCouples(addresses) - if err != nil { - return nil, err - } - - targetZones := targetZones(couples) - fmt.Println("TargetZones: ", targetZones) - matches := timeCoherency(fingerprints, targetZones) - - var matchList []Match1 - for songID, coherency := range matches { - song, songExists, err := db.GetSongByID(songID) - if err != nil || !songExists { - return nil, err - } - - timestamp := targetZones[songID][0] - match := Match1{songID, song.Title, song.Artist, song.YouTubeID, timestamp, float64(coherency)} - - matchList = append(matchList, match) - } - - sort.Slice(matchList, func(i, j int) bool { - return matchList[i].Coherency > matchList[j].Coherency - }) - - return matchList, nil -} - -func targetZones(m map[uint32][]models.Couple) map[uint32][]uint32 { - songs := make(map[uint32]map[uint32]int) - - for _, couples := range m { - for _, couple := range couples { - if _, ok := songs[couple.SongID]; !ok { - songs[couple.SongID] = make(map[uint32]int) - } - songs[couple.SongID][couple.AnchorTimeMs]++ - } - } - fmt.Println("couples: ", songs) - - for songID, anchorTimes := range songs { - for msTime, count := range anchorTimes { - if count < 5 { - delete(songs[songID], msTime) - } - } - } - fmt.Println("anchorTimes: ", songs) - - targetZones := make(map[uint32][]uint32) - for songID, anchorTimes := range songs { - for anchorTime, _ := range anchorTimes { - targetZones[songID] = append(targetZones[songID], anchorTime) - } - } - - return targetZones -} - -func timeCoherency(record map[uint32]models.Couple, songs map[uint32][]uint32) map[uint32]int { - // var threshold float64 - matches := make(map[uint32]int) - - for songID, songAnchorTimes := range songs { - deltas := make(map[float64]int) - for _, songAnchorTime := range songAnchorTimes { - for _, recordAnchor := range record { - recordAnchorTimeMs := float64(recordAnchor.AnchorTimeMs) - delta := recordAnchorTimeMs - float64(songAnchorTime) - deltas[delta]++ - } - } - - // Find the maximum number of time-coherent notes - var maxOccurrences int - for _, occurrences := range deltas { - if occurrences > maxOccurrences { - maxOccurrences = occurrences - } - } - - matches[songID] = maxOccurrences - } - - // Apply threshold for coherency - /** - for songID, coherency := range matches { - if float64(coherency) < threshold*float64(len(record)) { - delete(matches, songID) // Remove songs with insufficient coherency - } - } - */ - - return matches -} From 5411913a980da3c0a11442f8ffd723d75d5fb265 Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Sat, 8 Mar 2025 07:43:24 +0100 Subject: [PATCH 5/9] refactor: improve comments and variable names for clarity. --- shazam/fft.go | 8 ++++---- shazam/fingerprint.go | 1 - shazam/image.go | 3 --- shazam/spectrogram.go | 17 ++++++++--------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/shazam/fft.go b/shazam/fft.go index a3d3198..2fee6cb 100644 --- a/shazam/fft.go +++ b/shazam/fft.go @@ -4,20 +4,20 @@ import ( "math" ) -// Fft performs the Fast Fourier Transform on the input signal. +// FFT computes the Fast Fourier Transform (FFT) of the input data, +// converting the signal from the time domain to the frequency domain. +// For better understanding, refer to this video: https://www.youtube.com/watch?v=spUNpyF58BY func FFT(input []float64) []complex128 { - // Convert input to complex128 complexArray := make([]complex128, len(input)) for i, v := range input { complexArray[i] = complex(v, 0) } fftResult := make([]complex128, len(complexArray)) - copy(fftResult, complexArray) // Copy input to result buffer + copy(fftResult, complexArray) return recursiveFFT(fftResult) } -// recursiveFFT performs the recursive FFT algorithm. func recursiveFFT(complexArray []complex128) []complex128 { N := len(complexArray) if N <= 1 { diff --git a/shazam/fingerprint.go b/shazam/fingerprint.go index 046cffd..a29398b 100644 --- a/shazam/fingerprint.go +++ b/shazam/fingerprint.go @@ -11,7 +11,6 @@ const ( ) // Fingerprint generates fingerprints from a list of peaks and stores them in an array. -// The fingerprints are encoded using a 32-bit integer format and stored in an array. // Each fingerprint consists of an address and a couple. // The address is a hash. The couple contains the anchor time and the song ID. func Fingerprint(peaks []Peak, songID uint32) map[uint32]models.Couple { diff --git a/shazam/image.go b/shazam/image.go index fb87a22..0a758a2 100644 --- a/shazam/image.go +++ b/shazam/image.go @@ -11,11 +11,9 @@ import ( // ConvertSpectrogramToImage converts a spectrogram to a heat map image func SpectrogramToImage(spectrogram [][]complex128, outputPath string) error { - // Determine dimensions of the spectrogram numWindows := len(spectrogram) numFreqBins := len(spectrogram[0]) - // Create a new grayscale image img := image.NewGray(image.Rect(0, 0, numFreqBins, numWindows)) // Scale the values in the spectrogram to the range [0, 255] @@ -38,7 +36,6 @@ func SpectrogramToImage(spectrogram [][]complex128, outputPath string) error { } } - // Save the image to a PNG file file, err := os.Create(outputPath) if err != nil { return err diff --git a/shazam/spectrogram.go b/shazam/spectrogram.go index 6f2ebc8..e18ae53 100644 --- a/shazam/spectrogram.go +++ b/shazam/spectrogram.go @@ -14,19 +14,18 @@ const ( hopSize = freqBinSize / 32 ) -func Spectrogram(samples []float64, sampleRate int) ([][]complex128, error) { +func Spectrogram(sample []float64, sampleRate int) ([][]complex128, error) { lpf := NewLowPassFilter(maxFreq, float64(sampleRate)) - filteredSamples := lpf.Filter(samples) + filteredSample := lpf.Filter(sample) - downsampledSamples, err := Downsample(filteredSamples, sampleRate, sampleRate/dspRatio) + downsampledSample, err := Downsample(filteredSample, sampleRate, sampleRate/dspRatio) if err != nil { - return nil, fmt.Errorf("couldn't downsample audio samples: %v", err) + return nil, fmt.Errorf("couldn't downsample audio sample: %v", err) } - numOfWindows := len(downsampledSamples) / (freqBinSize - hopSize) + numOfWindows := len(downsampledSample) / (freqBinSize - hopSize) spectrogram := make([][]complex128, numOfWindows) - // Apply Hamming window function window := make([]float64, freqBinSize) for i := range window { window[i] = 0.54 - 0.46*math.Cos(2*math.Pi*float64(i)/(float64(freqBinSize)-1)) @@ -36,12 +35,12 @@ func Spectrogram(samples []float64, sampleRate int) ([][]complex128, error) { for i := 0; i < numOfWindows; i++ { start := i * hopSize end := start + freqBinSize - if end > len(downsampledSamples) { - end = len(downsampledSamples) + if end > len(downsampledSample) { + end = len(downsampledSample) } bin := make([]float64, freqBinSize) - copy(bin, downsampledSamples[start:end]) + copy(bin, downsampledSample[start:end]) // Apply Hamming window for j := range window { From 177c654cc81641424c024f3fd888b9d726844db2 Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Tue, 11 Mar 2025 09:16:57 +0100 Subject: [PATCH 6/9] refactor: remove LowPassFilter implementation from filter.go --- shazam/filter.go | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 shazam/filter.go diff --git a/shazam/filter.go b/shazam/filter.go deleted file mode 100644 index d8b3db5..0000000 --- a/shazam/filter.go +++ /dev/null @@ -1,36 +0,0 @@ -package shazam - -import ( - "math" -) - -// LowPassFilter is a first-order low-pass filter using H(p) = 1 / (1 + pRC) -type LowPassFilter struct { - alpha float64 // Filter coefficient - yPrev float64 // Previous output value -} - -// NewLowPassFilter creates a new low-pass filter -func NewLowPassFilter(cutoffFrequency, sampleRate float64) *LowPassFilter { - rc := 1.0 / (2 * math.Pi * cutoffFrequency) - dt := 1.0 / sampleRate - alpha := dt / (rc + dt) - return &LowPassFilter{ - alpha: alpha, - yPrev: 0, - } -} - -// Filter processes the input signal through the low-pass filter -func (lpf *LowPassFilter) Filter(input []float64) []float64 { - filtered := make([]float64, len(input)) - for i, x := range input { - if i == 0 { - filtered[i] = x * lpf.alpha - } else { - filtered[i] = lpf.alpha*x + (1-lpf.alpha)*lpf.yPrev - } - lpf.yPrev = filtered[i] - } - return filtered -} From 495163ebb43fa8b8cea868c2be3df18aa64eb5df Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Tue, 11 Mar 2025 09:18:29 +0100 Subject: [PATCH 7/9] refactor: update FindMatches function --- shazam/shazam.go | 21 +++++++++++++++++++-- shazam/spectrogram.go | 26 ++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/shazam/shazam.go b/shazam/shazam.go index 503b605..0683019 100644 --- a/shazam/shazam.go +++ b/shazam/shazam.go @@ -1,3 +1,6 @@ +//go:build !js && !wasm +// +build !js,!wasm + package shazam import ( @@ -21,7 +24,6 @@ type Match struct { // FindMatches analyzes the audio sample to find matching songs in the database. func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ([]Match, time.Duration, error) { startTime := time.Now() - logger := utils.GetLogger() spectrogram, err := Spectrogram(audioSample, sampleRate) if err != nil { @@ -31,6 +33,21 @@ func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ( peaks := ExtractPeaks(spectrogram, audioDuration) sampleFingerprint := Fingerprint(peaks, utils.GenerateUniqueID()) + sampleFingerprintMap := make(map[uint32]uint32) + for address, couple := range sampleFingerprint { + sampleFingerprintMap[address] = couple.AnchorTimeMs + } + + matches, _, err := FindMatchesFGP(sampleFingerprintMap) + + return matches, time.Since(startTime), nil +} + +// FindMatchesFGP uses the sample fingerprint to find matching songs in the database. +func FindMatchesFGP(sampleFingerprint map[uint32]uint32) ([]Match, time.Duration, error) { + startTime := time.Now() + logger := utils.GetLogger() + addresses := make([]uint32, 0, len(sampleFingerprint)) for address := range sampleFingerprint { addresses = append(addresses, address) @@ -55,7 +72,7 @@ func FindMatches(audioSample []float64, audioDuration float64, sampleRate int) ( for _, couple := range couples { matches[couple.SongID] = append( matches[couple.SongID], - [2]uint32{sampleFingerprint[address].AnchorTimeMs, couple.AnchorTimeMs}, + [2]uint32{sampleFingerprint[address], couple.AnchorTimeMs}, ) if existingTime, ok := timestamps[couple.SongID]; !ok || couple.AnchorTimeMs < existingTime { diff --git a/shazam/spectrogram.go b/shazam/spectrogram.go index e18ae53..2488019 100644 --- a/shazam/spectrogram.go +++ b/shazam/spectrogram.go @@ -15,8 +15,7 @@ const ( ) func Spectrogram(sample []float64, sampleRate int) ([][]complex128, error) { - lpf := NewLowPassFilter(maxFreq, float64(sampleRate)) - filteredSample := lpf.Filter(sample) + filteredSample := LowPassFilter(maxFreq, float64(sampleRate), sample) downsampledSample, err := Downsample(filteredSample, sampleRate, sampleRate/dspRatio) if err != nil { @@ -53,6 +52,29 @@ func Spectrogram(sample []float64, sampleRate int) ([][]complex128, error) { return spectrogram, nil } +// LowPassFilter is a first-order low-pass filter that attenuates high +// frequencies above the cutoffFrequency. +// It uses the transfer function H(s) = 1 / (1 + sRC), where RC is the time constant. +func LowPassFilter(cutoffFrequency, sampleRate float64, input []float64) []float64 { + rc := 1.0 / (2 * math.Pi * cutoffFrequency) + dt := 1.0 / sampleRate + alpha := dt / (rc + dt) + + filteredSignal := make([]float64, len(input)) + var prevOutput float64 = 0 + + for i, x := range input { + if i == 0 { + filteredSignal[i] = x * alpha + } else { + + filteredSignal[i] = alpha*x + (1-alpha)*prevOutput + } + prevOutput = filteredSignal[i] + } + return filteredSignal +} + // Downsample downsamples the input audio from originalSampleRate to targetSampleRate func Downsample(input []float64, originalSampleRate, targetSampleRate int) ([]float64, error) { if targetSampleRate <= 0 || originalSampleRate <= 0 { From af6319147ce0ab212844a3a22f2a6e88d8d21c3c Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Tue, 11 Mar 2025 09:21:53 +0100 Subject: [PATCH 8/9] feat: add WASM support for audio fingerprint generation --- wasm/wasm_main.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 wasm/wasm_main.go diff --git a/wasm/wasm_main.go b/wasm/wasm_main.go new file mode 100644 index 0000000..2afb489 --- /dev/null +++ b/wasm/wasm_main.go @@ -0,0 +1,64 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import ( + "song-recognition/shazam" + "song-recognition/utils" + "syscall/js" +) + +func generateFingerprint(this js.Value, args []js.Value) interface{} { + if len(args) < 2 { + return js.ValueOf(map[string]interface{}{ + "error": 1, + "data": "Expected audio array and sample rate", + }) + } + + if args[0].Type() != js.TypeObject || args[1].Type() != js.TypeNumber { + return js.ValueOf(map[string]interface{}{ + "error": 2, + "data": "Invalid argument types; Expected audio array and samplerate (type: int)", + }) + } + + inputArray := args[0] + sampleRate := args[1].Int() + + audioData := make([]float64, inputArray.Length()) + for i := 0; i < inputArray.Length(); i++ { + audioData[i] = inputArray.Index(i).Float() + } + + spectrogram, err := shazam.Spectrogram(audioData, sampleRate) + if err != nil { + return js.ValueOf(map[string]interface{}{ + "error": 3, + "data": "Error generating spectrogram: " + err.Error(), + }) + } + + peaks := shazam.ExtractPeaks(spectrogram, float64(len(audioData)/sampleRate)) + fingerprint := shazam.Fingerprint(peaks, utils.GenerateUniqueID()) + + fingerprintArray := []interface{}{} + for address, couple := range fingerprint { + entry := map[string]interface{}{ + "address": address, + "anchorTime": couple.AnchorTimeMs, + } + fingerprintArray = append(fingerprintArray, entry) + } + + return js.ValueOf(map[string]interface{}{ + "error": 0, + "data": fingerprintArray, + }) +} + +func main() { + js.Global().Set("generateFingerprint", js.FuncOf(generateFingerprint)) + select {} +} From 34122d10a5840378d001433ad3a5c98cc62d3ce1 Mon Sep 17 00:00:00 2001 From: Chigozirim Igweamaka Date: Tue, 11 Mar 2025 19:53:02 +0100 Subject: [PATCH 9/9] feat: add WASM binary and include wasm_exec.js in index.html --- client/public/index.html | 1 + client/public/main.wasm | Bin 0 -> 2819772 bytes client/public/wasm_exec.js | 561 +++++++++++++++++++++++++++++++++++++ 3 files changed, 562 insertions(+) create mode 100755 client/public/main.wasm create mode 100644 client/public/wasm_exec.js diff --git a/client/public/index.html b/client/public/index.html index 3528872..c4512ad 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -27,6 +27,7 @@ SeekTune +
O(-GL5g;ep==_>2 z47((Ve6hD`I}B|$MR{ncmve3{RKOFEx&u2UEcenwYV}4|43sX9bj`+XTL#wwJI%9z zWz2Q8L$|(g;MGy)!@2Sr3Z%;8q-f{Bg{ooJ46cIP(%7|sXA1SFP zQ(1L14_B3jZkdmf3%CmWMPAwlfzsM59)vG9W2M*MmXCqYl&7m?*>PZTIAB zy*y%3mlbL;CL~E0ZO+%)HL7(*p;i~f4OMJE7%4AUuw7iQ-0#XzeR69wV9sy>(Hppc z{o+~T!cXpra`7N9Hu|3r?#C!TOSYpQ(P4rfj;ooOmW1}Zko8bv^k0t4Q`N^WDM`f7 z|0*2t)5M43<==~$N%Yg#hk$+c^{c0!Sr8expCv$)1y{NwUl9;!JF@usEzxcT*5|ge zN``e*+Fp^rSXEmI*1rbT;CH!I04d6<7z^%H44pUD#&`yiiUo){f@fvN3>jf!fuyq# zGs@7J!%<^e?bs<2$oV2&7l#WCtEtLY2(AhYZCUm%4HSj76rw|Bz zkwH~PVsQ(_<~68&<1r}Q6FOylCaNC{QC*5i*!Kwa=ya4-^uJkZJiw-(tsgPpl=8*< zrspq_uMT!kZVkMyr@tC;m-%3fBO}%{AddD1>GTY_do7LYLmuanxYpZUp}WQuv@2S5P+ERYfPIRxM#7Y;emv^+ZCy)BGc4?A zKYz>%+NA9+2x#okq&>|^gW6u0v?mMTcWaw8sVJeS+nh8VJSar+*g&?c8v7P+)8;^| zr^T^9=LK!-)TK~vhsORhjvabdVeCICfEPVKBUq^@+W9ueo<7`BzW;@{WU`0&!_$o& zWshGLYN^EX&lULM@kAZr*)|G?4nh|K2}{8Ye(r z%7S54v(e-MJpQK{&-7k)I{=uaROaREmN=l%;0Zva4A`IY1VxF1@xi@`haEl@A6%P= zgMhD_j++yaP8ml5vzrsazsl79bz$!`A)^UWHM3%#!8i*UY-Lq!rx*|x(jz;JY!b+L zn{MU)h@BDMhM+-Ff0Fk{+afCo5E*jl=DOgKDA|X=Jgmh5>4<2oEfUZ4KaXmWTopcL<)&le-IjtfzVHB^n~4=Y!Z0)R2|-dUWJIIM+1> zN9>S{?33#Wok2UyVTbuZU3?h`u*|wd5kU?TAncJtdLlTo9KhKQ;0`t0^VE)v3HVlF z4W4n<-c+o8UZM6h)SgaHdH``^m@#5ye~g9Ulli_#K?@;J$7^hOztCHGql}Tloa{>G zuaN^FhgCD%%N&4hG>0l<&T>dQLay|(I>(0ue4sHS8QudAF-|5V+N!Sh>c*}Q)^hn| zvVvJnWeSm^G)VeLG;t7{@RXO{$7{V26LugNR_!!5c;TE7SZ*nW1Q3nqX0L7I;ZY)# z%bi)F?HU3jeTmK6xY6u`B2s8a&JvD!OB(%7C}Ea;VQpXoeP}HlaV;4C?{GlYWgI@D zUWw@292Y7C!rx`}ih=Nr?GJ93?%^yFCN4YK!b%Z#Am{dQ45?z} z-RF>O-DW9isX-{0%%35)M)x*-w**s`t(vOlzvUpTdHT%@1D4!GYtxC0#)t(f z&mmYJXcXL$KK#HTSRZIfFYN<}X>%Z^JX3e_|FHwpbKEq`#l6vNP30K*i370ShzY#p zW{@J>aC1-@rhi(1OVfDSD0d9*3kA4h9{+Fm_ZJ1Y5=c@4SM6;-rZ0N??dP%V3TgAr zHGC4ffOuVB?gNWD@AQCWOFnSOG372LP)%J0XEZMDri}|3cY)Lj$?@7CSefKVj$@J| zD5Oksnb*qd7zd>GIlbP~#zraKQ3cJz$P;I|#DOcXLw{7_AlC_d5v3{yUWs$Kj7BiT zG9xDLyAD8YUqa~i_L!jLP758kWM{Z?)>rgqT#zfLo+`3Pn$+oTfx7TH z@$nf+c*sY)Ifs52cvuR`F)q^1dZEyFX)_G4o+#Bh-MC~8mJnZjW@eqYWHDr9jY4tU z&TGu@e>-<==*(4E|DwY%tZUd`91*kL%NZS7xgEmU7<*IZ*}kO!zTLAe6}Qi|{kx9G zbMNqm3J$|B^!_&(9@yPml-)rS#78~e;xn}Ci`xG&-bdDl5IcnClIPK^zdPAR1;bk( zD9ae$wj*$G`EjUY^Cl5EFw)l5ohTWbnV`5!euIH#(OI}!II0U?%mz%5Hhi+E! zcUCadRbIo8K__dq>u^o3yt)o4jTITnaITy#c`H}S($6d@od9d@I*d)3=atl8KNzW7 zOvCm0veIPxnDRJW=AzQFc55(sM!{x&h1bxidh$-;&3csRcORM?6p4h?!CWrlh_34H zRSwW51-44*ZX&F=(1R~{1@nli)4CiR*LrCKFM&oxws=HKST9G-E5&m(d4of-JH$m> z`|*$Sron?Fec(o~)YAP(Mc3jb-#2;5Un>DoL{0DSXnDNZD;apQWM40u#oxz?b zlReiUkBUIbn;$|<63|k?aBmI_&=@W$ExSjW8fAO*87?jl6`a@%|>JW@Wk^r4TmihZ!K0@;mA>*)c zM(Zp_F-ZdUxoRxX$swKUkjzHWPK&E@@0D0YS1Jkg#W0@iXr@2SLA8<48B6md6sP4_ zQ>f)6^vHPZ`ynj4YoNXze;}s=YWxT>sl#3(=OxTGMA_=TUQJzNfmn>XC}d}rZ`n*m zY%4K1&TnOP_Z90V1@CFKa52Yzpo|b08j8D&5l3`Df(U z$DR?3z0g6mt*ZH}^0oGeYF%8Y<*RD>j)mWG)nQe=7E6i_D!l~S<6kVbMfF;`vwAHV z*JZT6_`RRK?GX=zX4Y>#pNvJ2b7iMn*2RHPb6Z&>XX!4jOGnQ1#oT+o7dC2|R5*#n zgJ1CAwx7h3KGemP&JZ#=i6ug(nzQ*8Mf5aQYqmy!6_r9JIF+ zxH2*}D4d@u8Y3jB*G26iaZ>1I-Wfak#=VNbg7Br^XV$J(xH4uoFIOtaMG8CvBqx5j z*I}gyB2Z$4S@qlZ-j&{*Asm}s9Ug~Ca`+N+F98{p%zXu&=9TP}827Eb1%n|ZdX$|J zLE=Xy70D$J8Sg<{lEXa1VP2*#I|@eZdCri*pOd21bf#CdQ)eO^3C?itKPr8;m;Pms z>;aw4c?*xlE;sT2iEDt#3;zG#e+L8Lq-P*FQhogz&n$<6K z{O*YlFE+#7$qjt^`lis?FP?rl`VTo2AEXGvXDU4X@bw=7Pd|M9Ncsz3zdHLh;V-lO zO0r+xn2apH$npEGcfaSofBqNmoBjU3{J>xR^#}3!5I%F}{LSCu^WhKw-QR!YqaXd~ zamRh^d074) zN%;3D{=SxcJ=v5zmOP$(BY7hEX7Z%`J(WC_Y)+m|zLh+aJezzw`A+iPWJ~hB_*$D~bOT%>Xgd^M;Ep)$%4>#B{A{qQJ-q(QeX~g*3@G_J zhf>Re0p1gye+|#8w2Ciy|4bixlp}2$8s@NIZ@tdzEmC{yBRV}sFeYDbO<>7jCLoMt zX6>z2y|}@lnQrqip-rp36>?qkraA`q5C?%NxD&BW5sq*5+G>S-P`iSD!wi}3oY^3E zme_^+=y%u{aFW5(wem7K(OmA+yVKjz_>!+?LuMEe(JjFn4{#nv&(Wi1eRU1IzAwSt zDBn_JxOC%Q6a{XIt5qB~g(J)NdJoRkmHQR8wc-ruFiK7BT=;%3Z`|d}=rVbtQXaef zAknd*svphFQoqq**)g+ww34$MFZ_@fUajPDS2ic1akM>`WPc~XWJK$(SN3f)`*sK3 zR}s!8Z^y{@cD7R>-#D4`<6h6m_qJ><(Z66ucz*lH_eqCl_{IHDPYG~xCDHM<>;U%jW-CFZYqSHI z(WseG->i{i9$%3?j|1AtNHzKGh2{`<10npV1L@6N)c5=8UDOBco=el9H zm$9Q)$>@AMcrJHNUn$RZq!E{zdvdnZpQCGcZB{tmHRgNSFKgM$v(e_3&&_0vmkE@$ zY5$2pt#Y9ucJb1zi@+|$t4kDw?k<@>ehJIsmEd4DNlnrpU(6?RbZs_0$MWo=P>RUF z)S9@^r75S+S2QB=IfsvZ6J<<{kXT|t^)(MAxHlh0t?!Z?&@WoxCJ4?|3JR48LQ@bVTF=O7~a1#9LiGd-C5210gNwbe3ZyHt$yDtC1aT=bb&6qXga#)`%T8Lk zim#UARaIx!I(*wjmgziG-;ou(1Q_3n06DRZ=fl{)&56TXP`L9XHhK*kU5pp9ZCuY^ zL&{^bK)YtP%C7Sc=stv^#tJD%CGW~{!7mZi0%=j(jJVwm4$HXRe8X&sY5Nv0Y$Kl+ zjeL;5y{BVd!R-H10IZRn8IeeYHQaVj&U!eKld)JRX1TXJNQ)C~>FC=9+BsFTcemHK zF?=pR8@eyeti1)h2MBitP=|aVLmcLI76}H=ba-TP9?i0dj5BfQcc=*q1vU|v3 z*c$ju8I_TmzQ$hu2YXGUEpY8K@~A_x=&uu`C+(c>8>G{JukOp^4#kl8Z-Bz-8j7ZG zNV6%(Q2Cygadb%77JqLHznO^vzfZ8&*d_T>?hMZN{|RsTE4t(UtMq?r`>D;%#K6B0 z0|g%Yq(iVsgil-FH)vlRSx$pK#_TK`FUg~vOJ zs?i8lv)WO0to;Ng*ywEe>79GVJ9n>k?vv7i&9ztWx&wP6kN{Tv-u36_56jVhMrv=1xC zn+fQHI+7J3oZ2UB!C)qbnkfI*y-im6fQ&t>A!9F_@g)vmtS00C0Jc*1k=#Wb_`7@%Ska8G z(R}9e@>ss~FL$<==s-Wj(*#OWOJMx1*#`gLF_?-@5J7 zjIVNF#?b#1z^E8==5HVZ{)A&>N7lt`@*1yf9QAF|6Ha&=u5KZmS%fpK_7*!jyq?eA z8ekV#@bwPcSnyj|HpheiSh^+T6|uYXWR29-3I`6X-pf0P92|=b%STK? zBkXVGpzkUSnkONhHt+WL<~*G#;8g$!JsEB%8zLO)A9{%d0K@Dp9^@sp6dBrOusfI7 zAiwNQnos$ST%Ccs9^fG5<~Twf4md=IKhEivoE_UU-C3(;)4j=Cvgw|> zz0>`eSGDOrT$fqk3R%N1VYPrVDAapYYbrta*scii#$g&F2*E!Uqd)1QBgk z7fgv>_OydD?tPHqVN55@>FgOVYIJs>6)n)&vtHEbtY11AFtj-k#ExPpznDl&vC;QC z-mqbZCsBpn=61F%mFW1s*Ee+R*VgeZFSX;C1g42{Z2r(&vemVZw6x>QbVZKsbzEJ~ zdy}@hrsP-G3tU}&LNeRIOtV(YR@YCxC0kv*j<3U_b-et{tJ><4gR;&{vxaw&cDU;V zE=Tvsq_AO$uh={AOYeY946Yb?No;Sscexk6Nt>8G@)L71*D_AxcEn(&CTFddP0a7S zC7YO#)um$dbiB0YW=e%kjGXNCiOCw?!HIDQ&%eag^?MViFwF&FtX-_r^Jo=f9T|74 zZIv%@q!}^gPs{}h+@3wOwY{r+kvD0?^ol$&EhjNCgtyaj%!ny#wTzgSdP_!3jO^X6 zRldxt8Zk-uuaiR7aFLigPC>e1b3TAolM^`+ukdy)>{TOouf989YoACPIJHnqv(kLN zV~FkQLM@3VOuy3c?8E7WT6$vQryYajhYPhlF!vWcF|i}z?g%Dp)N0rxleXM3%qGoK zY#N%2nXWiPabFY_M2fdF6h+fE1QuwCdxItszpjD1@+iMm&TVBN&Be`QTwviNu$qJ3 zxa^=o_$)8<3YIX2QJ^AE1%C(cC`Tx6mBDJMLr_x;&L}B>%wWZv;t*FWR(FPq8n6;q z?W!0USD*AI1I9BU2zSkZ^>E3$k;;d5hQl(`PCkHDEmOgb$ygkmp=4W-4n^aUAF!7i zQ)`wU$dK@q5SZ|iO6Y;|`MZJecK$ySfeI3yUDJ=pL`s;sBtZ&ck+|wW0wd)dx(7z? zA@?RSjiQ7?GUE{fArS>R6}gV4Q@gUDxSnE6+{TFdnZBnvl;lF<3iLhL$|@Osqmt8i zrsKCj6EuVObOjy`oW>=_pabf?`C7W5iCM!rN_QxmYv@=Ih%p-pIl z6JU;Q?b6pHU*_!sZ(C3LiUp0H`BQot6X(O#HZh$v)Yu76Ep^C*q51PsygTiJ<7g*1 z@GfagJQ^VQrRV7Syj{|<%izlbnNOL3@Bt7t0G)^$yVM1GiYW|Wt@1()nP{(zHH zEE((hW4tEn7)r_wKvTu`O^4>%RK9o91!~`WFw*dSXABnxkHh=WooTg0Fbnm*aY%It z7b=|`lp%1zA7@@`a0a9ofZCXFbRDJ_f!aXa6Cmk#Ob+f16bCms;i1BMdYk*=`@y}K zW{}dv#)N=N&fU%+KCLV8F%z6eorecF529@9?QDTQFlFdV=om39L!7Qd?*Y9udUzsk z0;(f95#|OMJRbi5cZcF1Aon=@(>t?vxG?GxkL~47$Lg_FMH5<7=CHk&y*mISG*31z zb>L8DXfGJzZa}Ey3(!%W{6a5H|i2!#zg)|X2fU5>jWG}Q{J@Np_WOM zx*WHq6J*Xbq3Co}+D8!GC8uO@n`7qc#-U{OdiP*q+t*2$=CHTPZb1v0%q+NhMr#nv zn0FG-eUrG(k+N9r=p-It>ohuI_#~um5E42K#~Fh?BvmN0b&9%GHm=)=$Jg4xq#wXC z>mL_wzc+aE7JSNDA($jHQOpE5~5Q09QAt`(!dXIERU|{G}^V3NpErXd^lU(o-I*Cj|{C66fF_^tIp>9P`dv7@MZ?P(55aiw5MXL^;>axEJZ+ z0N8LM%Z?L`NZLmC%-Uhv=CL8JfjrvcAQ1ErzDl%1MuAudLiia<5f~xgIn4-(knfyk zELwc`X}-(5Zqv*Ig<+bB7l~EOPCMDym6kZ~xpqhqQD&ig9Q+!kA|8Kgn?^JtDQ88W zfa6dxBdJ=a)ef_@prRA`s0nIid=%XYUPxi3_^ocpwW)AZ^zX3F3>DZSaSLS>8p|x| z!n6T=z{VaLQ|FF1c4+6u*g%I}0S)M)fI`D@}osES)bmqOhs4^^bO#i zMJ3&SIXeb!$SIpGaulU#)tA9caA_3_a#{vNM9W+oGw?Jw?Zz6ztaB;jZCG(-OnZJS z>$t6EJtu2ijvEyBN$AOtQtD0Kj>z+>1Un;|&mjk8`VUJ6{U;yCSqGF32U3?iAaf~7 zo`Tq0zQT)HWJq2q$r%2t(3lVw(N~#x_%sJ*CRiS)^E-5>>R>mN*!h(a)HWNdYbFL`@PjCmP(1 z1P$o{*_nl)LABkDak9~WF8GhmiGkR^q}As%ED$HZQu0RtjM43axJmu%ouDPr(Rw+bORt0X@)@^y%O zTLrFC+R%Alv(paqw)#NXAL!zUj4pU_m)xC$JVBbI7gu#jhF<39eWj%SNAmyI>_N+P z1s8lGBA&fa)bID?X_$y<47Rc=YVx4ac4WN=$ON9o0Z$^Vm4(;N^QucUd^O86X#(1K zR2NB3q@HZ&!eB{hM84P1g*F*YmIMN?^j0lUIkOiV zCaKoLRK`Wo&^=4;%PvzCe0yH)U@h|T<(2pu_Oix*Re*!Ov;*^25-NR`1GZ4;zL z*IiVoJHEHc`^ShjFSgW+-LCOLvWb*k!maoirGU`c2oJ2hT(JVQnB-fHmS-chh!!we zWCJt##ph55TUi}5Gjb<{(u5nNC|z=tOu4|!WZRkBWub$#;AGB@n47cQF2r|_^e*GA zi@lm*kMmz{jNXR3WT}H{%VToe+Pp(&?`Rn>FVu2%W!bKnsqxz(%9U;7FYOs-{=$S) z&n1CH(Iv^MsQb|8f|RM@Iw0Ds^`jR9W!Y1aJe40x36K*+N(f>C&-pWzzZ~?(DSv(% z*{6>m9{I-yDgK${Up~n6j}Na6!Ux&@nFavn?`nG7644`Lg__U1uzCV^!u$%F+sbMf zHL?a&B(d*Ej3j81!~+8_cDJB*!64|%aIph1_bPix1Cc3db4)hiBj|LLbdP0$<8t=% zKR_F_^`f_BDgeY~bZI2%n~<<(g97#V#sr46Wws!@oMtvJ$&rICjRtQ4<*H(4ewbJB z7{a7jiEKN<4%0DOW3`XX{J4k6*p8+C280N7%J2sZbauuevv0wkV!G#SdkfvOAnXi# zTjt3Ii*Vt{2*7#h}ZU zjzcSD7a3gR(CmQXz8H_lp5S&QITbRpopM7EZ=tbwH6O2`5k%Y&dWn*{-l3aZIz^xZ zJ$ex_=NtreKf&~24m6=>emRUe2pg{2eFX$86`+4X*9cBx@r)70u5gpBSU4L1+f~R5 zdQSmt11qvi)T5E4c`rM(S8uw7AVp>BRq!%vcF-jA{L0LqXfBGfsjtG%gWe01WcGwf zf88$1FF=Z1oS;TN+ZVXXBj#bNTg=)|MWuYp4-#SNiv zknRc&6YA8zH-1(=0`rMrhVP7Mk{ew_;LJc2?V@yw{8SKq00~rj- z>x1+qau+k#PoC^|GD=b<2wHvenUZ}FQy{_?1ujTj6t*B|*{w=)rxIfffc*+}=($MF zdOeDoU8iCb9BgHEY*C<&rIZgROf@&OFo+|u0>C~)X6I4tlXy!}W+%)#A`0@1b%Dda zL-in);+UIv6fzrV)@VSs$q;suX#$J9{@q&t2*5YOqi%wm)6ueAdo)!QF^XyhQDdpY zxl`e=SO$mkTVY#U;dKlzhsmO5C$}7v*Nm8&hcX;?rq(p{X3{Pz2av$D`b*GPC$hzHrTT$WYVp;Hy`0n3Ocyx6dpIT zwimdqZbQ5b7~XT?t5+56dS`jlRUUGN0Kh>`g!NNQo>tqNEl;cM)ieky5Gr_MD~X6Kp0{y9Rga99s0LMiH$!Ae_CKrS)s8Slb`gB$W# zy-AtxwV3fhO-i8_U6@hmHQvyZdM-qr+2C$~Nm;zSS)qhs4S%oc zMXsb=myZU3SEI`bx_mpK@q!F#(?###P|SKg=3uSYtd+&d!o)3Sl)puI=GN|yM1s2< zge{NVi7cAfxJN;TT)|#azyOO71|e4l!5WI4IW|7rSAe!#m@lBFE`c#**y36&$WXVR=ZONaSd|0@|nxT{uSdb9Vy_DOaAQ9_~VquL_+okC4|=E`Ud!V)L_ zha3eZlpMi6=RnNnp{^QVa8$M0rLrG+9h(Bc)P(%Xgs2|VWYb2%WEi!4(?4-A#$?5Y zHnmLKO|Q5;Z#bDEH0I0hrw+=@E#lBiH{GAVX>n3i>xDus5gyX7|1w|eRZ*>96l%GV zX5-GkC%;R5ApXO?|Bjk{Xp9M_I1PP;4;oHr+5dTa4R5W1)kQY{SHmB}M2bwMNE3_1 z4`Q$936hG<{P}1zr+$nzka_s#Lt9k7Hj^RPR! z+UxL;$|qQX_#z=y@-aOp7LSAgtL9|<6prnEI^FwZM~2Csg((pU!bS*|nn$L%G5Z^M zvdK%aVs)m2G$cbwG<6LM1Ypi-baSKU&S>FDbJ9ESs%0}U#_u)-v&yF60&hZnZQ0#v z43fi+!V3xZv}u?Xt&5A}%JhI8nO&y{2x)!DLigTBcEsUQ7gcQVpBZvwUo$tEEVVY> zN)1;9cJ;OgX>C)5U2IM|VuNP20Ww~z@AGq|ch>ed<@W;(zz4 zbkexnfwfe!!mc1hgxMO1Ci-44Y`Zs;PWbFT8rph#CR@s^?hs%nQKl&0$p^ z;2-vZ2{nGO2cW464*s)w91Fwuj+N z=UBL*i9X9f_~M8D2K6@ZiXvW5R6X{s(by?sHpg`LQj!d|3uF@ac^_RJ0Gef!vveRk@h|o$CI%QT8O|;z( zc}-hD%DkpR^xryOC4c5GZ%vSn0zx1|v4vpsG43U%5TQWLUV}y3CZR3%hSus*fcZ`M zUnxKz+Y+y6CU!)78as$Z@a10CMgt~GWB7_qhCDml!Fo-SVGc|`1^%{V-4a%y@VGUjfaz%S8J-1nCeOVF>Qq&V|+BAjHXL` z>;gI@g9knKgneKYn<8p!V;@!jIf9lTLFw>sVnmvhC^ zh$gq<=^Yl76B+Od>k259r5oOwRQA0gg)G;$ADre5UZMNJZh9u^54HnXY{5FS04zHZ zP}(J*({sG0Eg!Je3YI?NWo=#|epl=+=a-b-*6wmyNu7$WTv1Xq+FeLRWzLlAylUoz zV%flkM{=IipyJ*UtUX1m*Ub*jkfN6uP32B%Tipm_dO;{Ag3#)vo>UUKE+IY>hpnhr z>PpQVs&G+uuzGFu9+465{pHYL@I7QVbVVF4|^>$Rz(p!k9q-Pfb`{;-E*t7 zuj05j@O7_mm&q}{D*fRX^9CXP?v*xqB{M#`LFgQ?#8`Zn3o!t;g0ZzuXS#uAZIa!_ zB`kcBF}C~)C410|p}jAIt*rVZx&u&%FN^;Y!Tm9Ye|5TB8QlGqweS&_6Qp!?J5`Th zUNi!RKw4;=^gj~N23uJLqehLHvzb-GD5_+-wZ8<)8;a(wBjv^He6W>O-yX_)==W(U1(nLBw?e3pK~w~h zOK;=<=$$D0hLJUEz&A4<_&yL?LGM{GE`N3?c7H8 zFDD<-z=3HUml-5;)ig#kR+jli?Xg9LcTpoc7p3H#qq3 zD6wnkjv6@Udb-SrCpUs+JqscG%d%J>{p0@O<2`ci#^HnTO|PKNghEq5n+)YX+nq+WWqz5 zjjr5U75GAj#Z4&InU{KtrZZ1e%h28;0#bMbnPa6oL(BMEP)or-3aar+hij@aeGrvE zoUT@RMPrg!Fcn+T>0Z|Gjlk}TU2(Xi?6!8rk&-$UT{*v`Xkl06@Pn<{OwVD@ z_fm(sQu%e;T1+qV_j&Wyj4BcMk$Ok%2Fq%vnz$ZBp{ZUq!_lgdFoj0Rs>ur4s)>&q zyKL11I#Zh;M8Nh-JaGiisbfJagL%1HT&U0zuQ^?TKPN?ec%fG`veMXw%~!4`at{#`Z%g4 zAB;GdHG?B6L|f2qS@=w%o48`T8Ho@k0WLmi@lZwlTmt{R+}VpGvhg!Nez4ShLDP0w}<#9@Z}E2PKz;_!Pt*r zjDpA5V#sJNFk6^xWnq>YlP;OaO&;PgshC>?IzenuJcolZeBCJXqCsk-5}d~(Gi%rA ze98Cga167(S?)&?Nu~Hj@EPV8ETb{$inv)&g0?2T=*j(LTH8sv-$VF4Irn=L0p@;h z#&4Xk$-0in&oBzx%Q?0!S;yh}$$j6PeVEpu!}*hL6nxT- zlcZaJW5AVwfew^)rhJ|^bazc!;`yq8qLHqu?fU|+XNw42uwwhZsHE(+w(qqibt<;+ zt4fNNZr@?$wr$^ziZ42x|H^Uc$}RaRZpod_n}X>g5k(Cmdx&rYYR!09F_yvBg@LCU zS|P0xt+jpXG{`X;QEh-QzW5_h7yxqKu$2xJ+{OzEP z5dtK4Hp>wMan!+fR{q%?AEbnk>zu_kw=skdI!*>#qhaDqVA8}9;7)?hqVadG*RZKZ z4LdV{^N$G2pq60v6!0opzd6N0xFFJ!ZNlyxZLaf6Kyz}R{8unfeu2UBdN2nzG27UP zEs8l2e^}_Cp47P}20&**Z76-b{{pBJ6lhMMYxDZWS=5Fcx^3C&sie95_$k6z`L*_5 zkUj|!LE73Jc52Hg$Ji1(AXa|RhnSN$t=2l3B0i9#X1ZL!4y{Nu^d;W8U-t-Wv6Xi5LHrD_Z@6Qm;CA%# zOs{CYlsFF(T$u;uY%gw$2nH<`BMTmnk@B*7xtusXWLS(X5$Px$yE-1$g$}}o6%5qd zK>kHu(S~J`Q2DSf@!~ct=xH{r*zRz7Sy>*H*^Me1b_FcDp$TEx6#3AVW%Vmicui^9 zv>WV-O_BOxf((-tNBv$`TAP^fmZ(^ju*xxVQ)yjkaRhXgx1Act@g;MM*Zo6T$Cgl} zB-iC_UR3FxmXC<;@1`$7cD!_Rf$;Bi07fB{r?!Le@A8U9A(k_#93S_1aib7ux|nR+ zKt2bD^(Hji+*lbevo*u#xdF~bf z*aq_RRPY!L8eatLtJFY1?Oo`}xT}b%oBh<1PL@lE4_<%m@F*zB4YL02B zL>;i{BtFl9nPH#3O~0@-Z`glL#N_9DB{S^PG)jEy{^Q^mc*);{jE4aKv*mg7d8}G2 z^hyRkgp{dx;NXY6q=DzMOSgV;H=p-ZFD~#(i@cJ759XwpJQjON1J5;?4xiGg_e;Ez zfp0VOq)Yw1`60xH{>zTx70bMmNxOCI!9tXCyRzttu7Q{s#rQwD#*c`xj4y^`n4xKm zO~Ys^2=m$?lv)6W_);_gL$>~sBnk6dStX0lfmIn)k(P)Fi@dC|OLk>ittV$_2uf~N zTexY#UV4f{I#_r#Co^q*vmlwQ^n$ml@4`ab%VLdA_rkf5_A*690}bw*0G_yGc{Djy z;1vhS<0>S5)Tv$($ z?v4IY)MPFR2Ed3c$V_m03MKEG&F6^b7_3_itBoT`hP^9S>y{RLr;#rq=D25gMcdMN zW~BsUXL&)xnEJP-ie535a-f8-?UtJ~{o1If6caE%7E6SfA4aP&L%9U2rOy`?gLAz( zTYf#7(;Vxy4RwT~?$RRaoVqS>aBkYK;!LAnIKP@J4K(X$pebL?Sh3^5OS!jKug06h zYBWiQn^MRF5J@^PaAVD7S95dco>rTzYfsLWw+TLZ0*d2O2U}UQ>)cC%~Ne%KwHb69$u39h^;g+ctvy#v(L{%SC+~`+y0N{ZMX+gpa{i zR_{y|DLVQ*u6b#kS;t`G*xP2Do2J~eVu4f4=_O%PoL@U!ldYC1VRhJ-2{gko5Ls*y zPRVmycLqEdpJc2I4nmNh@JkkQ9g+{^xuo$M+BnR}FpHshsTlp3*%Q$(!a8XH3=03O zYL*yZ+P2Vpr!0{$vSSqyOB5^PA4q%Tju`JN(JC`ywykgmXo>z`Lfej)72dL~aCkD( zjg}k91O>VVgwL)`_om^*0)wiID16>(Z4i8O7-B?WRiQCHnW|sC8>_qO1OzitU;Aho+4*>e1Zp!9#Hf9iXie6 z@Ez=fm|>mFNkvDn2;Ud-{-!FAD)x$3*TP4Q$eKJ*4u_zTuqTks740j7t@{FFunihh z&OqBC9%*Bp5o{)r9JHXS(ry$^Q0?8^sd-piZ1ej-!0X{?2lC{w3tr0>tl@TQ4C`bJa4{)tb`S|F67`eNQdrm<0Q?eB>ZNkPucdXC+d-a zRMVNMftWF;N;1X80-=&AQB*n18Q?r>p%=4D!D$uG*>oT`Vn@u*GA$zf8}Y%ih+QxAcI(6xm9gNN_CCCY%@1juQJU9PR;0)a4I z)z%qCRHm)3@q(GUpe>YG9?tf{Cg0Nt;G%B>9n3`ESYbNPEB{vcKNpy0_p@!fzsUVV z3ELk^O6bzR_xIz9Y$w$43k3VgK(G=IDW)ABIV}6|rVb@xIbX;VhT-G+X~mUEucfB4 zM}Ar)s0~-H?YrOpKurak+7u?%sJ|aY%>*HoF%JiuoL8iAOyc}j2EYPP4j_Om)M=r= zH!+!g5h|lREZTxZaO773=rr?$A`FFe8+Ia)cB=Zn#(>BDS?ukZhtZN}MdK(2(!}G~ zHy>NctSQX7B7F%40P;>QDMJtU(AXZZ(z|2Y-IPv19B-$3MHAQhDnlzz^Rl*$;mF+v9FDa_v4f+Z;RBD*I%&Qtt-8-MQi}DZ8 zKFh(8m?C1dU+J()GK>lLI|8uu_5~$xNBz}a*|;`c;7+oVS?ASc&)mgmYs3C68U12L zzLB0IaaL1OYj+}i(Oa=en=Fhn3&qJDi-Z~`=Bc{~kNP50ptgg%wE#}f(+)_YRQTJF z40R-F+NE29Kcw!=Cz#Ei>9I%(1*OL#r5q+72hX^5bvK`6uXc_05l81E-ZR~!Xb%-^ z>K?_3{U$-cTDU!X(c=zJ_ag4sWqZ*RUeNX;Xs?(x?h{PtwO17-e#bHpEFaSm(GIikE|ibd028 z%X%W(vc$=uk~Jt{-4ed-KUbRfC zwOIziI@HdB0N;=A1 zAR;JSc@cVN1Vvnx9a9%wZjfug+$Jx!=3XHM5ChLK08qt}*UmdZkUQiFLMTrN0$2BN zQ^sWVI1kKL3x}@iZk|14(4SX!r=~WT^G(W$LvZFAGSMHS7t{zxf53mU<|rX5rRYq> za>_ynWT!^PN2vs8Ar>9T`@6_1-5%Jpprfen=py{>?wEsd|AW_WV2OW({G49v?1TAL zD?C(kRcOpbYG;^{>6xjXc?DPaymqg>8-0C!Nc3z8p7-0Id7b+YJn%rdKm*rjRFlke zflO@?F$;2YONit|*abv3n&Fz=K(X9roG$)uK0-i)5ghMxTN%h_)Ppg#QY1$b=g_=S z$l>IZ(-EX&`R_GQt0b6naYj0EgrNgbpJ{EL#R-M(V9l1uj0-vDzV;0(USV zl3%Iy(K~;dmt3uFAB2)9N4Y%4&>Amn6LO&HNIjCoy7?@{tA3HxuwZ92>T=Q@YLQ|O zo0~U$uEWxk?gxZ1Gg0Jt=CS-1!R=3Q=nr{OI!97j5)BP`2q#9En%P+!%G=|Ce%{+# zr!iRj8DK(!`U(898T-{zY?0^Rc_GOSA>v*Dz-K8wXXp4M>K`95dZeKaBz&K4p8HCx z@vSo#csawYmec;6fWZ$VlxKzbob(wl%^jxZk5*5JTD{1j{X{2qpZs|iQdA&MN&uo3 z9ROTQ`cv}VW7|`K9ZUFFYl0w{-hqEHoF8kcz|bR98j;=|AFZlU?~9F06JqNqj;B0u z6jHM5Ky=bY-WW4?`JEoq*xZYDd7Vb_DRej38i^Xs<#xc(keBn?8HnBc8&BH%VI})*0#6I*3=6 z0*YcO=qTeO>(}`xHSF@$y-k97RoOwN*lu&9S2jBm^jzD1a1oI!;+EulhFE=WO?<7BNOjqZyjXIkY>uIk;s_=&K2vtL z5Ab9?Qzj?vGJ3kl-@Ft86Q_V&y(`b!+ z*~{5nSUFH=ON3=+G9BuYNE5M#KncgnPh0H=+a?J{oX0-ka9_7i4JX!VInShULGT>l zF8vg^TWAMA$?S$y!_igB)>~NN!@_M)g=3IsQdX<>s~1fsjgmF)9>~5AcLAd^>5@oY zyw*Coyo^DSI^e5<6;*nc&I%lR3pv|%K_BMVe3+NE#|xFh^on2iie?`S@j@bGOku1K z7emM@M5hl#ej#ZD3Nzo&G1#XzbDu)+PvJ<(Hyyss>qIWcX|=00TZ>eLj*Mq|%tWR~ zZg?f?bT+q?1S4bWU+8#3=oxR>VkSlYO&{x6>)VA|p2+GHo}Yc`_nknReN9aiR!tt2 zCE!uP6$1wn5GC9k>H2sfQuz6>hnf`1tbomtZqen^%|FxIDll8M_hB{|>tp7^(b&K@ zx0Qji3DJ0yQDz?=)+A8Wyd|6jccgby&eCZZ=`Lypqz0)v!aJ|l!k$QSpm~C9jk*ji z@cb8s5xE%~VSr#~G>Kpc+%z{(vV=0V3IC=e=Zd=3Md@}Nc$)lQEg$dM!|g0w02@(W zxRb30T`0PYHbR?%@e*se|!IydjEc>MkS3poO*gLeC>UVE#nYZ>eJLff-Yq9$`0g5DR@r17RW$- za4WnYSF3zWHzxAWc8QFF4AfnM)PBLK{><8FfzY*^hQ6Wr!3uj|E5(Wpw>>5Gt&##V{byt)}J zX_W}Ox*gfxh+g1eoFIVTC*{TKh>2g_kHq&qB8gbbN^Yy+<62vf7b*rT*xbqb6Z&rK`Vo?Z-gf=4V@;G}2QH{@b z6`+u5Mt9}bBBSH0qxoPAs-5mFVz%c2Madf0ZdC_6v##c@gX2;PcS*`1=Ja6`##YCe zItdFZySo7T1CQ$VrVT7Q_DR~kokA27z)~$6*)_VJkb%+Mr>Ix(J(S-Oi1YZVAS5Vz zbXUY$wiC<61#xREZd!kzs{|+}G zf$*J}Mw@)2aU`}8l)>dT8Ng!0%0s1R3^zRrRJ?Ma^wUtK+D#m9BgJ6zdwj^|0%nyk zKr!`)AEH@lBxvBoJoGCJ3*c$7Z9ZtiZMu%f7lm%jdrzXyl&zA{(^}eZ2e@}+xrLB z{?-~LZny-+e$2>A1a;}@CkDwdH@5}F@J5B~W7jm(l8g^yU@)W#?!7hQ-g}{_`4q@> z`upl45AUg7=@L9HXk{P_kIK$b6YD!#&WpUTkr^1dG7QEesv(Cr8-trWfnXJ>WX=E- zURjorvb+J2vkG*EswK6?o3$mS{FkG9J*wjLP!|;dtXDP#ML?n>cshoP0Lczy7MPa4 zJQy!Ek75u%+BunM4>TT|Rzdq5j{=2_NYx6Y(vcmBZ%vMeEM+=NI|t|UR;#r}j|9p) zTVz}ah4|Bpeaz;f(!B|qjHx!lK@UR1a@brHkPZ#MD1^eM$n})nls0^6p<&HU-Gjvm zMT+Y+=WLbk04Ujn0xk0)vLTeP?v<6$HJ`0~JLD7ltdQ_O(7Vwec&pJLXenGh%;a<# zx7n5RZ*$9(*rT#<$zI%$tTuI^5JrHEUP~zHk5DAzc^NT*iFRKC>OBN+q26a?T>?OY zUf?q~P8`TF524=MWKR-vI=5b$(^;q4eT=cwnX0fUT5vXJ)Es$4khDMGc^f|Gtenlq z@6qQxzZkg;o(i`+xGI~e;E1}yA`DD!4_P)0AR9@<;0A4hx>FPgB!S7ci+_8>GaGhm zik-2}GtWVo*vZaHSc0k-;j2h&2v)#T9*dq_v~P&>`P7~rHv9lhZzq!Y(XEFX1$Q2P zBqMVO(nMe;FR-JI!M#X6eldh3Rzt96HMZvoKLfhVG$c0OEY(aqQ!>toaYy%|ha4z^ zk=Z80c40A~VG~vhGc~W3HEpDa!p;gPh|0p`VW0T2ZR!s=&_l+sL(=SuLYuHPxHQZC z#tsB=L?oIrsZ&I}Aa>ea+``Mb{PS|Bh`l5)VGJro0*6&_@hX#MRhrA7G7Kw7vt<7n zP=+8-n%%i->%H2CY+Em@gqyR4mxLcC&4SNo!s85lr&S^dvv0%*$wxB*piGus?@ijw zw#klC*ESTOg0drj31X~2h9r??DZGc}O1U=Q9pTF4+#O}v+Y9{}S1QY*CF_sNvO8xx zS|?fdE8c?1V8XVm(_jois@%bFoS|r;N(*{K3H!hct2Aqjnn8|^OlS4HzGh*cj#7=-$|Kg=BMst*pxJwS8)Eu4uqZw!KYCP%+Cq z=w^ZWUhxjC$dDZ{kPBP@p7!~8t z?l{r6VO#D>d+6Edrd78a7LIWM57Ts`o2SkvV3okmM5+c&Ari0NFbe@J?0e`|U9wwf zoXjITzFnwBgUP~KQ*ImPA<&IM@{P%zZ%|Rtr;1&7MKpiBy3LN55xFxW>>!zQs9@M( z1-hh&>;V}ge=$s6j(p%Nmil_Jq~JTf?_;o?{3zszq)>wj>4se;B7t9Vha+(-Ops0q zadnax^n>St12!gImNz7d!wWlt^a7~}c6F;{gFCdpI?kdC`!Rzk-4JJj^c}$0g)XAI z$1AWD^-s{RZ1*@cFMS=_FE+O>aI~yfdn5N#f@bWsh59gg7!5T(a-V1{pdXFJu zAH8YRPs9t2M+Iu+NjhilN!dfn=;^qHSVkaqF*`9?0rtFf#i*<|idXLLBk3=oCniM@ zDvtqmWx&S)LLF9oJZp5n_ZX}iT8YJd7+}FglGYal_XtI`*xA3@$J{SN#vM4%hlO7c zU!mfJuX2?L58h`n=f}&KIaabQL$tAbZ>2m+9MgkTN;E5H09b*xDMzH2XvQG ziJYhO0%v)Fjaopxk3Z^|Z~k1bRj`Wu?iC%_yz-Bi@?Y?rf8Yj@392E`PR^>j$ePKm zYBdpg)n!&(Bw~Upj}-+wIBekAr6Ra>xzUGvmx1pJ{~DfLCXZ($qBGr5xH)1PpZn} zY=B1%a4L6b+tBnzVw;9J+OphuZj&^dC=Me7x3-u~N8wj|G#c$CH^36m9LG(e*8Op< z-53nSTDfRMA^_O+wl;dLv6}2QdfbThEYu|PJ3iuNO(DPqf7Zr81b0)RtOhTUWA&yR zz854UnE*NFMAbLo#ryY!!?{s64GI8;)Xn?GqRKbD_?;TwrDj5Dmf$Ih<&fBt(25*B zo^k*utH8l-36To%h#bY)uaNbIAl-|wrxM=NBrWQ>vL=t6bZK6;%LPyh+2w^hd9ZOG zJ3JCXD)E%-K&3hM0@Jy1p9~+6E%Rej8gpEJt1vd|GuhZgE+o(f1jF1K2}@Q)S8yDe zG70LH4W1pU4vds>U`dM+WtK&T+Att;>Xq0=m2maLK#~1;o+XtCtNU%XdS``ndIC!#VrskT2DvlP1vlj8rRw^4SB#_-wXo~?n8A1ob#6(^E zzE}IX2I5lf&8n^6H~c`bBzQ)%l@yI|s#ZNqV`O`hq56RSL5RVH;HowOdWC?YLv^zx zwbf>oSrW@98p)vH3INX1)k3Y6-H(F~=X%N8l|wUIRN7kpSj>^;dC40S;YdVkI`-7# zT2dw4k|ySR#p|^*L~B_SxIQBYRyS)CG=^5%Iv-|AlPYUM4&QqM-}({BkMD`y|jqT$lv$pi`NmZz0{d2c<6)x%#@(QDBGKRl4;_{c@71^Yo}E`cgOb*b~0c zmKPc~aS*(!`;v5p!NYv(cTppX;^dM-eRPj<;(=Wo_7Fb?t(uI^=>q#;?&&&yBhMKH zOAwn1MH}|OkYHCNry-3^;;vFQai@B7CWw$=o!-Oj*$D&aoPEun35JiHi$ITQ%>|K5MWvR+8AhUgvOktUo} zHO)%B0CF{>9cj{Q8TZDg(dg@g5`|0*2lVso(!qzw=g`9rKm72iQ>RX|&k?Vio_&tY zKCeHDpBXb|{NWqk@P;?e{G+4aG|)`{_|5nnbIe=bIt!n-z3pv(@+W`tr*D79pXukF z?|kR6@8aj(@3GH&|NJlTdEfiqH+%N`-~X5VeBc9r_17Q#&>VjL=5PM?Z$JEZfB%t> z;&a@`KKAi{_{2Yc@}G|X)IWdvU;g#q{{4ji_{@Kvc+zJ-*Gf)ijJxt^1P&KtT}J}H zGDUDM%QrWxCYZiRaSD+MQxIoEQ{7J!tggX?V#oCQmIUUBDP1^?JEdD=VdXVi#A_NK zxJ^NK7{NLsIWwHwWrlgJtZ9pfZ?S1vXy7-r=77bRkDcu4a|#xb-he|5&(<^|qfv{3 zy!0t@?G1S|=V%b-XW2Ud*c3d|v}@q^ig=OXmY9GX3)enL!r*-j%e=(Xy7&BNZ#s&6 zsN|SB0_`gFUJ-7Xi2gHl2(XO)7bVqn1WV9KpQV_^{sC>X6_W5rL-*b%szC_TkvB)_ z+Y^yn&Fn>)J+~(_Ak}Gy5{w;Pe=`^<20W4oADxaqonXNTTb?Tzp3m?x8fW}>q=r+g z(Ii47poW>j#Ky&FtbgNVF~*tH0k%E+7*NkoNIn0G;csGf;c(3JS~%DHXhE?ADVC`mp=?Y-zu~$`HwVKP1fi^P6#?>1VGu!Yq<$E< zG7Qx=F-{G*rX|peMHl~b7^*?hMu`~0%t^vR9DP8=r+Rzaj{E&Cw)ObyH7>V|HamLGFk{t?r%Ri=fb4>m

#Un z1`J41!ERtdh0!Vt6}V1CCuPz@2N>JA6DX1Mljo410!;R754BcKiNalYn7(|MOW5J6j$X!wsAQYom(tu*$%8doQ2?Q`@q8__t zur=+x#Af_CCO8>VWn5FM2+8O6+1muF&pTXsQQ1w*>G@wPUQI@zv6IAN_cM$XYE3Q zm^PO9%PQvf?^O%uc&~ z{OOXim@6^N;5c-)kbMC+g~Z-L(qR^}?!u9`Zx=xRS=9;D@0Ju5eG+sX_w)x#D-$F z5#h`}?$*hf@b@d_#;iWQ{&qV7!TktAf;0uQ6Xj)Q84CN-TsGJQD)_8#TxhSSZ#+)r zjVBNP9jR%cxA|?Bd{Z{LJ=tMV2zq)>8U`Biw4A^Led26et##0$NV;ZV4Mtb8(+~N0 zOzb~8YtzJj?k*x3K11+-1f;55k4AXoW6i}--KA9y&9s51S!*V0sn|&YDTr2VASew_ zvDh0?Oobp8tLpO#@3ToKZ;(02VV)Q`fqSg@9nmjxz`pUYflX+xVM|AGZV;tHyB&A_ zQ4A6t@9TjB9@^AG5u;J1w)5*TTYQ37Mjbua!+RPNMT~fYMUF&~@zJ_F(>rM^|Hurn zY$BN6dK%+};ZBU|pIuVFF%d;alD>&a13h5!^V8Y*f5}bLQSma% ze<3AkKs4q;_$_RH@K2zaB+VGr?K68QBJ2h{Y+g&?hjHdGuNzdLRFw|D)mi0Z`GSvS znT~}u;YtJ~d}&*-d=9eVKO`4)YXVEmLYso=gOPV^>@uoFu&me8jr=OiC1tm6Ww#C@ zwa96x@syH7)JJ2SETIQk>+c1A@`7d1zFe~w%{eiX=aG8$6?Su9AzCc+G0Y-$?BmSARjbU_Hp7_{)@uW_)FJ|rFcX+B*miwDPQl~wCeZ{=~7HzdU-oL&if z3;?61O>Hjm@Hp*F9K`UuF;^2wl41V{y3E(v!`JjS@8rMqwUdWnusU$1Ag6~Xy-Q}2 z-u98j&0gorYCIQbd1hsCyN|c?twJ3Qbi<%!*9rZLMYLxN_4GJQh)iZ%YJu{$zT