├── AgoraLyricsScore
├── Tests
│ ├── Resource
│ │ ├── krc-error-pattern.krc
│ │ ├── readme.txt
│ │ ├── CJhd625070.lrc
│ │ ├── EnhancedLRCformat3.lrc
│ │ ├── testissue60022.xml
│ │ ├── lrc.lrc
│ │ ├── kj5396d18ebe5811ed9efdb2f16c44e48f.lrc
│ │ ├── EnhancedLRCformat.lrc
│ │ └── 4875936889260991133.krc
│ ├── TestParser
│ │ ├── Extensions.swift
│ │ └── TestLrcParser.swift
│ ├── TestLyricsView
│ │ ├── TestKaraokeView.swift
│ │ └── TestLyricsView.swift
│ ├── TestScoringMachine
│ │ ├── LogFileReader.swift
│ │ └── TestMockScoring.swift
│ ├── TestDownload
│ │ ├── TestDownloadCancle.swift
│ │ ├── TestDownloadMuti.swift
│ │ └── TestFileCache.swift
│ └── TestLineScoreRecorder
│ │ └── TestLineScoreRecorder.swift
├── Resources
│ └── Media.xcassets
│ │ ├── Contents.json
│ │ ├── star1.imageset
│ │ ├── star1.png
│ │ ├── star1@2x.png
│ │ ├── star1@3x.png
│ │ └── Contents.json
│ │ ├── star2.imageset
│ │ ├── star2.png
│ │ ├── star2@2x.png
│ │ ├── star2@3x.png
│ │ └── Contents.json
│ │ ├── star3.imageset
│ │ ├── star3.png
│ │ ├── star3@2x.png
│ │ ├── star3@3x.png
│ │ └── Contents.json
│ │ ├── star4.imageset
│ │ ├── star4.png
│ │ ├── star4@2x.png
│ │ ├── star4@3x.png
│ │ └── Contents.json
│ │ ├── star5.imageset
│ │ ├── star5.png
│ │ ├── star5@2x.png
│ │ ├── star5@3x.png
│ │ └── Contents.json
│ │ ├── star6.imageset
│ │ ├── star6.png
│ │ ├── star6@2x.png
│ │ ├── star6@3x.png
│ │ └── Contents.json
│ │ ├── star7.imageset
│ │ ├── star7.png
│ │ ├── star7@2x.png
│ │ ├── star7@3x.png
│ │ └── Contents.json
│ │ ├── star8.imageset
│ │ ├── star8.png
│ │ ├── star8@2x.png
│ │ ├── star8@3x.png
│ │ └── Contents.json
│ │ ├── icon_trangle.imageset
│ │ ├── icon_trangle@2X.png
│ │ ├── icon_trangle@3X.png
│ │ └── Contents.json
│ │ ├── bg_scoring_left.imageset
│ │ ├── bg_scoring_left@2X.png
│ │ ├── bg_scoring_left@3X.png
│ │ └── Contents.json
│ │ └── icon_vertical_line.imageset
│ │ ├── icon_vertical_line@2x.png
│ │ ├── icon_vertical_line@3x.png
│ │ └── Contents.json
└── Class
│ ├── Downloader
│ ├── LyricsFileDownloader+Info.swift
│ ├── Extentions.swift
│ ├── DownloaderManager.swift
│ └── LyricsFileDownloaderProtocol.swift
│ ├── Scoring
│ ├── Other
│ │ ├── ScoreAlgorithm.swift
│ │ ├── ToneCalculator.swift
│ │ └── VoicePitchChanger.swift
│ ├── View
│ │ ├── ScoringView+Events.swift
│ │ ├── ConsoleView.swift
│ │ └── ScoringCanvasView.swift
│ ├── ScoringMachineProtocol+Infos.swift
│ ├── ScoringMachineProtocol.swift
│ └── ScoringMachineProtocol+Events.swift
│ ├── Al
│ ├── Algorithm.h
│ └── Algorithm.c
│ ├── Other
│ ├── DataStructs.swift
│ ├── Logger.swift
│ ├── PitchParser.swift
│ ├── Parser.swift
│ ├── ProgressChecker.swift
│ ├── LineScoreRecorder.swift
│ ├── Log.swift
│ └── Extensions.swift
│ ├── Events.swift
│ └── Lyrics
│ ├── LyricLabel.swift
│ ├── LyricMachine+Events.swift
│ └── FirstToneHintView.swift
├── TimingDiagram.png
├── Demo
├── Demo
│ ├── Resource
│ │ ├── music.mp3
│ │ └── lrc.lrc
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── t1.imageset
│ │ │ ├── t1.png
│ │ │ └── Contents.json
│ │ ├── star1.imageset
│ │ │ ├── star1@2x.png
│ │ │ ├── star1@3x.png
│ │ │ └── Contents.json
│ │ ├── star2.imageset
│ │ │ ├── star2@2x.png
│ │ │ ├── star2@3x.png
│ │ │ └── Contents.json
│ │ ├── star3.imageset
│ │ │ ├── star3@2x.png
│ │ │ ├── star3@3x.png
│ │ │ └── Contents.json
│ │ ├── star4.imageset
│ │ │ ├── star4@2x.png
│ │ │ ├── star4@3x.png
│ │ │ └── Contents.json
│ │ ├── star5.imageset
│ │ │ ├── star5@2x.png
│ │ │ ├── star5@3x.png
│ │ │ └── Contents.json
│ │ ├── star6.imageset
│ │ │ ├── star6@2x.png
│ │ │ ├── star6@3x.png
│ │ │ └── Contents.json
│ │ ├── star7.imageset
│ │ │ ├── star7@2x.png
│ │ │ ├── star7@3x.png
│ │ │ └── Contents.json
│ │ ├── star8.imageset
│ │ │ ├── star8@2x.png
│ │ │ ├── star8@3x.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── icon-20@2x.jpg
│ │ │ ├── icon-20@3x.jpg
│ │ │ ├── icon-29@2x.jpg
│ │ │ ├── icon-29@3x.jpg
│ │ │ ├── icon-40@2x.jpg
│ │ │ ├── icon-40@3x.jpg
│ │ │ ├── icon-60@2x.jpg
│ │ │ ├── icon-60@3x.jpg
│ │ │ ├── icon-1024@2x.png
│ │ │ └── Contents.json
│ │ ├── backgroundImage.imageset
│ │ │ ├── bg.png
│ │ │ └── Contents.json
│ │ ├── ktv_top_bgIcon.imageset
│ │ │ ├── ktv_top_bgIcon@2x.png
│ │ │ ├── ktv_top_bgIcon@3x.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Demo-Bridging-Header.h
│ ├── VC
│ │ ├── OCVC.h
│ │ ├── ScoreAniVC.swift
│ │ ├── NoLyricsTestVC.swift
│ │ ├── IncentiveVC.swift
│ │ ├── LyricLabelVC.swift
│ │ ├── MainVCEx
│ │ │ └── MainTestVCEx+Debug.swift
│ │ ├── FirstToneHintViewTestVC.swift
│ │ ├── SongListVC.swift
│ │ ├── ProfileVC.swift
│ │ ├── AVPlayerTestVC.swift
│ │ ├── OCVC.m
│ │ ├── DownloadVC.swift
│ │ ├── SearchVC.swift
│ │ ├── EmitterVC.swift
│ │ └── GCDTimer.swift
│ ├── Config.swift
│ ├── Info.plist
│ ├── AppDelegate.swift
│ ├── Other
│ │ ├── Utils
│ │ │ ├── AccessProvider.swift
│ │ │ ├── SongSourceProvider.swift
│ │ │ ├── ProgressProvider.swift
│ │ │ └── LyricsCutter.swift
│ │ ├── Log
│ │ │ └── Log.swift
│ │ └── Extensions.swift
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── SceneDelegate.swift
│ └── View
│ │ └── KTVView.swift
├── Demo.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Demo.xcscheme
├── Demo.xcworkspace
│ └── contents.xcworkspacedata
└── Podfile
├── AgoraLyricsScore.podspec
└── .gitignore
/AgoraLyricsScore/Tests/Resource/krc-error-pattern.krc:
--------------------------------------------------------------------------------
1 | [ar:
2 |
--------------------------------------------------------------------------------
/TimingDiagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/TimingDiagram.png
--------------------------------------------------------------------------------
/Demo/Demo/Resource/music.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Resource/music.mp3
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/t1.imageset/t1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/t1.imageset/t1.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star1.imageset/star1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star1.imageset/star1@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star1.imageset/star1@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star1.imageset/star1@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star2.imageset/star2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star2.imageset/star2@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star2.imageset/star2@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star2.imageset/star2@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star3.imageset/star3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star3.imageset/star3@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star3.imageset/star3@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star3.imageset/star3@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star4.imageset/star4@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star4.imageset/star4@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star4.imageset/star4@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star4.imageset/star4@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star5.imageset/star5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star5.imageset/star5@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star5.imageset/star5@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star5.imageset/star5@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star6.imageset/star6@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star6.imageset/star6@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star6.imageset/star6@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star6.imageset/star6@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star7.imageset/star7@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star7.imageset/star7@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star7.imageset/star7@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star7.imageset/star7@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star8.imageset/star8@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star8.imageset/star8@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star8.imageset/star8@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/star8.imageset/star8@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Demo-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "OCVC.h"
6 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-20@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-20@2x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-20@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-20@3x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-29@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-29@2x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-29@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-29@3x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-40@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-40@2x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-40@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-40@3x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-60@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-60@2x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-60@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-60@3x.jpg
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/backgroundImage.imageset/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/backgroundImage.imageset/bg.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-1024@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon-1024@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/star1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/star1.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/star2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/star2.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/star3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/star3.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/star4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/star4.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/star5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/star5.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/star6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/star6.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/star7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/star7.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/star8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/star8.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/star1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/star1@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/star1@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/star1@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/star2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/star2@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/star2@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/star2@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/star3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/star3@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/star3@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/star3@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/star4@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/star4@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/star4@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/star4@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/star5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/star5@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/star5@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/star5@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/star6@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/star6@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/star6@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/star6@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/star7@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/star7@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/star7@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/star7@3x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/star8@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/star8@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/star8@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/star8@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/ktv_top_bgIcon.imageset/ktv_top_bgIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/ktv_top_bgIcon.imageset/ktv_top_bgIcon@2x.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/ktv_top_bgIcon.imageset/ktv_top_bgIcon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/Demo/Demo/Assets.xcassets/ktv_top_bgIcon.imageset/ktv_top_bgIcon@3x.png
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/icon_trangle.imageset/icon_trangle@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/icon_trangle.imageset/icon_trangle@2X.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/icon_trangle.imageset/icon_trangle@3X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/icon_trangle.imageset/icon_trangle@3X.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/bg_scoring_left.imageset/bg_scoring_left@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/bg_scoring_left.imageset/bg_scoring_left@2X.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/bg_scoring_left.imageset/bg_scoring_left@3X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/bg_scoring_left.imageset/bg_scoring_left@3X.png
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/icon_vertical_line.imageset/icon_vertical_line@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/icon_vertical_line.imageset/icon_vertical_line@2x.png
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/icon_vertical_line.imageset/icon_vertical_line@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AgoraIO-Community/LrcView-iOS/HEAD/AgoraLyricsScore/Resources/Media.xcassets/icon_vertical_line.imageset/icon_vertical_line@3x.png
--------------------------------------------------------------------------------
/Demo/Demo/VC/OCVC.h:
--------------------------------------------------------------------------------
1 | //
2 | // OCVC.h
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/2/2.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface OCVC : UIViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/readme.txt:
--------------------------------------------------------------------------------
1 | 资源说明:
2 |
3 | 1.lrc.lrc 正常内容
4 | 2.745012.xml 正常内容
5 | 3.147383.xml 后面句子是pitch = 0
6 | 4.CJ1420023417.xml 异常,正常程序无法处理。有明显的时间错误。
7 |
8 | 5.4875936889260991133.krc 正常内容
9 | 6.4875936889260991133.pitch 正常内容
10 | 7.3017502026609527683.krc 以\n分行
--------------------------------------------------------------------------------
/Demo/Demo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Downloader/LyricsFileDownloader+Info.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LyricsFileDownloader+Info.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/12/14.
6 | //
7 |
8 | import Foundation
9 |
10 | extension LyricsFileDownloader {
11 | struct TaskInfo {
12 | let requestId: Int
13 | let urlString: String
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/t1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "t1.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/backgroundImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "bg.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star1@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star1@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star2@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star2@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star3@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star3@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star4.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star4@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star4@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star5.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star5@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star5@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star6.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star6@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star6@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star7.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star7@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star7@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/star8.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "star8@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "star8@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/Other/ScoreAlgorithm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScoreAlgorithm.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/1/11.
6 | //
7 |
8 | import Foundation
9 |
10 | class ScoreAlgorithm: IScoreAlgorithm {
11 | func getLineScore(with toneScores: [ToneScoreModel]) -> Int {
12 | if toneScores.isEmpty { return 0 }
13 | let ret = toneScores.map({ $0.score }).reduce(0.0, +) / Float(toneScores.count)
14 | return Int(ret)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Al/Algorithm.h:
--------------------------------------------------------------------------------
1 | //
2 | // Algorithm.h
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/11/9.
6 | //
7 |
8 | #ifndef Algorithm_h
9 | #define Algorithm_h
10 |
11 | #include
12 |
13 | double pitchToToneC(double pitch);
14 | float calculedScoreC(double voicePitch, double stdPitch, int scoreLevel, int scoreCompensationOffset);
15 |
16 | void resetC(void);
17 | double handlePitchC(double stdPitch, double voicePitch, double stdMaxPitch);
18 | #endif /* Algorithm_h */
19 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/ktv_top_bgIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "ktv_top_bgIcon@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "ktv_top_bgIcon@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/icon_trangle.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "icon_trangle@2X.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "icon_trangle@3X.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/bg_scoring_left.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "bg_scoring_left@2X.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "bg_scoring_left@3X.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/icon_vertical_line.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "icon_vertical_line@2x.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "filename" : "icon_vertical_line@3x.png",
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star1.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star1@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star1@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star2.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star2@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star2@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star3.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star3@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star3@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star4.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star4.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star4@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star4@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star5.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star5.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star5@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star5@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star6.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star6.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star6@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star6@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star7.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star7.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star7@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star7@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Resources/Media.xcassets/star8.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "star8.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "star8@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "star8@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestParser/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2022/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class TG {
11 |
12 | }
13 |
14 | extension Bundle {
15 | static var current: Bundle {
16 | Bundle(for: TG.self)
17 | }
18 | }
19 |
20 | extension Data {
21 | func subdata(in range: CountableClosedRange) -> Data {
22 | return self.subdata(in: range.lowerBound..
19 | static let mccAppId = <#mccAppId#>
20 | static let mccCertificate = <#mccCertificate#>
21 |
22 | /// ysd important vars
23 | static let pid = <#pid#>
24 | static let pKey = <#pKey#>
25 | static var token: String? = nil
26 | static var userId: String? = nil
27 | }
28 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/CJhd625070.lrc:
--------------------------------------------------------------------------------
1 | [00:11.67]他想知道那是谁
2 | [00:18.77]为何总沉默寡言
3 | [00:25.52]人群中也算抢眼
4 | [00:32.67]抢眼的孤独难免
5 | [00:40.24]快乐当然有一点
6 | [00:47.05]不过寂寞更强烈
7 | [00:53.84]难过时候不流泪
8 | [01:01.06]流泪也不算伤悲
9 | [01:09.26]天真以为是他的独特品味
10 | [01:14.86]殊不知是他
11 | [01:17.96]难以言喻的对决
12 | [01:23.94]子母画面分割上演谍对谍
13 | [01:29.71]而谁是谁
14 | [01:38.40]对于第三人称的角度而言
15 | [01:43.96]也明白其实
16 | [01:46.88]每个人都有缺陷
17 | [01:52.87]不自觉遮掩 多少也算
18 | [01:58.98]自然的行为
19 | [02:20.61]
20 | [02:34.73]快乐当然有一点
21 | [02:42.02]不过寂寞更强烈
22 | [02:49.13]难过时候不流泪
23 | [02:56.11]流泪也不算伤悲
24 | [03:04.14]天真以为是他的独特品味
25 | [03:09.92]殊不知是他
26 | [03:12.67]难以言喻的对决
27 | [03:18.66]子母画面分割上演谍对谍
28 | [03:24.98]而谁是谁
29 | [03:32.39]对于第三人称的角度而言
30 | [03:38.71]也明白其实
31 | [03:41.36]每个人都有缺陷
32 | [03:46.56]才不断的追寻 更好的自己
33 | [03:53.81]直到青春一定程度的浪费
34 | [03:59.88]才觉得可贵
35 | [04:07.85]他想知道那是谁
--------------------------------------------------------------------------------
/Demo/Demo/VC/ScoreAniVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScoreAni.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/1/18.
6 | //
7 |
8 | import Foundation
9 |
10 | import AgoraLyricsScore
11 |
12 | class ScoreAniVC: UIViewController {
13 |
14 | // let localPitchView = LocalPitchView()
15 | // var start = true
16 | // private var timer = GCDTimer()
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | // view.addSubview(localPitchView)
21 | // localPitchView.frame = .init(x: 0, y: 150, width: 150, height: 100)
22 | // timer.scheduledMillisecondsTimer(withName: "EmitterVC", countDown: 1000000, milliseconds: 800, queue: .main) { [weak self](_, time) in
23 | // guard let self = self else { return }
24 | // self.localPitchView.showScoreView(score: Int.random(in: 1...100))
25 | // }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Demo/Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 | UIBackgroundModes
25 |
26 | audio
27 |
28 | UIFileSharingEnabled
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Demo/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'Demo' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 | source 'https://github.com/CocoaPods/Specs.git'
8 |
9 | pod "AgoraLyricsScore", :path => "../AgoraLyricsScore.podspec", :testspecs => ['Tests']
10 | pod 'RTMTokenBuilder', "1.0.2"
11 | # pod 'AgoraRtcEngine_iOS', '4.1.1'
12 | pod 'AgoraRtcEngine_Special_iOS', '4.1.1.24'
13 | pod 'AgoraMccExService', :path => "/Volumes/T5/PodSpec/AgoraMccExService/AgoraMccExService.podspec"
14 | pod "Zip"
15 | pod "ScoreEffectUI", :path => "~/work/DevHistoryProject/ScoreEffectUI/ScoreEffectUI.podspec", :testspecs => ['Tests']
16 | pod 'AgoraComponetLog', '0.0.3'
17 | pod 'SVProgressHUD'
18 | end
19 |
20 | #post_install do |installer|
21 | # installer.pods_project.build_configurations.each do |config|
22 | # config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
23 | # end
24 | #end
25 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/View/ConsoleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConsoleView.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/1/30.
6 | //
7 |
8 | import UIKit
9 |
10 | /// use for debug only
11 | class ConsoleView: UIView {
12 | private let label = UILabel()
13 |
14 | override init(frame: CGRect) {
15 | super.init(frame: frame)
16 | backgroundColor = UIColor.black.withAlphaComponent(0.2)
17 |
18 | label.numberOfLines = 0
19 | label.textColor = .white
20 | label.font = .systemFont(ofSize: 9)
21 | addSubview(label)
22 | label.translatesAutoresizingMaskIntoConstraints = false
23 | label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
24 | label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
25 | label.topAnchor.constraint(equalTo: topAnchor).isActive = true
26 | label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
27 | }
28 |
29 | required init?(coder: NSCoder) {
30 | fatalError("init(coder:) has not been implemented")
31 | }
32 |
33 | func set(text: String) {
34 | label.text = text
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/Other/ToneCalculator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToneCalculator.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/1/11.
6 | //
7 |
8 | import Foundation
9 |
10 | var useC = true
11 |
12 | class ToneCalculator {
13 | /// 计算tone分数
14 | static func calculedScore(voicePitch: Double,
15 | stdPitch: Double,
16 | scoreLevel: Int,
17 | scoreCompensationOffset: Int) -> Float {
18 | if useC {
19 | return calculedScoreC(voicePitch, stdPitch, Int32(scoreLevel), Int32(scoreCompensationOffset))
20 | }
21 |
22 | let stdTone = ToneCalculator.pitchToTone(pitch: stdPitch)
23 | let voiceTone = ToneCalculator.pitchToTone(pitch: voicePitch)
24 | var match = 1 - Float(scoreLevel)/100 * Float(abs(voiceTone - stdTone)) + Float(scoreCompensationOffset)/100
25 | match = max(0, match)
26 | match = min(1, match)
27 | return match * 100
28 | }
29 |
30 | static func pitchToTone(pitch: Double) -> Double {
31 | if useC {
32 | return pitchToToneC(pitch)
33 | }
34 | let eps = 1e-6
35 | return (max(0, log(pitch / 55 + eps) / log(2))) * 12
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/NoLyricsTestVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NoLyricsTestVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2022/12/23.
6 | //
7 |
8 | import UIKit
9 | import AgoraLyricsScore
10 |
11 | class NoLyricsTestVC: UIViewController {
12 | let karaokeView = KaraokeView()
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 | setupUI()
17 |
18 | karaokeView.lyricsView.noLyricTipsFont = .systemFont(ofSize: 20)
19 | karaokeView.lyricsView.noLyricTipsText = "哈哈asdasd"
20 | karaokeView.lyricsView.noLyricTipsColor = .red
21 | karaokeView.setLyricData(data: nil, usingInternalScoring: true)
22 | }
23 |
24 | func setupUI() {
25 | view.backgroundColor = .black
26 | view.addSubview(karaokeView)
27 | karaokeView.translatesAutoresizingMaskIntoConstraints = false
28 |
29 | karaokeView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
30 | karaokeView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
31 | karaokeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
32 | karaokeView.heightAnchor.constraint(equalToConstant: 350).isActive = true
33 | }
34 |
35 | func commonInit() {}
36 |
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/EnhancedLRCformat3.lrc:
--------------------------------------------------------------------------------
1 | [00:18.912]<00:18.912>他<00:39.322>们<01:01.474>总<01:02.972>是<01:10.112>说<01:10.774>我<01:18.123>有<01:23.974>时<01:25.577>不<01:29.896>会<01:31.080>怎<01:31.463>么<01:31.637>讲<01:32.125>话
2 | [01:34.075]<01:34.075>于<01:35.050>是<01:36.026>那<01:37.280>些<01:38.429>不<01:38.742>该<01:39.021>有<01:39.265>的<01:39.578>错<01:40.728>误<01:48.982>总<01:54.416>会<01:58.143>放<01:58.386>下
3 | [01:59.222]<01:59.222>我<02:01.556>身<02:04.482>上<02:05.109>的<02:05.213>缺<02:05.422>点<02:07.024>被<02:07.303>无<02:08.000>限<02:09.462>的<02:14.095>放<02:15.592>大
4 | [02:16.289]<02:16.289>跟<02:19.772>你<02:21.026>的<02:26.285>矛<02:30.883>盾 <02:32.833>我<02:34.679>的<02:35.585>一<02:35.794>个<02:36.038>惩<02:37.291>罚
5 |
6 | [02:37.570]<02:37.570>他<02:37.709>们<02:38.162>总<02:38.406>是<02:47.183>说<02:56.936>我<02:59.339>有<03:01.568>时<03:25.183>不<03:39.985>会<03:47.543>怎<03:47.822>么<03:53.221>讲<03:54.370>话
7 | [04:01.545]<04:01.545>于<04:04.401>是<04:05.690>那<04:06.839>些<04:06.979>不<04:08.407>该<04:08.825>有<04:09.103>的<04:09.626>错<04:10.636>误<04:10.984>总<04:13.074>会<04:13.283>放<04:17.184>下
8 | [04:33.380]<04:33.380>我<04:33.763>身<04:39.440>上<04:42.505>的<04:42.853>缺<04:43.202>点<04:45.918>被<04:46.545>无<04:47.207>限<04:47.590>的<04:50.725>放<04:51.526>大
9 | [04:53.964]<04:53.964>跟<04:55.949>你<04:56.124>的<04:56.367>矛<04:56.646>盾 <04:57.308>我<04:57.412>的<04:57.795>一<04:57.970>个<04:59.781>惩<05:50.145>罚
--------------------------------------------------------------------------------
/Demo/Demo/VC/IncentiveVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IncentiveVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/1/29.
6 | //
7 |
8 | import UIKit
9 |
10 | import ScoreEffectUI
11 | import AgoraLyricsScore
12 |
13 | class IncentiveVC: UIViewController {
14 |
15 | let incentiveView = IncentiveView()
16 | var start = true
17 | private var timer = GCDTimer()
18 | let list = [0, 80, 60, 83, 89, 90, 83, 84, 85]
19 | var index = 0
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 | view.backgroundColor = .black
24 | view.addSubview(incentiveView)
25 | incentiveView.frame = .init(x: 100, y: 100, width: 200, height: 200)
26 | timer.scheduledMillisecondsTimer(withName: "EmitterVC", countDown: 1000000, milliseconds: 1000, queue: .main) { [weak self](_, time) in
27 | guard let self = self else { return }
28 |
29 | if self.index > self.list.count-1 {
30 | // self.index = 0
31 | }
32 | else {
33 | let score = self.list[self.index]
34 | self.incentiveView.show(score: score)
35 | }
36 | self.index += 1
37 | }
38 |
39 | }
40 |
41 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
42 |
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/AgoraLyricsScore.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = "AgoraLyricsScore"
3 | spec.version = "2.1.6"
4 | spec.summary = "AgoraLyricsScore"
5 | spec.description = "AgoraLyricsScore"
6 |
7 | spec.homepage = "https://github.com/AgoraIO-Community"
8 | spec.license = "MIT"
9 | spec.author = { "ZYQ" => "zhaoyongqiang@agora.io" }
10 | spec.source = { :git => "https://github.com/AgoraIO-Community/LrcView-iOS.git", :tag => '2.1.6' }
11 | spec.source_files = ["AgoraLyricsScore/Class/**/*.swift", "AgoraLyricsScore/Class/AL/*"]
12 | spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 'DEFINES_MODULE' => 'YES' }
13 | spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 'DEFINES_MODULE' => 'YES' }
14 | spec.ios.deployment_target = '10.0'
15 | spec.swift_versions = "5.0"
16 | spec.requires_arc = true
17 | spec.dependency 'AgoraComponetLog', '~> 0.0.3'
18 | spec.dependency 'Zip', '2.1.2'
19 | spec.resource_bundles = {
20 | 'AgoraLyricsScoreBundle' => ['AgoraLyricsScore/Resources/*.xcassets']
21 | }
22 |
23 | spec.test_spec 'Tests' do |test_spec|
24 | test_spec.source_files = "AgoraLyricsScore/Tests/**/*.{swift}"
25 | test_spec.resource = "AgoraLyricsScore/Tests/Resource/*"
26 | test_spec.frameworks = 'UIKit','Foundation'
27 | test_spec.requires_app_host = true
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/LyricLabelVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LyricLabelVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/1/31.
6 | //
7 |
8 | import UIKit
9 | import AgoraLyricsScore
10 |
11 | class LyricLabelVC: UIViewController {
12 | // let label = LyricLabel()
13 | var count = 0
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 | view.backgroundColor = .blue
17 |
18 | // label.maxWidth = 350
19 | // label.text = "我们得失的安徽的"
20 | // view.addSubview(label)
21 | // label.translatesAutoresizingMaskIntoConstraints = false
22 | //
23 | // label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
24 | // label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
25 | //
26 | // label.status = .normal
27 | }
28 |
29 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
30 |
31 | // if count < 5 {
32 | // label.status = .selectedOrHighlighted
33 | // label.progressRate = .random(in: 0.1...1)
34 | // }
35 | // else {
36 | // label.text = ""
37 | // label.status = .normal
38 | // label.progressRate = 0
39 | //
40 | // label.text = "我们得失的安徽的"
41 | // label.status = .selectedOrHighlighted
42 | // }
43 | //
44 | // count += 1
45 | }
46 |
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Demo/Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2022/12/21.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
13 | Log.setupLogger()
14 | return true
15 | }
16 |
17 | // MARK: UISceneSession Lifecycle
18 |
19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
20 | // Called when a new scene session is being created.
21 | // Use this method to select a configuration to create the new scene with.
22 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
23 | }
24 |
25 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
26 | // Called when the user discards a scene session.
27 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
28 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
29 | }
30 |
31 |
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/xcode
3 |
4 | ### Xcode ###
5 | # Xcode
6 | #
7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8 |
9 | ## Build generated
10 | build/
11 | DerivedData/
12 |
13 | ## Various settings
14 | *.pbxuser
15 | !default.pbxuser
16 | *.mode1v3
17 | !default.mode1v3
18 | *.mode2v3
19 | !default.mode2v3
20 | *.perspectivev3
21 | !default.perspectivev3
22 | xcuserdata/
23 |
24 | ## Other
25 | *.moved-aside
26 | *.xccheckout
27 | *.xcscmblueprint
28 |
29 | ### Xcode Patch ###
30 | *.xcodeproj/*
31 | !*.xcodeproj/project.pbxproj
32 | !*.xcodeproj/xcshareddat
33 | !*/xcuserdata/admin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlista/
34 | !*.xcworkspace/contents.xcworkspacedata
35 | /*.gcno
36 |
37 |
38 | # End of https://www.gitignore.io/api/xcode
39 |
40 |
41 |
42 | # Created by https://www.gitignore.io/api/cocoapods
43 |
44 | ### CocoaPods ###
45 | ## CocoaPods GitIgnore Template
46 |
47 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing
48 | # - Also handy if you have a large number of dependant pods
49 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
50 | Pods/
51 | Podfile.lock
52 | .DS_Store
53 | Demo/Demo/Config.swift
54 | Demo/Demo/Other/Config.swift
55 |
56 | # End of https://www.gitignore.io/api/cocoapods
57 |
58 |
59 | Demo/Demo/Config.swift
60 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestLyricsView/TestKaraokeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestKaraokeView.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2023/9/26.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | final class TestKaraokeView: XCTestCase, KaraokeDelegate {
12 | var karaokeView: KaraokeView!
13 | let exp = XCTestExpectation(description: "test TestKaraokeView")
14 |
15 | func testExample() throws { /** 重现#CSD-59221、#CSD-60022 **/
16 | karaokeView = KaraokeView(frame: .init(x: 0, y: 0, width: 350, height: 400),loggers:[ConsoleLogger()])
17 | let url = URL(fileURLWithPath: Bundle.current.path(forResource: "testissue60022", ofType: "xml")!)
18 | let data = try! Data(contentsOf: url)
19 | guard let model = KaraokeView.parseLyricData(lyricFileData: data) else {
20 | XCTFail()
21 | return
22 | }
23 | karaokeView.delegate = self
24 | karaokeView.setLyricData(data: model, usingInternalScoring: true)
25 | for time: UInt in 0...1001000 {
26 | karaokeView.setProgress(progress: time)
27 | }
28 | sleep(1)
29 | karaokeView.reset()
30 | wait(for: [exp], timeout: 60 * 3)
31 | }
32 |
33 | func onKaraokeView(view: KaraokeView, didFinishLineWith model: LyricLineModel, score: Int, cumulativeScore: Int, lineIndex: Int, lineCount: Int) {
34 | exp.fulfill()
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon-20@2x.jpg",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "icon-20@3x.jpg",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "icon-29@2x.jpg",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "icon-29@3x.jpg",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "icon-40@2x.jpg",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "icon-40@3x.jpg",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "icon-60@2x.jpg",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "icon-60@3x.jpg",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "icon-1024@2x.png",
53 | "idiom" : "ios-marketing",
54 | "scale" : "1x",
55 | "size" : "1024x1024"
56 | }
57 | ],
58 | "info" : {
59 | "author" : "xcode",
60 | "version" : 1
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/testissue60022.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 晴天
5 | 周杰伦
6 | 1
7 |
8 |
9 |
10 |
11 |
12 | 故
13 |
14 |
15 | 事
16 |
17 |
18 | 的
19 |
20 |
21 | 小
22 |
23 |
24 | 黄
25 |
26 |
27 | 花
28 |
29 |
30 |
31 |
32 | 从
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/MainVCEx/MainTestVCEx+Debug.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainTestVCEx+Debug.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2024/5/13.
6 | //
7 |
8 | import Foundation
9 | import AgoraMccExService
10 |
11 | extension MainTestVCEx { /** for debug **/
12 | static var lastProgressInMs: UInt = 0
13 | static var lastPitchTime: CFAbsoluteTime = 0
14 |
15 | func updateLastProgressInMs_debug(progressInMs: UInt) {
16 | MainTestVCEx.lastProgressInMs = progressInMs
17 | }
18 |
19 | func calculateProgressGap_debug(progressInMs: UInt) -> Int {
20 | let progressGap = Int(progressInMs) - Int(MainTestVCEx.lastProgressInMs)
21 | MainTestVCEx.lastProgressInMs = progressInMs
22 | return progressGap
23 | }
24 |
25 | /// 打印onPitch回调间隔
26 | func logOnPitchInvokeGap_debug() {
27 | let startTime = CFAbsoluteTimeGetCurrent()
28 | let gap = startTime - MainTestVCEx.lastPitchTime
29 | MainTestVCEx.lastPitchTime = startTime
30 | if (gap > 0.1) {
31 | Log.warning(text: "OnPitch invoke gap \(gap)", tag: self.logTag)
32 | }
33 | else {
34 | Log.debug(text: "OnPitch invoke gap \(gap)", tag: self.logTag)
35 | }
36 | }
37 |
38 | // 打印非法的speakerPitch
39 | func logNInvalidSpeakPitch_debug(data: AgoraRawScoreDataEx) {
40 | if (data.speakerPitch < 0) {
41 | Log.errorText(text: "speakerPitch less than 0, \(data.speakerPitch)", tag: self.logTag)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Demo/Demo/Other/Utils/AccessProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessProvider.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2024/5/14.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | class AccessProvider {
12 | typealias AccessBlock = (_ userId: String, _ token: String, _ errorMsg: String?) -> Void
13 | static var userId: String?
14 | static var token: String?
15 |
16 | static func fetchAccessData(completed: @escaping AccessBlock) {
17 | let url = Config.accessUrl
18 | let session = URLSession.shared
19 | let task = session.dataTask(with: URL(string: url)!) { (data, response, error) in
20 |
21 | if let e = error {
22 | completed("", "", e.localizedDescription)
23 | return
24 | }
25 |
26 | guard let data = data else {
27 | completed("", "", "data is nil")
28 | return
29 | }
30 |
31 | guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers), let dict = json as? [String: Any] else {
32 | completed("", "", "json error")
33 | return
34 | }
35 |
36 | guard let error = dict["error"] as? Int,
37 | error == 0, let data = dict["data"] as? [String: Any], let userId = data["yinsuda_uid"] as? String, let token = data["token"] as? String else {
38 | completed("", "", "data error")
39 | return
40 | }
41 | AccessProvider.userId = userId
42 | AccessProvider.token = token
43 | completed(userId, token, nil)
44 | }
45 | task.resume()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Demo/Demo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Demo/Demo/Resource/lrc.lrc:
--------------------------------------------------------------------------------
1 | [00:23.56]什么是幸福
2 | [00:26.01]什么叫相爱
3 | [00:28.65]什么才是
4 | [00:29.82]永远和未来
5 | [00:33.55]看不到的情
6 | [00:36.03]啊摸不着的爱
7 | [00:38.76]看不见的幸福
8 | [00:41.23]甜蜜和期待
9 | [00:43.82]失去了关心
10 | [00:46.35]失去了关怀
11 | [00:48.82]失去了就
12 | [00:50.03]永远回不来
13 | [00:53.79]怎么去挽回
14 | [00:56.19]啊怎么去等待
15 | [00:58.80]怎么改变也不会
16 | [01:01.24]让一切再重来
17 | [01:04.01]没了心的爱
18 | [01:05.94]就像过了夜的菜
19 | [01:08.95]经不起那
20 | [01:10.26]风吹和雨晒
21 | [01:14.05]两个人的情
22 | [01:16.17]如果出现了意外
23 | [01:19.15]还有什么值得去爱
24 | [01:21.62]舍不得分开
25 | [01:24.24]没了心的爱
26 | [01:26.16]就像鱼儿离了海
27 | [01:29.29]被迫选择
28 | [01:30.45]逃离的无奈
29 | [01:34.33]两个人的心
30 | [01:36.45]如果走不到一块
31 | [01:39.34]结果最后只会
32 | [01:41.22]再让彼此受伤害
33 | [02:04.59]什么是幸福
34 | [02:07.16]什么叫相爱
35 | [02:09.68]什么才是
36 | [02:10.94]永远和未来
37 | [02:14.72]看不到的情
38 | [02:17.04]啊摸不着的爱
39 | [02:19.82]看不见的幸福
40 | [02:22.30]甜蜜和期待
41 | [02:24.83]失去了关心
42 | [02:27.35]失去了关怀
43 | [02:29.88]失去了就
44 | [02:31.14]永远回不来
45 | [02:34.99]怎么去挽回
46 | [02:37.20]啊怎么去等待
47 | [02:40.07]怎么改变也不会
48 | [02:42.14]让一切再重来
49 | [02:44.97]没了心的爱
50 | [02:46.74]就像过了夜的菜
51 | [02:50.11]经不起那
52 | [02:51.32]风吹和雨晒
53 | [02:55.22]两个人的情
54 | [02:57.14]如果出现了意外
55 | [03:00.17]还有什么值得去爱
56 | [03:02.75]舍不得分开
57 | [03:05.38]没了心的爱
58 | [03:07.14]就像鱼儿离了海
59 | [03:10.33]被迫选择
60 | [03:11.54]逃离的无奈
61 | [03:15.28]两个人的心
62 | [03:17.30]如果走不到一块
63 | [03:20.39]结果最后只会
64 | [03:22.26]再让彼此受伤害
65 | [03:25.54]没了心的爱
66 | [03:27.36]就像过了夜的菜
67 | [03:30.54]经不起那
68 | [03:31.75]风吹和雨晒
69 | [03:35.59]两个人的情
70 | [03:37.50]如果出现了意外
71 | [03:40.58]还有什么值得去爱
72 | [03:43.15]舍不得分开
73 | [03:45.78]没了心的爱
74 | [03:47.71]就像鱼儿离了海
75 | [03:50.79]被迫选择
76 | [03:51.95]逃离的无奈
77 | [03:55.84]两个人的心
78 | [03:57.81]如果走不到一块
79 | [04:00.89]结果最后只会
80 | [04:02.71]再让彼此受伤害
81 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/lrc.lrc:
--------------------------------------------------------------------------------
1 | [00:23.56]什么是幸福
2 | [00:26.01]什么叫相爱
3 | [00:28.65]什么才是
4 | [00:29.82]永远和未来
5 | [00:33.55]看不到的情
6 | [00:36.03]啊摸不着的爱
7 | [00:38.76]看不见的幸福
8 | [00:41.23]甜蜜和期待
9 | [00:43.82]失去了关心
10 | [00:46.35]失去了关怀
11 | [00:48.82]失去了就
12 | [00:50.03]永远回不来
13 | [00:53.79]怎么去挽回
14 | [00:56.19]啊怎么去等待
15 | [00:58.80]怎么改变也不会
16 | [01:01.24]让一切再重来
17 | [01:04.01]没了心的爱
18 | [01:05.94]就像过了夜的菜
19 | [01:08.95]经不起那
20 | [01:10.26]风吹和雨晒
21 | [01:14.05]两个人的情
22 | [01:16.17]如果出现了意外
23 | [01:19.15]还有什么值得去爱
24 | [01:21.62]舍不得分开
25 | [01:24.24]没了心的爱
26 | [01:26.16]就像鱼儿离了海
27 | [01:29.29]被迫选择
28 | [01:30.45]逃离的无奈
29 | [01:34.33]两个人的心
30 | [01:36.45]如果走不到一块
31 | [01:39.34]结果最后只会
32 | [01:41.22]再让彼此受伤害
33 | [02:04.59]什么是幸福
34 | [02:07.16]什么叫相爱
35 | [02:09.68]什么才是
36 | [02:10.94]永远和未来
37 | [02:14.72]看不到的情
38 | [02:17.04]啊摸不着的爱
39 | [02:19.82]看不见的幸福
40 | [02:22.30]甜蜜和期待
41 | [02:24.83]失去了关心
42 | [02:27.35]失去了关怀
43 | [02:29.88]失去了就
44 | [02:31.14]永远回不来
45 | [02:34.99]怎么去挽回
46 | [02:37.20]啊怎么去等待
47 | [02:40.07]怎么改变也不会
48 | [02:42.14]让一切再重来
49 | [02:44.97]没了心的爱
50 | [02:46.74]就像过了夜的菜
51 | [02:50.11]经不起那
52 | [02:51.32]风吹和雨晒
53 | [02:55.22]两个人的情
54 | [02:57.14]如果出现了意外
55 | [03:00.17]还有什么值得去爱
56 | [03:02.75]舍不得分开
57 | [03:05.38]没了心的爱
58 | [03:07.14]就像鱼儿离了海
59 | [03:10.33]被迫选择
60 | [03:11.54]逃离的无奈
61 | [03:15.28]两个人的心
62 | [03:17.30]如果走不到一块
63 | [03:20.39]结果最后只会
64 | [03:22.26]再让彼此受伤害
65 | [03:25.54]没了心的爱
66 | [03:27.36]就像过了夜的菜
67 | [03:30.54]经不起那
68 | [03:31.75]风吹和雨晒
69 | [03:35.59]两个人的情
70 | [03:37.50]如果出现了意外
71 | [03:40.58]还有什么值得去爱
72 | [03:43.15]舍不得分开
73 | [03:45.78]没了心的爱
74 | [03:47.71]就像鱼儿离了海
75 | [03:50.79]被迫选择
76 | [03:51.95]逃离的无奈
77 | [03:55.84]两个人的心
78 | [03:57.81]如果走不到一块
79 | [04:00.89]结果最后只会
80 | [04:02.71]再让彼此受伤害
81 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/ScoringMachineProtocol+Infos.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScoringMachineProtocol+Infos.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2024/6/4.
6 | //
7 |
8 | import Foundation
9 |
10 | class ScoringMachineInfo {
11 | /// 标准开始时间 (来源自歌词文件)
12 | let beginTime: UInt
13 | /// 标准时长 (来源自歌词文件)
14 | let duration: UInt
15 | /// 需要绘制的开始时间
16 | let drawBeginTime: UInt
17 | /// 需要绘制的时长
18 | var drawDuration: UInt
19 | let word: String
20 | let pitch: Double
21 |
22 | required init(beginTime: UInt,
23 | duration: UInt,
24 | word: String,
25 | pitch: Double,
26 | drawBeginTime: UInt,
27 | drawDuration: UInt) {
28 | self.beginTime = beginTime
29 | self.duration = duration
30 | self.word = word
31 | self.pitch = pitch
32 | self.drawBeginTime = drawBeginTime
33 | self.drawDuration = drawDuration
34 | }
35 |
36 | var endTime: UInt {
37 | beginTime + duration
38 | }
39 |
40 | var drawEndTime: UInt {
41 | drawBeginTime + drawDuration
42 | }
43 |
44 | var tone: LyricToneModel {
45 | return LyricToneModel(beginTime: beginTime,
46 | duration: duration,
47 | word: word,
48 | pitch: pitch,
49 | lang: .zh,
50 | pronounce: "")
51 | }
52 | }
53 |
54 | struct ScoringMachineDrawInfo {
55 | let rect: CGRect
56 | }
57 |
58 | struct ScoringMachineDebugInfo {
59 | /// 原始pitch
60 | let originalPitch: Double
61 | /// 男女音调算法改变后的pitch
62 | let pitch: Double
63 | let hitedInfo: ScoringMachineInfo?
64 | let progress: UInt
65 | }
66 |
--------------------------------------------------------------------------------
/Demo/Demo/Other/Log/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2024/5/11.
6 | //
7 |
8 | import Foundation
9 | import AgoraComponetLog
10 |
11 | class Log {
12 | static let shared = Log()
13 | var componetLog: AgoraComponetLog!
14 |
15 | static func setupLogger() {
16 | let fileLogger = AgoraComponetFileLogger(logFilePath: nil,
17 | filePrefixName: "AgoraLyricsDemo",
18 | maxFileSizeOfBytes: 1 * 1024 * 1024,
19 | maxFileCount: 5,
20 | domainName: "ALD")
21 | let consoleLogger = AgoraComponetConsoleLogger(domainName: "ALD")
22 | shared.componetLog = AgoraComponetLog(queueTag: "AgoraLyricsDemo")
23 | shared.componetLog.configLoggers([fileLogger, consoleLogger])
24 | }
25 |
26 | static func errorText(text: String,
27 | tag: String? = nil) {
28 | shared.componetLog.error(withText: text, tag: tag)
29 | }
30 |
31 | static func error(error: CustomStringConvertible,
32 | tag: String? = nil) {
33 | shared.componetLog.error(withText: error.description, tag: tag)
34 | }
35 |
36 | static func info(text: String,
37 | tag: String? = nil) {
38 | shared.componetLog.info(withText: text, tag: tag)
39 | }
40 |
41 | static func debug(text: String,
42 | tag: String? = nil) {
43 | shared.componetLog.debug(withText: text, tag: tag)
44 | }
45 |
46 | static func warning(text: String,
47 | tag: String? = nil) {
48 | shared.componetLog.warning(withText: text, tag: tag)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestLyricsView/TestLyricsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestLyricsView.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2023/2/17.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | class TestLyricsView: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testLyricsToneSpace() { /** 测试计算进度算法,tone之间有空隙 **/
22 | let url = URL(fileURLWithPath: Bundle.current.path(forResource: "153378", ofType: "xml")!)
23 | let data = try! Data(contentsOf: url)
24 | guard let model = KaraokeView.parseLyricData(lyricFileData: data) else {
25 | XCTFail()
26 | return
27 | }
28 |
29 | let dataList = model.lines.map({ LyricCell.Model(text: $0.content,
30 | progressRate: 0,
31 | beginTime: $0.beginTime,
32 | duration: $0.duration,
33 | status: .normal,
34 | tones: $0.tones) })
35 | let line = dataList[3]
36 |
37 | XCTAssertNotNil(LyricMachine.calculateProgressRate(progress: 43285, model: line, scrollByWord: true))
38 | XCTAssertNil(LyricMachine.calculateProgressRate(progress: 43295, model: line, scrollByWord: true))
39 | XCTAssertNotNil(LyricMachine.calculateProgressRate(progress: 43795, model: line, scrollByWord: true))
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Demo/Demo/Other/Utils/SongSourceProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongSourceProvider.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2024/6/6.
6 | //
7 |
8 | import Foundation
9 | import AgoraLyricsScore
10 |
11 | struct Song {
12 | let name: String
13 | let id: Int
14 | }
15 |
16 | class SongSourceProvider {
17 | var currentIndex = 0
18 |
19 | let songs: [Song]
20 | let sourceType: SourceType
21 |
22 | init(sourceType: SourceType) {
23 | self.sourceType = sourceType
24 | switch sourceType {
25 | case .useForMcc:
26 | self.songs = [
27 | Song(name: "十年", id: 6625526605291650),
28 | Song(name: "爱情转移", id: 6246262727282860),
29 | Song(name: "说爱你", id: 6654550221757560),
30 | Song(name: "江南", id: 6246262727300580),
31 | Song(name: "容易受伤的女人", id: 6625526608670440)
32 | ]
33 | case .useForMccEx:
34 | self.songs = [Song(name: "offset test", id: 32056111),
35 | Song(name: "绿光", id: 32133593),
36 | Song(name: "在你的身边", id: 89488966),
37 | Song(name: "奢香夫人", id: 32259070),
38 | Song(name: "十年", id: 40289835),
39 | Song(name: "明月几时有", id: 239038150)]
40 | }
41 | }
42 |
43 | func getNextSong() -> Song {
44 | let index = genNextIndex()
45 | let song = songs[index]
46 | return song
47 | }
48 |
49 | func genNextIndex() -> Int {
50 | let index = currentIndex
51 | if currentIndex == songs.count - 1 {
52 | currentIndex = 0
53 | }
54 | else {
55 | currentIndex += 1
56 | }
57 | return index
58 | }
59 | }
60 |
61 | extension SongSourceProvider {
62 | enum SourceType {
63 | case useForMcc
64 | case useForMccEx
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/FirstToneHintViewTestVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExpTestVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2022/12/22.
6 | //
7 |
8 | import UIKit
9 | import AgoraLyricsScore
10 |
11 | /// 测试等待视图
12 | class FirstToneHintViewTestVC: UIViewController {
13 |
14 | let karaokeView = KaraokeView()
15 | private var timer = GCDTimer()
16 | var progress: UInt = 0
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | setupUI()
21 | let url = URL(fileURLWithPath: Bundle.main.path(forResource: "745012", ofType: "xml")!)
22 | let data = try! Data(contentsOf: url)
23 | let model = KaraokeView.parseLyricData(lyricFileData: data)!
24 | model.preludeEndPosition = 6 * 1000
25 | karaokeView.setLyricData(data: model, usingInternalScoring: true)
26 | }
27 |
28 | func setupUI() {
29 | view.backgroundColor = .white
30 | view.addSubview(karaokeView)
31 | karaokeView.translatesAutoresizingMaskIntoConstraints = false
32 |
33 | karaokeView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
34 | karaokeView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
35 | karaokeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
36 | karaokeView.heightAnchor.constraint(equalToConstant: 350).isActive = true
37 | }
38 |
39 | func commonInit() {}
40 |
41 | deinit {
42 | timer.destoryTimer(withName: "ExpTestVC")
43 | }
44 |
45 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
46 | timer.scheduledMillisecondsTimer(withName: "ExpTestVC", countDown: 1000000, milliseconds: 10, queue: .main) { [weak self](_, time) in
47 | guard let self = self else { return }
48 | self.progress += 10
49 | self.karaokeView.setProgress(progress: self.progress)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/Other/VoicePitchChanger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VoicePitchChanger.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2022/11/23.
6 | //
7 |
8 | import Foundation
9 |
10 | class VoicePitchChanger {
11 | /// 累计的偏移值
12 | var offset: Double = 0.0
13 | /// 记录调用次数
14 | var n: Double = 0
15 |
16 | /// 处理Pitch
17 | /// - Parameters:
18 | /// - stdPitch: 标准值 来自歌词文件
19 | /// - voicePitch: 实际值 来自rtc回调
20 | /// - stdMaxPitch: 最大值 来自标准值
21 | /// - Returns: 处理后的值
22 | func handlePitch(stdPitch: Double,
23 | voicePitch: Double,
24 | stdMaxPitch: Double) -> Double {
25 | if useC {
26 | return handlePitchC(stdPitch, voicePitch, stdMaxPitch)
27 | }
28 |
29 | if voicePitch <= 0 {
30 | return 0
31 | }
32 |
33 | n += 1.0
34 | let gap = stdPitch - voicePitch
35 |
36 | offset = offset * (n - 1)/n + gap/n
37 |
38 | if offset < 0 {
39 | offset = max(offset, -1 * stdMaxPitch * 0.4)
40 | }
41 | else {
42 | offset = min(offset, stdMaxPitch * 0.4)
43 | }
44 |
45 | if abs(voicePitch - stdPitch) < 1 { /** 差距过小,直接返回 **/
46 | return voicePitch
47 | }
48 |
49 | switch n {
50 | case 1:
51 | return min(voicePitch + 0.5 * offset, stdMaxPitch)
52 | case 2:
53 | return min(voicePitch + 0.6 * offset, stdMaxPitch)
54 | case 3:
55 | return min(voicePitch + 0.7 * offset, stdMaxPitch)
56 | case 4:
57 | return min(voicePitch + 0.8 * offset, stdMaxPitch)
58 | case 5:
59 | return min(voicePitch + 0.9 * offset, stdMaxPitch)
60 | default:
61 | return min(voicePitch + offset, stdMaxPitch)
62 | }
63 | }
64 |
65 | func reset() {
66 | if useC {
67 | resetC()
68 | return
69 | }
70 | offset = 0.0
71 | n = 0.0
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/SongListVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongListVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/3/30.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol SongListVCDelegate: NSObjectProtocol {
11 | func songListVCDidSelectedSong(song: SongListVC.Song)
12 | }
13 |
14 | class SongListVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
15 | var songs = [Song]()
16 | let tableview = UITableView(frame: .zero, style: .grouped)
17 | weak var delegate: SongListVCDelegate?
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | setupUI()
22 | commonInit()
23 | }
24 |
25 | func setupUI() {
26 | view.backgroundColor = .white
27 | view.addSubview(tableview)
28 | tableview.frame = view.bounds
29 | }
30 |
31 | func commonInit() {
32 | tableview.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
33 | tableview.delegate = self
34 | tableview.dataSource = self
35 | }
36 |
37 | // MARK: - UITableViewDelegate, UITableViewDataSource
38 |
39 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
40 | return songs.count
41 | }
42 |
43 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
44 | let cell = tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
45 | let song = songs[indexPath.row]
46 | cell.textLabel?.text = song.name + " [ " + song.singer + " ] "
47 | cell.accessoryType = .disclosureIndicator
48 | return cell
49 | }
50 |
51 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
52 | delegate?.songListVCDidSelectedSong(song: songs[indexPath.row])
53 | dismiss(animated: true)
54 | }
55 | }
56 |
57 | extension SongListVC {
58 | struct Song {
59 | let name: String
60 | let singer: String
61 | let code: Int
62 | let highStartTime: Int
63 | let highEndTime: Int
64 | }
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/DataStructs.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataStructs.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/12/14.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Queue {
11 | private var elements: [T] = []
12 |
13 | mutating func enqueue(_ element: T) {
14 | elements.append(element)
15 | }
16 |
17 | mutating func dequeue() -> T? {
18 | return elements.isEmpty ? nil : elements.removeFirst()
19 | }
20 |
21 | var isEmpty: Bool {
22 | return elements.isEmpty
23 | }
24 |
25 | var count: Int {
26 | return elements.count
27 | }
28 |
29 | func peek() -> T? {
30 | return elements.first
31 | }
32 |
33 | mutating func removeAll() {
34 | elements.removeAll()
35 | }
36 |
37 | func getAll() -> [T] {
38 | return elements
39 | }
40 |
41 | mutating func reset(newElements: [T]) {
42 | elements = newElements
43 | }
44 | }
45 |
46 | /// Dictionary for safe, use rwlock
47 | class SafeDictionary {
48 | private var dict: Dictionary = Dictionary()
49 | private var rwlock = pthread_rwlock_t()
50 | private let logTag = "SafeDictionary"
51 |
52 | init() {
53 | Log.debug(text: "init", tag: logTag)
54 | pthread_rwlock_init(&rwlock, nil)
55 | }
56 |
57 | deinit {
58 | Log.debug(text: "deinit", tag: logTag)
59 | pthread_rwlock_destroy(&rwlock)
60 | }
61 |
62 | func set(value: T_VALUE, forkey: T_KEY) {
63 | pthread_rwlock_wrlock(&rwlock)
64 | dict[forkey] = value
65 | pthread_rwlock_unlock(&rwlock)
66 | }
67 |
68 | func getValue(forkey: T_KEY) -> T_VALUE? {
69 | pthread_rwlock_rdlock(&rwlock)
70 | let value = dict[forkey]
71 | pthread_rwlock_unlock(&rwlock)
72 | return value
73 | }
74 |
75 | func removeValue(forkey: T_KEY) {
76 | pthread_rwlock_wrlock(&rwlock)
77 | dict.removeValue(forKey: forkey)
78 | pthread_rwlock_unlock(&rwlock)
79 | }
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Events.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Events.swift
3 | // NewApi
4 | //
5 | // Created by ZYP on 2022/11/22.
6 | //
7 |
8 | import Foundation
9 |
10 | @objc public protocol KaraokeDelegate: NSObjectProtocol {
11 | /// 拖拽歌词结束后回调
12 | /// - Note: 当 `KaraokeConfig.lyricConfig.draggable == true` 且 用户进行拖动歌词时候 调用
13 | /// - Parameters:
14 | /// - view: KaraokeView
15 | /// - position: 当前时间点 (ms)
16 | @objc optional func onKaraokeView(view: KaraokeView, didDragTo position: UInt)
17 |
18 | /// 歌曲播放完一行(Line)时的歌词回调
19 | /// - Parameters:
20 | /// - model: 行信息
21 | /// - score: 当前行得分 [0, 100]
22 | /// - cumulativeScore: 累计分数
23 | /// - lineIndex: 行索引号 最小值:0
24 | /// - lineCount: 总行数
25 | @objc optional func onKaraokeView(view: KaraokeView,
26 | didFinishLineWith model: LyricLineModel,
27 | score: Int,
28 | cumulativeScore: Int,
29 | lineIndex: Int,
30 | lineCount: Int)
31 | }
32 |
33 | /// 分数计算协议
34 | @objc public protocol IScoreAlgorithm {
35 | // MARK: - 自定义分数
36 |
37 | /// 计算当前行(Line)的分数
38 | /// - Parameters:
39 | /// - models: 字得分信息集合
40 | /// - Returns: 计算后的分数 [0, 100]
41 | @objc func getLineScore(with toneScores: [ToneScoreModel]) -> Int
42 | }
43 |
44 | /// 日志协议
45 | @objc public protocol ILogger {
46 | /// 日志输出
47 | /// - Note: 在子线程执行
48 | /// - Parameters:
49 | /// - content: 内容
50 | /// - tag: 标签
51 | /// - time: 时间
52 | /// - level: 等级
53 | @objc func onLog(content: String, tag: String?, time: String, level: LoggerLevel)
54 | }
55 |
56 | @objc public enum LoggerLevel: UInt8, CustomStringConvertible {
57 | case debug, info, warning, error
58 |
59 | public var description: String {
60 | switch self {
61 | case .debug:
62 | return "D"
63 | case .info:
64 | return "I"
65 | case .warning:
66 | return "W"
67 | case .error:
68 | return "E"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/kj5396d18ebe5811ed9efdb2f16c44e48f.lrc:
--------------------------------------------------------------------------------
1 | [00:47.473]<00:47.473>So<00:47.786> many<00:48.796> times
2 | [00:50.294]<00:50.294>I'm<00:50.607> asking<00:51.617> why
3 | [00:53.011]<00:53.011>Why<00:53.359> not<00:53.742> they<00:54.787> stay<00:55.553> for<00:55.797> a<00:55.867> little<00:56.912> while
4 | [00:58.653]<00:58.653>Why<00:59.036> not<00:59.385> they<01:00.464> stop<01:01.231> and<01:01.509> look<01:02.275> inside
5 | [01:04.330]<01:04.330>But<01:04.714> it's<01:05.131> alright
6 | [01:10.356]<01:10.356>Things<01:11.122> will<01:11.401> change
7 | [01:13.142]<01:13.142>Not<01:13.595> always<01:14.222> great
8 | [01:15.650]<01:15.650>Why<01:15.998> don't<01:16.382> you<01:17.426> slow<01:17.914> down<01:18.158> and<01:18.437> take<01:19.238> a<01:19.516> break
9 | [01:21.258]<01:21.258>Tomorrow<01:23.139> is<01:23.835> a<01:24.114> different<01:25.194> day
10 | [01:27.423]<01:27.423>Cause<01:31.602> it's<01:34.075> alright
11 | [01:39.683]<01:39.683>I<01:42.852> will<01:53.824> be<01:54.137> there<01:56.297> for<01:59.466> you<02:04.830> x3
12 | [02:08.626]<02:08.626>Yes<02:10.786> I<02:11.134> always<02:13.886> will
13 | [02:40.322]<02:40.322>Turn<02:40.774> off<02:41.401> the<02:41.750> light
14 | [02:43.178]<02:43.178>Then<02:43.491> close<02:44.188> your<02:44.606> eyes
15 | [02:45.999]<02:45.999>We<02:46.347> are<02:46.696> here<02:47.775> playing<02:48.472> this<02:48.820> song<02:49.517> for<02:49.900> you
16 | [02:51.607]<02:51.607>Whatever<02:53.453> we<02:54.149> will<02:54.463> make<02:55.229> it<02:55.508> through
17 | [02:55.856]<02:55.856>Cause<02:57.319> it's<02:58.085> alright
18 | [03:03.344]<03:03.344>All<03:04.041> that<03:04.319> pains
19 | [03:06.096]<03:06.096>Come<03:06.862> and<03:07.210> go
20 | [03:08.638]<03:08.638>There<03:08.778> is<03:08.952> one<03:09.300> thing<03:10.380> only<03:11.216> you<03:12.087> should<03:12.470> know
21 | [03:14.211]<03:14.211>Whatever<03:16.022> you<03:16.684> can<03:17.032> let<03:17.834> it<03:18.112> go
22 | [03:19.888]<03:19.888>Yes<03:24.521> it's<03:26.994> alright
23 | [03:51.758]<03:51.758>I<03:57.644> will<03:58.027> be<04:03.321> there<04:56.959> for<05:00.199> you<05:05.493> x3
24 | [05:09.812]<05:09.812>Yes<05:11.484> I<05:11.832> always<05:28.028> will
--------------------------------------------------------------------------------
/Demo/Demo/Other/Utils/ProgressProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressProvider.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2024/4/18.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ProgressProviderDelegate: NSObjectProtocol {
11 | /// 读取播放器的进度,用于校准。
12 | func progressProviderGetPlayerPosition(_ provider: ProgressProvider) -> UInt?
13 | /// 通知外部广播发送进度
14 | func progressProvider(_ provider: ProgressProvider, shouldSend postion: UInt)
15 | /// 进度更新
16 | func progressProvider(_ provider: ProgressProvider, didUpdate progressInMs: UInt)
17 | }
18 |
19 | /// 提供20ms级别的歌曲进度
20 | class ProgressProvider: NSObject {
21 | weak var delegate: ProgressProviderDelegate?
22 | private var timer = GCDTimer()
23 | private var isPause = false
24 | private var lastPrpgress: UInt = 0
25 |
26 | func start() {
27 | timer.scheduledMillisecondsTimer(withName: "ProgressProvider",
28 | countDown: 1000000,
29 | milliseconds: 20,
30 | queue: .main) { [weak self](_, time) in
31 |
32 | guard let self = self else { return }
33 | if self.isPause {
34 | return
35 | }
36 |
37 | var current = self.lastPrpgress
38 | if time.truncatingRemainder(dividingBy: 1000) == 0 {
39 | current = delegate!.progressProviderGetPlayerPosition(self) ?? current
40 | delegate?.progressProvider(self, shouldSend: current + 20)
41 | }
42 | current += 20
43 |
44 | self.lastPrpgress = current
45 | delegate?.progressProvider(self, didUpdate: current)
46 | }
47 | }
48 |
49 | func seek(position: UInt) {
50 | timer.destoryTimer(withName: "ProgressProvider")
51 | lastPrpgress = position
52 | start()
53 | }
54 |
55 | func pause() {
56 | isPause = true
57 | }
58 |
59 | func resume() {
60 | isPause = false
61 | }
62 |
63 | func stop() {
64 | isPause = false
65 | lastPrpgress = 0
66 | timer.destoryTimer(withName: "ProgressProvider")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/ScoringMachineProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScoringMachineProtocol.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2024/6/4.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ScoringMachineProtocol {
11 | typealias Info = ScoringMachineInfo
12 | typealias DrawInfo = ScoringMachineDrawInfo
13 | typealias DebugInfo = ScoringMachineDebugInfo
14 |
15 | var delegate: ScoringMachineDelegate? { get set }
16 | /// 游标的起始位置
17 | var defaultPitchCursorX: CGFloat { get set }
18 | /// 音准线的高度
19 | var standardPitchStickViewHeight: CGFloat { get set }
20 | /// 音准线的基准因子
21 | var movingSpeedFactor: CGFloat { get set }
22 | /// 打分容忍度 范围:0-1
23 | var hitScoreThreshold: Float { get set }
24 | var scoreLevel: Int { get set }
25 | var scoreCompensationOffset: Int { get set }
26 |
27 | func setLyricData(data: LyricModel?)
28 | func setProgress(progress: UInt)
29 | func setPitch(speakerPitch: Double,
30 | progressInMs: UInt)
31 | func dragBegain()
32 | func dragDidEnd(position: UInt)
33 | func getCumulativeScore() -> Int
34 | func reset()
35 |
36 | var scoreAlgorithm: IScoreAlgorithm { get set }
37 |
38 | }
39 |
40 | protocol ScoringMachineDelegate: NSObjectProtocol {
41 | /// 获取渲染视图的尺寸
42 | func sizeOfCanvasView(_ scoringMachine: ScoringMachineProtocol) -> CGSize
43 |
44 | /// 更新渲染信息
45 | func scoringMachine(_ scoringMachine: ScoringMachineProtocol,
46 | didUpdateDraw standardInfos: [ScoringMachineDrawInfo],
47 | highlightInfos: [ScoringMachineDrawInfo])
48 | /// 更新游标位置
49 | func scoringMachine(_ scoringMachine: ScoringMachineProtocol,
50 | didUpdateCursor centerY: CGFloat,
51 | showAnimation: Bool,
52 | debugInfo: ScoringMachineDebugInfo)
53 |
54 | /// 更新句子分数
55 | func scoringMachine(_ scoringMachine: ScoringMachineProtocol,
56 | didFinishLineWith model: LyricLineModel,
57 | score: Int,
58 | cumulativeScore: Int,
59 | lineIndex: Int,
60 | lineCount: Int)
61 | }
62 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Downloader/Extentions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AgoraStringExtention.swift
3 | // AgoraKaraokeScore
4 | //
5 | // Created by zhaoyongqiang on 2021/12/10.
6 | //
7 |
8 | import UIKit
9 |
10 | extension String {
11 | // 获取时间格式
12 | func timeIntervalToMMSSFormat(interval: TimeInterval) -> String {
13 | if interval >= 3600 {
14 | let hour = interval / 3600
15 | let min = interval.truncatingRemainder(dividingBy: 3600) / 60
16 | let sec = interval.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60)
17 | return String(format: "%02d:%02d:%02d", Int(hour), Int(min), Int(sec))
18 | } else {
19 | let min = interval / 60
20 | let sec = interval.truncatingRemainder(dividingBy: 60)
21 | return String(format: "%02d:%02d", Int(min), Int(sec))
22 | }
23 | }
24 |
25 | /**
26 | * 缓存文件夹路径
27 | */
28 | static func cacheFolderPath() -> String {
29 | return NSHomeDirectory().appending("/Library").appending("/MusicCaches").appending("/lyricFiles")
30 | }
31 |
32 | /// 下载目录
33 | static func downloadedFloderPath() -> String {
34 | return NSHomeDirectory().appending("/tmp").appending("/LyricDownloadFiles")
35 | }
36 |
37 | /**
38 | * 获取网址中的文件名
39 | */
40 | var fileName: String {
41 | components(separatedBy: "/").last ?? ""
42 | }
43 | }
44 |
45 | extension FileManager {
46 | static func createDirectoryIfNeeded(atPath path: String) {
47 | let fileManager = FileManager.default
48 | var isDirectory: ObjCBool = false
49 | if fileManager.fileExists(atPath: path, isDirectory: &isDirectory) {
50 | if !isDirectory.boolValue {
51 | Log.debug(text: "给定的路径是一个文件: \(path)", tag: "Downloader Extension")
52 | return
53 | }
54 | Log.debug(text: "已存在:\(path)")
55 | } else {
56 | do {
57 | try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
58 | Log.debug(text: "已成功创建目录: \(path)", tag: "Downloader Extension")
59 | } catch {
60 | Log.errorText(text: "创建目录失败: \(error.localizedDescription)", tag: "Downloader Extension")
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/ProfileVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/2/27.
6 | //
7 |
8 | import Foundation
9 | import AgoraLyricsScore
10 |
11 | class ProfileVC: UIViewController {
12 | let karaokeView = KaraokeView()
13 | var progress: UInt = 0
14 | private var timer = GCDTimer()
15 | var model: LyricModel!
16 |
17 | override func viewDidLoad() {
18 | super.viewDidLoad()
19 | view.backgroundColor = .white
20 | karaokeView.backgroundImage = UIImage(named: "ktv_top_bgIcon")
21 | karaokeView.scoringView.viewHeight = 100
22 | karaokeView.scoringView.topSpaces = 80
23 | karaokeView.lyricsView.showDebugView = false
24 | view.addSubview(karaokeView)
25 |
26 | karaokeView.translatesAutoresizingMaskIntoConstraints = false
27 |
28 | karaokeView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
29 | karaokeView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
30 | karaokeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
31 | karaokeView.heightAnchor.constraint(equalToConstant: 350).isActive = true
32 | }
33 |
34 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
35 | let url = URL(fileURLWithPath: Bundle.main.path(forResource: "745012", ofType: "xml")!)
36 | let data = try! Data(contentsOf: url)
37 | model = KaraokeView.parseLyricData(lyricFileData: data)!
38 | karaokeView.setLyricData(data: model, usingInternalScoring: true)
39 | var count = 0
40 | timer.scheduledMillisecondsTimer(withName: "ProfileVC", countDown: 1000000, milliseconds: 20, queue: .main) { [weak self](_, time) in
41 | guard let self = self else { return }
42 | self.progress += 20
43 |
44 | if self.progress > self.model.duration {
45 | self.progress = 0
46 | self.timer.destoryAllTimer()
47 | }
48 |
49 | self.karaokeView.setProgress(progress: self.progress)
50 |
51 | count += 1
52 | if count == 3 {
53 | count = 0
54 | self.karaokeView.setPitch(speakerPitch: Double.random(in: 87...300), progressInMs: 0)
55 | }
56 |
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Demo/Demo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2022/12/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/2/3.
6 | //
7 |
8 | import Foundation
9 | import AgoraComponetLog
10 |
11 | // MARK:- ConsoleLogger
12 |
13 | public class ConsoleLogger: NSObject, ILogger {
14 | @objc public func onLog(content: String,
15 | tag: String?,
16 | time: String,
17 | level: LoggerLevel) {
18 |
19 | let text = tag == nil ? "[\(time)][ALS][\(level)]: " + content : "[\(time)][ALS][\(level)][\(tag!)]: " + content
20 | print(text)
21 | }
22 | }
23 |
24 | // MARK:- FileLogger
25 |
26 | public class FileLogger: NSObject, ILogger {
27 | let componetFileLogger: AgoraComponetFileLogger!
28 | let filePrefixName = "agora.AgoraLyricsScore"
29 | let maxFileSizeOfBytes: UInt64 = 1024 * 1024 * 1
30 | let maxFileCount: UInt = 4
31 | let domainName = "ALS"
32 |
33 | @objc public override init() {
34 | self.componetFileLogger = AgoraComponetFileLogger(logFilePath: nil,
35 | filePrefixName: filePrefixName,
36 | maxFileSizeOfBytes: maxFileSizeOfBytes,
37 | maxFileCount: maxFileCount,
38 | domainName: domainName)
39 | super.init()
40 | }
41 |
42 | /// init
43 | /// - Parameter logFilePath: custom log file path.
44 | @objc public init(logFilePath: String) {
45 | componetFileLogger = AgoraComponetFileLogger(logFilePath: logFilePath,
46 | filePrefixName: filePrefixName,
47 | maxFileSizeOfBytes: maxFileSizeOfBytes,
48 | maxFileCount: maxFileCount,
49 |
50 | domainName: domainName)
51 | }
52 |
53 | @objc public func onLog(content: String,
54 | tag: String?,
55 | time: String,
56 | level: LoggerLevel) {
57 | let newLevel = AgoraComponetLoggerLevel(rawValue: UInt(level.rawValue))!
58 | componetFileLogger.onLog(withContent: content, tag: tag, time: time, level: newLevel)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Downloader/DownloaderManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloaderManager.swift
3 | // URLSessionDownloadDemo
4 | //
5 | // Created by FancyLou on 2019/2/22.
6 | // Copyright © 2019 O2OA. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | class DownloaderManager: NSObject {
13 | // 下载缓存池
14 | private var downloadCache = SafeDictionary()
15 | private var failback: DownloadFailClosure?
16 | private let logTag = "DownloaderManager"
17 |
18 | deinit {
19 | Log.info(text: "deinit", tag: logTag)
20 | let downloadFloderURL = NSURL(fileURLWithPath: String.downloadedFloderPath())
21 | FileManager.createDirectoryIfNeeded(atPath: downloadFloderURL.path!)
22 | }
23 |
24 | override init() {
25 | Log.info(text: "init", tag: logTag)
26 | }
27 |
28 | func download(url: URL,
29 | progress: @escaping DownloadProgressClosure,
30 | completion: @escaping DownloadCompletionClosure,
31 | fail: @escaping DownloadFailClosure) {
32 | self.failback = fail
33 | // 判断缓存池中是否已经存在
34 | var downloader = downloadCache.getValue(forkey: url.absoluteString)
35 | if downloader != nil {
36 | Log.errorText(text: "当前下载已存在不需要重复下载!", tag: logTag)
37 | let e = DownloadError(domainType: .repeatDownloading,
38 | code: DownloadErrorDomainType.repeatDownloading.rawValue,
39 | msg: "当前下载已存在不需要重复下载")
40 | self.failback?(e)
41 | return
42 | }
43 | downloader = Downloader()
44 | downloadCache.set(value: downloader!, forkey: url.absoluteString)
45 | downloader?.download(url: url, progress: progress, completion: { [weak self](filePath) in
46 | guard let self = self else {
47 | return
48 | }
49 | downloadCache.removeValue(forkey: url.absoluteString)
50 | completion(filePath)
51 | }, fail: fail)
52 | }
53 |
54 | func cancelTask(url: URL) {
55 | let downloader = downloadCache.getValue(forkey: url.absoluteString)
56 | if downloader == nil {
57 | Log.debug(text: "任务已经移除,不需要重复移除!", tag: logTag)
58 | return
59 | }
60 | // 从缓存池中删除
61 | downloadCache.removeValue(forkey: url.absoluteString)
62 | //结束任务
63 | downloader?.resetEventCloure()
64 | downloader?.cancel()
65 | }
66 |
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Lyrics/LyricLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LyricsLabel.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2022/12/23.
6 | //
7 |
8 | import UIKit
9 |
10 | class LyricLabel: UILabel {
11 | /// [0, 1]
12 | var progressRate: CGFloat = 0 { didSet { setNeedsDisplay() } }
13 | /// 正常歌词颜色
14 | var textNormalColor: UIColor = .gray
15 | /// 选中的歌词颜色
16 | var textSelectedColor: UIColor = .white
17 | /// 高亮的歌词颜色
18 | var textHighlightedColor: UIColor = .orange
19 | /// 正常歌词文字大小
20 | var textNormalFontSize: UIFont = .systemFont(ofSize: 15)
21 | /// 高亮歌词文字大小
22 | var textHighlightFontSize: UIFont = .systemFont(ofSize: 18)
23 |
24 | var status: Status = .normal { didSet { updateState() } }
25 |
26 | override init(frame: CGRect) {
27 | super.init(frame: frame)
28 | textAlignment = .center
29 | }
30 |
31 | required init?(coder: NSCoder) {
32 | fatalError("init(coder:) has not been implemented")
33 | }
34 |
35 | private func updateState() {
36 | if status == .selectedOrHighlighted {
37 | textColor = textSelectedColor
38 | font = textHighlightFontSize
39 | }
40 | else {
41 | textColor = textNormalColor
42 | font = textNormalFontSize
43 | }
44 | }
45 |
46 | override func draw(_ rect: CGRect) {
47 | super.draw(rect)
48 | if progressRate <= 0 {
49 | return
50 | }
51 | let textWidth = sizeThatFits(CGSize(width: CGFloat(MAXFLOAT),
52 | height: font.lineHeight)).width
53 | var leftRightSpace = (bounds.width - textWidth) / 2
54 | if textAlignment == .left { leftRightSpace = 0.0 }
55 | if textAlignment == .right { leftRightSpace = bounds.width - textWidth }
56 | let path = CGMutablePath()
57 | let fillRect = CGRect(x: leftRightSpace,
58 | y: 0,
59 | width: progressRate * textWidth,
60 | height: font.lineHeight)
61 | path.addRect(fillRect)
62 |
63 | if let context = UIGraphicsGetCurrentContext(), !path.isEmpty {
64 | context.setLineWidth(1.0)
65 | context.setLineCap(.butt)
66 | context.addPath(path)
67 | context.clip()
68 | let _textColor = textColor
69 | textColor = textHighlightedColor
70 | super.draw(rect)
71 | textColor = _textColor
72 | }
73 | }
74 | }
75 |
76 | extension LyricLabel {
77 | enum Status {
78 | case normal
79 | case selectedOrHighlighted
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/AVPlayerTestVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AVPlayerTestVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2022/12/23.
6 | //
7 |
8 | import UIKit
9 | import AgoraRtcKit
10 | import RTMTokenBuilder
11 | import AgoraLyricsScore
12 | import AVFoundation
13 |
14 | class AVPlayerTestVC: UIViewController {
15 | let karaokeView = KaraokeView()
16 | private var audioPlayer: AVAudioPlayer?
17 | let lrcUrl = Bundle.main.path(forResource: "105780", ofType: "xml")!
18 | let songUrl = Bundle.main.path(forResource: "music", ofType: "mp3")!
19 | private var timer = GCDTimer()
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 | setupUI()
24 | commonInit()
25 | }
26 |
27 | func setupUI() {
28 | view.backgroundColor = .black
29 | view.addSubview(karaokeView)
30 | karaokeView.translatesAutoresizingMaskIntoConstraints = false
31 |
32 | karaokeView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
33 | karaokeView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
34 | karaokeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
35 | karaokeView.heightAnchor.constraint(equalToConstant: 350).isActive = true
36 | }
37 |
38 | func commonInit() {
39 | let data = try! Data(contentsOf: URL(fileURLWithPath: lrcUrl))
40 | let model = KaraokeView.parseLyricData(lyricFileData: data)!
41 | karaokeView.setLyricData(data: model, usingInternalScoring: true)
42 | initAudioPlayer(url:URL(fileURLWithPath: songUrl))
43 | let ret = audioPlayer!.play()
44 | print("play ret \(ret)")
45 |
46 |
47 |
48 | timer.scheduledMillisecondsTimer(withName: "AVPlayerTestVC",
49 | countDown: 1000000,
50 | milliseconds: 50,
51 | queue: .main) { [weak self](_, time) in
52 | guard let self = self else { return }
53 | if let currentTime = self.audioPlayer?.currentTime {
54 | self.karaokeView.setProgress(progress: UInt(currentTime) * 1000)
55 | }
56 | }
57 | }
58 |
59 | private func initAudioPlayer(url: URL) {
60 | do {
61 | audioPlayer = try AVAudioPlayer(contentsOf: url)
62 | } catch let error {
63 | print("\(error)")
64 | print("")
65 | }
66 | audioPlayer?.rate = 1.0
67 | audioPlayer?.prepareToPlay()
68 | }
69 |
70 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
71 |
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/PitchParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PitchParser.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2024/5/9.
6 | //
7 |
8 | import Foundation
9 |
10 | class PitchParser {
11 | fileprivate let logTag = "PitchParser"
12 |
13 | func parse(fileContent data: Data) -> PitchModel? {
14 | /// 把data转成PitchModel
15 | guard !data.isEmpty else {
16 | return nil
17 | }
18 |
19 | do {
20 | let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
21 |
22 | guard let jsonDict = jsonObject as? [String: Any],
23 | let pitchDatasArray = jsonDict["pitchDatas"] as? [[String: Any]] else {
24 | Log.errorText(text: "Invalid JSON format: missing pitchDatas array", tag: logTag)
25 | return nil
26 | }
27 |
28 | var pitchDatas: [KrcPitchData] = []
29 |
30 | for pitchDataDict in pitchDatasArray {
31 | // 处理pitch字段 - 支持Int和Double
32 | guard let pitch = (pitchDataDict["pitch"] as? Double) ??
33 | (pitchDataDict["pitch"] as? Int).map(Double.init) else {
34 | Log.errorText(text: "Invalid pitch data format in array", tag: logTag)
35 | continue
36 | }
37 |
38 | // 处理startTime字段 - 支持Int和UInt
39 | guard let startTime = (pitchDataDict["startTime"] as? UInt) ??
40 | (pitchDataDict["startTime"] as? Int).map(UInt.init) else {
41 | Log.errorText(text: "Invalid pitch data format in array", tag: logTag)
42 | continue
43 | }
44 |
45 | // 处理duration字段 - 支持Int和UInt
46 | guard let duration = (pitchDataDict["duration"] as? UInt) ??
47 | (pitchDataDict["duration"] as? Int).map(UInt.init) else {
48 | Log.errorText(text: "Invalid pitch data format in array", tag: logTag)
49 | continue
50 | }
51 |
52 | let krcPitchData = KrcPitchData(pitch: pitch, startTime: startTime, duration: duration)
53 | pitchDatas.append(krcPitchData)
54 | }
55 |
56 | return PitchModel(pitchDatas: pitchDatas)
57 |
58 | } catch let error {
59 | Log.error(error: error.localizedDescription, tag: logTag)
60 | return nil
61 | }
62 | }
63 | }
64 |
65 |
66 | extension PitchParser {
67 | struct PitchModel {
68 | let pitchDatas: [KrcPitchData]
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestScoringMachine/LogFileReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogFileReader.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2023/11/8.
6 | //
7 |
8 | import Foundation
9 |
10 | class LogFileReader {
11 | struct Item {
12 | let progress: Int?
13 | let pitch: Double?
14 | }
15 |
16 | static func readFromFile(fileName: String) -> (result1: [Item], result2: [Item]) {
17 | let name = "TestLogFile/\(fileName).txt"
18 | let fileUrl = URL(fileURLWithPath: Bundle.current.path(forResource: name, ofType:nil)!)
19 | let fileData = try! Data(contentsOf: fileUrl)
20 | let string = String(data: fileData, encoding: .utf8)!
21 | let array = string.split(separator: "\n").map({ String($0) })
22 |
23 |
24 | var result1 = [Item]()
25 | var result2 = [Item]()
26 | var songIndex: Int = -1
27 | for str in array {
28 | if str.contains("[ALS][I][KaraokeView]: setLyricData") {
29 | if songIndex == -1 {
30 | songIndex = 0
31 | continue
32 | }
33 | if songIndex == 0 {
34 | songIndex = 1
35 | continue
36 | }
37 | }
38 | if songIndex == -1 { continue }
39 |
40 | if str.contains("[ALS][D][ScoringMachine]: progress: ") {
41 | let strVal = String(str.split(separator: " ").last!)
42 | let progress = Int(strVal)!
43 | let item = Item(progress: progress, pitch: nil)
44 | if songIndex == 0 {
45 | result1.append(item)
46 | }
47 | else {
48 | result2.append(item)
49 | }
50 | continue
51 | }
52 |
53 | if str.contains("[ALS][D][ScoringMachine]: pitch:") {
54 | let parts = str.components(separatedBy: "pitch: ")
55 | if let lastPart = parts.last {
56 | let parts2 = lastPart.components(separatedBy: " after: ")
57 | if let firstPart = parts2.first {
58 | if let pitch = Double(firstPart) {
59 | let item = Item(progress: nil, pitch: pitch)
60 | if songIndex == 0 {
61 | result1.append(item)
62 | }
63 | else {
64 | result2.append(item)
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | return (result1, result2)
72 | }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/OCVC.m:
--------------------------------------------------------------------------------
1 | //
2 | // OCVC.m
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/2/2.
6 | //
7 |
8 | #import "OCVC.h"
9 | @import AgoraLyricsScore;
10 | @import ScoreEffectUI;
11 |
12 | @interface OCVC ()
13 |
14 | @end
15 |
16 | @implementation OCVC
17 |
18 | - (void)viewDidLoad {
19 | [super viewDidLoad];
20 | self.view.backgroundColor = [UIColor blackColor];
21 | FileLogger *fileLogger = [[FileLogger alloc] init];
22 | KaraokeView *karaokeView = [[KaraokeView alloc] initWithFrame:CGRectZero
23 | loggers:@[[ConsoleLogger new],fileLogger]];
24 | karaokeView.backgroundImage = [UIImage imageNamed:@"ktv_top_bgIcon"];
25 | karaokeView.spacing = 5;
26 | karaokeView.scoringEnabled = YES;
27 | karaokeView.frame = CGRectMake(0, 100, self.view.bounds.size.width, 380);
28 | [self.view addSubview:karaokeView];
29 | karaokeView.delegate = self;
30 |
31 | ScoringView *sView = karaokeView.scoringView;
32 | LyricsView *lView= karaokeView.lyricsView;
33 | sView.viewHeight = 160;
34 | sView.topSpaces = 70;
35 |
36 | GradeView *gview = [[GradeView alloc] initWithFrame:CGRectMake(15, 110, UIScreen.mainScreen.bounds.size.width - 30, 50)];
37 | [gview setTitleWithTitle:@"123" customCumulativeScoreText:nil];
38 |
39 | [self.view addSubview:gview];
40 | [self.view layoutIfNeeded];
41 | [gview setScoreWithCumulativeScore:1110 totalScore:4000 customCumulativeScoreText:nil];
42 |
43 | karaokeView.lyricsView.firstToneHintViewStyle.size = 15;
44 | karaokeView.lyricsView.firstToneHintViewStyle.bottomMargin = 25;
45 | karaokeView.lyricsView.firstToneHintViewStyle.backgroundColor = [UIColor redColor];
46 |
47 | IncentiveView *incentiveView = [IncentiveView new];
48 | incentiveView.frame = karaokeView.scoringView.bounds;
49 | [self.view addSubview:incentiveView];
50 |
51 | [incentiveView showWithScore:80];
52 |
53 | lView.draggable = YES;
54 | lView.noLyricTipsFont = [UIFont systemFontOfSize:23];
55 | lView.noLyricTipsText = @"没有歌词呢";
56 | lView.noLyricTipsColor = [UIColor redColor];
57 | // [karaokeView setLyricDataWithData:nil];
58 | }
59 |
60 |
61 | - (void)onKaraokeViewWithView:(KaraokeView *)view didDragTo:(NSInteger)position {
62 |
63 | }
64 |
65 | - (void)onKaraokeViewWithView:(KaraokeView *)view
66 | didFinishLineWith:(LyricLineModel *)model
67 | score:(NSInteger)score
68 | cumulativeScore:(NSInteger)cumulativeScore
69 | lineIndex:(NSInteger)lineIndex lineCount:(NSInteger)lineCount {}
70 |
71 | - (void)onLogWithContent:(NSString *)content tag:(NSString *)tag time:(NSString *)time level:(enum LoggerLevel)level {
72 |
73 | }
74 |
75 | @end
76 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Al/Algorithm.c:
--------------------------------------------------------------------------------
1 | //
2 | // Algorithm.c
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/11/9.
6 | //
7 |
8 | #include "Algorithm.h"
9 | #import
10 | //modify by xuguangjian
11 | #define PTS_version "20231021001"
12 |
13 | #define min(a, b) ((a) < (b) ? (a) : (b))
14 | #define max(a, b) ((a) > (b) ? (a) : (b))
15 |
16 |
17 | double pitchToToneC(double pitch) {
18 | double eps = 1e-6;
19 | return (fmax(0, log(pitch / 55 + eps) / log(2))) * 12;
20 | }
21 |
22 | float calculedScoreC(double voicePitch, double stdPitch, int scoreLevel, int scoreCompensationOffset) {
23 | if (voicePitch <= 0) {
24 | return 0;
25 | }
26 | if(stdPitch <= 0){
27 | return 0;
28 | }
29 |
30 | if(scoreLevel<=0){
31 | scoreLevel = 1;
32 | }else if(scoreLevel > 100){
33 | scoreLevel = 100;
34 | }
35 |
36 | if(scoreCompensationOffset<0){
37 | scoreCompensationOffset = 0;
38 | }else if(scoreCompensationOffset > 100){
39 | scoreCompensationOffset = 100;
40 | }
41 |
42 | double stdTone = pitchToToneC(stdPitch);
43 | double voiceTone = pitchToToneC(voicePitch);
44 |
45 | float match = 1 - (float)scoreLevel / 100 * fabs(voiceTone - stdTone) + (float)scoreCompensationOffset / 100;
46 | float rate = 1 + ((float)scoreLevel/(float)50);
47 |
48 | match = match * 100 * rate;
49 |
50 | match = max(0, match);
51 | match = min(100, match);
52 | return match;
53 | }
54 |
55 | static double n;
56 | static double offset;
57 |
58 |
59 | // octave pitch compensation v0.2
60 | double handlePitchC(double stdPitch, double voicePitch, double stdMaxPitch) {
61 |
62 | int cnt = 0;
63 | double stdTone = pitchToToneC(stdPitch);
64 | double voiceTone = pitchToToneC(voicePitch);
65 |
66 | if (voicePitch <= 0) {
67 | return 0;
68 | }
69 | if(stdPitch <= 0){
70 | return 0;
71 | }
72 |
73 | if(fabs(voiceTone - stdTone) <= 6){
74 | return voicePitch;
75 | }
76 | else if(voicePitch < stdPitch){
77 | for(cnt = 0; cnt <11; cnt++){
78 | voicePitch = 2*voicePitch;
79 | voiceTone = pitchToToneC(voicePitch);
80 | if(fabs(voiceTone - stdTone) <= 6){
81 | return voicePitch;
82 | }
83 | }
84 | }
85 | else if(voicePitch > stdPitch){
86 | for(cnt = 0; cnt <11; cnt++){
87 | voicePitch = voicePitch/2;
88 | voiceTone = pitchToToneC(voicePitch);
89 | if(fabs(voiceTone - stdTone) <= 6){
90 | return voicePitch;
91 | }
92 | }
93 | }
94 | return voicePitch;
95 | }
96 |
97 | void resetC(void) {
98 | offset = 0.0;
99 | n = 0.0;
100 | }
101 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/Parser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2022/12/21.
6 | //
7 |
8 | import Foundation
9 |
10 | class Parser {
11 | private let logTag = "Parser"
12 | func parseLyricData(data: Data,
13 | pitchFileData: Data? = nil,
14 | lyricOffset: Int,
15 | includeCopyrightSentence: Bool = false) -> LyricModel? {
16 | guard data.count > 0 else {
17 | Log.errorText(text: "data.count == 0", tag: logTag)
18 | return nil
19 | }
20 |
21 | let format = detectLyricFormat(from: data)
22 |
23 | switch format {
24 | case .krc:
25 | let parser = KRCParser()
26 | return parser.parse(krcFileData: data,
27 | pitchFileData: pitchFileData,
28 | lyricOffset: lyricOffset,
29 | includeCopyrightSentence: includeCopyrightSentence)
30 | case .xml:
31 | let parser = XmlParser()
32 | return parser.parseLyricData(data: data)
33 | case .lrc:
34 | let parser = LrcParser()
35 | return parser.parseLyricData(data: data)
36 | case .unknown:
37 | Log.errorText(text: "unknow file type", tag: logTag)
38 | return nil
39 | }
40 | }
41 |
42 | enum LyricFormat {
43 | case lrc
44 | case krc
45 | case xml
46 | case unknown
47 | }
48 |
49 | func detectLyricFormat(from data: Data) -> LyricFormat {
50 | // 将Data类型转换为String
51 | guard let string = String(data: data, encoding: .utf8) else {
52 | return .unknown
53 | }
54 |
55 | // 检测LRC格式的特征,通常是时间戳和歌词行
56 | // 正则表达式匹配LRC格式的时间戳
57 | let lrcPattern = "\\[\\d{2}:\\d{2}\\.\\d{2,3}\\]"
58 | if let _ = string.range(of: lrcPattern, options: .regularExpression) {
59 | return .lrc
60 | }
61 |
62 | /// enhance lrc:[00:50.677]<00:50.677>走<00:50.956>走
63 | let enhanceLrcPattern = "\\[\\d{2}:\\d{2}\\.\\d{2,3}\\]\\<\\d{2}:\\d{2}\\.\\d{2,3}\\>"
64 | if let _ = string.range(of: enhanceLrcPattern, options: .regularExpression) {
65 | return .lrc
66 | }
67 |
68 | // 检测KRC格式的特征
69 | let krcPattern = "\\[(\\w+):([^]]*)]"
70 | if let _ = string.range(of: krcPattern, options: .regularExpression) {
71 | return .krc
72 | }
73 |
74 | // 检测XML格式的特征,即XML的开始标签
75 | if string.contains(""), string.contains("") {
81 | return .xml
82 | }
83 |
84 | // 如果没有匹配的特征,返回unknown
85 | return .unknown
86 | }
87 |
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/Demo/Demo/View/KTVView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KTVView.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/3/21.
6 | //
7 |
8 | import UIKit
9 | import AgoraLyricsScore
10 | import ScoreEffectUI
11 |
12 | /// 包装 KaraokeView & LineScoreView & GradeView & incentiveView
13 | class KTVView: UIView {
14 | let karaokeView = KaraokeView(frame: .zero, loggers: [ConsoleLogger()])
15 | let lineScoreView = LineScoreView()
16 | let gradeView = GradeView()
17 | let incentiveView = IncentiveView()
18 |
19 | override init(frame: CGRect) {
20 | super.init(frame: frame)
21 | setupUI()
22 | }
23 |
24 | required init?(coder: NSCoder) {
25 | fatalError("init(coder:) has not been implemented")
26 | }
27 |
28 | func setupUI() {
29 | karaokeView.backgroundImage = UIImage(named: "ktv_top_bgIcon")
30 | karaokeView.scoringView.viewHeight = 100
31 | karaokeView.scoringView.topSpaces = 80
32 | karaokeView.lyricsView.showDebugView = false
33 |
34 | backgroundColor = .black
35 | addSubview(karaokeView)
36 | addSubview(gradeView)
37 | addSubview(incentiveView)
38 | addSubview(lineScoreView)
39 |
40 | karaokeView.translatesAutoresizingMaskIntoConstraints = false
41 | gradeView.translatesAutoresizingMaskIntoConstraints = false
42 | incentiveView.translatesAutoresizingMaskIntoConstraints = false
43 | lineScoreView.translatesAutoresizingMaskIntoConstraints = false
44 |
45 | karaokeView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
46 | karaokeView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
47 | karaokeView.topAnchor.constraint(equalTo: topAnchor).isActive = true
48 | karaokeView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
49 |
50 | gradeView.topAnchor.constraint(equalTo: karaokeView.topAnchor, constant: 15).isActive = true
51 | gradeView.leftAnchor.constraint(equalTo: karaokeView.leftAnchor, constant: 15).isActive = true
52 | gradeView.rightAnchor.constraint(equalTo: karaokeView.rightAnchor, constant: -15).isActive = true
53 | gradeView.heightAnchor.constraint(equalToConstant: 40).isActive = true
54 |
55 | incentiveView.centerYAnchor.constraint(equalTo: karaokeView.scoringView.centerYAnchor).isActive = true
56 | incentiveView.centerXAnchor.constraint(equalTo: karaokeView.centerXAnchor, constant: -10).isActive = true
57 |
58 | lineScoreView.leftAnchor.constraint(equalTo: leftAnchor, constant: karaokeView.scoringView.defaultPitchCursorX).isActive = true
59 | lineScoreView.topAnchor.constraint(equalTo: karaokeView.topAnchor, constant: karaokeView.scoringView.topSpaces).isActive = true
60 | lineScoreView.heightAnchor.constraint(equalToConstant: karaokeView.scoringView.viewHeight).isActive = true
61 | lineScoreView.widthAnchor.constraint(equalToConstant: 50).isActive = true
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestDownload/TestDownloadCancle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestDownloadCancle.swift
3 | // AgoraComponetLog-Unit-Tests
4 | //
5 | // Created by ZYP on 2024/1/17.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | final class TestDownloadCancle: XCTestCase, LyricsFileDownloaderDelegate {
12 | let exp = XCTestExpectation(description: "TestDownloadCancle.cancel")
13 | var actualSuccessCount = 0
14 | var lyricsFileDownloader: LyricsFileDownloader!
15 | let urlStrings = ["https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/1.zip",
16 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/2.zip",
17 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/3.zip",
18 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/4.zip",
19 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/5.zip",
20 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/6.zip",
21 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/7.zip",
22 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/8.lrc",
23 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/9.lrc",
24 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/10.lrc"]
25 |
26 |
27 | override func setUpWithError() throws {
28 | exp.expectedFulfillmentCount = 7
29 | Downloader.requestTimeoutInterval = 9
30 | Log.setLoggers(loggers: [ConsoleLogger()])
31 | }
32 |
33 | override func tearDownWithError() throws {
34 | lyricsFileDownloader = nil
35 | }
36 |
37 | func testExample() throws {
38 | lyricsFileDownloader = LyricsFileDownloader()
39 | lyricsFileDownloader.delegate = self
40 | lyricsFileDownloader.cleanAll()
41 |
42 | for url in urlStrings {
43 | let id = lyricsFileDownloader.download(urlString: url)
44 | print("[test] download for id:\(id)")
45 | }
46 |
47 | lyricsFileDownloader.cancelDownload(requestId: 7)
48 | lyricsFileDownloader.cancelDownload(requestId: 8)
49 | lyricsFileDownloader.cancelDownload(requestId: 9)
50 |
51 | wait(for: [exp], timeout: 10)
52 |
53 | let fileCache = FileCache()
54 | let count = fileCache.findFiles(inDirectory: .cacheFolderPath()).count
55 | XCTAssertEqual(count, actualSuccessCount)
56 | }
57 |
58 | func onLyricsFileDownloadCompleted(requestId: Int,
59 | fileData: Data?,
60 | error: DownloadError?) {
61 | if error == nil {
62 | actualSuccessCount += 1
63 | }
64 | exp.fulfill()
65 | }
66 |
67 | func onLyricsFileDownloadProgress(requestId: Int, progress: Float) {
68 |
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestDownload/TestDownloadMuti.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestDownloadMuti.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/12/14.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | final class TestDownloadMuti: XCTestCase, LyricsFileDownloaderDelegate {
12 | var lyricsFileDownloader: LyricsFileDownloader!
13 | let exp = XCTestExpectation(description: "TestDownloadMuti")
14 | let expFail = XCTestExpectation(description: "TestDownloadMuti fail")
15 | let urlStrings = ["https://127.0.0.1/lyricsMockDownload/1.zip",
16 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/1.zip",
17 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/2.zip",
18 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/3.zip",
19 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/4.zip",
20 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/5.zip",
21 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/6.zip",
22 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/7.zip",
23 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/8.lrc",
24 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/9.lrc",
25 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/10.lrc"]
26 | override func setUpWithError() throws {
27 | exp.expectedFulfillmentCount = 10
28 | Log.setLoggers(loggers: [ConsoleLogger()])
29 | lyricsFileDownloader = LyricsFileDownloader()
30 | lyricsFileDownloader.cleanAll()
31 | }
32 |
33 | override func tearDownWithError() throws {
34 | Downloader.requestTimeoutInterval = 60
35 | lyricsFileDownloader.cleanAll()
36 | lyricsFileDownloader = nil
37 | }
38 |
39 | func testMuti() throws {
40 | Downloader.requestTimeoutInterval = 10
41 | lyricsFileDownloader.delegate = self
42 |
43 | for urlString in urlStrings {
44 | let id = lyricsFileDownloader.download(urlString: urlString)
45 | print("[test] download for id:\(id)")
46 | }
47 |
48 | wait(for: [exp, expFail], timeout: 40)
49 | }
50 |
51 | func onLyricsFileDownloadProgress(requestId: Int, progress: Float) {
52 |
53 | }
54 |
55 | func onLyricsFileDownloadCompleted(requestId: Int,
56 | fileData: Data?,
57 | error: DownloadError?) {
58 | let result = error != nil ? "fail" : "success"
59 | print("[test]requestId:\(requestId) \(result)")
60 | exp.fulfill()
61 |
62 | if requestId == 0, error != nil {
63 | expFail.fulfill()
64 | }
65 | if requestId == 0, error == nil {
66 | fatalError()
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/ProgressChecker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressChecker.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/3/21.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ProgressCheckerDelegate: NSObjectProtocol {
11 | func progressCheckerDidProgressPause()
12 | }
13 |
14 | /// check progress if pause
15 | class ProgressChecker: NSObject {
16 | private var lastProgress: UInt = 0
17 | private var progress: UInt = 0
18 | private var isStart = false
19 | private let queue = DispatchQueue(label: "queue.progressChecker")
20 | weak var delegate: ProgressCheckerDelegate?
21 | private var isPause = false
22 | private var timer: DispatchSourceTimer?
23 | private let logTag = "ProgressChecker"
24 |
25 | // MARK: - Internal
26 |
27 | /// progress input
28 | func set(progress: UInt) {
29 | queue.async { [weak self] in
30 | self?._set(progress: progress)
31 | }
32 | }
33 |
34 | func reset() {
35 | queue.async { [weak self] in
36 | self?._reset()
37 | }
38 | }
39 |
40 | deinit {
41 | Log.info(text: "deinit", tag: logTag)
42 | }
43 |
44 | // MARK: - Private
45 |
46 | private func _set(progress: UInt) {
47 | self.progress = progress
48 | _start()
49 | }
50 |
51 | private func _start() {
52 | if isStart { return }
53 | isStart = true
54 | setupTimer()
55 | }
56 |
57 | private func _check() {
58 | guard progress > 0 else {
59 | return
60 | }
61 | if lastProgress == progress {
62 | if !isPause {
63 | invokeProgressCheckerDidProgressPause()
64 | }
65 | isPause = true
66 | }
67 | else {
68 | isPause = false
69 | }
70 | lastProgress = progress
71 | }
72 |
73 | private func _reset() {
74 | cancleTimer()
75 | isPause = false
76 | isStart = false
77 | lastProgress = 0
78 | progress = 0
79 | }
80 |
81 | private func setupTimer() {
82 | timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
83 | timer?.schedule(deadline: .now(), repeating: .seconds(1), leeway: .seconds(0))
84 | timer?.setEventHandler { [weak self] in
85 | self?._check()
86 | }
87 | timer?.resume()
88 | }
89 |
90 | private func cancleTimer() {
91 | guard let t = timer else {
92 | return
93 | }
94 | t.cancel()
95 | timer = nil
96 | }
97 | }
98 |
99 | extension ProgressChecker {
100 | fileprivate func invokeProgressCheckerDidProgressPause() {
101 | if Thread.isMainThread {
102 | delegate?.progressCheckerDidProgressPause()
103 | return
104 | }
105 |
106 | DispatchQueue.main.async { [weak self] in
107 | guard let self = self else { return }
108 | self.delegate?.progressCheckerDidProgressPause()
109 | }
110 | }
111 | }
112 |
113 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/LineScoreRecorder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LineScoreRecorder.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2024/6/3.
6 | //
7 |
8 | import Foundation
9 |
10 | @objc public class LineScoreRecorder: NSObject {
11 | var lineScoreDict = [UInt : LineScoreInfo]()
12 | let logTag = "LineScoreReacorder>>>>"
13 |
14 | @objc public func setLyricData(data: LyricModel) {
15 | lineScoreDict = [UInt : LineScoreInfo]()
16 | for (offset, element) in data.lines.enumerated() {
17 | let info = LineScoreInfo(begin: element.beginTime,
18 | duration: element.duration,
19 | score: 0)
20 | lineScoreDict[UInt(offset)] = info
21 | }
22 |
23 | Log.info(text: "lineScoreDict count: \(lineScoreDict.count)", tag: logTag)
24 | }
25 |
26 | /// setLineScore
27 | /// - Parameters:
28 | /// - index: the index of line in original lyric file
29 | /// - score: the score of line
30 | /// - Returns: CumulativeScore
31 | @objc public func setLineScore(index: UInt, score: UInt) -> UInt {
32 | guard let info = lineScoreDict[index] else {
33 | Log.errorText(text: "can not find line score info", tag: logTag)
34 | return calcluateCumulativeScore()
35 | }
36 | info.score = score
37 | Log.info(text: "setLineScore index:(\(index)), score: \(score)", tag: logTag)
38 | Log.info(text: "\(lineScoreDict.sorted(by:{ $0.key < $1.key}).map({ "i:\($0.key) s:\($0.value.score)" }))", tag: logTag)
39 | return calcluateCumulativeScore()
40 | }
41 |
42 | /// seek
43 | /// - Parameter position: current position
44 | /// - Returns: CumulativeScore
45 | @objc public func seek(position: UInt) -> UInt {
46 | Log.info(text: "seek position:\(position)", tag: logTag)
47 | for index in 0..= position {
50 | info.updateScore(score: 0)
51 | Log.info(text: "reset (\(index)) reset, score: \(info.score)", tag: logTag)
52 | }
53 | }
54 |
55 | return calcluateCumulativeScore()
56 | }
57 |
58 | private func calcluateCumulativeScore() -> UInt {
59 | var score: UInt = 0
60 | for (_, linscore) in lineScoreDict {
61 | score += linscore.score
62 | }
63 | Log.info(text: "calcluateCumulativeScore:\(score)")
64 | return score
65 | }
66 | }
67 |
68 | extension LineScoreRecorder {
69 | class LineScoreInfo: CustomStringConvertible {
70 | let begin: UInt
71 | let end: UInt
72 | let duration: UInt
73 | var score: UInt
74 |
75 | init(begin: UInt, duration: UInt, score: UInt) {
76 | self.begin = begin
77 | self.end = begin + duration
78 | self.duration = duration
79 | self.score = score
80 | }
81 |
82 | func updateScore(score: UInt) {
83 | self.score = score
84 | }
85 |
86 | var description: String {
87 | return "score: \(score)"
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestParser/TestLrcParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestLrcParser.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2023/7/5.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | final class TestLrcParser: XCTestCase {
12 |
13 | func testLrcParseOfEnhancedFormat() { /** ablout enhancedFormat **/
14 | let lrcParser = LrcParser()
15 |
16 | let sampleFormat1 = "[00:22.30]我不会发现 我难受"
17 | let sampleFormat2 = "[00:25.745]怎么说出口"
18 | let enhancedFormat1 = "[00:23.997]<00:23.997>又<00:24.694>是<00:25.356>九<00:25.704>月<00:26.017>九<00:26.644>重<00:27.028>阳<00:27.376>夜<00:28.003>难<00:28.351>聚<00:28.665>首"
19 | let enhancedFormat2 = "[00:29.326]<00:29.326>思<00:30.023>乡<00:30.476>的<00:30.719>人<00:31.416>儿<00:32.008>飘<00:32.322>流<00:32.705>在<00:33.053>外<00:33.332>头"
20 | let enhancedFormat3 = "[00:34.690]<00:34.690>又<00:35.352>是<00:36.014>九<00:36.327>月<00:36.675>九<00:37.337>愁<00:37.685>更<00:38.034>愁<00:38.661>情<00:39.009>更<00:39.392>忧"
21 | let enhancedFormat4 = "[00:53.011]<00:53.011> Why<00:53.015> hh"
22 |
23 | XCTAssertFalse(lrcParser.containsWordStartTime(sampleFormat1))
24 | XCTAssertFalse(lrcParser.containsWordStartTime(sampleFormat2))
25 | XCTAssertTrue(lrcParser.containsWordStartTime(enhancedFormat1))
26 | XCTAssertTrue(lrcParser.containsWordStartTime(enhancedFormat2))
27 | XCTAssertTrue(lrcParser.containsWordStartTime(enhancedFormat3))
28 | XCTAssertTrue(lrcParser.containsWordStartTime(enhancedFormat4))
29 |
30 | let tones1 = lrcParser.parseLineStringOfEnhancedFormat(enhancedFormat1)
31 | XCTAssertTrue(tones1.map({ $0.word }).joined() == "又是九月九重阳夜难聚首")
32 | XCTAssertTrue(tones1[0].beginTime == 23 * 1000 + 997)
33 | XCTAssertTrue(tones1[0].duration == (24 * 1000 + 694) - (23 * 1000 + 997))
34 | XCTAssertTrue(tones1[1].beginTime == 24 * 1000 + 694)
35 | XCTAssertTrue(tones1[1].duration == (25 * 1000 + 356) - (24 * 1000 + 694))
36 | XCTAssertTrue(tones1[10].beginTime == 28 * 1000 + 665)
37 | XCTAssertTrue(tones1[10].duration == 0)
38 |
39 | let tones2 = lrcParser.parseLineStringOfEnhancedFormat(enhancedFormat2)
40 | XCTAssertTrue(tones2.map({ $0.word }).joined() == "思乡的人儿飘流在外头")
41 | XCTAssertTrue(tones2[0].beginTime == 29 * 1000 + 326)
42 | XCTAssertTrue(tones2[0].duration == (30 * 1000 + 23) - (29 * 1000 + 326))
43 | XCTAssertTrue(tones2[9].duration == 0)
44 |
45 | let tones4 = lrcParser.parseLineStringOfEnhancedFormat(enhancedFormat4)
46 | XCTAssertEqual(tones4[0].word, " Why")
47 | XCTAssertEqual(tones4[0].beginTime, 53 * 1000 + 11)
48 | XCTAssertEqual(tones4[1].word, " hh")
49 | XCTAssertEqual(tones4[1].beginTime, 53 * 1000 + 15)
50 | }
51 |
52 | func testTimeParae() {
53 | let lrcParser = LrcParser()
54 | XCTAssertTrue(lrcParser.parseTime("00:24.694") == 24694)
55 | XCTAssertTrue(lrcParser.parseTime("01:02.345") == 62345)
56 | XCTAssertTrue(lrcParser.parseTime("01:02.45") == 62045)
57 | XCTAssertTrue(lrcParser.parseTime("00:00.000") == 0)
58 | XCTAssertTrue(lrcParser.parseTime("00:53.011") == 53 * 1000 + 11)
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
56 |
57 |
58 |
64 |
66 |
72 |
73 |
74 |
75 |
77 |
78 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestLineScoreRecorder/TestLineScoreRecorder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestLineScoreRecorder.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2024/6/3.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 | final class TestLineScoreRecorder: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | Log.setLoggers(loggers: [ConsoleLogger()])
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | // Put teardown code here. This method is called after the invocation of each test method in the class.
18 | }
19 |
20 | func testLineScoreRecorder() {
21 | let recoder = LineScoreRecorder()
22 |
23 | let krcFileData = try! Data(contentsOf: URL(fileURLWithPath: Bundle.current.path(forResource: "4875936889260991133", ofType: "krc")!))
24 | let pitchFileData = try! Data(contentsOf: URL(fileURLWithPath: Bundle.current.path(forResource: "4875936889260991133.pitch", ofType: nil)!))
25 |
26 | let model = KaraokeView.parseLyricData(lyricFileData: krcFileData, pitchFileData: pitchFileData)!
27 |
28 | recoder.setLyricData(data: model)
29 |
30 | var score = recoder.setLineScore(index: 5, score: 1)
31 | XCTAssertEqual(score, 1)
32 |
33 | score = recoder.setLineScore(index: 6, score: 2)
34 | XCTAssertEqual(score, 1+2)
35 |
36 | score = recoder.setLineScore(index: 7, score: 3)
37 | XCTAssertEqual(score, 1+2+3)
38 |
39 | score = recoder.setLineScore(index: 8, score: 4)
40 | XCTAssertEqual(score, 1+2+3+4)
41 |
42 | /// index = 8
43 | score = recoder.seek(position: 30572)
44 | XCTAssertEqual(score, 1+2+3)
45 |
46 | /// index = 6
47 | score = recoder.seek(position: 22638)
48 | XCTAssertEqual(score, 1)
49 |
50 | /// index = 5
51 | score = recoder.seek(position: 19044)
52 | XCTAssertEqual(score, 0)
53 |
54 | /** line begainTime
55 | - 0 : 1067
56 | - 1 : 1720
57 | - 2 : 1870
58 | - 3 : 2173
59 | - 4 : 15106
60 | - 5 : 19044
61 | - 6 : 22638
62 | - 7 : 25955
63 | - 8 : 30572
64 | - 9 : 34724
65 | - 10 : 37621
66 | - 11 : 41673
67 | - 12 : 47453
68 | - 13 : 50725
69 | - 14 : 53597
70 | - 15 : 60854
71 | - 16 : 62717
72 | - 17 : 64580
73 | - 18 : 66642
74 | - 19 : 69058
75 | - 20 : 72791
76 | - 21 : 76369
77 | - 22 : 78082
78 | - 23 : 80050
79 | - 24 : 82165
80 | - 25 : 84532
81 | - 26 : 88308
82 | - 27 : 117010
83 | - 28 : 120282
84 | - 29 : 123250
85 | - 30 : 130478
86 | - 31 : 132445
87 | - 32 : 134410
88 | - 33 : 136321
89 | - 34 : 138689
90 | - 35 : 142461
91 | - 36 : 146035
92 | - 37 : 149712
93 | - 38 : 154196
94 | - 39 : 158071
95 | - 40 : 168121
96 | - 41 : 172203
97 | - 42 : 175070
98 | - 43 : 178917
99 | */
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/Demo/Demo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/View/ScoringCanvasView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScoringCanvasView.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/1/3.
6 | //
7 |
8 | import UIKit
9 |
10 | class ScoringCanvasView: UIView {
11 | /// 游标的起始位置
12 | var defaultPitchCursorX: CGFloat = 100
13 | /// 音准线的高度
14 | var standardPitchStickViewHeight: CGFloat = 3
15 | /// 音准线的基准因子
16 | var movingSpeedFactor: CGFloat = 120
17 | /// 音准线默认的背景色
18 | var standardPitchStickViewColor: UIColor = .gray
19 | /// 音准线匹配后的背景色
20 | var standardPitchStickViewHighlightColor: UIColor = .orange
21 |
22 | fileprivate var standardInfos = [DrawInfo]()
23 | fileprivate var highlightInfos = [DrawInfo]()
24 | fileprivate var widthPreMs: CGFloat { movingSpeedFactor / 1000 }
25 |
26 | override func draw(_ rect: CGRect) {
27 | drawStaff()
28 | drawStandardInfos()
29 | drawHighlightInfos()
30 | }
31 |
32 | func draw(standardInfos: [DrawInfo],
33 | highlightInfos: [DrawInfo]) {
34 | self.standardInfos = standardInfos
35 | self.highlightInfos = highlightInfos
36 | setNeedsDisplay()
37 | }
38 |
39 | func reset() {
40 | self.standardInfos = []
41 | self.highlightInfos = []
42 | setNeedsDisplay()
43 | }
44 |
45 | override init(frame: CGRect) {
46 | super.init(frame: frame)
47 | backgroundColor = .clear
48 | }
49 |
50 | required init?(coder: NSCoder) {
51 | fatalError("init(coder:) has not been implemented")
52 | }
53 | }
54 |
55 | // MARK: - Draw
56 | extension ScoringCanvasView {
57 | /// 五线谱
58 | fileprivate func drawStaff() {
59 | let height = bounds.height
60 | let width = bounds.width
61 | let lineHeight: CGFloat = 1
62 | let spaceY = (height - lineHeight * 5) / 4
63 |
64 | for i in 0...5 {
65 | let y = CGFloat(i) * (spaceY + lineHeight)
66 | let rect = CGRect(x: 0, y: y, width: width, height: lineHeight)
67 | let linePath = UIBezierPath(rect: rect)
68 | UIColor.white.withAlphaComponent(0.08).setFill()
69 | linePath.fill()
70 | }
71 | }
72 |
73 | fileprivate func drawStandardInfos() {
74 | drawInfosStandrad(infos: standardInfos, fillColor: standardPitchStickViewColor)
75 | }
76 |
77 | fileprivate func drawHighlightInfos() {
78 | drawInfosStandrad(infos: highlightInfos, fillColor: standardPitchStickViewHighlightColor)
79 | }
80 |
81 | private func drawInfosHighlight(infos: [DrawInfo], fillColor: UIColor) {
82 | for info in infos {
83 | let rect = info.rect
84 | let gradient = CAGradientLayer()
85 | gradient.frame = rect
86 | gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
87 |
88 | layer.addSublayer(gradient)
89 | }
90 | }
91 |
92 | private func drawInfosStandrad(infos: [DrawInfo], fillColor: UIColor) {
93 | for info in infos {
94 | let rect = info.rect
95 | let path = UIBezierPath(roundedRect: rect, cornerRadius: standardPitchStickViewHeight/2)
96 | fillColor.setFill()
97 | path.fill()
98 | }
99 | }
100 | }
101 |
102 | extension ScoringCanvasView {
103 | typealias DrawInfo = ScoringMachineDrawInfo
104 | }
105 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Downloader/LyricsFileDownloaderProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LyricsFileDownloaderProtocol.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/12/13.
6 | //
7 |
8 | import Foundation
9 |
10 | /// the enum type to describe the error
11 | @objc public enum DownloadErrorDomainType: Int {
12 | case general = 0
13 | /// repeat url request, a same url is requesting
14 | case repeatDownloading = 1
15 | /// error from http framework in ios, such as time out
16 | case httpDownloadError = 2
17 | /// http logic error, such as 400/500
18 | case httpDownloadErrorLogic = 3
19 | /// unzip fail
20 | case unzipFail = 4
21 |
22 | /// the string to indicate the type
23 | public var domain: String {
24 | switch self {
25 | case .general:
26 | return "io.agora.LyricsFileDownloader.general"
27 | case .repeatDownloading:
28 | return "io.agora.LyricsFileDownloader.repeatDownloading"
29 | case .httpDownloadError:
30 | return "io.agora.LyricsFileDownloader.httpDownloadError"
31 | case .httpDownloadErrorLogic:
32 | return "io.agora.LyricsFileDownloader.httpDownloadErrorLogic"
33 | case .unzipFail:
34 | return "io.agora.LyricsFileDownloader.unzipFail"
35 | }
36 | }
37 |
38 | /// the name to describe the type
39 | public var name: String {
40 | switch self {
41 | case .general:
42 | return "general"
43 | case .repeatDownloading:
44 | return "repeatDownloading"
45 | case .httpDownloadError:
46 | return "httpDownloadError"
47 | case .httpDownloadErrorLogic:
48 | return "httpDownloadErrorLogic"
49 | case .unzipFail:
50 | return "unzipFail"
51 | }
52 | }
53 | }
54 |
55 | /// the class to describe the error
56 | public class DownloadError: NSError {
57 | /// the message to describe the error
58 | public let msg: String
59 | /// the type of different error
60 | public let domainType: DownloadErrorDomainType
61 | /// original error from http framework in ios, such as time out
62 | public var originalError: NSError?
63 |
64 | init(domainType: DownloadErrorDomainType, code: Int, msg: String) {
65 | self.domainType = domainType
66 | self.msg = msg
67 | super.init(domain: domainType.domain, code: code)
68 | }
69 |
70 | init(domainType: DownloadErrorDomainType, error: NSError) {
71 | self.domainType = domainType
72 | self.msg = error.localizedDescription
73 | self.originalError = error
74 | super.init(domain: domainType.domain, code: error.code)
75 | }
76 |
77 | required init?(coder: NSCoder) {
78 | fatalError("init(coder:) has not been implemented")
79 | }
80 |
81 | public override var description: String {
82 | return "error: \(domainType.name) domain: \(domain) code:\(code) msg:\(msg) originalError:\(originalError?.localizedDescription ?? "nil")"
83 | }
84 | }
85 |
86 | @objc public protocol LyricsFileDownloaderDelegate: NSObjectProtocol {
87 | /// progress event
88 | /// - Parameters:
89 | /// - progress: [0, 1], if equal `1`, means success
90 | func onLyricsFileDownloadProgress(requestId: Int, progress: Float)
91 |
92 | /// Completed event
93 | /// - Parameters:
94 | /// - fileData: lyric data from file, if `nil` means fail
95 | /// - error: if `nil` means success
96 | func onLyricsFileDownloadCompleted(requestId: Int, fileData: Data?, error: DownloadError?)
97 | }
98 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Scoring/ScoringMachineProtocol+Events.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScoringMachineProtocol+Events.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2024/6/4.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ScoringMachineEventInvoker {
11 | static func invokeScoringMachine(scoringMachine: ScoringMachineProtocol,
12 | didUpdateDraw standardInfos: [ScoringMachineDrawInfo],
13 | highlightInfos: [ScoringMachineDrawInfo]) {
14 | if Thread.isMainThread {
15 | scoringMachine.delegate?.scoringMachine(scoringMachine,
16 | didUpdateDraw: standardInfos,
17 | highlightInfos: highlightInfos)
18 | return
19 | }
20 |
21 | DispatchQueue.main.async {
22 | scoringMachine.delegate?.scoringMachine(scoringMachine,
23 | didUpdateDraw: standardInfos,
24 | highlightInfos: highlightInfos)
25 | }
26 | }
27 |
28 | static func invokeScoringMachine(scoringMachine: ScoringMachineProtocol, didUpdateCursor centerY: CGFloat,
29 | showAnimation: Bool,
30 | debugInfo: ScoringMachineDebugInfo) {
31 | if Thread.isMainThread {
32 | scoringMachine.delegate?.scoringMachine(scoringMachine,
33 | didUpdateCursor: centerY,
34 | showAnimation: showAnimation,
35 | debugInfo: debugInfo)
36 | return
37 | }
38 |
39 | DispatchQueue.main.async {
40 | scoringMachine.delegate?.scoringMachine(scoringMachine,
41 | didUpdateCursor: centerY,
42 | showAnimation: showAnimation,
43 | debugInfo: debugInfo)
44 | }
45 | }
46 |
47 | static func invokeScoringMachine(scoringMachine: ScoringMachineProtocol,
48 | didFinishLineWith model: LyricLineModel,
49 | score: Int,
50 | cumulativeScore: Int,
51 | lineIndex: Int,
52 | lineCount: Int) {
53 | if Thread.isMainThread {
54 | scoringMachine.delegate?.scoringMachine(scoringMachine,
55 | didFinishLineWith: model,
56 | score: score,
57 | cumulativeScore: cumulativeScore,
58 | lineIndex: lineIndex,
59 | lineCount: lineCount)
60 | return
61 | }
62 |
63 | DispatchQueue.main.async {
64 | scoringMachine.delegate?.scoringMachine(scoringMachine,
65 | didFinishLineWith: model,
66 | score: score,
67 | cumulativeScore: cumulativeScore,
68 | lineIndex: lineIndex,
69 | lineCount: lineCount)
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogProvider.swift
3 | //
4 | //
5 | // Created by ZYP on 2021/5/28.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class Log {
12 | static let provider = LogManager.share
13 |
14 | static func errorText(text: String,
15 | tag: String? = nil) {
16 | provider.errorText(text: text, tag: tag)
17 | }
18 |
19 | static func error(error: CustomStringConvertible,
20 | tag: String? = nil) {
21 | provider.errorText(text: error.description, tag: tag)
22 | }
23 |
24 | static func info(text: String,
25 | tag: String? = nil) {
26 | provider.info(text: text, tag: tag)
27 | }
28 |
29 | static func debug(text: String,
30 | tag: String? = nil) {
31 | provider.debug(text: text, tag: tag)
32 | }
33 |
34 | static func warning(text: String,
35 | tag: String? = nil) {
36 | provider.warning(text: text, tag: tag)
37 | }
38 |
39 | static func setLoggers(loggers: [ILogger]) {
40 | provider.loggers = loggers
41 | }
42 | }
43 |
44 | class LogManager {
45 | static let share = LogManager()
46 | var loggers = [ILogger]()
47 | private let queue = DispatchQueue(label: "LogManager")
48 | let dateFormatter: DateFormatter
49 |
50 | init() {
51 | dateFormatter = DateFormatter()
52 | dateFormatter.dateFormat = "dd/MM/YY HH:mm:ss:SSS"
53 | }
54 |
55 | fileprivate func error(error: Error?,
56 | tag: String?,
57 | domainName: String) {
58 | guard let e = error else {
59 | return
60 | }
61 | var text = ""
62 | if e.localizedDescription.count > 1 {
63 | text = e.localizedDescription
64 | }
65 |
66 | let err = e as CustomStringConvertible
67 | if err.description.count > 1 {
68 | text = err.description
69 | }
70 |
71 | errorText(text: text,
72 | tag: tag)
73 | }
74 |
75 | fileprivate func errorText(text: String,
76 | tag: String?) {
77 | log(type: .error,
78 | text: text,
79 | tag: tag)
80 | }
81 |
82 | fileprivate func info(text: String,
83 | tag: String?) {
84 | log(type: .info,
85 | text: text,
86 | tag: tag)
87 | }
88 |
89 | fileprivate func warning(text: String,
90 | tag: String?) {
91 | log(type: .warning,
92 | text: text,
93 | tag: tag)
94 | }
95 |
96 | fileprivate func debug(text: String,
97 | tag: String?) {
98 | log(type: .debug,
99 | text: text,
100 | tag: tag)
101 | }
102 |
103 | fileprivate func log(type: LoggerLevel,
104 | text: String,
105 | tag: String?) {
106 | queue.async { [weak self] in
107 | guard let self = self, !self.loggers.isEmpty else { return }
108 | let time = self.dateFormatter.string(from: .init())
109 | self.log(content: text, tag: tag, time: time, level: type)
110 | }
111 | }
112 |
113 | func log(content: String, tag: String?, time: String, level: LoggerLevel) {
114 | for logger in loggers {
115 | logger.onLog(content: content, tag: tag, time: time, level: level)
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Other/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2022/12/21.
6 | //
7 |
8 | import Foundation
9 |
10 | class AgoraLyricsScore {}
11 |
12 | extension String {
13 | // 字符串截取
14 | func textSubstring(startIndex: Int, length: Int) -> String {
15 | let startIndex = index(self.startIndex, offsetBy: startIndex)
16 | let endIndex = index(startIndex, offsetBy: length)
17 | let subvalues = self[startIndex ..< endIndex]
18 | return String(subvalues)
19 | }
20 | }
21 |
22 | extension LyricLineModel {
23 | var endTime: UInt {
24 | beginTime + duration
25 | }
26 | }
27 |
28 | extension LyricToneModel {
29 | var endTime: UInt {
30 | beginTime + duration
31 | }
32 | }
33 |
34 | extension Bundle {
35 | static var currentBundle: Bundle {
36 | let bundle = Bundle(for: AgoraLyricsScore.self)
37 | let path = bundle.path(forResource: "AgoraLyricsScoreBundle", ofType: "bundle")
38 | if path == nil {
39 | Log.error(error: "bundle not found path", tag: "Bundle")
40 | }
41 | let current = Bundle(path: path!)
42 | if current == nil {
43 | Log.error(error: "bundle not found path: \(path!)", tag: "Bundle")
44 | }
45 | return current!
46 | }
47 |
48 | func image(name: String) -> UIImage? {
49 | return UIImage(named: name, in: self, compatibleWith: nil)
50 | }
51 | }
52 |
53 | extension UIColor{
54 | class func colorWithHex(hexStr:String) -> UIColor{
55 | return UIColor.colorWithHex(hexStr : hexStr, alpha:1)
56 | }
57 |
58 | class func colorWithHex(hexStr:String, alpha:Float) -> UIColor{
59 |
60 | var cStr = hexStr.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased() as NSString;
61 |
62 | if(cStr.length < 6){
63 | return UIColor.clear;
64 | }
65 |
66 | if(cStr.hasPrefix("0x")){
67 | cStr = cStr.substring(from: 2) as NSString
68 | }
69 |
70 | if(cStr.hasPrefix("#")){
71 | cStr = cStr.substring(from: 1) as NSString
72 | }
73 |
74 | if(cStr.length != 6){
75 | return UIColor.clear
76 | }
77 |
78 | let rStr = (cStr as NSString).substring(to: 2)
79 | let gStr = ((cStr as NSString).substring(from: 2) as NSString).substring(to: 2)
80 | let bStr = ((cStr as NSString).substring(from: 4) as NSString).substring(to: 2)
81 |
82 | var r: UInt32 = 0x0
83 | var g: UInt32 = 0x0
84 | var b: UInt32 = 0x0
85 |
86 | Scanner.init(string: rStr).scanHexInt32(&r)
87 | Scanner.init(string: gStr).scanHexInt32(&g)
88 | Scanner.init(string: bStr).scanHexInt32(&b)
89 |
90 | return UIColor.init(red: CGFloat(r)/255.0, green: CGFloat(g)/255.0, blue: CGFloat(b)/255.0, alpha: CGFloat(alpha));
91 |
92 | }
93 | }
94 |
95 | extension Date {
96 | /// 获取当前 秒级 时间戳 - 10位
97 | var timeStamp: Int {
98 | let timeInterval = timeIntervalSince1970
99 | let timeStamp = Int(timeInterval)
100 | return timeStamp
101 | }
102 |
103 | /// 获取当前 毫秒级 时间戳 - 13位
104 | var milliStamp: CLongLong {
105 | let timeInterval = timeIntervalSince1970
106 | let millisecond = CLongLong(round(timeInterval * 1000))
107 | return millisecond
108 | }
109 | }
110 |
111 | extension Double {
112 | /// 保留2位小数
113 | var keep2: Double {
114 | return Double(Darwin.round(self * 100)/100)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestDownload/TestFileCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestFileCache.swift
3 | // AgoraComponetLog-Unit-Tests
4 | //
5 | // Created by ZYP on 2023/12/13.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | final class TestFileCache: XCTestCase {
12 | var fileCache: FileCache!
13 |
14 | override func setUpWithError() throws {
15 | Log.setLoggers(loggers: [ConsoleLogger()])
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | fileCache = nil
20 | }
21 |
22 | func testMaxFile() {
23 | fileCache = FileCache()
24 | fileCache.maxFileNum = 10
25 | fileCache.clearAll()
26 |
27 | let fileManager = FileManager.default
28 | let directory = String.cacheFolderPath()
29 | for index in 0...fileCache.maxFileNum {
30 | let filePath = directory + "/" + "\(index).xml"
31 | fileManager.createFile(atPath: filePath,
32 | contents: "123".data(using: .utf8),
33 | attributes: nil)
34 | }
35 |
36 | var files = fileCache.findFiles(inDirectory: .cacheFolderPath()).sorted { l, r in
37 | l.createdTimeStamp < r.createdTimeStamp
38 | }
39 |
40 | for file in files {
41 | print("\(file.createdTimeStamp)-\(file.path.fileName)")
42 | }
43 | XCTAssertEqual(files.count, Int(fileCache.maxFileNum)+1)
44 |
45 | fileCache.removeFilesIfNeeded()
46 |
47 | files = fileCache.findFiles(inDirectory: .cacheFolderPath()).sorted { l, r in
48 | l.createdTimeStamp < r.createdTimeStamp
49 | }
50 | for file in files {
51 | print("\(file.createdTimeStamp)-\(file.path.fileName)")
52 | }
53 |
54 | let fileNames = files.map({ $0.path.fileName })
55 | XCTAssertEqual(fileNames.count, Int(fileCache.maxFileNum))
56 | XCTAssertFalse(fileNames.contains("0.xml"))
57 |
58 | fileCache.clearAll()
59 |
60 | files = fileCache.findFiles(inDirectory: .cacheFolderPath()).sorted { l, r in
61 | l.createdTimeStamp < r.createdTimeStamp
62 | }
63 |
64 | XCTAssertEqual(files.count, 0)
65 | }
66 |
67 | func testMaxFileAge() {
68 | fileCache = FileCache()
69 | fileCache.maxFileNum = 5
70 | fileCache.maxFileAge = 1
71 | fileCache.clearAll()
72 |
73 | let fileManager = FileManager.default
74 | let directory = String.cacheFolderPath()
75 | for index in 0...fileCache.maxFileNum {
76 | let filePath = directory + "/" + "\(index).xml"
77 | fileManager.createFile(atPath: filePath,
78 | contents: "123".data(using: .utf8),
79 | attributes: nil)
80 | }
81 |
82 | var files = fileCache.findFiles(inDirectory: .cacheFolderPath()).sorted { l, r in
83 | l.createdTimeStamp < r.createdTimeStamp
84 | }
85 |
86 | for file in files {
87 | print("\(file.createdTimeStamp)-\(file.path.fileName)")
88 | }
89 | XCTAssertEqual(files.count, Int(fileCache.maxFileNum)+1)
90 |
91 | Thread.sleep(forTimeInterval: 2)
92 | fileCache.removeFilesIfNeeded()
93 |
94 | files = fileCache.findFiles(inDirectory: .cacheFolderPath()).sorted { l, r in
95 | l.createdTimeStamp < r.createdTimeStamp
96 | }
97 |
98 | XCTAssertEqual(files.count, 0)
99 |
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/EnhancedLRCformat.lrc:
--------------------------------------------------------------------------------
1 | [00:23.997]<00:23.997>又<00:24.694>是<00:25.356>九<00:25.704>月<00:26.017>九<00:26.644>重<00:27.028>阳<00:27.376>夜<00:28.003>难<00:28.351>聚<00:28.665>首
2 | [00:29.326]<00:29.326>思<00:30.023>乡<00:30.476>的<00:30.719>人<00:31.416>儿<00:32.008>飘<00:32.322>流<00:32.705>在<00:33.053>外<00:33.332>头
3 | [00:34.690]<00:34.690>又<00:35.352>是<00:36.014>九<00:36.327>月<00:36.675>九<00:37.337>愁<00:37.685>更<00:38.034>愁<00:38.661>情<00:39.009>更<00:39.392>忧
4 | [00:40.019]<00:40.019>回<00:40.681>家<00:41.134>的<00:41.343>打<00:42.004>算<00:42.736>始<00:43.014>终<00:43.328>在<00:43.711>心<00:44.025>头
5 | [00:50.677]<00:50.677>走<00:50.956>走<00:51.339>走<00:51.652>走<00:52.035>走<00:52.349>啊<00:52.628>走<00:53.359>走<00:53.672>到<00:54.056>九<00:54.717>月<00:55.205>九
6 | [00:56.006]<00:56.006>他<00:56.250>乡<00:56.703>没<00:57.016>有<00:57.399>烈<00:58.026>酒<00:58.653>没<00:59.036>有<00:59.663>问<01:00.011>候
7 | [01:01.265]<01:01.265>走<01:01.370>走<01:01.683>走<01:02.275>走<01:02.693>走<01:03.111>啊<01:03.320>走<01:04.017>走<01:04.330>到<01:04.679>九<01:05.375>月<01:05.863>九
8 | [01:06.699]<01:06.699>家<01:06.977>中<01:07.361>才<01:07.674>有<01:08.057>自<01:08.684>由<01:09.346>才<01:10.042>有<01:10.356>九<01:10.704>月<01:16.730>九
9 | [01:25.333]<01:25.333>又<01:26.029>是<01:26.691>九<01:27.005>月<01:27.353>九<01:27.980>重<01:28.363>阳<01:28.746>夜<01:29.338>难<01:29.687>聚<01:30.000>首
10 | [01:30.662]<01:30.662>思<01:31.358>乡<01:31.811>的<01:32.055>人<01:32.752>儿<01:33.344>飘<01:33.657>流<01:34.040>在<01:34.389>外<01:34.667>头
11 | [01:36.026]<01:36.026>又<01:36.687>是<01:37.349>九<01:37.663>月<01:38.011>九<01:38.673>愁<01:39.021>更<01:39.404>愁<01:39.996>情<01:40.345>更<01:40.693>忧
12 | [01:41.355]<01:41.355>回<01:42.016>家<01:42.469>的<01:42.678>打<01:43.340>算<01:44.037>始<01:44.350>终<01:44.698>在<01:45.047>心<01:45.395>头
13 | [01:49.365]<01:49.365>走<01:49.679>走<01:50.027>走<01:50.341>走<01:50.689>走<01:51.037>啊<01:51.281>走<01:52.013>走<01:52.361>到<01:52.709>九<01:53.371>月<01:53.859>九
14 | [01:54.660]<01:54.660>他<01:55.008>乡<01:55.356>没<01:55.705>有<01:56.053>烈<01:56.680>酒<01:57.342>没<01:57.690>有<01:58.352>问<01:58.665>候
15 | [02:00.023]<02:00.023>走<02:00.337>走<02:00.685>走<02:01.034>走<02:01.347>走<02:01.695>啊<02:01.939>走<02:02.671>走<02:03.019>到<02:03.367>九<02:03.715>月<02:04.064>九
16 | [02:05.352]<02:05.352>家<02:05.631>中<02:06.014>才<02:06.362>有<02:06.746>自<02:07.338>由<02:08.000>才<02:08.313>有<02:08.696>九<02:09.010>月<02:09.358>九
17 | [02:23.986]<02:23.986>亲<02:24.683>人<02:25.136>和<02:25.345>朋<02:26.007>友<02:26.668>举<02:27.051>起<02:27.330>杯<02:27.992>倒<02:28.340>满<02:28.688>酒
18 | [02:29.350]<02:29.350>饮<02:30.012>尽<02:30.500>这<02:30.743>乡<02:31.336>愁<02:31.997>醉<02:32.346>倒<02:32.485>在<02:32.694>家<02:33.007>门<02:33.321>口
19 | [02:37.361]<02:37.361>走<02:37.605>走<02:38.023>走<02:38.336>走<02:38.685>走<02:39.033>啊<02:39.277>走<02:40.008>走<02:40.357>到<02:40.705>九<02:41.018>月<02:41.367>九
20 | [02:42.655]<02:42.655>他<02:43.038>乡<02:43.352>没<02:43.700>有<02:44.048>烈<02:44.675>酒<02:45.337>没<02:45.685>有<02:46.347>问<02:46.661>候
21 | [02:48.019]<02:48.019>走<02:48.333>走<02:48.681>走<02:49.029>走<02:49.343>走<02:49.691>啊<02:49.935>走<02:50.666>走<02:51.014>到<02:51.363>九<02:51.676>月<02:52.059>九
22 | [02:53.348]<02:53.348>家<02:53.627>中<02:54.010>才<02:54.358>有<02:54.741>自<02:55.333>由<02:55.995>才<02:56.309>有<02:56.692>九<02:57.005>月<02:57.354>九
23 | [02:58.677]<02:58.677>走<02:58.991>走<02:59.339>走<02:59.687>走<03:00.035>走<03:00.384>啊<03:00.628>走<03:01.359>走<03:01.672>到<03:02.021>九<03:02.717>月<03:03.205>九
24 | [03:04.006]<03:04.006>他<03:04.285>乡<03:04.703>没<03:05.016>有<03:05.399>烈<03:06.026>酒<03:06.653>没<03:07.001>有<03:07.663>问<03:08.011>候
25 | [03:09.370]<03:09.370>走<03:09.683>走<03:10.032>走<03:10.345>走<03:10.693>走<03:11.042>啊<03:11.285>走<03:12.017>走<03:12.330>到<03:12.714>九<03:13.027>月<03:13.375>九
26 | [03:14.664]<03:14.664>家<03:14.943>中<03:15.326>才<03:15.709>有<03:16.266>自<03:16.684>由<03:17.346>才<03:17.868>有<03:18.042>九<03:18.356>月<03:18.704>九<03:21.108>噢
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Lyrics/LyricMachine+Events.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LyricMachine+Events.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2023/3/13.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LyricMachineDelegate: NSObjectProtocol {
11 | /// 在 `setLyricData` 完成后调用
12 | func lyricMachine(_ lyricMachine: LyricMachine,
13 | didSetLyricData datas: [LyricCell.Model])
14 |
15 | /// 在 `setLyricData` 完成后调用
16 | /// - Parameters:
17 | /// - remainingTime: 倒计时剩余时间
18 | func lyricMachine(_ lyricMachine: LyricMachine, didUpdate remainingTime: Int)
19 |
20 | /// 换行时调用
21 | func lyricMachine(_ lyricMachine: LyricMachine,
22 | didStartLineAt newIndexPath: IndexPath,
23 | oldIndexPath: IndexPath,
24 | animated: Bool)
25 | /// 行更新时调用
26 | func lyricMachine(_ lyricMachine: LyricMachine, didUpdateLineAt indexPath: IndexPath)
27 |
28 | /// Consloe信息 (仅用于debug)
29 | func lyricMachine(_ lyricMachine: LyricMachine, didUpdateConsloe text: String)
30 | }
31 |
32 | extension LyricMachine { /** invoke **/
33 | func invokeLyricMachine(didSetLyricData datas: [LyricCell.Model]) {
34 | if Thread.isMainThread {
35 | delegate?.lyricMachine(self, didSetLyricData: datas)
36 | return
37 | }
38 |
39 | DispatchQueue.main.async { [weak self] in
40 | guard let self = self else { return }
41 | self.delegate?.lyricMachine(self, didSetLyricData: datas)
42 | }
43 | }
44 |
45 | func invokeLyricMachine(didUpdate remainingTime: Int) {
46 | if Thread.isMainThread {
47 | delegate?.lyricMachine(self, didUpdate: remainingTime)
48 | return
49 | }
50 |
51 | DispatchQueue.main.async { [weak self] in
52 | guard let self = self else { return }
53 | self.delegate?.lyricMachine(self, didUpdate: remainingTime)
54 | }
55 | }
56 |
57 | func invokeLyricMachine(didStartLineAt newIndexPath: IndexPath,
58 | oldIndexPath: IndexPath, animated: Bool) {
59 | if Thread.isMainThread {
60 | delegate?.lyricMachine(self,
61 | didStartLineAt: newIndexPath,
62 | oldIndexPath: oldIndexPath,
63 | animated: animated)
64 | return
65 | }
66 |
67 | DispatchQueue.main.async { [weak self] in
68 | guard let self = self else { return }
69 | self.delegate?.lyricMachine(self,
70 | didStartLineAt: newIndexPath,
71 | oldIndexPath: oldIndexPath,
72 | animated: animated)
73 | }
74 | }
75 |
76 | func invokeLyricMachine(didUpdateLineAt indexPath: IndexPath) {
77 | if Thread.isMainThread {
78 | delegate?.lyricMachine(self, didUpdateLineAt: indexPath)
79 | return
80 | }
81 |
82 | DispatchQueue.main.async { [weak self] in
83 | guard let self = self else { return }
84 | self.delegate?.lyricMachine(self, didUpdateLineAt: indexPath)
85 | }
86 | }
87 |
88 | func invokeLyricMachine(didUpdateConsloe text: String) {
89 | if Thread.isMainThread {
90 | delegate?.lyricMachine(self, didUpdateConsloe: text)
91 | return
92 | }
93 |
94 | DispatchQueue.main.async { [weak self] in
95 | guard let self = self else { return }
96 | self.delegate?.lyricMachine(self, didUpdateConsloe: text)
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/DownloadVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/12/14.
6 | //
7 |
8 | import UIKit
9 | import AgoraLyricsScore
10 |
11 | class DownloadVC: UIViewController {
12 | let downloadView = DownloadView()
13 | let lyricsFileDownloader = LyricsFileDownloader()
14 | var currentUrlIndex = 0
15 | let urlStrings = ["https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/1.zip",
16 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/2.zip",
17 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/3.zip",
18 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/4.zip",
19 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/5.zip",
20 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/6.zip",
21 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/7.zip",
22 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/8.lrc",
23 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/9.lrc",
24 | "https://fullapp.oss-cn-beijing.aliyuncs.com/lyricsMockDownload/10.lrc"]
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 |
29 | view.addSubview(downloadView)
30 | downloadView.frame = view.bounds
31 | downloadView.delegate = self
32 |
33 | /// init internal log
34 | let _ = KaraokeView.init(frame: .zero, loggers: [ConsoleLogger(), FileLogger()])
35 |
36 | lyricsFileDownloader.delegate = self
37 | }
38 | }
39 |
40 | extension DownloadVC: DownloadViewDelegate, LyricsFileDownloaderDelegate {
41 | func downloadViewDidTapAction(action: DownloadView.Action, info: DownloadView.Info?) {
42 | if action == .addOne {
43 | let urlString = urlStrings[currentUrlIndex]
44 | let requesetId = lyricsFileDownloader.download(urlString: urlString)
45 | let item = DownloadView.Info(requestId: requesetId, urlString: urlString)
46 | downloadView.addInfos(infos: [item])
47 | currentUrlIndex = currentUrlIndex == urlStrings.count - 1 ? 0 : currentUrlIndex + 1
48 | return
49 | }
50 |
51 | if action == .addAll {
52 | for urlString in urlStrings {
53 | let requesetId = lyricsFileDownloader.download(urlString: urlString)
54 | let item = DownloadView.Info(requestId: requesetId, urlString: urlString)
55 | downloadView.addInfos(infos: [item])
56 | }
57 | return
58 | }
59 |
60 | if action == .clear {
61 | lyricsFileDownloader.cleanAll()
62 | return
63 | }
64 |
65 | if action == .cancel, let info = info {
66 | if info.state == .created || info.state == .progress {
67 | lyricsFileDownloader.cancelDownload(requestId: info.requestId)
68 | info.state = .canceled
69 | downloadView.reloadData()
70 | }
71 | }
72 | }
73 |
74 | func onLyricsFileDownloadProgress(requestId: Int, progress: Float) {
75 | print("progress:\(progress)")
76 | if let item = downloadView.getInfo(requestId: requestId) {
77 | item.state = .progress
78 | item.progress = progress
79 | downloadView.reloadData()
80 | }
81 | }
82 |
83 | func onLyricsFileDownloadCompleted(requestId: Int,
84 | fileData: Data?,
85 | error: AgoraLyricsScore.DownloadError?) {
86 | if let item = downloadView.getInfo(requestId: requestId) {
87 | item.state = fileData == nil ? .doneFail : .doneSuccess
88 | downloadView.reloadData()
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/SearchVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/2/7.
6 | //
7 |
8 | import UIKit
9 | import AgoraRtcKit
10 |
11 | protocol SearchVCDelegate: NSObjectProtocol {
12 | func searchVCDidSelected(songCode: Int)
13 | }
14 |
15 | class SearchVC: UIViewController {
16 | let tableview = UITableView()
17 | let textFeild = UITextField()
18 | var list = [AgoraMusic]()
19 | weak var delegate: SearchVCDelegate?
20 | weak var mcc: AgoraMusicContentCenter!
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | setupUI()
25 | commonInit()
26 | }
27 |
28 | func setup(mcc: AgoraMusicContentCenter) {
29 | self.mcc = mcc
30 | mcc.register(self)
31 | list = []
32 | }
33 |
34 | func append(musics: [AgoraMusic]) {
35 | list.append(contentsOf: musics)
36 | tableview.reloadData()
37 | }
38 |
39 | func setupUI() {
40 | view.addSubview(textFeild)
41 | view.addSubview(tableview)
42 | tableview.translatesAutoresizingMaskIntoConstraints = false
43 | textFeild.translatesAutoresizingMaskIntoConstraints = false
44 |
45 | textFeild.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
46 | textFeild.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -100).isActive = true
47 | textFeild.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40).isActive = true
48 |
49 | tableview.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
50 | tableview.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
51 | tableview.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
52 | tableview.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
53 | }
54 |
55 | func commonInit() {
56 | tableview.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
57 | tableview.dataSource = self
58 | tableview.delegate = self
59 | tableview.reloadData()
60 | }
61 | }
62 |
63 | extension SearchVC: UITableViewDelegate, UITableViewDataSource {
64 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
65 | return list.count
66 | }
67 |
68 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
69 | let cell = tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
70 | let item = list[indexPath.row]
71 | cell.textLabel?.text = item.name + "[\(item.singer)][\(item.songCode)]"
72 | cell.accessoryType = .disclosureIndicator
73 | return cell
74 | }
75 |
76 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
77 | tableView.deselectRow(at: indexPath, animated: true)
78 | let item = list[indexPath.row]
79 | delegate?.searchVCDidSelected(songCode: item.songCode)
80 | }
81 | }
82 |
83 | extension SearchVC: AgoraMusicContentCenterEventDelegate {
84 | func onMusicChartsResult(_ requestId: String, result: [AgoraMusicChartInfo], errorCode: AgoraMusicContentCenterStatusCode) {
85 |
86 | }
87 |
88 | func onMusicCollectionResult(_ requestId: String, result: AgoraMusicCollection, errorCode: AgoraMusicContentCenterStatusCode) {
89 |
90 | }
91 |
92 | func onLyricResult(_ requestId: String, songCode: Int, lyricUrl: String?, errorCode: AgoraMusicContentCenterStatusCode) {
93 |
94 | }
95 |
96 | func onSongSimpleInfoResult(_ requestId: String, songCode: Int, simpleInfo: String?, errorCode: AgoraMusicContentCenterStatusCode) {
97 |
98 | }
99 |
100 | func onPreLoadEvent(_ requestId: String, songCode: Int, percent: Int, lyricUrl: String?, status: AgoraMusicContentCenterPreloadStatus, errorCode: AgoraMusicContentCenterStatusCode) {
101 |
102 | }
103 | }
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/TestScoringMachine/TestMockScoring.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestMockScoring.swift
3 | // AgoraLyricsScore-Unit-Tests
4 | //
5 | // Created by ZYP on 2023/2/9.
6 | //
7 |
8 | import XCTest
9 | @testable import AgoraLyricsScore
10 |
11 | class TestMockScoring: XCTestCase, ScoringMachineDelegate {
12 | override func setUpWithError() throws {
13 | Log.setLoggers(loggers: [ConsoleLogger()])
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | vm.reset()
18 | }
19 |
20 | var cumulativeScore = 0
21 | var testCaseNum = 0
22 | var vm = ScoringMachine()
23 | let exp = XCTestExpectation(description: "test score")
24 | let exp2 = XCTestExpectation(description: "test score2")
25 |
26 | func testAll() { /** test score 每个tone命中一次 **/
27 | let url = URL(fileURLWithPath: Bundle.current.path(forResource: "825003", ofType: "xml")!)
28 | let data = try! Data(contentsOf: url)
29 | guard let model = KaraokeView.parseLyricData(lyricFileData: data) else {
30 | XCTFail()
31 | return
32 | }
33 | vm = ScoringMachine()
34 | vm.scoreLevel = 10
35 | vm.delegate = self
36 | vm.setLyricData(data: model)
37 | for index in 0...5 {
38 | let line = model.lines[index]
39 | for tone in line.tones {
40 | let time = tone.beginTime + tone.duration/2
41 | vm.setProgress(progress: time)
42 | vm.setPitch(speakerPitch: tone.pitch - 1, progressInMs: 0)
43 | }
44 | }
45 | wait(for: [exp], timeout: 3)
46 | }
47 |
48 | func testAll2() { /** test score 每个tone命中多次 **/
49 | testCaseNum = 1
50 | let url = URL(fileURLWithPath: Bundle.current.path(forResource: "825003", ofType: "xml")!)
51 | let data = try! Data(contentsOf: url)
52 | guard let model = KaraokeView.parseLyricData(lyricFileData: data) else {
53 | XCTFail()
54 | return
55 | }
56 |
57 | vm = ScoringMachine()
58 | vm.delegate = self
59 | vm.scoreLevel = 15
60 | vm.setLyricData(data: model)
61 |
62 | let line = model.lines.first!
63 | var time = line.beginTime
64 | var gap = 0
65 | while time <= line.endTime+20 {
66 | vm.setProgress(progress: time)
67 | if gap == 40 {
68 | gap = 0
69 | vm.setPitch(speakerPitch: 50, progressInMs: 0)
70 | }
71 | gap += 20
72 | time += 20
73 | Thread.sleep(forTimeInterval: 0.02)
74 | }
75 | wait(for: [exp2], timeout: 10)
76 | }
77 |
78 | func sizeOfCanvasView(_ scoringMachine: ScoringMachineProtocol) -> CGSize {
79 | return .init(width: 380, height: 100)
80 | }
81 |
82 | func scoringMachine(_ scoringMachine: ScoringMachineProtocol, didUpdateDraw standardInfos: [ScoringMachine.DrawInfo], highlightInfos: [ScoringMachine.DrawInfo]) {
83 |
84 | }
85 |
86 | func scoringMachine(_ scoringMachine: ScoringMachineProtocol, didUpdateCursor centerY: CGFloat, showAnimation: Bool, debugInfo: ScoringMachine.DebugInfo) {
87 |
88 | }
89 |
90 | func scoringMachine(_ scoringMachine: ScoringMachineProtocol,
91 | didFinishLineWith model: LyricLineModel,
92 | score: Int,
93 | cumulativeScore: Int,
94 | lineIndex: Int,
95 | lineCount: Int) {
96 | if testCaseNum == 0 {
97 | self.cumulativeScore = cumulativeScore
98 | print("didFinishLineWith cumulativeScore: \(cumulativeScore)")
99 | if cumulativeScore == 500 {
100 | exp.fulfill()
101 | }
102 | }
103 | else {
104 | XCTAssertEqual(cumulativeScore, 66)
105 | if cumulativeScore == 66 {
106 | exp2.fulfill()
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Demo/Demo/Other/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2024/6/5.
6 | //
7 |
8 | import AgoraRtcKit
9 | import AgoraMccExService
10 |
11 | extension Double {
12 | var keep3: Double {
13 | return Double(Darwin.round(self * 1000)/1000)
14 | }
15 | }
16 |
17 | extension AgoraMusicContentCenter {
18 | enum LyricFileType: Int, CustomStringConvertible {
19 | case xml = 0
20 | case lrc = 1
21 |
22 | var description: String {
23 | return self == .xml ? "xml" : "lrc"
24 | }
25 | }
26 | }
27 |
28 | extension AgoraMusicContentCenterPreloadStatus: CustomStringConvertible {
29 | public var description: String {
30 | switch self {
31 | case .OK:
32 | return "OK"
33 | case .error:
34 | return "error"
35 | case .preloading:
36 | return "preloading"
37 | case .removeCache:
38 | return "removeCache"
39 | @unknown default:
40 | fatalError()
41 | }
42 | }
43 | }
44 |
45 | extension AgoraMusicContentCenterExState: CustomStringConvertible {
46 | public var description: String {
47 | switch self {
48 | case .initialized:
49 | return "initialized"
50 | case .initializeFailed:
51 | return "initializeFailed"
52 | case .preloadOK:
53 | return "preloadOK"
54 | case .preloadError:
55 | return "preloadError"
56 | case .preloading:
57 | return "preloading"
58 | case .preloadRemoveCache:
59 | return "preloadRemoveCache"
60 | case .startScoreCompleted:
61 | return "startScoreCompleted"
62 | case .startScoreFailed:
63 | return "startScoreFailed"
64 | @unknown default:
65 | fatalError()
66 | }
67 | }
68 | }
69 |
70 | extension AgoraMusicContentCenterExStateReason: CustomStringConvertible {
71 | public var description: String {
72 | switch self {
73 | case .OK:
74 | return "OK"
75 | case .error:
76 | return "error"
77 | case .errorInvalidSignature:
78 | return "errorInvalidSignature"
79 | case .errorHttpInternalError:
80 | return "errorHttpInternalError"
81 | case .ysdErrorLyricError:
82 | return "ysdErrorLyricError"
83 | case .ysdErrorPtsError:
84 | return "ysdErrorPtsError"
85 | case .ysdErrorParamError:
86 | return "ysdErrorParamError"
87 | case .ysdErrorTokenError:
88 | return "ysdErrorTokenError"
89 | case .ysdErrorPitchError:
90 | return "ysdErrorPitchError"
91 | case .ysdErrorNetworkError:
92 | return "ysdErrorNetworkError"
93 | case .ysdErrorRequestError:
94 | return "ysdErrorRequestError"
95 | case .ysdErrorPrivilegeError:
96 | return "ysdErrorPrivilegeError"
97 | case .ysdErrorNoActivateError:
98 | return "ysdErrorNoActivateError"
99 | case .ysdErrorRepeatRequestError:
100 | return "ysdErrorRepeatRequestError"
101 | @unknown default:
102 | fatalError()
103 | }
104 | }
105 | }
106 |
107 | extension AgoraMusicContentCenterStatusCode: CustomStringConvertible {
108 | public var description: String {
109 | switch self {
110 | case .OK:
111 | return "OK"
112 | case .error:
113 | return "error"
114 | case .errorGateway:
115 | return "errorGateway"
116 | case .errorMusicLoading:
117 | return "errorMusicLoading"
118 | case .errorInternalDataParse:
119 | return "errorInternalDataParse"
120 | case .errorPermissionAndResource:
121 | return "errorPermissionAndResource"
122 | case .errorHttpInternalError:
123 | return "errorHttpInternalError"
124 | case .errorMusicDecryption:
125 | return "errorMusicDecryption"
126 | @unknown default:
127 | fatalError()
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/EmitterVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmitterVC.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/1/5.
6 | //
7 |
8 | import UIKit
9 |
10 | class EmitterVC: UIViewController {
11 |
12 | let emitter = Emitter()
13 | var start = true
14 | private var timer = GCDTimer()
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | view.backgroundColor = .red
19 | emitter.setupEmitterPoint(point: .init(x: 300, y: 300))
20 | view.layer.addSublayer(emitter.layer)
21 |
22 | timer.scheduledMillisecondsTimer(withName: "EmitterVC", countDown: 1000000, milliseconds: 200, queue: .main) { [weak self](_, time) in
23 | guard let self = self else { return }
24 | self.start ? self.emitter.stop() : self.emitter.start()
25 | self.start = !self.start
26 | }
27 | }
28 |
29 | // override func touchesBegan(_ touches: Set, with event: UIEvent?) {
30 | // timer.scheduledMillisecondsTimer(withName: "EmitterVC", countDown: 1000000, milliseconds: 1000, queue: .main) { [weak self](_, time) in
31 | // guard let self = self else { return }
32 | // self.start ? self.emitter.stop() : self.emitter.start()
33 | // self.start = !self.start
34 | // }
35 | // }
36 |
37 | }
38 |
39 |
40 |
41 | class Emitter {
42 | var layer = CAEmitterLayer()
43 | var images: [UIImage]? {
44 | didSet {
45 | updateLayer()
46 | }
47 | }
48 | private var count = 0
49 | private var lastPoint: CGPoint = .zero
50 | private let logTag = "Emitter"
51 |
52 | var defaultImages: [UIImage] {
53 | var list = [UIImage]()
54 | for i in 1...8 {
55 | let image = UIImage(named: "star\(i)")!
56 | list.append(image)
57 | }
58 | return list
59 | }
60 |
61 | init() {
62 | updateLayer()
63 | }
64 |
65 | func updateLayer() {
66 | let superLayer = layer.superlayer
67 | layer.removeFromSuperlayer()
68 | layer = CAEmitterLayer()
69 | superLayer?.addSublayer(layer)
70 | layer.emitterPosition = .zero
71 | layer.preservesDepth = true
72 | layer.renderMode = .oldestLast
73 | layer.masksToBounds = false
74 | layer.emitterMode = .points
75 | layer.emitterShape = .circle
76 | layer.birthRate = 0
77 | layer.emitterPosition = lastPoint
78 | let imgs = (images != nil) ? images! : defaultImages
79 | let count = imgs.count
80 | layer.emitterCells = imgs.enumerated().map({ Emitter.createEmitterCell(name: "cell", image: $0.1, birthRate: count) })
81 | }
82 |
83 | func setupEmitterPoint(point: CGPoint) {
84 | lastPoint = point
85 | layer.emitterPosition = point
86 | }
87 |
88 | func start() {
89 | count += 1
90 | if count >= 150 {
91 | print("change ==")
92 | count = 0
93 | updateLayer()
94 | }
95 | layer.birthRate = 1
96 | }
97 |
98 | func stop() {
99 | layer.birthRate = 0
100 | }
101 |
102 | static func createEmitterCell(name: String, image: UIImage, birthRate: Int) -> CAEmitterCell {
103 | /// 创建粒子, 并且设置例子相关的属性
104 | let cell = CAEmitterCell()
105 | /// 设置粒子速度
106 | cell.velocity = 1
107 | cell.velocityRange = 1
108 | /// 设置例子的大小
109 | cell.scale = 1
110 | cell.scaleRange = 0.5
111 | /// 设置粒子方向
112 | cell.emissionLongitude = CGFloat.pi * 3
113 | cell.emissionRange = CGFloat.pi / 6
114 | /// 设置粒子的存活时间
115 | cell.lifetime = 3
116 | cell.lifetimeRange = 0.1
117 | /// 设置粒子旋转
118 | cell.spin = CGFloat.pi / 2
119 | cell.spinRange = CGFloat.pi / 4
120 | /// 设置例子每秒弹出的个数
121 | cell.birthRate = Float(birthRate)
122 | cell.alphaRange = 0.75
123 | cell.alphaSpeed = -0.35
124 | /// 初始速度
125 | cell.velocity = 90
126 | cell.name = name
127 | cell.isEnabled = true
128 | cell.contents = image.cgImage
129 | return cell
130 | }
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/Demo/Demo/VC/GCDTimer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GCDTimer.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2022/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | class GCDTimer {
12 | typealias ActionBlock = (String, TimeInterval) -> Void
13 | private var timerContainer = [String : DispatchSourceTimer]()
14 | private var currentDuration: TimeInterval = 0
15 | /// 秒级定时器
16 | ///
17 | /// - Parameters:
18 | /// - name: 定时器的名字
19 | /// - timeInterval: 时间间隔
20 | /// - queue: 线程
21 | /// - repeats: 是否重复
22 | /// - action: 执行的操作
23 | func scheduledSecondsTimer(withName name: String?,
24 | timeInterval: Int,
25 | queue: DispatchQueue,
26 | action: @escaping ActionBlock)
27 | {
28 | currentDuration = TimeInterval(timeInterval)
29 | let scheduledName = name ?? Date().timeString()
30 | var timer = timerContainer[scheduledName]
31 | if timer == nil {
32 | timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
33 | timer?.resume()
34 | timerContainer[scheduledName] = timer
35 | }
36 | timer?.schedule(deadline: .now(), repeating: .seconds(1), leeway: .milliseconds(100))
37 | timer?.setEventHandler(handler: { [weak self] in
38 | guard let self = self else { return }
39 | self.currentDuration -= 1
40 | action(scheduledName, self.currentDuration)
41 | if self.currentDuration <= 0 {
42 | self.destoryTimer(withName: scheduledName)
43 | }
44 | })
45 | }
46 |
47 | /// 毫秒级定时器
48 | /// - Parameters:
49 | /// - name: 名称
50 | /// - countDown: 倒计时毫秒
51 | /// - timeInterval: 多少毫秒回调一次
52 | /// - queue: 线程
53 | /// - action: 回调
54 | func scheduledMillisecondsTimer(withName name: String?,
55 | countDown: TimeInterval,
56 | milliseconds: TimeInterval,
57 | queue: DispatchQueue,
58 | action: @escaping ActionBlock)
59 | {
60 | currentDuration = countDown
61 | let scheduledName = name ?? Date().timeString()
62 | var timer = timerContainer[scheduledName]
63 | if timer == nil {
64 | timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
65 | timer?.resume()
66 | timerContainer[scheduledName] = timer
67 | }
68 | timer?.schedule(deadline: .now(), repeating: .milliseconds(Int(milliseconds)), leeway: .milliseconds(1))
69 | timer?.setEventHandler(handler: { [weak self] in
70 | guard let self = self else { return }
71 | self.currentDuration -= milliseconds
72 | action(scheduledName, self.currentDuration)
73 | if self.currentDuration <= 0 {
74 | self.destoryTimer(withName: scheduledName)
75 | }
76 | })
77 | }
78 |
79 | /// 销毁名字为name的计时器
80 | ///
81 | /// - Parameter name: 计时器的名字
82 | func destoryTimer(withName name: String?) {
83 | guard let name = name else { return }
84 | let timer = timerContainer[name]
85 | if timer == nil { return }
86 | timerContainer.removeValue(forKey: name)
87 | timer?.cancel()
88 | }
89 |
90 | /// 销毁所有计时器
91 | func destoryAllTimer() {
92 | timerContainer.forEach {
93 | destoryTimer(withName: $0.key)
94 | }
95 | }
96 |
97 | /// 检测是否已经存在名字为name的计时器
98 | ///
99 | /// - Parameter name: 计时器的名字
100 | /// - Returns: 返回bool值
101 | func isExistTimer(withName name: String?) -> Bool {
102 | guard let name = name else { return false }
103 | return timerContainer[name] != nil
104 | }
105 | }
106 |
107 | extension Date {
108 | func timeString(ofStyle style: DateFormatter.Style = .medium) -> String {
109 | let dateFormatter = DateFormatter()
110 | dateFormatter.timeStyle = style
111 | dateFormatter.dateStyle = .none
112 | return dateFormatter.string(from: self)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Demo/Demo/Other/Utils/LyricsCutter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LyricsCutter.swift
3 | // Demo
4 | //
5 | // Created by ZYP on 2023/4/25.
6 | //
7 |
8 | import Foundation
9 | import AgoraLyricsScore
10 |
11 | class LyricsCutter {
12 | /// 每一行歌词的信息
13 | public struct Line {
14 | var beginTime: Int
15 | var duration: Int
16 | var endTime: Int {
17 | return beginTime + duration
18 | }
19 | }
20 |
21 | /// 处理时间副歌片段时间(对齐句子)
22 | /// - Parameters:
23 | /// - startTime: 副歌开始时间
24 | /// - endTime: 副歌结束时间
25 | /// - lines: 歌词所有行数据
26 | /// - Returns: (startTime, endTime), nil为无法处理
27 | public static func handleFixTime(startTime: Int,
28 | endTime: Int,
29 | lines: [Line]) -> (Int, Int)? {
30 | guard startTime < endTime else {
31 | return nil
32 | }
33 |
34 | guard startTime != endTime else {
35 | return nil
36 | }
37 |
38 | guard !lines.isEmpty else {
39 | return nil
40 | }
41 |
42 | var start = startTime
43 | var end = endTime
44 |
45 | if start < lines.first!.beginTime,
46 | end < lines.first!.beginTime {
47 | return nil
48 | }
49 |
50 | if start > lines.last!.endTime,
51 | end > lines.last!.endTime {
52 | return nil
53 | }
54 |
55 | /// 跨过第一个
56 | if start < lines.first!.beginTime, end < lines.first!.endTime {
57 | start = lines.first!.beginTime
58 | end = lines.first!.endTime
59 | return (start, end)
60 | }
61 |
62 | /// 跨过最后一个
63 | if start > lines.last!.beginTime,
64 | end > lines.last!.endTime {
65 | start = lines.last!.beginTime
66 | end = lines.last!.endTime
67 | return (start, end)
68 | }
69 |
70 | if start < lines.first!.beginTime {
71 | start = lines.first!.beginTime
72 | }
73 |
74 | if end > lines.last!.endTime {
75 | end = lines.last!.endTime
76 | }
77 |
78 | var startIndex = 0
79 | var startGap = Int.max
80 | var endIndex = 0
81 | var endGap = Int.max
82 | for (offset, line) in lines.enumerated() {
83 | if abs(line.beginTime - start) < startGap {
84 | startGap = abs(line.beginTime - start)
85 | startIndex = offset
86 | }
87 | if abs(line.endTime - end) < endGap {
88 | endGap = abs(line.endTime - end)
89 | endIndex = offset
90 | }
91 | }
92 |
93 | let result = (lines[startIndex].beginTime, lines[endIndex].endTime)
94 | if result.0 < result.1 { /** valid **/
95 | return result
96 | }
97 | return nil
98 | }
99 |
100 | /// 裁剪副歌片段
101 | /// - Parameters:
102 | /// - model: 歌词模型
103 | /// - start: 副歌开始时间
104 | /// - end: 副歌结束时间
105 | /// - Returns: 副歌模型
106 | public static func cut(model: LyricModel, startTime: Int, endTime: Int) -> LyricModel {
107 | var lines = [LyricLineModel]()
108 |
109 | var flag = false
110 | for line in model.lines {
111 | if line.beginTime == startTime {
112 | flag = true
113 | }
114 | if line.beginTime + line.duration == endTime {
115 | lines.append(line)
116 | break
117 | }
118 | if flag {
119 | lines.append(line)
120 | }
121 | }
122 |
123 | var pitchDatas = [KrcPitchData]()
124 | for pitchData in model.pitchDatas {
125 | if pitchData.startTime >= startTime && pitchData.startTime + pitchData.duration <= endTime {
126 | pitchDatas.append(pitchData)
127 | }
128 | }
129 | model.pitchDatas = pitchDatas
130 |
131 | model.lines = lines
132 | model.preludeEndPosition = 0
133 | model.duration = (lines.last?.beginTime ?? 0) + (lines.last?.duration ?? 0)
134 | return model
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Class/Lyrics/FirstToneHintView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstToneHintView.swift
3 | // AgoraLyricsScore
4 | //
5 | // Created by ZYP on 2022/12/22.
6 | //
7 |
8 | import UIKit
9 |
10 | public class FirstToneHintViewStyle: NSObject {
11 | /// 背景色
12 | @objc public var backgroundColor: UIColor = .gray { didSet { didUpdate?() } }
13 | /// 大小
14 | @objc public var size: CGFloat = 10 { didSet { didUpdate?() } }
15 | /// 底部间距
16 | @objc public var bottomMargin: CGFloat = 0 { didSet { didUpdate?() } }
17 |
18 | typealias VoidBlock = () -> Void
19 |
20 | var didUpdate: VoidBlock?
21 | }
22 |
23 | class FirstToneHintView: UIView {
24 | var style = FirstToneHintViewStyle() { didSet { updateUI() } }
25 | var mustHidden = false
26 | private let loadViews: [UIView] = [.init(), .init(), .init()]
27 | private var loadViewConstraints = [NSLayoutConstraint]()
28 | /// 剩余开始时间 ms
29 | private var remainingTime = 0
30 | private var lastRemainingTime = 0
31 | fileprivate let logTag = "FirstToneHintView"
32 |
33 | override init(frame: CGRect) {
34 | super.init(frame: frame)
35 | setupUI()
36 | }
37 |
38 | @available(*, unavailable)
39 | required init?(coder _: NSCoder) {
40 | fatalError("init(coder:) has not been implemented")
41 | }
42 |
43 | private func setupUI() {
44 | for view in loadViews {
45 | view.backgroundColor = style.backgroundColor
46 | view.layer.cornerRadius = style.size / 2
47 | view.layer.masksToBounds = true
48 | addSubview(view)
49 | view.translatesAutoresizingMaskIntoConstraints = false
50 | let widthConstraint = view.widthAnchor.constraint(equalToConstant: style.size)
51 | let heightConstraint = view.heightAnchor.constraint(equalToConstant: style.size)
52 | widthConstraint.isActive = true
53 | heightConstraint.isActive = true
54 | loadViewConstraints.append(widthConstraint)
55 | loadViewConstraints.append(heightConstraint)
56 | }
57 |
58 | loadViews[1].centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
59 | loadViews[1].centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
60 |
61 | loadViews[0].leftAnchor.constraint(equalTo: loadViews[1].rightAnchor, constant: 10).isActive = true
62 | loadViews[0].centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
63 |
64 | loadViews[2].rightAnchor.constraint(equalTo: loadViews[1].leftAnchor, constant: -10).isActive = true
65 | loadViews[2].centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
66 | isHidden = true
67 | }
68 |
69 | private func updateUI() {
70 | for view in loadViews {
71 | view.backgroundColor = style.backgroundColor
72 | view.layer.cornerRadius = style.size / 2
73 | }
74 |
75 | for constraint in loadViewConstraints {
76 | constraint.constant = style.size
77 | }
78 | }
79 |
80 | func updateStyle(style: FirstToneHintViewStyle) {
81 | self.style = style
82 | updateUI()
83 | }
84 |
85 | /// 设置剩余开始时间 (前奏时间 - 当前歌曲进度)
86 | /// - Parameter time: 剩余开始唱第一句的时间
87 | func setRemainingTime(time: Int) {
88 | if time < 0 {
89 | reset()
90 | return
91 | }
92 |
93 | /** 过滤,500ms设置一次 **/
94 | if lastRemainingTime == 0 {
95 | lastRemainingTime = time
96 | }
97 | if lastRemainingTime - time < 500 {
98 | return
99 | }
100 | lastRemainingTime = time
101 |
102 | remainingTime = time
103 | Log.info(text: "remainingTime: \(remainingTime)", tag: logTag)
104 | loadViews[0].isHidden = (remainingTime >= 3 * 1000) ? !loadViews[0].isHidden : true
105 | loadViews[1].isHidden = !(remainingTime >= 2 * 1000)
106 | loadViews[2].isHidden = !(remainingTime >= 1 * 1000)
107 | }
108 |
109 | func reset() {
110 | isHidden = true
111 | self.loadViews[0].isHidden = false
112 | self.loadViews[1].isHidden = false
113 | self.loadViews[2].isHidden = false
114 | lastRemainingTime = 0
115 | remainingTime = 0
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/AgoraLyricsScore/Tests/Resource/4875936889260991133.krc:
--------------------------------------------------------------------------------
1 | [id:$00000000]
2 | [ar:陈奕迅]
3 | [ti:十年]
4 | [by:]
5 | [hash:180f842eeada43cad376a20db1cb5ec6]
6 | [al:]
7 | [sign:]
8 | [qq:]
9 | [total:202684]
10 | [offset:0]
11 | [language:eyJjb250ZW50IjpbXSwidmVyc2lvbiI6MX0=]
12 | [1067,653]<0,502,0>陈<502,50,0>奕<552,0,0>迅 <552,50,0>- <602,51,0>十<653,0,0>年
13 | [1720,150]<0,50,0>作<50,50,0>词:<100,0,0>林<100,50,0>夕
14 | [1870,303]<0,51,0>作<51,0,0>曲:<51,52,0>陈<103,50,0>小<153,150,0>霞
15 | [2173,5238]<0,253,0>编<253,150,0>曲:<403,205,0>陈<608,303,0>辉<911,4327,0>阳
16 | [15106,3841]<0,300,0>如<300,302,0>果<602,352,0>那<954,452,0>两<1406,454,0>个<1860,504,0>字<2364,420,0>没<2784,201,0>有<2985,252,0>颤<3237,604,0>抖
17 | [19044,3594]<0,237,0>我<237,252,0>不<489,757,0>会<1246,202,0>发<1448,453,0>现 <1901,352,0>我<2253,285,0>难<2538,1056,0>受
18 | [22638,3218]<0,152,0>怎<152,200,0>么<352,150,0>说<502,254,0>出<756,2462,0>口
19 | [25955,3361]<0,287,0>也<287,253,0>不<540,199,0>过<739,203,0>是<942,353,0>分<1295,2066,0>手
20 | [30572,4074]<0,252,0>如<252,251,0>果<503,401,0>对<904,454,0>于<1358,502,0>明<1860,555,0>天<2415,251,0>没<2666,202,0>有<2868,200,0>要<3068,1006,0>求
21 | [34724,2897]<0,180,0>牵<180,402,0>牵<582,504,0>手<1086,252,0>就<1338,253,0>像<1591,301,0>旅<1892,1005,0>游
22 | [37621,2921]<0,202,0>成<202,201,0>千<403,202,0>上<605,251,0>万<856,302,0>个<1158,302,0>门<1460,1461,0>口
23 | [41673,2768]<0,151,0>总<151,201,0>有<352,201,0>一<553,251,0>个<804,204,0>人<1008,452,0>要<1460,304,0>先<1764,1004,0>走
24 | [47453,3272]<0,149,0>怀<149,353,0>抱<502,202,0>既<704,303,0>然<1007,453,0>不<1460,251,0>能<1711,503,0>逗<2214,1058,0>留
25 | [50725,2872]<0,202,0>何<202,201,0>不<403,202,0>在<605,252,0>离<857,253,0>开<1110,252,0>的<1362,252,0>时<1614,1258,0>候
26 | [53597,5785]<0,203,0>一<203,202,0>边<405,302,0>享<707,1861,0>受 <2568,656,0>一<3224,703,0>边<3927,552,0>泪<4479,1306,0>流
27 | [60854,1863]<0,151,0>十<151,201,0>年<352,705,0>之<1057,806,0>前
28 | [62717,1863]<0,454,0>我<454,201,0>不<655,252,0>认<907,250,0>识<1157,706,0>你
29 | [64580,2062]<0,554,0>你<554,201,0>不<755,252,0>属<1007,252,0>于<1259,803,0>我
30 | [66642,2416]<0,252,0>我<252,405,0>们<657,201,0>还<858,302,0>是<1160,401,0>一<1561,855,0>样
31 | [69058,3733]<0,203,0>陪<203,202,0>在<405,301,0>一<706,406,0>个<1112,251,0>陌<1363,252,0>生<1615,452,0>人<2067,302,0>左<2369,1364,0>右
32 | [72791,3578]<0,302,0>走<302,202,0>过<504,302,0>渐<806,655,0>渐<1461,254,0>熟<1715,505,0>悉<2220,251,0>的<2471,453,0>街<2924,654,0>头
33 | [76369,1713]<0,202,0>十<202,201,0>年<403,605,0>之<1008,705,0>后
34 | [78082,1968]<0,403,0>我<403,253,0>们<656,302,0>是<958,303,0>朋<1261,707,0>友
35 | [80050,2115]<0,552,0>还<552,251,0>可<803,302,0>以<1105,253,0>问<1358,757,0>候
36 | [82165,2367]<0,403,0>只<403,253,0>是<656,201,0>那<857,302,0>种<1159,403,0>温<1562,805,0>柔
37 | [84532,3776]<0,252,0>再<252,202,0>也<454,251,0>找<705,353,0>不<1058,351,0>到<1409,203,0>拥<1612,454,0>抱<2066,302,0>的<2368,503,0>理<2871,905,0>由
38 | [88308,6101]<0,251,0>情<251,355,0>人<606,203,0>最<809,604,0>后<1413,303,0>难<1716,452,0>免<2168,402,0>沦<2570,1514,0>为<4084,555,0>朋<4639,1462,0>友
39 | [117010,3272]<0,202,0>怀<202,352,0>抱<554,201,0>既<755,302,0>然<1057,455,0>不<1512,252,0>能<1764,604,0>逗<2368,904,0>留
40 | [120282,2968]<0,202,0>何<202,201,0>不<403,251,0>在<654,202,0>离<856,301,0>开<1157,251,0>的<1408,253,0>时<1661,1307,0>候
41 | [123250,6399]<0,202,0>一<202,200,0>边<402,302,0>享<704,1867,0>受 <2571,704,0>一<3275,705,0>边<3980,504,0>泪<4484,1915,0>流
42 | [130478,1967]<0,252,0>十<252,151,0>年<403,755,0>之<1158,809,0>前
43 | [132445,1965]<0,503,0>我<503,202,0>不<705,252,0>认<957,252,0>识<1209,756,0>你
44 | [134410,1911]<0,404,0>你<404,251,0>不<655,200,0>属<855,303,0>于<1158,753,0>我
45 | [136321,2368]<0,402,0>我<402,253,0>们<655,152,0>还<807,302,0>是<1109,453,0>一<1562,806,0>样
46 | [138689,3772]<0,201,0>陪<201,201,0>在<402,251,0>一<653,454,0>个<1107,201,0>陌<1308,252,0>生<1560,604,0>人<2164,200,0>左<2364,1408,0>右
47 | [142461,3574]<0,202,0>走<202,252,0>过<454,202,0>渐<656,858,0>渐<1514,201,0>熟<1715,503,0>悉<2218,251,0>的<2469,502,0>街<2971,603,0>头
48 | [146035,3677]<0,152,0>十<152,252,0>年<404,605,0>之<1009,704,0>后 <1713,502,0>我<2215,254,0>们<2469,202,0>是<2671,252,0>朋<2923,754,0>友
49 | [149712,4484]<0,506,0>还<506,201,0>可<707,251,0>以<958,252,0>问<1210,805,0>候 <2015,454,0>只<2469,250,0>是<2719,202,0>那<2921,253,0>种<3174,453,0>温<3627,857,0>柔
50 | [154196,3875]<0,252,0>再<252,251,0>也<503,252,0>找<755,354,0>不<1109,200,0>到<1309,301,0>拥<1610,453,0>抱<2063,251,0>的<2314,554,0>理<2868,1007,0>由
51 | [158071,7859]<0,201,0>情<201,203,0>人<404,202,0>最<606,706,0>后<1312,251,0>难<1563,557,0>免<2120,303,0>沦<2423,1560,0>为<3983,553,0>朋<4536,3323,0>友
52 | [168121,4082]<0,202,0>直<202,152,0>到<354,503,0>和<857,455,0>你<1312,452,0>做<1764,554,0>了<2318,202,0>多<2520,253,0>年<2773,251,0>朋<3024,1058,0>友
53 | [172203,2867]<0,150,0>才<150,452,0>明<602,603,0>白<1205,201,0>我<1406,253,0>的<1659,301,0>眼<1960,907,0>泪
54 | [175070,3573]<0,202,0>不<202,251,0>是<453,352,0>为<805,251,0>你<1056,252,0>而<1308,2265,0>流
55 | [178917,202684]<0,202,0>也<202,202,0>为<404,254,0>别<658,252,0>人<910,453,0>而<1363,1715,0>流
56 |
--------------------------------------------------------------------------------