├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── libs
│ ├── arm64-v8a
│ │ ├── libliquidfun.so
│ │ └── libliquidfun_jni.so
│ └── libliquidfun.jar
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── me
│ │ └── spica27
│ │ └── spicamusic
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── me
│ │ │ └── spica27
│ │ │ └── spicamusic
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── db
│ │ │ ├── AppDatabase.kt
│ │ │ ├── dao
│ │ │ │ ├── PlaylistDao.kt
│ │ │ │ └── SongDao.kt
│ │ │ └── entity
│ │ │ │ ├── Playlist.kt
│ │ │ │ ├── PlaylistSongCrossRef.kt
│ │ │ │ └── Song.kt
│ │ │ ├── dsp
│ │ │ ├── BandProcessor.kt
│ │ │ ├── ByteUtils.kt
│ │ │ ├── Equalizer.kt
│ │ │ ├── EqualizerAudioProcessor.kt
│ │ │ ├── EqualizerBand.kt
│ │ │ ├── HighPassFilter.kt
│ │ │ ├── LowPassFilter.kt
│ │ │ ├── ReplayGainAudioProcessor.kt
│ │ │ └── Utils.kt
│ │ │ ├── module
│ │ │ ├── NavigationModule.kt
│ │ │ ├── PersistenceModule.kt
│ │ │ └── ToolModule.kt
│ │ │ ├── playback
│ │ │ ├── PlaybackStateManager.kt
│ │ │ └── RepeatMode.kt
│ │ │ ├── player
│ │ │ ├── IPlayer.kt
│ │ │ └── Queue.kt
│ │ │ ├── route
│ │ │ └── Routes.kt
│ │ │ ├── service
│ │ │ ├── ForegroundManager.kt
│ │ │ ├── ForegroundServiceNotification.kt
│ │ │ ├── MuiscSearchWorker.kt
│ │ │ ├── MusicService.kt
│ │ │ └── notification
│ │ │ │ ├── MediaSessionComponent.kt
│ │ │ │ └── NotificationComponent.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ ├── ui
│ │ │ ├── AddSongScrren.kt
│ │ │ ├── AppMain.kt
│ │ │ ├── CurrentListPage.kt
│ │ │ ├── EqScreen.kt
│ │ │ ├── HomePage.kt
│ │ │ ├── MainScreen.kt
│ │ │ ├── PlayerPage.kt
│ │ │ ├── PlayerScreen.kt
│ │ │ ├── PlaylistDetailScreen.kt
│ │ │ ├── SearchAllScreen.kt
│ │ │ ├── SettingPage.kt
│ │ │ └── SplashScreen.kt
│ │ │ ├── utils
│ │ │ ├── AppTools.kt
│ │ │ ├── AudioTool.kt
│ │ │ ├── BlurTransformation.kt
│ │ │ ├── ComposeExt.kt
│ │ │ ├── DataStoreUtil.kt
│ │ │ ├── MimeTypeExt.kt
│ │ │ ├── MusicExt.kt
│ │ │ └── StorageUtil.kt
│ │ │ ├── viewModel
│ │ │ ├── MusicSearchViewModel.kt
│ │ │ ├── PlayBackViewModel.kt
│ │ │ ├── PlaylistViewModel.kt
│ │ │ ├── SelectSongViewModel.kt
│ │ │ ├── SettingViewModel.kt
│ │ │ └── SongViewModel.kt
│ │ │ ├── visualiser
│ │ │ ├── FFTAudioProcessor.kt
│ │ │ ├── FFTListener.java
│ │ │ ├── MusicVisualiser.kt
│ │ │ ├── VisualizerDrawableManager.kt
│ │ │ └── drawable
│ │ │ │ ├── BlurVisualiser.kt
│ │ │ │ ├── CircleVisualiser.kt
│ │ │ │ ├── LineVisualiser.kt
│ │ │ │ ├── RainVisualiserDrawable.kt
│ │ │ │ └── VisualiserDrawable.kt
│ │ │ └── widget
│ │ │ ├── BottomPlayerBar.kt
│ │ │ ├── EqSettingView.kt
│ │ │ ├── PlaylistItem.kt
│ │ │ ├── SongControllerPanel.kt
│ │ │ ├── SongItem.kt
│ │ │ ├── VisualizerView.kt
│ │ │ ├── VisualizerWidget.kt
│ │ │ └── audio_seekbar
│ │ │ ├── AmplitudeType.kt
│ │ │ ├── AudioSeekBar.kt
│ │ │ ├── BrushExt.kt
│ │ │ ├── Ext.kt
│ │ │ └── WaveformAlignment.kt
│ └── res
│ │ ├── drawable
│ │ ├── default_cover.jpg
│ │ ├── ic_app.xml
│ │ ├── ic_audio_line.xml
│ │ ├── ic_dvd.xml
│ │ ├── ic_new.xml
│ │ ├── ic_next.xml
│ │ ├── ic_pause.xml
│ │ ├── ic_play.xml
│ │ ├── ic_playlist_remove.xml
│ │ ├── ic_plus.xml
│ │ ├── ic_pre.xml
│ │ └── ic_remove.xml
│ │ ├── mipmap-anydpi-v26
│ │ └── ic_launcher.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_background.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap
│ │ └── default_cover.jpg
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── me
│ └── spica27
│ └── spicamusic
│ └── ExampleUnitTest.kt
├── build.gradle
├── decoder_flac
├── README.md
├── build.gradle
├── build
│ ├── .transforms
│ │ ├── 1d03ef9414a0bc197928d8d7193223af
│ │ │ └── transformed
│ │ │ │ └── bundleLibRuntimeToDirRelease
│ │ │ │ └── bundleLibRuntimeToDirRelease_dex
│ │ │ │ └── androidx
│ │ │ │ └── media3
│ │ │ │ └── decoder
│ │ │ │ └── flac
│ │ │ │ ├── FlacBinarySearchSeeker$1.dex
│ │ │ │ └── FlacExtractor.dex
│ │ ├── 31546844da043d1214a3e0db40582720
│ │ │ └── results.bin
│ │ ├── 9f269d71b886e2dd8426bc057a192b3c
│ │ │ └── transformed
│ │ │ │ └── bundleLibRuntimeToDirDebug
│ │ │ │ └── androidx
│ │ │ │ └── media3
│ │ │ │ └── decoder
│ │ │ │ └── flac
│ │ │ │ ├── FlacDecoderJni$FlacFrameDecodeException.dex
│ │ │ │ ├── FlacDecoderJni.dex
│ │ │ │ └── package-info.dex
│ │ ├── a5958f52ec8a25958ae333556be3547d
│ │ │ └── transformed
│ │ │ │ └── bundleLibRuntimeToDirDebug
│ │ │ │ └── androidx
│ │ │ │ └── media3
│ │ │ │ └── decoder
│ │ │ │ └── flac
│ │ │ │ └── FlacBinarySearchSeeker.dex
│ │ ├── e64e43cc4410128874ea299d3e4cc1ae
│ │ │ └── transformed
│ │ │ │ └── bundleLibRuntimeToDirDebug
│ │ │ │ └── androidx
│ │ │ │ └── media3
│ │ │ │ └── decoder
│ │ │ │ └── flac
│ │ │ │ └── FlacBinarySearchSeeker.dex
│ │ ├── eeb65c14c090fc702a9eb4b02b51640b
│ │ │ └── transformed
│ │ │ │ └── bundleLibRuntimeToDirDebug
│ │ │ │ └── androidx
│ │ │ │ └── media3
│ │ │ │ └── decoder
│ │ │ │ └── flac
│ │ │ │ └── FlacBinarySearchSeeker$FlacTimestampSeeker.dex
│ │ └── fb458d17bb1ab52cb1b4e8f8aa3aa7c8
│ │ │ └── transformed
│ │ │ └── bundleLibRuntimeToDirDebug
│ │ │ └── androidx
│ │ │ └── media3
│ │ │ └── decoder
│ │ │ └── flac
│ │ │ └── FlacDecoderJni.dex
│ ├── intermediates
│ │ ├── aapt_friendly_merged_manifests
│ │ │ └── debug
│ │ │ │ └── processDebugManifest
│ │ │ │ └── aapt
│ │ │ │ └── AndroidManifest.xml
│ │ ├── compile_symbol_list
│ │ │ └── release
│ │ │ │ └── generateReleaseRFile
│ │ │ │ └── R.txt
│ │ ├── desugar_graph
│ │ │ └── debugAndroidTest
│ │ │ │ └── dexBuilderDebugAndroidTest
│ │ │ │ └── out
│ │ │ │ └── currentProject
│ │ │ │ ├── dirs_bucket_0
│ │ │ │ └── graph.bin
│ │ │ │ └── jar_21b782bae186444e85dc99b838ca35529d673440025a33f4918e6efb26fc32ea_bucket_1
│ │ │ │ └── graph.bin
│ │ ├── incremental
│ │ │ ├── debugAndroidTest-mergeJavaRes
│ │ │ │ └── zip-cache
│ │ │ │ │ ├── 4JlYajEDSkuwp6dx_XQjkFvUNE8=
│ │ │ │ │ └── 98ai+Hnq3fTWBmflSOJdI6UZ6Ns=
│ │ │ ├── debugAndroidTest
│ │ │ │ └── mergeDebugAndroidTestResources
│ │ │ │ │ └── merged.dir
│ │ │ │ │ ├── values-az
│ │ │ │ │ └── values-az.xml
│ │ │ │ │ ├── values-es
│ │ │ │ │ └── values-es.xml
│ │ │ │ │ ├── values-fr
│ │ │ │ │ └── values-fr.xml
│ │ │ │ │ ├── values-ka
│ │ │ │ │ └── values-ka.xml
│ │ │ │ │ ├── values-th
│ │ │ │ │ └── values-th.xml
│ │ │ │ │ ├── values-tr
│ │ │ │ │ └── values-tr.xml
│ │ │ │ │ ├── values-ur
│ │ │ │ │ └── values-ur.xml
│ │ │ │ │ └── values-v24
│ │ │ │ │ └── values-v24.xml
│ │ │ ├── packageDebugAndroidTest
│ │ │ │ └── tmp
│ │ │ │ │ └── debugAndroidTest
│ │ │ │ │ └── zip-cache
│ │ │ │ │ └── androidResources
│ │ │ └── release
│ │ │ │ └── packageReleaseResources
│ │ │ │ └── compile-file-map.properties
│ │ ├── merged_native_libs
│ │ │ └── debug
│ │ │ │ └── mergeDebugNativeLibs
│ │ │ │ └── out
│ │ │ │ └── lib
│ │ │ │ └── arm64-v8a
│ │ │ │ └── libflacJNI.so
│ │ ├── merged_res
│ │ │ └── debugAndroidTest
│ │ │ │ └── mergeDebugAndroidTestResources
│ │ │ │ ├── values-af_values-af.arsc.flat
│ │ │ │ ├── values-hu_values-hu.arsc.flat
│ │ │ │ ├── values-ms_values-ms.arsc.flat
│ │ │ │ ├── values-or_values-or.arsc.flat
│ │ │ │ ├── values-ro_values-ro.arsc.flat
│ │ │ │ ├── values-si_values-si.arsc.flat
│ │ │ │ ├── values-sk_values-sk.arsc.flat
│ │ │ │ └── values-v24_values-v24.arsc.flat
│ │ ├── merged_res_blame_folder
│ │ │ └── debugAndroidTest
│ │ │ │ └── mergeDebugAndroidTestResources
│ │ │ │ └── out
│ │ │ │ └── multi-v2
│ │ │ │ ├── values-bn.json
│ │ │ │ ├── values-bs.json
│ │ │ │ ├── values-en-rGB.json
│ │ │ │ ├── values-iw.json
│ │ │ │ ├── values-lo.json
│ │ │ │ ├── values-lt.json
│ │ │ │ ├── values-mk.json
│ │ │ │ ├── values-pt.json
│ │ │ │ ├── values-sv.json
│ │ │ │ ├── values-th.json
│ │ │ │ ├── values-uk.json
│ │ │ │ └── values-zh-rCN.json
│ │ ├── nested_resources_validation_report
│ │ │ ├── debug
│ │ │ │ └── generateDebugResources
│ │ │ │ │ └── nestedResourcesValidationReport.txt
│ │ │ └── debugAndroidTest
│ │ │ │ └── generateDebugAndroidTestResources
│ │ │ │ └── nestedResourcesValidationReport.txt
│ │ ├── runtime_library_classes_dir
│ │ │ ├── debug
│ │ │ │ └── bundleLibRuntimeToDirDebug
│ │ │ │ │ └── androidx
│ │ │ │ │ └── media3
│ │ │ │ │ └── decoder
│ │ │ │ │ └── flac
│ │ │ │ │ ├── FlacExtractor.class
│ │ │ │ │ └── package-info.class
│ │ │ └── release
│ │ │ │ └── bundleLibRuntimeToDirRelease
│ │ │ │ └── androidx
│ │ │ │ └── media3
│ │ │ │ └── decoder
│ │ │ │ └── flac
│ │ │ │ ├── FlacBinarySearchSeeker.class
│ │ │ │ └── FlacDecoder.class
│ │ └── runtime_library_classes_jar
│ │ │ └── debug
│ │ │ └── bundleLibRuntimeToJarDebug
│ │ │ └── classes.jar
│ └── tmp
│ │ ├── compileDebugAndroidTestJavaWithJavac
│ │ └── previous-compilation-data.bin
│ │ └── compileDebugUnitTestJavaWithJavac
│ │ └── previous-compilation-data.bin
├── proguard-rules.txt
└── src
│ ├── androidTest
│ ├── AndroidManifest.xml
│ └── java
│ │ └── androidx
│ │ └── media3
│ │ └── decoder
│ │ └── flac
│ │ ├── FlacExtractorSeekTest.java
│ │ ├── FlacExtractorTest.java
│ │ └── FlacPlaybackTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── androidx
│ │ │ └── media3
│ │ │ └── decoder
│ │ │ └── flac
│ │ │ ├── FlacBinarySearchSeeker.java
│ │ │ ├── FlacDecoder.java
│ │ │ ├── FlacDecoderException.java
│ │ │ ├── FlacDecoderJni.java
│ │ │ ├── FlacExtractor.java
│ │ │ ├── FlacLibrary.java
│ │ │ ├── LibflacAudioRenderer.java
│ │ │ └── package-info.java
│ └── libs
│ │ ├── arm64-v8a
│ │ └── libflacJNI.so
│ │ └── armeabi-v7a
│ │ └── libflacJNI.so
│ └── test
│ ├── AndroidManifest.xml
│ └── java
│ └── androidx
│ └── media3
│ └── decoder
│ └── flac
│ └── DefaultRenderersFactoryTest.java
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── img
├── Screenshot_20250621_231150.png
├── Screenshot_20250621_231231.png
├── Screenshot_20250621_231303.png
└── Screenshot_20250621_231331.png
├── key.jks
└── settings.gradle
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-java@v3
15 | with:
16 | distribution: temurin
17 | java-version: 11
18 | - uses: gradle/gradle-build-action@v2
19 | with:
20 | gradle-version: current
21 | arguments: assembleRelease
22 | - uses: r0adkll/sign-android-release@v1
23 | id: sign_app
24 | with:
25 | releaseDirectory: app/build/outputs/apk/release
26 | - run: mv ${{steps.sign_app.outputs.signedReleaseFile}} LittleGooseOffice_$GITHUB_REF_NAME.apk
27 | - uses: ncipollo/release-action@v1
28 | with:
29 | artifacts: "*.apk"
30 | token: ${{ github.token }}
31 | generateReleaseNotes: true
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /decoder_flac/build/
17 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | xmlns:android
15 |
16 | ^$
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | xmlns:.*
26 |
27 | ^$
28 |
29 |
30 | BY_NAME
31 |
32 |
33 |
34 |
35 |
36 |
37 | .*:id
38 |
39 | http://schemas.android.com/apk/res/android
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | .*:name
49 |
50 | http://schemas.android.com/apk/res/android
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | name
60 |
61 | ^$
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | style
71 |
72 | ^$
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | .*
82 |
83 | ^$
84 |
85 |
86 | BY_NAME
87 |
88 |
89 |
90 |
91 |
92 |
93 | .*
94 |
95 | http://schemas.android.com/apk/res/android
96 |
97 |
98 | ANDROID_ATTRIBUTE_ORDER
99 |
100 |
101 |
102 |
103 |
104 |
105 | .*
106 |
107 | .*
108 |
109 |
110 | BY_NAME
111 |
112 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
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 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 杨为智
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 柠檬音乐【Compose版】
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## 项目简介
12 |
13 | EXOPlayer+Compose实现的音乐播放器
14 |
15 | ## 库
16 |
17 | - [Jetpack Compose](https://developer.android.com/compose): Jetpack Compose 是一个现代化的工具包,用于构建原生
18 | Android 应用的用户界面。它简化了 UI 开发,使您能够更快地构建应用。
19 | - [Media3](https://github.com/androidx/media): Media3 是一个用于播放音频和视频的库,它提供了一个用于播放媒体的
20 | API
21 | 和一组用于管理媒体会话的类。
22 | - [Amplituda](https://github.com/lincollincol/Amplituda): 一个用于分析音频的库,它提供了一种简单的方法来获取音频的振幅。
23 | - [Noise](https://github.com/paramsen/noise): 一个通用的FFT计算库,用于计算音频的频谱。
24 | - [Flac](https://xiph.org/flac/changelog.html):开源的无损格式音频编解码器, 用于解码flac格式的音频文件。
25 |
26 | 最近要开始找工作了,更新随缘🌈
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | id 'com.google.dagger.hilt.android'
5 | id 'kotlin-android'
6 | id 'kotlin-parcelize'
7 | id 'com.google.devtools.ksp'
8 | alias(libs.plugins.jetbrains.kotlin.serialization)
9 | alias(libs.plugins.compose.compiler)
10 | }
11 |
12 | android {
13 | namespace 'me.spica27.spicamusic'
14 | compileSdk 36
15 |
16 | defaultConfig {
17 | applicationId "me.spica27.spicamusic"
18 | minSdk 24
19 | targetSdk 35
20 | versionCode 2
21 | versionName "1.1.1"
22 |
23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
24 | vectorDrawables {
25 | useSupportLibrary true
26 | }
27 | }
28 |
29 | signingConfigs {
30 | signingConfig {
31 | storeFile rootProject.file("key.jks")
32 | storePassword 'SPICa27'
33 | keyAlias 'wuqi'
34 | keyPassword 'SPICa27'
35 | }
36 | }
37 |
38 | lintOptions {
39 | checkReleaseBuilds false
40 | abortOnError false
41 | }
42 |
43 | buildTypes {
44 | release {
45 | minifyEnabled false
46 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
47 | signingConfig signingConfigs.signingConfig
48 | }
49 | debug {
50 | signingConfig signingConfigs.signingConfig
51 | }
52 | }
53 | compileOptions {
54 | sourceCompatibility JavaVersion.VERSION_1_8
55 | targetCompatibility JavaVersion.VERSION_1_8
56 | }
57 | kotlinOptions {
58 | jvmTarget = '1.8'
59 | }
60 | buildFeatures {
61 | compose true
62 | }
63 | buildFeatures {
64 | // aidl=true
65 | buildConfig = true
66 | }
67 | sourceSets {
68 | main {
69 | jniLibs.srcDirs = ['libs']
70 | }
71 | }
72 | composeOptions {
73 | kotlinCompilerExtensionVersion '1.5.7'
74 | }
75 | // kapt {
76 | // correctErrorTypes = true
77 | // }
78 | packaging {
79 | resources {
80 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
81 | }
82 | }
83 | }
84 |
85 | dependencies {
86 | implementation fileTree(dir: "libs", include: ["*.jar"])
87 | // implementation project(":extension-flac2120")
88 | implementation libs.androidx.core.ktx
89 | implementation libs.androidx.lifecycle.runtime.ktx
90 | implementation libs.androidx.activity.compose
91 | implementation platform(libs.androidx.compose.bom)
92 | implementation libs.androidx.ui
93 | implementation libs.androidx.ui.graphics
94 | implementation libs.androidx.ui.tooling.preview
95 | implementation libs.androidx.material3
96 | implementation libs.androidx.constraintlayout
97 | testImplementation libs.junit
98 | androidTestImplementation libs.androidx.junit
99 | androidTestImplementation libs.androidx.espresso.core
100 | androidTestImplementation platform(libs.androidx.compose.bom)
101 | androidTestImplementation libs.androidx.ui.test.junit4
102 | debugImplementation libs.androidx.ui.tooling
103 | debugImplementation libs.androidx.ui.test.manifest
104 | implementation(libs.coil.compose)
105 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
106 |
107 | // implementation 'com.github.EspoirX:StarrySky:v2.6.9'
108 |
109 | def room_version = "2.7.2"
110 | implementation("androidx.room:room-runtime:$room_version")
111 | ksp("androidx.room:room-compiler:$room_version")
112 | implementation("androidx.room:room-ktx:$room_version")
113 |
114 | /** hilt **/
115 | implementation libs.hilt.android
116 | ksp libs.hilt.compiler
117 | implementation libs.androidx.hilt.navigation.compose
118 |
119 | implementation libs.androidx.datastore.preferences
120 |
121 | implementation libs.timber
122 | implementation libs.noise
123 |
124 | def accompanistVersion = "0.36.0"
125 | implementation(libs.accompanist.permissions)
126 |
127 | implementation project(":decoder_flac")
128 |
129 | implementation libs.kotlinx.serialization.core
130 | implementation libs.androidx.adaptive
131 | implementation libs.androidx.adaptive.layout
132 | implementation libs.androidx.adaptive.navigation
133 | implementation libs.androidx.lifecycle.viewmodel.navigation3
134 | implementation libs.androidx.navigation3.runtime
135 | implementation libs.androidx.navigation3.ui
136 |
137 |
138 | // 播放器
139 | def media3_version = "1.4.1"
140 | // For media playback using ExoPlayer
141 | implementation libs.androidx.media3.exoplayer
142 | // implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
143 | implementation libs.androidx.media
144 | implementation 'com.github.lincollincol:amplituda:2.2.2'
145 | // DSP 处理
146 | }
--------------------------------------------------------------------------------
/app/libs/arm64-v8a/libliquidfun.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/libs/arm64-v8a/libliquidfun.so
--------------------------------------------------------------------------------
/app/libs/arm64-v8a/libliquidfun_jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/libs/arm64-v8a/libliquidfun_jni.so
--------------------------------------------------------------------------------
/app/libs/libliquidfun.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/libs/libliquidfun.jar
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/me/spica27/spicamusic/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("me.spica27.spicamusic", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
65 |
66 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/App.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 | import timber.log.Timber
6 |
7 | @HiltAndroidApp
8 | class App : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 | instance = this
12 | Timber.plant(Timber.DebugTree())
13 | // SingletonImageLoader.setSafe(factory = {
14 | // ImageLoader.Builder(this)
15 | // .crossfade(true)
16 | // .serviceLoaderEnabled(true)
17 | // .logger(DebugLogger(Logger.Level.Error))
18 | // .components {
19 | //
20 | // }
21 | // .build()
22 | // })
23 | }
24 |
25 | companion object {
26 | private lateinit var instance: App
27 |
28 | fun getInstance(): App {
29 | return instance
30 | }
31 | }
32 | init {
33 | System.loadLibrary("liquidfun")
34 | System.loadLibrary("liquidfun_jni")
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic
2 |
3 | import android.content.Intent
4 | import android.graphics.Color
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.SystemBarStyle
8 | import androidx.activity.compose.setContent
9 | import androidx.activity.enableEdgeToEdge
10 | import androidx.lifecycle.lifecycleScope
11 | import dagger.hilt.android.AndroidEntryPoint
12 | import kotlinx.coroutines.flow.collectLatest
13 | import kotlinx.coroutines.launch
14 | import linc.com.amplituda.Amplituda
15 | import me.spica27.spicamusic.service.MusicService
16 | import me.spica27.spicamusic.ui.AppMain
17 | import me.spica27.spicamusic.utils.DataStoreUtil
18 | import javax.inject.Inject
19 |
20 | @AndroidEntryPoint
21 | class MainActivity : ComponentActivity() {
22 |
23 |
24 |
25 |
26 | @Inject
27 | lateinit var dataStoreUtil: DataStoreUtil
28 |
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | startService(Intent(this, MusicService::class.java))
33 | setContent {
34 | AppMain()
35 | }
36 | lifecycleScope.launch {
37 | dataStoreUtil.getForceDarkTheme.collectLatest {
38 | if (it) {
39 | enableEdgeToEdge(
40 | statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
41 | navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)
42 | )
43 | } else {
44 | enableEdgeToEdge(
45 | statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT,Color.TRANSPARENT),
46 | navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT,Color.TRANSPARENT)
47 | )
48 | }
49 | }
50 | }
51 | }
52 |
53 |
54 | @Inject
55 | lateinit var amplituda: Amplituda
56 |
57 | override fun onDestroy() {
58 | super.onDestroy()
59 | amplituda.clearCache()
60 | }
61 |
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/db/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import me.spica27.spicamusic.db.dao.PlaylistDao
6 | import me.spica27.spicamusic.db.dao.SongDao
7 | import me.spica27.spicamusic.db.entity.Playlist
8 | import me.spica27.spicamusic.db.entity.PlaylistSongCrossRef
9 | import me.spica27.spicamusic.db.entity.Song
10 |
11 |
12 | /**
13 | * 数据库
14 | */
15 | @Database(
16 | entities = [Song::class, Playlist::class, PlaylistSongCrossRef::class],
17 | version = 6,
18 | exportSchema = false
19 | )
20 | abstract class AppDatabase : RoomDatabase() {
21 |
22 | abstract fun songDao(): SongDao
23 |
24 | abstract fun playlistDao(): PlaylistDao
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/db/dao/PlaylistDao.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Transaction
8 | import kotlinx.coroutines.flow.Flow
9 | import me.spica27.spicamusic.db.entity.Playlist
10 | import me.spica27.spicamusic.db.entity.PlaylistSongCrossRef
11 | import me.spica27.spicamusic.db.entity.PlaylistWithSongs
12 | import me.spica27.spicamusic.db.entity.Song
13 |
14 |
15 | @Dao
16 | interface PlaylistDao {
17 |
18 |
19 | @Transaction
20 | @Query("SELECT * FROM Playlist")
21 | fun getPlaylistsWithSongs(): List
22 |
23 | @Transaction
24 | @Query("SELECT * FROM Playlist")
25 | fun getAllPlaylist(): Flow>
26 |
27 |
28 | @Query("SELECT * FROM Playlist WHERE playlistId == :playlistId")
29 | fun getPlayListByIdFlow(playlistId: Long): Flow
30 |
31 | @Query("SELECT * FROM Playlist WHERE playlistId == :playlistId")
32 | fun getPlayListById(playlistId: Long): Playlist
33 |
34 | @Query("update Playlist set playlistName = :newName where playlistId = :playlistId")
35 | suspend fun renamePlaylist(playlistId: Long, newName: String)
36 |
37 |
38 | @Transaction
39 | @Query("SELECT * FROM Playlist WHERE playlistId == :playlistId")
40 | fun getPlaylistsWithSongsWithPlayListId(playlistId: Long): PlaylistWithSongs?
41 |
42 |
43 | @Transaction
44 | @Query("SELECT * FROM Playlist WHERE playlistId == :playlistId")
45 | fun getPlaylistsWithSongsWithPlayListIdFlow(playlistId: Long): Flow
46 |
47 |
48 | @Query("SELECT * FROM Song WHERE songId IN (SELECT songId FROM PlaylistSongCrossRef WHERE playlistId == :playlistId)")
49 | fun getSongsByPlaylistIdFlow(playlistId: Long): Flow>
50 |
51 | @Query("SELECT * FROM Song WHERE songId IN (SELECT songId FROM PlaylistSongCrossRef WHERE playlistId == :playlistId)")
52 | fun getSongsByPlaylistId(playlistId: Long): List
53 |
54 | @Transaction
55 | @Query("DELETE FROM playlist WHERE playlistId ==:playlistId")
56 | fun deleteList(playlistId: Long)
57 |
58 | @Transaction
59 | @Insert
60 | suspend fun insertListItems(songs: List)
61 |
62 | @Transaction
63 | @Insert
64 | suspend fun insertListItem(songs: PlaylistSongCrossRef)
65 |
66 | @Transaction
67 | @Insert
68 | suspend fun insertPlaylist(list: Playlist)
69 |
70 | @Query("DELETE FROM playlist WHERE playlistId == :playlistId")
71 | suspend fun deleteById(playlistId: Long)
72 |
73 | @Query("UPDATE playlist SET playlistName = :newName WHERE playlistId == :playlistId")
74 | suspend fun saveNewNameById(playlistId: Long, newName: String)
75 |
76 | @Transaction
77 | @Delete
78 | suspend fun deleteListItem(song: PlaylistSongCrossRef)
79 |
80 |
81 | @Transaction
82 | @Delete
83 | suspend fun deleteListItems(songs: List)
84 |
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/db/dao/SongDao.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import androidx.room.Transaction
9 | import kotlinx.coroutines.flow.Flow
10 | import me.spica27.spicamusic.db.entity.Song
11 |
12 |
13 | @Dao
14 | interface SongDao {
15 |
16 | @Insert(onConflict = OnConflictStrategy.REPLACE)
17 | suspend fun insert(song: Song)
18 |
19 |
20 | // 切换是否收藏
21 | @Query("UPDATE song SET `like` = (CASE WHEN`LIKE` == 1 THEN 0 ELSE 1 END) WHERE( songId == :id)")
22 | suspend fun toggleLike(id: Long)
23 |
24 | @Insert(onConflict = OnConflictStrategy.IGNORE)
25 | suspend fun insert(songs: List)
26 |
27 | @Insert(onConflict = OnConflictStrategy.IGNORE)
28 | fun insertSync(songs: List)
29 |
30 | @Query("UPDATE song SET playTimes = (playTimes + 1) WHERE songId == :id")
31 | suspend fun addPlayTime(id: Long)
32 |
33 |
34 | @Query("DELETE FROM song WHERE mediaStoreId NOT IN (:mediaIds)")
35 | suspend fun deleteSongsNotInList(mediaIds: List)
36 |
37 | @Query(
38 | "SELECT * FROM song WHERE displayName LIKE '%' ||:name|| '%'" +
39 | "OR artist LIKE '%' ||:name|| '%'"
40 | )
41 | fun getsSongsFromName(name: String): Flow>
42 |
43 |
44 | @Query(
45 | "SELECT * FROM song WHERE displayName LIKE '%' ||:name|| '%'" +
46 | "OR artist LIKE '%' ||:name|| '%'"
47 | )
48 | fun getsSongsFromNameSync(name: String): List
49 |
50 | @Query("SELECT * FROM song WHERE songId == :id")
51 | fun getSongWithId(id: Long): Song
52 |
53 | @Query("SELECT * FROM song WHERE mediaStoreId == :id")
54 | fun getSongWithMediaStoreId(id: Long): Song?
55 |
56 | @Query("SELECT * FROM song WHERE songId == :id")
57 | fun getSongFlowWithId(id: Long): Flow
58 |
59 | @Query("SELECT `like` FROM song WHERE songId == :id")
60 | fun getSongIsLikeFlowWithId(id: Long): Flow
61 |
62 | @Query("SELECT * FROM song")
63 | fun getAll(): Flow>
64 |
65 | @Query("SELECT * FROM song")
66 | fun getAllSync(): List
67 |
68 | @Query("SELECT * FROM song WHERE `like` == 1")
69 | fun getAllLikeSong(): Flow>
70 |
71 |
72 | @Query("DELETE FROM song")
73 | suspend fun deleteAll()
74 |
75 | @Query("DELETE FROM song")
76 | fun deleteAllSync()
77 |
78 |
79 | @Query("SELECT * FROM song WHERE songId NOT IN (SELECT songId FROM playlistsongcrossref WHERE playlistId = :playlistId)")
80 | fun getSongsNotInPlayList(playlistId: Long): Flow>
81 |
82 | @Delete
83 | suspend fun delete(song: Song)
84 |
85 | @Delete
86 | suspend fun delete(song: List)
87 |
88 | @Transaction
89 | suspend fun updateSongs(songs: List) {
90 | val songIds = songs.map { it.mediaStoreId }
91 | deleteSongsNotInList(songIds)
92 | insert(songs)
93 | }
94 |
95 |
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/db/entity/Playlist.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.db.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import java.util.*
6 |
7 | @Entity
8 | data class Playlist(
9 | @PrimaryKey(autoGenerate = true)
10 | var playlistId: Long? = null,
11 | var playlistName: String = "自定义播放列表${Date().time}",
12 | var cover: String? = null
13 | ) {
14 |
15 | // @Ignore
16 | // constructor():this(0,"",null)
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/db/entity/PlaylistSongCrossRef.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.db.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Embedded
5 | import androidx.room.Entity
6 | import androidx.room.Junction
7 | import androidx.room.Relation
8 |
9 |
10 | @Entity(primaryKeys = ["playlistId", "songId"])
11 | data class PlaylistSongCrossRef(
12 | val playlistId: Long,
13 | @ColumnInfo(index = true)
14 | val songId: Long
15 | )
16 |
17 |
18 | data class PlaylistWithSongs(
19 | @Embedded val playlist: Playlist,
20 | @Relation(
21 | parentColumn = "playlistId",
22 | entityColumn = "songId",
23 | associateBy = Junction(PlaylistSongCrossRef::class)
24 | )
25 | val songs: List
26 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/db/entity/Song.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.db.entity
2 |
3 | import android.net.Uri
4 | import android.os.Parcelable
5 | import androidx.room.ColumnInfo
6 | import androidx.room.Entity
7 | import androidx.room.Index
8 | import androidx.room.PrimaryKey
9 | import me.spica27.spicamusic.utils.toAudioUri
10 | import me.spica27.spicamusic.utils.toCoverUri
11 |
12 | @kotlinx.parcelize.Parcelize
13 | @Entity(
14 | indices = [
15 | Index("displayName"),
16 | Index("mediaStoreId", unique = true),
17 | ]
18 | )
19 | data class Song constructor(
20 | @ColumnInfo(index = true)
21 | @PrimaryKey(autoGenerate = true)
22 | var songId: Long? = null,
23 | var mediaStoreId: Long,
24 | var path: String,
25 | var displayName: String,
26 | var artist: String,
27 | var size: Long,
28 | var like: Boolean,
29 | val duration: Long,
30 | var sort: Int,
31 | var playTimes: Int,
32 | var lastPlayTime: Int,
33 | var mimeType: String,
34 | var albumId: Long,
35 | var sampleRate: Int, // 采样率
36 | var bitRate: Int, // 比特率
37 | var channels: Int, // 声道数
38 | var digit: Int // 位深度
39 | ) : Parcelable {
40 |
41 | fun getFormatMimeType(): String {
42 | if (mimeType.contains("audio/")) {
43 | return mimeType.replace("audio/", "")
44 | }
45 | return mimeType
46 | }
47 |
48 | fun getCoverUri(): Uri {
49 | return albumId.toCoverUri()
50 | }
51 |
52 | fun getSongUri(): Uri {
53 | return mediaStoreId.toAudioUri()
54 | }
55 |
56 | override fun equals(other: Any?): Boolean {
57 | if (this === other) return true
58 | if (javaClass != other?.javaClass) return false
59 |
60 | other as Song
61 |
62 | if (songId != other.songId) return false
63 | if (mediaStoreId != other.mediaStoreId) return false
64 | if (path != other.path) return false
65 | if (displayName != other.displayName) return false
66 | if (artist != other.artist) return false
67 | if (size != other.size) return false
68 | if (like != other.like) return false
69 | if (duration != other.duration) return false
70 | if (sort != other.sort) return false
71 | if (playTimes != other.playTimes) return false
72 | if (lastPlayTime != other.lastPlayTime) return false
73 |
74 | return true
75 | }
76 |
77 | override fun hashCode(): Int {
78 | var result = songId?.hashCode() ?: 0
79 | result = 31 * result + mediaStoreId.hashCode()
80 | result = 31 * result + path.hashCode()
81 | result = 31 * result + displayName.hashCode()
82 | result = 31 * result + artist.hashCode()
83 | result = 31 * result + size.hashCode()
84 | result = 31 * result + like.hashCode()
85 | result = 31 * result + duration.hashCode()
86 | result = 31 * result + sort
87 | result = 31 * result + playTimes
88 | result = 31 * result + lastPlayTime
89 | return result
90 | }
91 |
92 | override fun toString(): String {
93 | return "Song(songId=$songId, mediaStoreId=$mediaStoreId, path='$path', displayName='$displayName', artist='$artist', size=$size, like=$like, duration=$duration, sort=$sort, playTimes=$playTimes, lastPlayTime=$lastPlayTime)"
94 | }
95 |
96 |
97 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/BandProcessor.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 |
4 | import timber.log.Timber
5 | import kotlin.math.PI
6 | import kotlin.math.abs
7 | import kotlin.math.cos
8 | import kotlin.math.pow
9 | import kotlin.math.sqrt
10 | import kotlin.math.tan
11 |
12 | class BandProcessor(val band: NyquistBand, val sampleRate: Int, val channelCount: Int, val referenceGain: Double) {
13 |
14 | private val G0 = referenceGain.fromDb()
15 | private val GB = band.bandwidthGain.fromDb()
16 | private val G1 = band.gain.fromDb()
17 |
18 | private val xHist = Array(channelCount) { FloatArray(2) { 0f } }
19 | private val yHist = Array(channelCount) { FloatArray(2) { 0f } }
20 |
21 | private val beta = tan((band.bandwidth / 2.0) * PI / (sampleRate / 2.0)) * (sqrt(abs((GB.pow(2)) - (G0.pow(2)))) / sqrt(abs((G1.pow(2)) - (GB.pow(2)))))
22 |
23 | private val a1 = -2.0 * cos(band.centerFrequency * PI / (sampleRate / 2.0)) / (1.0 + beta)
24 | private val a2 = (1.0 - beta) / (1.0 + beta)
25 |
26 | private val b0 = (G0 + (G1 * beta)) / (1.0 + beta)
27 | private val b1 = -2.0 * G0 * cos(band.centerFrequency * PI / (sampleRate / 2.0)) / (1.0 + beta)
28 | private val b2 = (G0 - (G1 * beta)) / (1.0 + beta)
29 |
30 | init {
31 | if (band.gain > 0) {
32 | // Boost
33 | if (band.bandwidthGain < referenceGain || band.gain < band.bandwidthGain) {
34 | throw IllegalArgumentException("Invalid parameters. Boost gain ($band.gain) must be greater than bandwidth gain ($band.bandwidthGain), which must be greater than reference ($referenceGain)")
35 | }
36 | } else if (band.gain < 0) {
37 | // Cut
38 | if (band.bandwidthGain > referenceGain || band.gain > band.bandwidthGain) {
39 | throw IllegalArgumentException("Invalid parameters. Cut gain ($band.gain) must be less than bandwidth gain ($band.bandwidthGain), which must be less than reference ($referenceGain)")
40 | }
41 | }
42 | }
43 |
44 | fun processSample(sample: Float, channelIndex: Int): Float {
45 |
46 | if (band.bandwidthGain == 0.0 && band.gain == 0.0) {
47 | return sample
48 | }
49 |
50 | if (channelIndex >= channelCount) {
51 | Timber.v("Invalid channel index")
52 | return sample
53 | }
54 |
55 | val adjustedSample = (
56 | (b0 * sample) +
57 | (b1 * xHist[channelIndex][0]) +
58 | (b2 * xHist[channelIndex][1]) -
59 | (a1 * yHist[channelIndex][0]) -
60 | (a2 * yHist[channelIndex][1])
61 | ).toFloat()
62 |
63 | xHist[channelIndex][1] = xHist[channelIndex][0]
64 | xHist[channelIndex][0] = sample
65 |
66 | yHist[channelIndex][1] = yHist[channelIndex][0]
67 | yHist[channelIndex][0] = adjustedSample
68 |
69 | return adjustedSample
70 | }
71 |
72 | fun reset() {
73 | for (i in 0 until channelCount) {
74 | xHist[i] = FloatArray(2) { 0f }
75 | yHist[i] = FloatArray(2) { 0f }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/ByteUtils.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 | import java.nio.ByteBuffer
4 |
5 | object ByteUtils {
6 |
7 | fun ByteBuffer.getInt24(): Int {
8 | val sample = this.getInt(position() + 2) shl 16 or (this.getInt(position() + 1) and 0xFF shl 8) or (this.getInt(position()) and 0xFF)
9 | position(position() + 3)
10 | return sample
11 | }
12 |
13 | fun ByteBuffer.putInt24(sample: Int): ByteBuffer {
14 | putInt(sample and 0xFF)
15 | putInt(sample ushr 8 and 0xFF)
16 | putInt(sample shr 16)
17 | return this
18 | }
19 |
20 | const val Int24_MIN_VALUE = -8388608
21 | const val Int24_MAX_VALUE = 8388607
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/Equalizer.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 | import androidx.annotation.StringRes
4 | import me.spica27.spicamusic.R
5 |
6 | object Equalizer {
7 |
8 | val centerFrequency = intArrayOf(
9 | 32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000
10 | )
11 |
12 | object Presets {
13 |
14 | sealed class Preset(
15 | val name: String,
16 | @StringRes val nameResId: Int,
17 | val bands: List
18 | ) {
19 |
20 | object Flat : Preset(
21 | "Flat",
22 | R.string.eq_preset_flat,
23 | centerFrequency.map {
24 | EqualizerBand(it, 0.0).toNyquistBand()
25 | }
26 | )
27 |
28 | object Custom : Preset(
29 | "Custom",
30 | R.string.eq_preset_custom,
31 | listOf(
32 | EqualizerBand(32, 3.0).toNyquistBand(),
33 | EqualizerBand(63, 5.0).toNyquistBand(),
34 | EqualizerBand(125, 4.0).toNyquistBand(),
35 | EqualizerBand(250, -4.5).toNyquistBand(),
36 | EqualizerBand(500, -6.0).toNyquistBand(),
37 | EqualizerBand(1000, -7.0).toNyquistBand(),
38 | EqualizerBand(2000, -4.0).toNyquistBand(),
39 | EqualizerBand(4000, -3.0).toNyquistBand(),
40 | EqualizerBand(8000, 0.5).toNyquistBand(),
41 | EqualizerBand(16000, 1.0).toNyquistBand()
42 | )
43 | )
44 |
45 | object BassBoost : Preset(
46 | "Bass Boost",
47 | R.string.eq_preset_bass_boost,
48 | listOf(
49 | EqualizerBand(32, 6.0).toNyquistBand(),
50 | EqualizerBand(63, 5.0).toNyquistBand(),
51 | EqualizerBand(125, 4.0).toNyquistBand(),
52 | EqualizerBand(250, 3.0).toNyquistBand(),
53 | EqualizerBand(500, 2.0).toNyquistBand(),
54 | EqualizerBand(1000, 0.0).toNyquistBand(),
55 | EqualizerBand(2000, 0.0).toNyquistBand(),
56 | EqualizerBand(4000, 0.0).toNyquistBand(),
57 | EqualizerBand(8000, 0.0).toNyquistBand(),
58 | EqualizerBand(16000, 0.0).toNyquistBand()
59 | )
60 | )
61 |
62 | object BassReducer : Preset(
63 | "Bass Reduction",
64 | R.string.eq_preset_bass_reduce,
65 | listOf(
66 | EqualizerBand(32, -6.0).toNyquistBand(),
67 | EqualizerBand(63, -5.0).toNyquistBand(),
68 | EqualizerBand(125, -4.0).toNyquistBand(),
69 | EqualizerBand(250, -3.0).toNyquistBand(),
70 | EqualizerBand(500, -2.0).toNyquistBand(),
71 | EqualizerBand(1000, 0.0).toNyquistBand(),
72 | EqualizerBand(2000, 0.0).toNyquistBand(),
73 | EqualizerBand(4000, 0.0).toNyquistBand(),
74 | EqualizerBand(8000, 0.0).toNyquistBand(),
75 | EqualizerBand(16000, 0.0).toNyquistBand()
76 | )
77 | )
78 |
79 | object VocalBoost : Preset(
80 | "Vocal Boost",
81 | R.string.eq_preset_vocal_boost,
82 | listOf(
83 | EqualizerBand(32, -2.0).toNyquistBand(),
84 | EqualizerBand(63, -3.0).toNyquistBand(),
85 | EqualizerBand(125, -3.0).toNyquistBand(),
86 | EqualizerBand(250, 2.0).toNyquistBand(),
87 | EqualizerBand(500, 5.0).toNyquistBand(),
88 | EqualizerBand(1000, 5.0).toNyquistBand(),
89 | EqualizerBand(2000, 4.0).toNyquistBand(),
90 | EqualizerBand(4000, 3.0).toNyquistBand(),
91 | EqualizerBand(8000, 0.0).toNyquistBand(),
92 | EqualizerBand(16000, -2.0).toNyquistBand()
93 | )
94 | )
95 |
96 | object VocalReducer : Preset(
97 | "Vocal Reduction",
98 | R.string.eq_preset_vocal_Reduce,
99 | listOf(
100 | EqualizerBand(32, 2.0).toNyquistBand(),
101 | EqualizerBand(63, 3.0).toNyquistBand(),
102 | EqualizerBand(125, 3.0).toNyquistBand(),
103 | EqualizerBand(250, -2.0).toNyquistBand(),
104 | EqualizerBand(500, -5.0).toNyquistBand(),
105 | EqualizerBand(1000, -5.0).toNyquistBand(),
106 | EqualizerBand(2000, -4.0).toNyquistBand(),
107 | EqualizerBand(4000, -3.0).toNyquistBand(),
108 | EqualizerBand(8000, -0.0).toNyquistBand(),
109 | EqualizerBand(16000, 2.0).toNyquistBand()
110 | )
111 | )
112 |
113 | override fun equals(other: Any?): Boolean {
114 | if (this === other) return true
115 | if (javaClass != other?.javaClass) return false
116 |
117 | other as Preset
118 |
119 | if (name != other.name) return false
120 |
121 | return true
122 | }
123 |
124 | override fun hashCode(): Int {
125 | return name.hashCode()
126 | }
127 | }
128 |
129 | val flat = Preset.Flat
130 | val custom = Preset.Custom
131 | val bassBoost = Preset.BassBoost
132 | val bassReducer = Preset.BassReducer
133 | val vocalBoost = Preset.VocalBoost
134 | val vocalReducer = Preset.VocalReducer
135 |
136 | val all = listOf(custom, flat, bassBoost, bassReducer, vocalBoost, vocalReducer)
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/EqualizerAudioProcessor.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 |
4 | import androidx.annotation.OptIn
5 | import androidx.media3.common.C
6 | import androidx.media3.common.audio.AudioProcessor
7 | import androidx.media3.common.audio.BaseAudioProcessor
8 | import androidx.media3.common.util.UnstableApi
9 | import me.spica27.spicamusic.dsp.ByteUtils.getInt24
10 | import me.spica27.spicamusic.dsp.ByteUtils.putInt24
11 | import timber.log.Timber
12 | import java.lang.Math.clamp
13 | import java.nio.ByteBuffer
14 |
15 | @OptIn(UnstableApi::class)
16 | class EqualizerAudioProcessor : BaseAudioProcessor() {
17 |
18 | private var bandProcessors = emptyList()
19 |
20 |
21 | // Maximum allowed gain/cut for each band
22 | val maxBandGain = 12
23 |
24 | val bands: ArrayList = ArrayList()
25 |
26 |
27 | /**
28 | * 设置曲线数据
29 | */
30 | fun setBands(bands: List) {
31 | this.bands.clear()
32 | this.bands.addAll(bands)
33 | updateBandProcessors()
34 | }
35 |
36 |
37 | private fun updateBandProcessors() {
38 | if (outputAudioFormat.channelCount <= 0) {
39 | return
40 | }
41 |
42 | bandProcessors = bands.map { band ->
43 | BandProcessor(
44 | band.toNyquistBand(),
45 | sampleRate = outputAudioFormat.sampleRate,
46 | channelCount = outputAudioFormat.channelCount,
47 | referenceGain = 0.0
48 | )
49 | }.toList()
50 | }
51 |
52 | override fun onConfigure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
53 | super.onConfigure(inputAudioFormat)
54 |
55 | if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT && inputAudioFormat.encoding != C.ENCODING_PCM_24BIT) {
56 | throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
57 | }
58 |
59 | updateBandProcessors()
60 |
61 | return inputAudioFormat
62 | }
63 |
64 | override fun onFlush() {
65 | super.onFlush()
66 |
67 | Timber.v("onFlush() called")
68 | updateBandProcessors()
69 | }
70 |
71 | override fun queueInput(inputBuffer: ByteBuffer) {
72 | if (bands.isNotEmpty()) {
73 | val size = inputBuffer.remaining()
74 | val buffer = replaceOutputBuffer(size)
75 |
76 | when (outputAudioFormat.encoding) {
77 | C.ENCODING_PCM_16BIT -> {
78 | while (inputBuffer.hasRemaining()) {
79 | for (channelIndex in 0 until outputAudioFormat.channelCount) {
80 | val sample = inputBuffer.short
81 | var targetSample = sample.toFloat()
82 | for (band in bandProcessors) {
83 | targetSample = band.processSample(targetSample, channelIndex)
84 | }
85 | buffer.putShort(
86 | clamp(
87 | targetSample,
88 | Short.MIN_VALUE.toFloat(),
89 | Short.MAX_VALUE.toFloat()
90 | ).toInt().toShort()
91 | )
92 | if (!inputBuffer.hasRemaining()) {
93 | break
94 | }
95 | }
96 | }
97 | }
98 |
99 | C.ENCODING_PCM_24BIT -> {
100 | while (inputBuffer.hasRemaining()) {
101 | for (channelIndex in 0 until outputAudioFormat.channelCount) {
102 | val sample = inputBuffer.getInt24()
103 | var targetSample = sample.toFloat()
104 | for (band in bandProcessors) {
105 | targetSample = band.processSample(targetSample, channelIndex)
106 | }
107 | buffer.putInt24(
108 | clamp(
109 | targetSample,
110 | ByteUtils.Int24_MIN_VALUE.toFloat(),
111 | ByteUtils.Int24_MAX_VALUE.toFloat()
112 | ).toInt()
113 | )
114 | if (!inputBuffer.hasRemaining()) {
115 | break
116 | }
117 | }
118 | }
119 | }
120 |
121 | else -> {
122 | // No op
123 | }
124 | }
125 | inputBuffer.position(inputBuffer.limit())
126 | buffer.flip()
127 | } else {
128 | val remaining = inputBuffer.remaining()
129 | if (remaining == 0) {
130 | return
131 | }
132 | replaceOutputBuffer(remaining).put(inputBuffer).flip()
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/EqualizerBand.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 |
4 | import java.io.Serializable
5 | import kotlin.math.pow
6 | import kotlin.math.sqrt
7 |
8 |
9 | open class EqualizerBand(val centerFrequency: Int, var gain: Double) : Serializable {
10 |
11 | override fun equals(other: Any?): Boolean {
12 | if (this === other) return true
13 | if (javaClass != other?.javaClass) return false
14 |
15 | other as EqualizerBand
16 |
17 | if (centerFrequency != other.centerFrequency) return false
18 | if (gain != other.gain) return false
19 |
20 | return true
21 | }
22 |
23 | override fun hashCode(): Int {
24 | var result = centerFrequency
25 | result = 31 * result + gain.hashCode()
26 | return result
27 | }
28 | }
29 |
30 | fun EqualizerBand.toNyquistBand(): NyquistBand {
31 |
32 | val bandWidthGain = if (gain > 0) {
33 | sqrt((gain.pow(2) / 2)) // Boost
34 | } else {
35 | -sqrt((gain.pow(2) / 2)) // Cut
36 | }
37 |
38 | return NyquistBand(centerFrequency, (centerFrequency * 0.35f).toInt(), gain, bandWidthGain)
39 | }
40 |
41 | class NyquistBand(centerFrequency: Int, val bandwidth: Int, peakGain: Double, val bandwidthGain: Double) :
42 | EqualizerBand(centerFrequency, peakGain)
43 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/HighPassFilter.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 | import kotlin.math.PI
4 | import kotlin.math.cos
5 | import kotlin.math.sin
6 |
7 | class HighPassFilter(
8 | val freq: Int,
9 | val sampleRate: Int,
10 | val channelCount: Int
11 | ) {
12 |
13 | private val q = 1.0
14 | private val omega = 2.0 * PI * freq / sampleRate
15 | private val sin = sin(omega)
16 | private val cos = cos(omega)
17 | private val alpha = sin / (2.0 * q)
18 |
19 | private val a0 = 1.0 + alpha
20 | private val a1 = -2.0 * cos
21 | private val a2 = 1.0 - alpha
22 |
23 | private val b0 = (1.0 + cos) / 2.0
24 | private val b1 = -(1.0 + cos)
25 | private val b2 = (1.0 + cos) / 2.0
26 |
27 | private val xHist = Array(channelCount) { FloatArray(2) { 0f } }
28 | private val yHist = Array(channelCount) { FloatArray(2) { 0f } }
29 |
30 | fun processSample(sample: Float, channelIndex: Int): Float {
31 |
32 | val adjustedSample = (
33 | ((b0 / a0) * sample) +
34 | ((b1 / a0) * xHist[channelIndex][0]) +
35 | ((b2 / a0) * xHist[channelIndex][1]) -
36 | ((a1 / a0) * yHist[channelIndex][0]) -
37 | ((a2 / a0) * yHist[channelIndex][1])
38 | ).toFloat()
39 |
40 | xHist[channelIndex][1] = xHist[channelIndex][0]
41 | xHist[channelIndex][0] = sample
42 |
43 | yHist[channelIndex][1] = yHist[channelIndex][0]
44 | yHist[channelIndex][0] = adjustedSample
45 |
46 | return adjustedSample
47 | }
48 |
49 | fun reset() {
50 | for (i in 0 until channelCount) {
51 | xHist[i] = FloatArray(2) { 0f }
52 | yHist[i] = FloatArray(2) { 0f }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/LowPassFilter.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 | import kotlin.math.PI
4 | import kotlin.math.cos
5 | import kotlin.math.sin
6 |
7 | class LowPassFilter(
8 | val freq: Int,
9 | val sampleRate: Int,
10 | val channelCount: Int
11 | ) {
12 |
13 | private val q = 1.0
14 | private val omega = 2.0 * PI * freq / sampleRate
15 | private val sin = sin(omega)
16 | private val cos = cos(omega)
17 | private val alpha = sin / (2.0 * q)
18 |
19 | private val a0 = 1.0 + alpha
20 | private val a1 = -2.0 * cos
21 | private val a2 = 1.0 - alpha
22 |
23 | private val b0 = (1.0 - cos) / 2.0
24 | private val b1 = 1.0 - cos
25 | private val b2 = (1.0 - cos) / 2.0
26 |
27 | private val xHist = Array(channelCount) { FloatArray(2) { 0f } }
28 | private val yHist = Array(channelCount) { FloatArray(2) { 0f } }
29 |
30 | fun processSample(sample: Float, channelIndex: Int): Float {
31 |
32 | val adjustedSample = (
33 | ((b0 / a0) * sample) +
34 | ((b1 / a0) * xHist[channelIndex][0]) +
35 | ((b2 / a0) * xHist[channelIndex][1]) -
36 | ((a1 / a0) * yHist[channelIndex][0]) -
37 | ((a2 / a0) * yHist[channelIndex][1])
38 | ).toFloat()
39 |
40 | xHist[channelIndex][1] = xHist[channelIndex][0]
41 | xHist[channelIndex][0] = sample
42 |
43 | yHist[channelIndex][1] = yHist[channelIndex][0]
44 | yHist[channelIndex][0] = adjustedSample
45 |
46 | return adjustedSample
47 | }
48 |
49 | fun reset() {
50 | for (i in 0 until channelCount) {
51 | xHist[i] = FloatArray(2) { 0f }
52 | yHist[i] = FloatArray(2) { 0f }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/ReplayGainAudioProcessor.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 | import androidx.annotation.OptIn
4 | import androidx.core.math.MathUtils
5 | import androidx.media3.common.C
6 | import androidx.media3.common.audio.AudioProcessor
7 | import androidx.media3.common.audio.BaseAudioProcessor
8 | import androidx.media3.common.util.UnstableApi
9 | import me.spica27.spicamusic.dsp.ByteUtils.Int24_MAX_VALUE
10 | import me.spica27.spicamusic.dsp.ByteUtils.Int24_MIN_VALUE
11 | import me.spica27.spicamusic.dsp.ByteUtils.getInt24
12 | import me.spica27.spicamusic.dsp.ByteUtils.putInt24
13 | import java.nio.ByteBuffer
14 |
15 | @OptIn(UnstableApi::class)
16 | class ReplayGainAudioProcessor(var preAmpGain: Double = 0.0) : BaseAudioProcessor() {
17 |
18 |
19 | // 歌曲的增益
20 | private var trackGain: Double? = null
21 |
22 |
23 | private val gain: Double
24 | get() = preAmpGain + (trackGain ?: 0.0)
25 |
26 | override fun onConfigure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
27 | if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT &&
28 | inputAudioFormat.encoding != C.ENCODING_PCM_24BIT
29 | ) {
30 | throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
31 | }
32 | return inputAudioFormat
33 | }
34 |
35 | override fun queueInput(inputBuffer: ByteBuffer) {
36 |
37 |
38 | if (gain != 0.0) {
39 | val size = inputBuffer.remaining()
40 | val buffer = replaceOutputBuffer(size)
41 | val delta = gain.fromDb()
42 | when (outputAudioFormat.encoding) {
43 | C.ENCODING_PCM_16BIT -> {
44 | while (inputBuffer.hasRemaining()) {
45 | val sample = inputBuffer.short
46 | val targetSample = MathUtils.clamp((sample * delta), Short.MIN_VALUE.toDouble(), Short.MAX_VALUE.toDouble()).toInt().toShort()
47 | buffer.putShort(targetSample)
48 | }
49 | }
50 |
51 | C.ENCODING_PCM_24BIT -> {
52 | while (inputBuffer.hasRemaining()) {
53 | val sample = inputBuffer.getInt24()
54 | val targetSample = MathUtils.clamp(sample * delta, Int24_MIN_VALUE.toDouble(), Int24_MAX_VALUE.toDouble()).toInt()
55 | buffer.putInt24(targetSample)
56 | }
57 | }
58 |
59 | else -> {
60 | // No op
61 | }
62 | }
63 | inputBuffer.position(inputBuffer.limit())
64 | buffer.flip()
65 | } else {
66 | val remaining = inputBuffer.remaining()
67 | if (remaining == 0) {
68 | return
69 | }
70 | replaceOutputBuffer(remaining).put(inputBuffer).flip()
71 | }
72 | }
73 |
74 | companion object {
75 | const val maxPreAmpGain = 12
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/dsp/Utils.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.dsp
2 |
3 | import kotlin.math.log10
4 | import kotlin.math.pow
5 |
6 | fun Double.fromDb(): Double {
7 | return 10.0.pow(this / 20.0)
8 | }
9 |
10 | fun Double.toDb(): Double {
11 | return 20 * log10(this)
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/module/NavigationModule.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.module
2 |
3 | import dagger.Module
4 | import dagger.hilt.InstallIn
5 | import dagger.hilt.components.SingletonComponent
6 |
7 | @Module
8 | @InstallIn(SingletonComponent::class)
9 | internal abstract class NavigationModule {
10 |
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/module/PersistenceModule.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.module
2 |
3 | import android.app.Application
4 | import androidx.room.Room
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import me.spica27.spicamusic.db.AppDatabase
10 | import me.spica27.spicamusic.utils.DataStoreUtil
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | object PersistenceModule {
16 |
17 | @Provides
18 | @Singleton
19 | fun provideAppDatabase(
20 | application: Application,
21 | ): AppDatabase {
22 | return Room
23 | .databaseBuilder(
24 | application, AppDatabase::class.java,
25 | "spica_music.db"
26 | )
27 | .fallbackToDestructiveMigration()
28 | .build()
29 | }
30 |
31 | @Provides
32 | @Singleton
33 | fun provideDataStoreUtil(application: Application) = DataStoreUtil(application)
34 |
35 | @Provides
36 | @Singleton
37 | fun provideSongDao(appDatabase: AppDatabase) = appDatabase.songDao()
38 |
39 | @Provides
40 | @Singleton
41 | fun providePlaylistDao(appDatabase: AppDatabase) = appDatabase.playlistDao()
42 |
43 |
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/module/ToolModule.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.module
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import linc.com.amplituda.Amplituda
10 | import javax.inject.Singleton
11 |
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | object ToolModule {
16 |
17 |
18 | @Provides
19 | @Singleton
20 | fun provideAmplituda(
21 | application: Application,
22 | ): Amplituda {
23 | return Amplituda(application).apply {
24 | clearCache()
25 | setLogConfig(Log.ERROR,true)
26 | }
27 | }
28 |
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/playback/RepeatMode.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.playback
2 |
3 |
4 | // 循环方式
5 | enum class RepeatMode {
6 |
7 | NONE,// 顺序播放
8 | ALL, // 循环
9 | TRACK; // 单曲循环
10 |
11 | // 切换循环方式
12 | fun increment() =
13 | when (this) {
14 | NONE -> ALL
15 | ALL -> TRACK
16 | TRACK -> NONE
17 | }
18 |
19 |
20 | val icon: Int
21 | get() =
22 | when (this) {
23 | NONE -> -1
24 | ALL -> -1
25 | TRACK -> -1
26 | }
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/player/IPlayer.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.player
2 |
3 | import me.spica27.spicamusic.db.entity.Song
4 |
5 |
6 | interface IPlayer {
7 |
8 |
9 | fun loadSong(song: Song?, play: Boolean)
10 |
11 | fun getState(durationMs: Long): State
12 |
13 | fun seekTo(positionMs: Long)
14 |
15 | fun setPlaying(isPlaying: Boolean)
16 |
17 |
18 | class State(
19 | val isPlaying: Boolean,
20 | val currentPositionMs: Long,
21 | ) {
22 |
23 |
24 | companion object {
25 |
26 | fun from(isPlaying: Boolean, positionMs: Long) =
27 | State(
28 | isPlaying,
29 | positionMs
30 | )
31 | }
32 |
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/player/Queue.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.player
2 |
3 |
4 | import me.spica27.spicamusic.db.entity.Song
5 | import java.util.concurrent.atomic.AtomicReference
6 |
7 |
8 | /**
9 | * 播放列表
10 | */
11 | @Suppress("unused")
12 | class Queue {
13 |
14 | // 指针
15 | private val index = AtomicReference(0)
16 |
17 | // 播放列表
18 | private val heap = ArrayList()
19 |
20 | fun getPlayList(): List {
21 | return ArrayList(heap)
22 | }
23 |
24 | fun getIndex(): Int {
25 | return index.get()
26 | }
27 |
28 |
29 | fun currentSong(): Song? {
30 | return if (heap.isNotEmpty()) {
31 | heap[index.get()]
32 | } else {
33 | null
34 | }
35 | }
36 |
37 |
38 | fun clear() {
39 | synchronized(this) {
40 | heap.clear()
41 | index.set(0)
42 | }
43 | }
44 |
45 | fun remove(index: Int) {
46 | if (index < 0 || index >= heap.size) return
47 | synchronized(this) {
48 | heap.removeAt(index)
49 |
50 | if (index > heap.size - 1) {
51 | // 如果删除的是最后一个元素 避免越界
52 | this.index.set(heap.size - 1)
53 | } else if (index < this.index.get()) {
54 | // 如果删除的是当前播放的歌曲之前的歌曲 指针前移
55 | this.index.getAndUpdate { it - 1 }
56 | }
57 | }
58 | }
59 |
60 | fun add(song: Song): Boolean {
61 | synchronized(this) {
62 | if (heap.find { it.songId == song.songId } == null) {
63 | heap.add(0, song)
64 | return true
65 | }
66 | return false
67 | }
68 | }
69 |
70 | fun playNextSong(): Boolean {
71 | synchronized(this) {
72 | if (index.get() >= heap.size - 1) return false
73 | index.getAndUpdate { it + 1 }
74 | return true
75 | }
76 | }
77 |
78 | fun playPreSong(): Boolean {
79 | synchronized(this) {
80 | if (index.get() <= 0) return false
81 | index.getAndUpdate { it - 1 }
82 | return true
83 | }
84 | }
85 |
86 | fun reloadNewList(song: Song, songs: List) {
87 | synchronized(this) {
88 | heap.clear()
89 | heap.addAll(songs)
90 | this.index.set(0)
91 | heap.forEachIndexed { index, sg ->
92 | run {
93 | if (song.songId == sg.songId) {
94 | this.index.set(index)
95 | return
96 | }
97 | }
98 | }
99 | heap.add(0, song)
100 | index.set(0)
101 | }
102 | }
103 |
104 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/route/Routes.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.route
2 |
3 | import androidx.navigation3.runtime.NavKey
4 | import kotlinx.serialization.Serializable
5 |
6 | /// App的导航
7 |
8 | object Routes {
9 |
10 |
11 | @Serializable
12 | data object Splash : NavKey
13 |
14 | @Serializable
15 | data object Main : NavKey
16 |
17 | @Serializable
18 | data class AddSong(val playlistId: Long) : NavKey
19 |
20 | @Serializable
21 | data class PlaylistDetail(val playlistId: Long) : NavKey
22 |
23 | @Serializable
24 | data object Player : NavKey
25 |
26 | @Serializable
27 | data object SearchAll : NavKey
28 |
29 |
30 | @Serializable
31 | data object EQ : NavKey
32 |
33 | }
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/service/ForegroundManager.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.service
2 |
3 | import android.app.Service
4 | import androidx.core.app.ServiceCompat
5 | import timber.log.Timber
6 |
7 | /**
8 | * 用来给后台服务绑定前台通知的工具类
9 | */
10 | class ForegroundManager(private val service: Service) {
11 | private var isForeground = false
12 |
13 |
14 | fun release() {
15 | tryStopForeground()
16 | }
17 |
18 | fun tryStartForeground(notification: ForegroundServiceNotification): Boolean {
19 | if (isForeground) {
20 | return false
21 | }
22 |
23 | Timber.d("Starting foreground state")
24 | service.startForeground(notification.code, notification.build())
25 | isForeground = true
26 | return true
27 | }
28 |
29 |
30 | fun tryStopForeground(): Boolean {
31 | if (!isForeground) {
32 | // Nothing to do.
33 | return false
34 | }
35 |
36 | Timber.d("Stopping foreground state")
37 | ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
38 | isForeground = false
39 | return true
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/service/ForegroundServiceNotification.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.service
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 | import androidx.core.app.NotificationChannelCompat
6 | import androidx.core.app.NotificationCompat
7 | import androidx.core.app.NotificationManagerCompat
8 |
9 | abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo) :
10 | NotificationCompat.Builder(context, info.id) {
11 | private val notificationManager = NotificationManagerCompat.from(context)
12 |
13 | init {
14 | val channel =
15 | NotificationChannelCompat.Builder(info.id, NotificationManagerCompat.IMPORTANCE_LOW)
16 | .setName(context.getString(info.nameRes))
17 | .setLightsEnabled(false)
18 | .setVibrationEnabled(false)
19 | .setShowBadge(false)
20 | .build()
21 | notificationManager.createNotificationChannel(channel)
22 | }
23 |
24 |
25 | abstract val code: Int
26 |
27 |
28 | fun post() {
29 | @Suppress("MissingPermission") notificationManager.notify(code, build())
30 | }
31 |
32 | data class ChannelInfo(val id: String, @StringRes val nameRes: Int)
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/service/MuiscSearchWorker.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.service
2 |
3 | import android.app.Service
4 | import android.content.Intent
5 | import android.os.IBinder
6 | import dagger.hilt.android.AndroidEntryPoint
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 | import me.spica27.spicamusic.db.dao.SongDao
11 | import me.spica27.spicamusic.utils.AudioTool
12 |
13 | import timber.log.Timber
14 | import javax.inject.Inject
15 |
16 |
17 | @AndroidEntryPoint
18 | class RefreshMusicListService : Service() {
19 |
20 | @Inject
21 | lateinit var songDao: SongDao
22 |
23 |
24 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
25 | doWork()
26 | return START_STICKY
27 | }
28 |
29 | private fun doWork() {
30 |
31 |
32 | CoroutineScope(Dispatchers.IO).launch {
33 | try {
34 | val songs = AudioTool.getSongsFromPhone(applicationContext)
35 | Timber.tag("更新曲目成功").e("共${songs.size}首")
36 | songDao.updateSongs(songs)
37 | stopSelf()
38 | } catch (e: Exception) {
39 | Timber.tag("更新曲目错误").e(e)
40 | }
41 | }
42 |
43 | }
44 |
45 |
46 | override fun onBind(intent: Intent?): IBinder? = null
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/service/notification/NotificationComponent.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.service.notification
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.support.v4.media.MediaMetadataCompat
6 | import android.support.v4.media.session.MediaSessionCompat
7 | import androidx.annotation.DrawableRes
8 | import androidx.core.app.NotificationCompat
9 | import me.spica27.spicamusic.R
10 | import me.spica27.spicamusic.playback.RepeatMode
11 | import me.spica27.spicamusic.service.ForegroundServiceNotification
12 | import me.spica27.spicamusic.service.MusicService
13 | import me.spica27.spicamusic.utils.newBroadcastPendingIntent
14 | import me.spica27.spicamusic.utils.newMainPendingIntent
15 |
16 | @SuppressLint("RestrictedApi")
17 | class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
18 | ForegroundServiceNotification(context, CHANNEL_INFO) {
19 |
20 |
21 | override val code: Int
22 | get() = 0x102030
23 |
24 | init {
25 | setSmallIcon(R.drawable.ic_app)
26 | setCategory(NotificationCompat.CATEGORY_SERVICE)
27 | setShowWhen(false)
28 | setSilent(true)
29 | setContentIntent(context.newMainPendingIntent())
30 | setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
31 |
32 | addAction(buildAction(context, MusicService.ACTION_SKIP_PREV, R.drawable.ic_pre))
33 | addAction(buildPlayPauseAction(context, true))
34 | addAction(buildAction(context, MusicService.ACTION_SKIP_NEXT, R.drawable.ic_next))
35 |
36 | setStyle(
37 | androidx.media.app
38 | .NotificationCompat
39 | .MediaStyle()
40 | .setMediaSession(sessionToken)
41 | .setShowActionsInCompactView(0, 1, 2)
42 | )
43 | }
44 |
45 |
46 | fun updateMetadata(metadata: MediaMetadataCompat) {
47 | setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART))
48 | setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE))
49 | setContentText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST))
50 | setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION))
51 | }
52 |
53 | private fun buildPlayPauseAction(
54 | context: Context,
55 | isPlaying: Boolean
56 | ): NotificationCompat.Action {
57 | val drawableRes =
58 | if (isPlaying) {
59 | R.drawable.ic_pause
60 | } else {
61 | R.drawable.ic_play
62 | }
63 | return buildAction(context, MusicService.ACTION_PLAY_PAUSE, drawableRes)
64 | }
65 |
66 | private fun buildRepeatAction(
67 | context: Context,
68 | repeatMode: RepeatMode
69 | ): NotificationCompat.Action {
70 | return buildAction(context, MusicService.ACTION_INC_REPEAT_MODE, repeatMode.icon)
71 | }
72 |
73 | private fun buildAction(context: Context, actionName: String, @DrawableRes iconRes: Int) =
74 | NotificationCompat.Action.Builder(
75 | iconRes, actionName, context.newBroadcastPendingIntent(actionName)
76 | )
77 | .build()
78 |
79 |
80 | fun updatePlaying(isPlaying: Boolean) {
81 | mActions[1] = buildPlayPauseAction(context, isPlaying)
82 | }
83 |
84 |
85 | private companion object {
86 | val CHANNEL_INFO =
87 | ChannelInfo(
88 | id = "me.spica27.spicamusic" + ".channel.PLAYBACK",
89 | nameRes = R.string.playing
90 | )
91 | }
92 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | val AppTypography = Typography()
6 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/ui/AddSongScrren.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.ui
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.itemsIndexed
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.TextButton
17 | import androidx.compose.material3.TopAppBar
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.collectAsState
20 | import androidx.compose.runtime.rememberCoroutineScope
21 | import androidx.compose.runtime.snapshots.SnapshotStateList
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.hilt.navigation.compose.hiltViewModel
25 | import androidx.navigation3.runtime.NavBackStack
26 | import kotlinx.coroutines.Dispatchers
27 | import kotlinx.coroutines.flow.combine
28 | import kotlinx.coroutines.launch
29 | import kotlinx.coroutines.withContext
30 | import me.spica27.spicamusic.viewModel.SelectSongViewModel
31 | import me.spica27.spicamusic.widget.SelectableSongItem
32 |
33 |
34 | /// 给歌单添加歌曲的页面
35 | @OptIn(ExperimentalMaterial3Api::class)
36 | @Composable
37 | fun AddSongScreen(
38 | viewModel: SelectSongViewModel = hiltViewModel(),
39 | navigator: NavBackStack,
40 | playlistId: Long
41 | ) {
42 | val coroutineScope = rememberCoroutineScope()
43 | viewModel.setPlaylistId(playlistId)
44 | Scaffold(topBar = {
45 | TopAppBar(title = {
46 | Text(text = "选择新增歌曲")
47 | }, navigationIcon = {
48 | // 返回按钮
49 | IconButton(onClick = {
50 | navigator.removeLastOrNull()
51 | }) {
52 | Icon(Icons.AutoMirrored.Default.KeyboardArrowLeft, contentDescription = "Back")
53 | }
54 | }, actions = {
55 | // 保存按钮
56 | TextButton(onClick = {
57 | coroutineScope.launch(Dispatchers.IO) {
58 | viewModel.addSongToPlaylist()
59 | withContext(Dispatchers.Main) {
60 | navigator.removeLastOrNull()
61 | }
62 | }
63 | }) {
64 | Text(
65 | "保存", style = MaterialTheme.typography.bodyLarge.copy(
66 | color = MaterialTheme.colorScheme.primary
67 | )
68 | )
69 | }
70 | })
71 | }, content = { paddingValues ->
72 | Box(
73 | modifier = Modifier
74 | .fillMaxSize()
75 | .padding(paddingValues)
76 | ) {
77 | // 歌曲列表
78 |
79 |
80 | val listDataState =
81 | combine(
82 | viewModel.getAllSongsNotInPlaylist(),
83 | viewModel.selectedSongsIds
84 | ) { allSongs, selectIds ->
85 | allSongs.map {
86 | Pair(it, selectIds.contains(it.songId))
87 | }
88 | }
89 | .collectAsState(initial = emptyList())
90 |
91 |
92 | if (listDataState.value.isEmpty()) {
93 | Text("没有更多歌曲了", modifier = Modifier.align(Alignment.Center))
94 | } else {
95 | LazyColumn(modifier = Modifier.fillMaxSize()) {
96 | itemsIndexed(listDataState.value, key = { _, item ->
97 | item.first.songId.toString()
98 | }) { _, song ->
99 | // 歌曲条目
100 | SelectableSongItem(
101 | song = song.first,
102 | selected = song.second,
103 | onToggle = { viewModel.toggleSongSelection(song.first.songId) })
104 | }
105 | }
106 | }
107 | }
108 | })
109 | }
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/ui/AppMain.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.ui
2 |
3 | import androidx.compose.animation.slideInHorizontally
4 | import androidx.compose.animation.slideOutHorizontally
5 | import androidx.compose.animation.togetherWith
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.platform.LocalContext
8 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
9 | import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
10 | import androidx.navigation3.runtime.entry
11 | import androidx.navigation3.runtime.entryProvider
12 | import androidx.navigation3.runtime.rememberNavBackStack
13 | import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
14 | import androidx.navigation3.ui.NavDisplay
15 | import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator
16 | import me.spica27.spicamusic.route.Routes
17 | import me.spica27.spicamusic.theme.AppTheme
18 | import me.spica27.spicamusic.utils.DataStoreUtil
19 | import me.spica27.spicamusic.utils.sliderFromBottomRouteAnim
20 |
21 |
22 | @Composable
23 | fun AppMain() {
24 | val systemIsDark = DataStoreUtil().isForceDarkTheme
25 | val darkTheme = DataStoreUtil()
26 | .getForceDarkTheme.collectAsStateWithLifecycle(systemIsDark)
27 | .value
28 | val backStack = rememberNavBackStack(Routes.Splash)
29 | AppTheme(
30 | darkTheme = darkTheme,
31 | dynamicColor = false
32 | ) {
33 | NavDisplay(
34 | entryDecorators = listOf(
35 | rememberSceneSetupNavEntryDecorator(),
36 | rememberSavedStateNavEntryDecorator(),
37 | rememberViewModelStoreNavEntryDecorator()
38 | ),
39 | backStack = backStack,
40 | onBack = { backStack.removeLastOrNull() },
41 | entryProvider = entryProvider {
42 | entry { key ->
43 | AddSongScreen(navigator = backStack, playlistId = key.playlistId)
44 | }
45 | entry {
46 | PlaylistDetailScreen(
47 | navigator = backStack,
48 | playlistId = it.playlistId
49 | )
50 | }
51 | entry { MainScreen(navigator = backStack) }
52 | entry { SplashScreen(navigator = backStack) }
53 | entry(
54 | metadata = sliderFromBottomRouteAnim()
55 | ) { SearchAllScreen() }
56 | entry {
57 | EqScreen(navigator = backStack)
58 | }
59 | },
60 | transitionSpec = {
61 | slideInHorizontally(initialOffsetX = { it }) togetherWith
62 | slideOutHorizontally(targetOffsetX = { -it })
63 | },
64 | popTransitionSpec = {
65 | slideInHorizontally(initialOffsetX = { -it }) togetherWith
66 | slideOutHorizontally(targetOffsetX = { it })
67 | },
68 | predictivePopTransitionSpec = {
69 | slideInHorizontally(initialOffsetX = { -it }) togetherWith
70 | slideOutHorizontally(targetOffsetX = { it })
71 | }
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/ui/SplashScreen.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.ui
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.Scaffold
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.LaunchedEffect
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.alpha
15 | import androidx.compose.ui.unit.dp
16 | import androidx.navigation3.runtime.NavBackStack
17 | import kotlinx.coroutines.delay
18 | import me.spica27.spicamusic.route.Routes
19 |
20 |
21 | // Splash Screen
22 | @Composable
23 | fun SplashScreen(modifier: Modifier = Modifier, navigator: NavBackStack) {
24 |
25 | val visibilityState = remember { mutableStateOf(false) }
26 |
27 | // 透明度动画
28 | val textAlphaState = animateFloatAsState(
29 | targetValue =
30 | if (visibilityState.value) 1f else 0f, label = "textAlphaState"
31 | )
32 |
33 | LaunchedEffect(Unit) {
34 | delay(1000) // 延迟2秒
35 | navigator.add(Routes.Main)
36 | }
37 |
38 | Scaffold { padding ->
39 | Box(
40 | modifier = modifier
41 | .fillMaxSize()
42 | .padding(padding),
43 | contentAlignment = androidx.compose.ui.Alignment.Center
44 | ) {
45 | Text(
46 | text = "Splash Screen", modifier = Modifier
47 | .alpha(textAlphaState.value)
48 | .align(alignment = androidx.compose.ui.Alignment.Center)
49 | .padding(16.dp)
50 | )
51 | }
52 |
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/utils/ComposeExt.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.utils
2 |
3 | import androidx.compose.animation.EnterTransition
4 | import androidx.compose.animation.ExitTransition
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.animation.slideInVertically
7 | import androidx.compose.animation.slideOutVertically
8 | import androidx.compose.animation.togetherWith
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.interaction.MutableInteractionSource
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.composed
14 | import androidx.navigation3.ui.NavDisplay
15 |
16 |
17 | fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed {
18 | this.clickable(
19 | indication = null,
20 | interactionSource = remember { MutableInteractionSource() }) {
21 | onClick()
22 | }
23 | }
24 |
25 | fun sliderFromBottomRouteAnim() = NavDisplay.transitionSpec {
26 | slideInVertically(
27 | initialOffsetY = { it },
28 | animationSpec = tween(450)
29 | ) togetherWith ExitTransition.KeepUntilTransitionsFinished
30 | } + NavDisplay.popTransitionSpec {
31 | EnterTransition.None togetherWith
32 | slideOutVertically(
33 | targetOffsetY = { it },
34 | animationSpec = tween(450)
35 | )
36 | } + NavDisplay.predictivePopTransitionSpec {
37 | EnterTransition.None togetherWith
38 | slideOutVertically(
39 | targetOffsetY = { it },
40 | animationSpec = tween(450)
41 | )
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/utils/DataStoreUtil.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.utils
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.booleanPreferencesKey
7 | import androidx.datastore.preferences.core.doublePreferencesKey
8 | import androidx.datastore.preferences.core.edit
9 | import androidx.datastore.preferences.core.intPreferencesKey
10 | import androidx.datastore.preferences.preferencesDataStore
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.distinctUntilChanged
13 | import kotlinx.coroutines.flow.first
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.runBlocking
16 | import me.spica27.spicamusic.App
17 | import me.spica27.spicamusic.dsp.Equalizer
18 | import me.spica27.spicamusic.dsp.EqualizerBand
19 | import me.spica27.spicamusic.dsp.NyquistBand
20 | import me.spica27.spicamusic.dsp.toNyquistBand
21 | import timber.log.Timber
22 |
23 | // 字典工具类
24 | class DataStoreUtil(private val context: Context = App.getInstance()) {
25 | companion object {
26 | private val Context.dataStore: DataStore by preferencesDataStore("settings")
27 |
28 | // 夜间模式
29 | val FORCE_DARK_THEME = booleanPreferencesKey("force_dark_theme")
30 |
31 | // 自动扫描
32 | val AUTO_SCANNER = booleanPreferencesKey("auto_scanner")
33 |
34 | // 自动播放
35 | val AUTO_PLAY = booleanPreferencesKey("auto_play")
36 |
37 | // 响度增益
38 | val REPLAY_GAIN = intPreferencesKey("REPLAY_GAIN")
39 |
40 |
41 | }
42 |
43 |
44 | suspend fun saveEq(
45 | eq: List
46 | ) {
47 | context.dataStore.edit { preferences ->
48 | for (equalizerBand in eq) {
49 | Timber.tag("eq").d("saveEq: ${equalizerBand.centerFrequency} ${equalizerBand.gain}")
50 | preferences[doublePreferencesKey("EQ-${equalizerBand.centerFrequency}")] =
51 | equalizerBand.gain
52 | }
53 | }
54 | }
55 |
56 | fun getEqualizerBand(): Flow> {
57 | return context.dataStore.data.map { preferences ->
58 | Equalizer.centerFrequency.map {
59 | val gain: Double = preferences[doublePreferencesKey("EQ-${it}")] ?: 0.0
60 | EqualizerBand(
61 | it,
62 | gain
63 | )
64 | }
65 | }.distinctUntilChanged()
66 | }
67 |
68 |
69 | fun getNyquistBand(): Flow> {
70 | return context.dataStore.data.map { preferences ->
71 | Equalizer.centerFrequency.map {
72 | val gain: Double = preferences[doublePreferencesKey("EQ-${it}")] ?: 0.0
73 | EqualizerBand(
74 | it,
75 | gain
76 | ).toNyquistBand()
77 | }
78 | }.distinctUntilChanged()
79 | }
80 |
81 |
82 | suspend fun saveReplayGain(
83 | replayGain: Int
84 | ) {
85 | context.dataStore.edit { preferences ->
86 | preferences[REPLAY_GAIN] = replayGain
87 | }
88 | }
89 |
90 | val getReplayGain: Flow
91 | get() = context.dataStore.data.map { preferences ->
92 | preferences[REPLAY_GAIN] ?: 0
93 | }.distinctUntilChanged()
94 |
95 |
96 | val isForceDarkTheme: Boolean
97 | get() = runBlocking { context.dataStore.data.first()[FORCE_DARK_THEME] ?: false }
98 |
99 | val getAutoPlay: Flow = context.dataStore.data
100 | .map { preferences ->
101 | preferences[AUTO_PLAY] ?: true
102 | }.distinctUntilChanged()
103 |
104 | suspend fun saveAutoPlay(value: Boolean) {
105 | context.dataStore.edit { preferences ->
106 | preferences[AUTO_PLAY] = value
107 | }
108 | }
109 |
110 | val getAutoScanner: Flow = context.dataStore.data
111 | .map { preferences ->
112 | preferences[AUTO_SCANNER] ?: true
113 | }.distinctUntilChanged()
114 |
115 | suspend fun saveAutoScanner(value: Boolean) {
116 | context.dataStore.edit { preferences ->
117 | preferences[AUTO_SCANNER] = value
118 | }
119 | }
120 |
121 | val getForceDarkTheme: Flow = context.dataStore.data
122 | .map { preferences ->
123 | preferences[FORCE_DARK_THEME] ?: false
124 | }.distinctUntilChanged()
125 |
126 | suspend fun saveForceDarkTheme(value: Boolean) {
127 | context.dataStore.edit { preferences ->
128 | preferences[FORCE_DARK_THEME] = value
129 | }
130 | }
131 |
132 |
133 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/utils/MimeTypeExt.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.utils
2 |
3 | import android.graphics.Color
4 | import androidx.annotation.ColorInt
5 | import androidx.media3.common.MimeTypes
6 |
7 |
8 | @ColorInt
9 | fun String.getColorFromMimeTypeString(): Int {
10 | return when (this) {
11 | MimeTypes.AUDIO_OGG -> Color.parseColor("#87e8de")
12 | MimeTypes.AUDIO_MP4 -> Color.parseColor("#95de64")
13 | MimeTypes.AUDIO_MPEG -> Color.parseColor("#b7eb8f")
14 | MimeTypes.AUDIO_FLAC -> Color.parseColor("#ffd666")
15 | else -> Color.parseColor("#d9d9d9")
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/utils/MusicExt.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.utils
2 |
3 | import android.text.format.DateUtils
4 |
5 |
6 | fun Long.formatDurationDs(isElapsed: Boolean = true) = dsToSecs().formatDurationSecs(isElapsed)
7 | fun Long.formatDurationSecs(isElapsed: Boolean = true): String {
8 | if (!isElapsed && this == 0L) {
9 | return "--:--"
10 | }
11 | var durationString = DateUtils.formatElapsedTime(this)
12 | if (durationString[0] == '0') {
13 | durationString = durationString.slice(1 until durationString.length)
14 | }
15 | return durationString
16 | }
17 |
18 |
19 | fun Long.msToDs() = floorDiv(100)
20 |
21 |
22 | fun Long.msToSecs() = floorDiv(1000)
23 |
24 |
25 | fun Long.dsToMs() = times(100)
26 |
27 | fun Long.dsToSecs() = floorDiv(10)
28 |
29 |
30 | fun Long.secsToMs() = times(1000)
31 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/viewModel/MusicSearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.viewModel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.combine
10 | import kotlinx.coroutines.flow.flowOn
11 | import kotlinx.coroutines.launch
12 | import me.spica27.spicamusic.db.dao.SongDao
13 | import me.spica27.spicamusic.db.entity.Song
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class MusicSearchViewModel @Inject constructor(
18 | val songDao: SongDao
19 | ) : ViewModel() {
20 |
21 |
22 | // 搜索关键字
23 | private val _searchKey = MutableStateFlow("")
24 |
25 | val searchKey: Flow
26 | get() = _searchKey
27 |
28 | // 是否过滤收藏的歌曲
29 | private val _filterNoLike = MutableStateFlow(false)
30 |
31 | val filterNoLike: Flow
32 | get() = _filterNoLike
33 |
34 | // 过滤过短的歌曲
35 | private val _filterShort = MutableStateFlow(false)
36 |
37 | val filterShort: Flow
38 | get() = _filterShort
39 |
40 |
41 | val songFlow:Flow> = combine(
42 | songDao.getAll(),
43 | _searchKey,
44 | _filterNoLike,
45 | _filterShort
46 | ) { songs, key, like, short ->
47 | songs
48 | .filter {
49 | (key.isEmpty() || it.displayName.contains(key, ignoreCase = true) ||
50 | it.artist.contains(
51 | key,
52 | ignoreCase = true
53 | )) &&
54 | (!like || it.like) &&
55 | (!short || it.duration > 3000)
56 | }
57 | }
58 | .flowOn(Dispatchers.IO)
59 |
60 | // 收藏/不收藏歌曲
61 | fun toggleLike(song: Song) {
62 | viewModelScope.launch {
63 | songDao.toggleLike(song.songId ?: -1)
64 | }
65 | }
66 |
67 |
68 | fun onSearchKeyChange(newKey: String) {
69 | _searchKey.value = newKey
70 | }
71 |
72 | fun toggleFilterNoLike() {
73 | _filterNoLike.value = !_filterNoLike.value
74 | }
75 |
76 | fun toggleFilterShort() {
77 | _filterShort.value = !_filterShort.value
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/viewModel/SelectSongViewModel.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.viewModel
2 |
3 | import androidx.annotation.OptIn
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import androidx.media3.common.util.UnstableApi
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.launch
12 | import me.spica27.spicamusic.db.dao.PlaylistDao
13 | import me.spica27.spicamusic.db.dao.SongDao
14 | import me.spica27.spicamusic.db.entity.PlaylistSongCrossRef
15 | import me.spica27.spicamusic.db.entity.Song
16 | import me.spica27.spicamusic.playback.PlaybackStateManager
17 | import javax.inject.Inject
18 |
19 |
20 | @HiltViewModel
21 | class SelectSongViewModel @Inject constructor(
22 | private val songDao: SongDao,
23 | private val playlistDao: PlaylistDao,
24 | ) : ViewModel() {
25 |
26 | private var playlistId: Long? = null
27 |
28 | fun getAllSongsNotInPlaylist() = songDao.getSongsNotInPlayList(playlistId ?: -1)
29 |
30 | fun getAllSongs() = songDao.getAll()
31 |
32 | private val selectIdsSet = hashSetOf()
33 |
34 | private val _selectedSongsIds = MutableStateFlow(hashSetOf())
35 |
36 | val selectedSongsIds: Flow>
37 | get() = _selectedSongsIds
38 |
39 | fun setPlaylistId(playlistId: Long) {
40 | this.playlistId = playlistId
41 | }
42 |
43 | fun clearSelectedSongs() {
44 | selectIdsSet.clear()
45 | _selectedSongsIds.value = selectIdsSet.toHashSet()
46 | }
47 |
48 | // 切换歌曲选择状态
49 | fun toggleSongSelection(songId: Long?) {
50 | if (songId == null) return
51 | if (selectIdsSet.contains(songId)) {
52 | selectIdsSet.remove(songId)
53 | } else {
54 | selectIdsSet.add(songId)
55 | }
56 | _selectedSongsIds.value = selectIdsSet.toHashSet()
57 | }
58 |
59 | // 添加到当前播放列表
60 | @OptIn(UnstableApi::class)
61 | fun addSongToCurrentPlaylist(song: Song) {
62 | viewModelScope.launch(Dispatchers.Default) {
63 | PlaybackStateManager.getInstance().play(song)
64 | }
65 | }
66 |
67 | // 添加到播放列表
68 | suspend fun addSongToPlaylist() {
69 | playlistDao.insertListItems(
70 | _selectedSongsIds.value.map { songId -> PlaylistSongCrossRef(playlistId ?: 0, songId) })
71 | }
72 |
73 |
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/viewModel/SettingViewModel.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.viewModel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.launch
8 | import me.spica27.spicamusic.utils.DataStoreUtil
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class SettingViewModel @Inject constructor(
13 | private val dataStoreUtil: DataStoreUtil
14 | ) : ViewModel() {
15 |
16 | val autoPlay = dataStoreUtil.getAutoPlay
17 |
18 | val autoScanner = dataStoreUtil.getAutoScanner
19 |
20 | val forceDarkTheme = dataStoreUtil.getForceDarkTheme
21 |
22 | fun saveAutoPlay(value: Boolean) {
23 | viewModelScope.launch(Dispatchers.IO) {
24 | dataStoreUtil.saveAutoPlay(value)
25 | }
26 | }
27 |
28 | fun saveAutoScanner(value: Boolean) {
29 | viewModelScope.launch(Dispatchers.IO) {
30 | dataStoreUtil.saveAutoScanner(value)
31 | }
32 | }
33 |
34 | fun saveForceDarkTheme(value: Boolean) {
35 | viewModelScope.launch(Dispatchers.IO) {
36 | dataStoreUtil.saveForceDarkTheme(value)
37 | }
38 | }
39 |
40 |
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/viewModel/SongViewModel.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.viewModel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.StateFlow
9 | import kotlinx.coroutines.flow.stateIn
10 | import kotlinx.coroutines.launch
11 | import me.spica27.spicamusic.db.dao.PlaylistDao
12 | import me.spica27.spicamusic.db.dao.SongDao
13 | import me.spica27.spicamusic.db.entity.Playlist
14 | import me.spica27.spicamusic.db.entity.Song
15 | import timber.log.Timber
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class SongViewModel
20 | @Inject constructor(
21 | private val songDao: SongDao,
22 | private val playlistDao: PlaylistDao
23 | ) : ViewModel() {
24 |
25 | fun getSongFlow(id: Long) = songDao.getSongFlowWithId(id)
26 |
27 | // 所有歌曲
28 | val allSongs: StateFlow> = songDao.getAll()
29 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
30 |
31 | // 所有收藏的歌曲
32 | val allLikeSongs: StateFlow> = songDao.getAllLikeSong()
33 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
34 |
35 | // 所有歌单
36 | val allPlayList: StateFlow> = playlistDao.getAllPlaylist()
37 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
38 |
39 |
40 | // 切换喜欢状态
41 | fun toggleFavorite(id: Long) {
42 | Timber.e("toggleFavorite: $id")
43 | viewModelScope.launch(Dispatchers.IO) {
44 | songDao.toggleLike(id)
45 | }
46 | }
47 |
48 | // 添加歌单
49 | fun addPlayList(value: String) {
50 | viewModelScope.launch {
51 | playlistDao.insertPlaylist(Playlist(playlistName = value))
52 | }
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/visualiser/FFTListener.java:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.visualiser;
2 |
3 | public interface FFTListener {
4 | void onFFTReady(int sampleRateHz, int channelCount, float[] fft);
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/visualiser/VisualizerDrawableManager.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.visualiser
2 |
3 | import androidx.annotation.OptIn
4 | import androidx.media3.common.util.UnstableApi
5 | import me.spica27.spicamusic.visualiser.drawable.BlurVisualiser
6 | import me.spica27.spicamusic.visualiser.drawable.CircleVisualiser
7 | import me.spica27.spicamusic.visualiser.drawable.LineVisualiser
8 | import me.spica27.spicamusic.visualiser.drawable.VisualiserDrawable
9 |
10 | @OptIn(UnstableApi::class)
11 | class VisualizerDrawableManager {
12 |
13 | enum class VisualiserType {
14 | LINE, CIRCLE, BLUR, RAIN
15 | }
16 |
17 |
18 | companion object {
19 | private val visualiserDrawables = hashMapOf(
20 | VisualiserType.CIRCLE to CircleVisualiser(),
21 | VisualiserType.BLUR to BlurVisualiser(),
22 | VisualiserType.LINE to LineVisualiser(),
23 | VisualiserType.RAIN to BlurVisualiser(),
24 | )
25 | }
26 |
27 | private var currentVisualiserType = VisualiserType.LINE
28 |
29 |
30 | fun nextVisualiserType() {
31 | currentVisualiserType = when (currentVisualiserType) {
32 | VisualiserType.LINE -> VisualiserType.CIRCLE
33 | VisualiserType.CIRCLE -> VisualiserType.BLUR
34 | VisualiserType.BLUR -> VisualiserType.RAIN
35 | VisualiserType.RAIN -> VisualiserType.LINE
36 | }
37 | }
38 |
39 |
40 | fun setVisualiserType(type: VisualiserType) {
41 | currentVisualiserType = type
42 | }
43 |
44 | fun setSecondaryColor(color: Int) {
45 | visualiserDrawables.values.forEach {
46 | it.secondaryColor = color
47 | }
48 | }
49 |
50 | fun setRadius(radius: Int) {
51 | visualiserDrawables.values.forEach {
52 | it.radius = radius
53 | }
54 | }
55 |
56 | fun setThemeColor(color: Int) {
57 | visualiserDrawables.values.forEach {
58 | it.themeColor = color
59 | }
60 | }
61 |
62 |
63 | fun setVisualiserBounds(width: Int, height: Int) {
64 | visualiserDrawables.values.forEach {
65 | it.setBounds(width, height)
66 | }
67 | }
68 |
69 |
70 | fun getVisualiserDrawable(): VisualiserDrawable {
71 | return visualiserDrawables[currentVisualiserType]
72 | ?: throw IllegalStateException("VisualiserDrawable not found")
73 | }
74 |
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/visualiser/drawable/BlurVisualiser.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.visualiser.drawable
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.CornerPathEffect
5 | import android.graphics.Paint
6 | import android.graphics.Path
7 | import androidx.annotation.OptIn
8 | import androidx.media3.common.util.UnstableApi
9 | import me.spica27.spicamusic.utils.dp
10 | import me.spica27.spicamusic.visualiser.MusicVisualiser
11 | import kotlin.math.cos
12 | import kotlin.math.sin
13 | import androidx.core.graphics.withSave
14 | import timber.log.Timber
15 |
16 | @OptIn(UnstableApi::class)
17 | class BlurVisualiser : VisualiserDrawable() {
18 |
19 |
20 | private val paint = Paint().apply {
21 | pathEffect = CornerPathEffect(10f)
22 | style = Paint.Style.STROKE
23 | strokeWidth = 3.dp
24 | strokeCap = Paint.Cap.ROUND
25 | }
26 |
27 |
28 | private val path1 = Path()
29 |
30 | private val path2 = Path()
31 |
32 | private val decelerateInterpolator by lazy {
33 | android.view.animation.DecelerateInterpolator()
34 | }
35 |
36 | /**
37 | * 计算圆弧上的某一点
38 | */
39 | private fun calcPoint(centerX: Int, centerY: Int, radius: Int, angle: Float): IntArray {
40 | val x = (centerX + radius * cos((angle) * Math.PI / 180)).toInt()
41 | val y = (centerY + radius * sin((angle) * Math.PI / 180)).toInt()
42 | return intArrayOf(x, y)
43 | }
44 |
45 | // 采集到的数据
46 | private val yList by lazy {
47 | arrayListOf().apply {
48 | for (i in 0 until MusicVisualiser.FREQUENCY_BAND_LIMITS.size) {
49 | add(radius)
50 | add(radius)
51 | }
52 | }
53 | }
54 |
55 | // 前一次采集的数据
56 | private val lastYList by lazy {
57 | arrayListOf().apply {
58 | for (i in 0 until MusicVisualiser.FREQUENCY_BAND_LIMITS.size) {
59 | add(radius)
60 | add(radius)
61 | }
62 | }
63 | }
64 |
65 |
66 | // 上次采样的时间
67 | private var lastSampleTime = 0L
68 |
69 | // 采样间隔
70 | private val interval = 125
71 |
72 |
73 | override fun draw(canvas: Canvas) {
74 | Timber.tag("BlurVisualiser").d("draw()")
75 | Timber.tag("BlurVisualiser").d("width = ${width}")
76 | Timber.tag("BlurVisualiser").d("height = ${height}")
77 | canvas.translate(width / 2f, height / 2f)
78 | if (yList.size != lastYList.size) {
79 | return
80 | }
81 | canvas.withSave {
82 | path1.reset()
83 | path2.reset()
84 | paint.color = themeColor
85 |
86 | val fraction =
87 | decelerateInterpolator.getInterpolation(((System.currentTimeMillis() - lastSampleTime).toFloat() / interval))
88 | .coerceIn(
89 | 0f, 1f
90 | )
91 |
92 |
93 | for (index in 0 until yList.size) {
94 | val lastY = lastYList[index]
95 | val y = yList[index]
96 |
97 | val curY = lastY + (y - lastY) * fraction
98 |
99 | val curY2 = radius - (curY - radius)
100 |
101 | val angle = 360f / yList.size * index
102 |
103 | val p1 = calcPoint(0, 0, curY.toInt(), angle)
104 |
105 | val p2 = calcPoint(0, 0, curY2.toInt(), angle)
106 |
107 | drawLine(p1[0].toFloat(), p1[1].toFloat(), p2[0].toFloat(), p2[1].toFloat(), paint)
108 |
109 | if (index == 0) {
110 | path1.moveTo(p1[0].toFloat(), p1[1].toFloat())
111 | path2.moveTo(p2[0].toFloat(), p2[1].toFloat())
112 | } else {
113 | path1.lineTo(p1[0].toFloat(), p1[1].toFloat())
114 | path2.lineTo(p2[0].toFloat(), p2[1].toFloat())
115 | }
116 | }
117 |
118 | path1.close()
119 | path2.close()
120 |
121 | drawPath(path1, paint)
122 |
123 | drawPath(path2, paint)
124 | }
125 | }
126 |
127 | override fun update(list: List) {
128 | if (((System.currentTimeMillis() - lastSampleTime) < interval) && yList.isNotEmpty() && lastYList.isNotEmpty()) {
129 | return
130 | }
131 |
132 | // 记录上次的结果
133 | if (yList.isNotEmpty()) {
134 | lastYList.clear()
135 | lastYList.addAll(yList)
136 | }
137 |
138 | // 计算
139 | yList.clear()
140 | list.forEachIndexed { index, value ->
141 | val cur = radius + 30.dp * value
142 | val next = if (index == list.size - 1) {
143 | radius + 30.dp * list[0]
144 | } else {
145 | radius + 30.dp * list[index + 1]
146 | }
147 | yList.add(cur.toInt())
148 | yList.add(next.toInt())
149 | }
150 | lastSampleTime = System.currentTimeMillis()
151 | }
152 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/visualiser/drawable/LineVisualiser.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.visualiser.drawable
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.Paint
6 | import androidx.annotation.OptIn
7 | import androidx.core.graphics.ColorUtils
8 | import androidx.media3.common.util.UnstableApi
9 | import me.spica27.spicamusic.utils.dp
10 | import me.spica27.spicamusic.visualiser.MusicVisualiser
11 | import timber.log.Timber
12 |
13 |
14 | @OptIn(UnstableApi::class)
15 | class LineVisualiser : VisualiserDrawable() {
16 |
17 |
18 | // 上次采样的时间
19 | private var lastSampleTime = 0L
20 |
21 | // 采样间隔
22 | private val interval = 125
23 |
24 |
25 | // 采集到的数据
26 | private val yList by lazy {
27 | arrayListOf().apply {
28 | for (i in 0 until MusicVisualiser.FREQUENCY_BAND_LIMITS.size) {
29 | add(radius)
30 | add(radius)
31 | }
32 | }
33 | }
34 |
35 | // 前一次采集的数据
36 | private val lastYList by lazy {
37 | arrayListOf().apply {
38 | for (i in 0 until MusicVisualiser.FREQUENCY_BAND_LIMITS.size) {
39 | add(radius)
40 | add(radius)
41 | }
42 | }
43 | }
44 |
45 | // 记录最高的Y值 用于回落动画
46 | private val maxYList by lazy {
47 | arrayListOf().apply {
48 | for (i in 0 until MusicVisualiser.FREQUENCY_BAND_LIMITS.size) {
49 | add(radius)
50 | add(radius)
51 | }
52 | }
53 | }
54 |
55 |
56 | private val pointPaint = Paint().apply {
57 | // set paint
58 | color = Color.BLACK
59 | // set style
60 | style = Paint.Style.FILL
61 | strokeWidth = 8.dp
62 | strokeCap = Paint.Cap.ROUND
63 | }
64 |
65 |
66 | private val decelerateInterpolator by lazy {
67 | android.view.animation.DecelerateInterpolator()
68 | }
69 |
70 | override fun draw(canvas: Canvas) {
71 | canvas.translate(width / 2f, height / 2f)
72 | val fraction =
73 | decelerateInterpolator.getInterpolation(((System.currentTimeMillis() - lastSampleTime).toFloat() / interval))
74 | .coerceIn(
75 | 0f, 1f
76 | )
77 |
78 | if (lastYList.size == yList.size) {
79 | canvas.save()
80 | for (i in 0 until lastYList.size) {
81 | canvas.rotate(360f / lastYList.size)
82 | val lastY = lastYList[i]
83 | val y = yList[i]
84 |
85 | val curY = lastY + (y - lastY) * fraction
86 |
87 | if (maxYList.size == yList.size) {
88 | val maxY = maxYList[i]
89 | if (maxY < curY) {
90 | maxYList[i] = curY.toInt()
91 | } else {
92 | maxYList[i] = (maxYList[i] - (1.dp) / 60f).toInt()
93 | }
94 | pointPaint.color = ColorUtils.setAlphaComponent(themeColor, 100)
95 | canvas.drawLine(
96 | 0f, maxYList[i] * 1f, 0f, radius * 1f, pointPaint
97 | )
98 | pointPaint.color = themeColor
99 | }
100 |
101 |
102 | canvas.drawLine(
103 | 0f, curY * 1f, 0f, radius * 1f, pointPaint
104 | )
105 | }
106 | canvas.restore()
107 | }
108 | }
109 |
110 | override fun update(list: List) {
111 | if (((System.currentTimeMillis() - lastSampleTime) < interval) &&
112 | yList.isNotEmpty() && lastYList.isNotEmpty()
113 | ) {
114 | return
115 | }
116 |
117 | // 记录上次的结果
118 | if (yList.isNotEmpty()) {
119 | lastYList.clear()
120 | lastYList.addAll(yList)
121 | }
122 |
123 | // 计算
124 | yList.clear()
125 | list.forEachIndexed { index, value ->
126 | val cur = radius + 30.dp * value
127 | val next = if (index == list.size - 1) {
128 | radius + 30.dp * list[0]
129 | } else {
130 | radius + 30.dp * list[index + 1]
131 | }
132 | yList.add(cur.toInt())
133 | yList.add(next.toInt())
134 | }
135 | if (maxYList.size != yList.size) {
136 | maxYList.clear()
137 | maxYList.addAll(yList)
138 | }
139 |
140 | lastSampleTime = System.currentTimeMillis()
141 | }
142 |
143 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/visualiser/drawable/VisualiserDrawable.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.visualiser.drawable
2 |
3 | import android.graphics.Canvas
4 |
5 |
6 | abstract class VisualiserDrawable {
7 |
8 | // 半径
9 | var radius = 0
10 |
11 | // 主题色
12 | var themeColor = 0
13 |
14 | // 二级颜色
15 | var secondaryColor = 0
16 |
17 | // 背景颜色
18 | var backgroundColor = 0
19 | var width = 0
20 |
21 | var height = 0
22 |
23 | open fun setBounds(width: Int, height: Int) {
24 | this.width = width
25 | this.height = height
26 | }
27 |
28 |
29 | abstract fun draw(canvas: Canvas)
30 |
31 | abstract fun update(list: List)
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/PlaylistItem.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.MoreVert
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.unit.dp
22 | import me.spica27.spicamusic.db.entity.Playlist
23 |
24 |
25 | /// 歌单条目
26 | @Composable
27 | fun PlaylistItem(
28 | modifier: Modifier = Modifier,
29 | playlist: Playlist,
30 | onClick: () -> Unit = {},
31 | onClickMenu: () -> Unit = {},
32 | showMenu: Boolean = false
33 | ) {
34 | Row(modifier = modifier
35 | .clickable {
36 | onClick()
37 | }
38 | .padding(horizontal = 16.dp, vertical = 6.dp)
39 | .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
40 | Box(
41 | modifier = Modifier
42 | .width(50.dp)
43 | .height(50.dp)
44 | .background(MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.shapes.medium),
45 | contentAlignment = Alignment.Center
46 | ) {
47 | Text(
48 | text = (playlist.playlistName.firstOrNull() ?: "A").toString(),
49 | style = MaterialTheme.typography.bodyLarge,
50 | color = MaterialTheme.colorScheme.onSurface
51 | )
52 | }
53 | Spacer(modifier = Modifier.width(16.dp))
54 | Text(
55 | modifier = Modifier.weight(1f),
56 | text = playlist.playlistName, style = MaterialTheme.typography.bodyMedium.copy(
57 | color = MaterialTheme.colorScheme.onSurface,
58 | )
59 | )
60 | if (showMenu) {
61 | IconButton(
62 | onClick = onClickMenu,
63 | ) {
64 | Icon(
65 | imageVector = Icons.Default.MoreVert,
66 | contentDescription = "More",
67 | tint = MaterialTheme.colorScheme.onSurface
68 | )
69 | }
70 | }
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/VisualizerView.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget
2 |
3 | import android.animation.ValueAnimator
4 | import android.content.Context
5 | import android.graphics.Canvas
6 | import android.os.Handler
7 | import android.os.HandlerThread
8 | import android.util.AttributeSet
9 | import android.view.View
10 | import androidx.media3.common.util.UnstableApi
11 | import me.spica27.spicamusic.utils.dp
12 | import me.spica27.spicamusic.visualiser.MusicVisualiser
13 | import me.spica27.spicamusic.visualiser.VisualizerDrawableManager
14 | import timber.log.Timber
15 | import java.util.concurrent.locks.ReentrantLock
16 |
17 |
18 | @UnstableApi
19 | class VisualizerView : View, MusicVisualiser.Listener {
20 |
21 |
22 | private lateinit var mThread: HandlerThread
23 |
24 | private lateinit var mHandler: Handler
25 |
26 | constructor(context: Context?) : super(context)
27 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
28 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
29 | context, attrs, defStyleAttr
30 | )
31 |
32 | init {
33 | setOnClickListener {
34 | nextVisualiserType()
35 | }
36 | }
37 |
38 | private val infiniteAnim = ValueAnimator.ofFloat(0f,1f).apply {
39 | repeatCount = ValueAnimator.INFINITE
40 | duration = 1000
41 | addUpdateListener {
42 | postInvalidateOnAnimation()
43 | }
44 | }
45 |
46 |
47 | override fun onAttachedToWindow() {
48 | super.onAttachedToWindow()
49 | mThread = HandlerThread("VisualizerSurfaceView")
50 | mThread.start()
51 | mHandler = Handler(mThread.looper)
52 | musicVisualiser.setListener(this)
53 | infiniteAnim.start()
54 | musicVisualiser.ready()
55 | Timber.tag("VisualizerSurfaceView").d("onAttachedToWindow()")
56 | }
57 |
58 | override fun onDetachedFromWindow() {
59 | super.onDetachedFromWindow()
60 | musicVisualiser.dispose()
61 | mHandler.removeCallbacksAndMessages(null)
62 | mThread.quitSafely()
63 | musicVisualiser.setListener(null)
64 | infiniteAnim.cancel()
65 | Timber.tag("VisualizerSurfaceView").d("onDetachedFromWindow()")
66 | }
67 |
68 |
69 | fun setThemeColor(color: Int) {
70 | visualizerDrawableManager.setThemeColor(color)
71 | }
72 |
73 |
74 | private val visualizerDrawableManager = VisualizerDrawableManager()
75 |
76 |
77 | private val lock = ReentrantLock()
78 |
79 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
80 | super.onSizeChanged(w, h, oldw, oldh)
81 | lock.lock()
82 | visualizerDrawableManager.setVisualiserBounds(w, h)
83 | visualizerDrawableManager.setRadius((width / 2 - 60.dp).toInt())
84 | lock.unlock()
85 | }
86 |
87 |
88 | fun nextVisualiserType() {
89 | visualizerDrawableManager.nextVisualiserType()
90 | }
91 |
92 |
93 | override fun onDraw(canvas: Canvas) {
94 | super.onDraw(canvas)
95 | lock.lock()
96 | visualizerDrawableManager.getVisualiserDrawable().draw(canvas)
97 | lock.unlock()
98 | }
99 |
100 |
101 | // 波形图绘制效果集合
102 | private val musicVisualiser: MusicVisualiser = MusicVisualiser()
103 |
104 | private val fft = arrayListOf()
105 |
106 | override fun getDrawData(list: List) {
107 | lock.lock()
108 | fft.clear()
109 | fft.addAll(list)
110 | lock.unlock()
111 | mHandler.post {
112 | lock.lock()
113 | visualizerDrawableManager.getVisualiserDrawable().update(fft)
114 | lock.unlock()
115 | mHandler.removeCallbacksAndMessages(null)
116 | }
117 | }
118 |
119 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/VisualizerWidget.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.aspectRatio
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.geometry.Size
14 |
15 |
16 | @Composable
17 | fun VisualizerView(modifier: Modifier = Modifier) {
18 | var canvasSize by remember { mutableStateOf(Size(0f, 0f)) }
19 | Box(
20 | modifier = modifier
21 | .aspectRatio(1f)
22 | ) {
23 | Canvas(
24 | modifier = Modifier.fillMaxSize()
25 | ) {
26 | canvasSize = size
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/audio_seekbar/AmplitudeType.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget.audio_seekbar
2 |
3 | // 振幅类型
4 | enum class AmplitudeType {
5 | Avg, Min, Max
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/audio_seekbar/BrushExt.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget.audio_seekbar
2 |
3 | import androidx.compose.animation.core.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.geometry.Offset
8 | import androidx.compose.ui.graphics.Brush
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.TileMode
11 |
12 |
13 | @Composable
14 | fun Brush.Companion.infiniteHorizontalGradient(
15 | infiniteTransition: InfiniteTransition = rememberInfiniteTransition(label = "infiniteTransition"),
16 | animation: DurationBasedAnimationSpec = tween(2000, easing = LinearEasing),
17 | colors: List,
18 | width: Float
19 | ): Brush {
20 | val offset by getInfiniteOffset(infiniteTransition, animation, width)
21 | return horizontalGradient(colors, offset, offset + width, TileMode.Mirror)
22 | }
23 |
24 | @Composable
25 | fun Brush.Companion.infiniteVerticalGradient(
26 | infiniteTransition: InfiniteTransition = rememberInfiniteTransition(label = "infiniteTransition"),
27 | animation: DurationBasedAnimationSpec = tween(2000, easing = LinearEasing),
28 | colors: List,
29 | width: Float
30 | ): Brush {
31 | val offset by getInfiniteOffset(infiniteTransition, animation, width)
32 | return verticalGradient(colors, offset, offset + width, TileMode.Mirror)
33 | }
34 |
35 | @Composable
36 | fun Brush.Companion.infiniteLinearGradient(
37 | infiniteTransition: InfiniteTransition = rememberInfiniteTransition(label = "infiniteTransition"),
38 | animation: DurationBasedAnimationSpec = tween(2000, easing = LinearEasing),
39 | colors: List,
40 | width: Float
41 | ): Brush {
42 | val offset by getInfiniteOffset(infiniteTransition, animation, width)
43 | return linearGradient(
44 | colors,
45 | Offset(offset, offset),
46 | Offset(offset + width, offset + width),
47 | TileMode.Mirror
48 | )
49 | }
50 |
51 | @Composable
52 | private fun getInfiniteOffset(
53 | infiniteTransition: InfiniteTransition,
54 | animation: DurationBasedAnimationSpec,
55 | width: Float
56 | ): State {
57 | return infiniteTransition.animateFloat(
58 | initialValue = 0f,
59 | targetValue = width * 2,
60 | animationSpec = infiniteRepeatable(
61 | animation = animation,
62 | repeatMode = RepeatMode.Restart
63 | ), label = "infiniteTransition"
64 | )
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/audio_seekbar/Ext.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget.audio_seekbar
2 |
3 | import kotlin.math.ceil
4 | import kotlin.math.roundToInt
5 |
6 |
7 | internal fun Iterable.fillToSize(size: Int, transform: (List) -> T): List {
8 | val capacity = ceil(size.safeDiv(count())).roundToInt()
9 | return map { data -> List(capacity) { data } }.flatten().chunkToSize(size, transform)
10 | }
11 |
12 | internal fun Iterable.chunkToSize(size: Int, transform: (List) -> T): List {
13 | val chunkSize = count() / size
14 | val remainder = count() % size
15 | val remainderIndex = ceil(count().safeDiv(remainder)).roundToInt()
16 | val chunkIteration = filterIndexed { index, _ ->
17 | remainderIndex == 0 || index % remainderIndex != 0
18 | }.chunked(chunkSize, transform)
19 | return when (size) {
20 | chunkIteration.count() -> chunkIteration
21 | else -> chunkIteration.chunkToSize(size, transform)
22 | }
23 | }
24 |
25 | internal fun Iterable.normalize(min: Float, max: Float): List {
26 | return map { (max - min) * ((it - min()) safeDiv (max() - min())) + min }
27 | }
28 |
29 | private fun Int.safeDiv(value: Int): Float {
30 | return if (value == 0) return 0F else this / value.toFloat()
31 | }
32 |
33 | private infix fun Float.safeDiv(value: Float): Float {
34 | return if (value == 0f) return 0F else this / value
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/spica27/spicamusic/widget/audio_seekbar/WaveformAlignment.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic.widget.audio_seekbar
2 |
3 |
4 | // 位置对齐
5 | enum class WaveformAlignment {
6 | Top, Center, Bottom
7 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/default_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/drawable/default_cover.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_app.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_audio_line.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_dvd.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_new.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_next.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pause.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_playlist_remove.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_plus.xml:
--------------------------------------------------------------------------------
1 |
7 |
14 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pre.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_remove.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/default_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/app/src/main/res/mipmap/default_cover.jpg
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #F44336
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SPICaMusic
3 | Settings
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
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 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/me/spica27/spicamusic/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package me.spica27.spicamusic
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | id 'com.google.dagger.hilt.android' version '2.56.2' apply false
6 | id 'com.google.devtools.ksp' version '2.1.21-2.0.1' apply false
7 |
8 | }
--------------------------------------------------------------------------------
/decoder_flac/README.md:
--------------------------------------------------------------------------------
1 | # Flac decoder module
2 |
3 | The Flac module provides `FlacExtractor` and `LibflacAudioRenderer`, which use
4 | libFLAC (the Flac decoding library) to extract and decode FLAC audio.
5 |
6 | ## License note
7 |
8 | Please note that whilst the code in this repository is licensed under
9 | [Apache 2.0][], using this module also requires building and including one or
10 | more external libraries as described below. These are licensed separately.
11 |
12 | [Apache 2.0]: ../../LICENSE
13 |
14 | ## Build instructions (Linux, macOS)
15 |
16 | To use the module you need to clone this GitHub project and depend on its
17 | modules locally. Instructions for doing this can be found in the
18 | [top level README][].
19 |
20 | In addition, it's necessary to build the module's native components as follows:
21 |
22 | * Set the following environment variables:
23 |
24 | ```
25 | cd ""
26 | FLAC_MODULE_PATH="$(pwd)/libraries/decoder_flac/src/main"
27 | ```
28 |
29 | * Download the [Android NDK][] and set its location in an environment variable.
30 | This build configuration has been tested on NDK r21.
31 |
32 | ```
33 | NDK_PATH=""
34 | ```
35 |
36 | * Download and extract flac-1.3.2 as "${FLAC_MODULE_PATH}/jni/flac" folder:
37 |
38 | ```
39 | cd "${FLAC_MODULE_PATH}/jni" && \
40 | curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.2.tar.xz | tar xJ && \
41 | mv flac-1.3.2 flac
42 | ```
43 |
44 | * Build the JNI native libraries from the command line:
45 |
46 | ```
47 | cd "${FLAC_MODULE_PATH}"/jni && \
48 | ${NDK_PATH}/ndk-build APP_ABI=all -j4
49 | ```
50 |
51 | [top level README]: ../../README.md
52 |
53 | [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
54 |
55 | ## Build instructions (Windows)
56 |
57 | We do not provide support for building this module on Windows, however it should
58 | be possible to follow the Linux instructions in [Windows PowerShell][].
59 |
60 | [Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
61 |
62 | ## Using the module
63 |
64 | Once you've followed the instructions above to check out, build and depend on
65 | the module, the next step is to tell ExoPlayer to use the extractor and/or
66 | renderer.
67 |
68 | ### Using `FlacExtractor`
69 |
70 | `FlacExtractor` is used via `ProgressiveMediaSource`. If you're using
71 | `DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read
72 | `.flac` files. If you're not using `DefaultExtractorsFactory`, return a
73 | `FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation.
74 |
75 | ### Using `LibflacAudioRenderer` with ExoPlayer
76 |
77 | * If you're passing a `DefaultRenderersFactory` to `ExoPlayer.Builder`, you
78 | can enable using the module by setting the `extensionRendererMode` parameter
79 | of the `DefaultRenderersFactory` constructor to
80 | `EXTENSION_RENDERER_MODE_ON`. This will use `LibflacAudioRenderer` for
81 | playback if `MediaCodecAudioRenderer` doesn't support the input format. Pass
82 | `EXTENSION_RENDERER_MODE_PREFER` to give `LibflacAudioRenderer` priority
83 | over `MediaCodecAudioRenderer`.
84 | * If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer`
85 | to the output list in `buildAudioRenderers`. ExoPlayer will use the first
86 | `Renderer` in the list that supports the input media format.
87 | * If you've implemented your own `RenderersFactory`, return a
88 | `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use
89 | the first `Renderer` in the returned array that supports the input media
90 | format.
91 | * If you're using `ExoPlayer.Builder`, pass a `LibflacAudioRenderer` in the
92 | array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list
93 | that supports the input media format.
94 |
95 | Note: These instructions assume you're using `DefaultTrackSelector`. If you have
96 | a custom track selector the choice of `Renderer` is up to your implementation,
97 | so you need to make sure you are passing an `LibflacAudioRenderer` to the
98 | player, then implement your own logic to use the renderer for a given track.
99 |
100 | ## Links
101 |
102 | * [Troubleshooting using decoding extensions][]
103 |
104 | [Troubleshooting using decoding extensions]: https://developer.android.com/media/media3/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback
105 |
--------------------------------------------------------------------------------
/decoder_flac/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | //apply plugin: 'com.novoda.bintray-release'
3 |
4 | android {
5 | compileSdkVersion 34
6 |
7 | defaultConfig {
8 | namespace "androidx.media3.decoder.flac"
9 | minSdkVersion 24
10 | targetSdkVersion 33
11 |
12 | consumerProguardFiles "consumer-rules.pro"
13 |
14 | sourceSets {
15 | main {
16 | jniLibs.srcDir 'src/main/libs'
17 | jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
18 | }
19 | }
20 |
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | }
26 |
27 | buildTypes {
28 | release {
29 | minifyEnabled false
30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
31 | }
32 | }
33 | }
34 |
35 | dependencies {
36 | implementation fileTree(dir: "libs", include: ["*.jar"])
37 | // 播放器
38 | def media3_version = "1.4.1"
39 | // For media playback using ExoPlayer
40 | implementation "androidx.media3:media3-exoplayer:$media3_version"
41 | // implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
42 | implementation "androidx.media:media:1.7.0"
43 | compileOnly 'org.checkerframework:checker-qual:3.3.0'
44 | }
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/1d03ef9414a0bc197928d8d7193223af/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/androidx/media3/decoder/flac/FlacBinarySearchSeeker$1.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/1d03ef9414a0bc197928d8d7193223af/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/androidx/media3/decoder/flac/FlacBinarySearchSeeker$1.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/1d03ef9414a0bc197928d8d7193223af/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/androidx/media3/decoder/flac/FlacExtractor.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/1d03ef9414a0bc197928d8d7193223af/transformed/bundleLibRuntimeToDirRelease/bundleLibRuntimeToDirRelease_dex/androidx/media3/decoder/flac/FlacExtractor.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/31546844da043d1214a3e0db40582720/results.bin:
--------------------------------------------------------------------------------
1 | o/bundleLibRuntimeToDirDebug
2 |
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/9f269d71b886e2dd8426bc057a192b3c/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacDecoderJni$FlacFrameDecodeException.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/9f269d71b886e2dd8426bc057a192b3c/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacDecoderJni$FlacFrameDecodeException.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/9f269d71b886e2dd8426bc057a192b3c/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacDecoderJni.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/9f269d71b886e2dd8426bc057a192b3c/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacDecoderJni.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/9f269d71b886e2dd8426bc057a192b3c/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/package-info.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/9f269d71b886e2dd8426bc057a192b3c/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/package-info.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/a5958f52ec8a25958ae333556be3547d/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacBinarySearchSeeker.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/a5958f52ec8a25958ae333556be3547d/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacBinarySearchSeeker.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/e64e43cc4410128874ea299d3e4cc1ae/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacBinarySearchSeeker.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/e64e43cc4410128874ea299d3e4cc1ae/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacBinarySearchSeeker.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/eeb65c14c090fc702a9eb4b02b51640b/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacBinarySearchSeeker$FlacTimestampSeeker.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/eeb65c14c090fc702a9eb4b02b51640b/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacBinarySearchSeeker$FlacTimestampSeeker.dex
--------------------------------------------------------------------------------
/decoder_flac/build/.transforms/fb458d17bb1ab52cb1b4e8f8aa3aa7c8/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacDecoderJni.dex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/.transforms/fb458d17bb1ab52cb1b4e8f8aa3aa7c8/transformed/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacDecoderJni.dex
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/desugar_graph/debugAndroidTest/dexBuilderDebugAndroidTest/out/currentProject/dirs_bucket_0/graph.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/desugar_graph/debugAndroidTest/dexBuilderDebugAndroidTest/out/currentProject/dirs_bucket_0/graph.bin
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/desugar_graph/debugAndroidTest/dexBuilderDebugAndroidTest/out/currentProject/jar_21b782bae186444e85dc99b838ca35529d673440025a33f4918e6efb26fc32ea_bucket_1/graph.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/desugar_graph/debugAndroidTest/dexBuilderDebugAndroidTest/out/currentProject/jar_21b782bae186444e85dc99b838ca35529d673440025a33f4918e6efb26fc32ea_bucket_1/graph.bin
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest-mergeJavaRes/zip-cache/4JlYajEDSkuwp6dx_XQjkFvUNE8=:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/incremental/debugAndroidTest-mergeJavaRes/zip-cache/4JlYajEDSkuwp6dx_XQjkFvUNE8=
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest-mergeJavaRes/zip-cache/98ai+Hnq3fTWBmflSOJdI6UZ6Ns=:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/incremental/debugAndroidTest-mergeJavaRes/zip-cache/98ai+Hnq3fTWBmflSOJdI6UZ6Ns=
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-az/values-az.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Endirmə tamamlandı
4 | Endirin
5 | Endirilir
6 | Endirmə alınmadı
7 | Endirmələr
8 | Endirmə durdurulub
9 | Endirmələr şəbəkəni gözləyir
10 | Endirmələr WiFi şəbəkəsini gözləyir
11 | Endirilənlər silinir
12 | "999+"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-es/values-es.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Descarga de archivos completado
4 | Descargar
5 | Descargando
6 | No se ha podido descargar
7 | Descargas
8 | Descargas pausadas
9 | Descargas en espera de red
10 | Descargas en espera de Wi-Fi
11 | Quitando descargas
12 | "999+"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-fr/values-fr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Téléchargement terminé
4 | Télécharger
5 | Téléchargement…
6 | Échec du téléchargement
7 | Téléchargements
8 | Téléchargements mis en pause
9 | Téléchargements en attente de réseau
10 | Téléchargements en attente de Wi-Fi
11 | Suppression des téléchargements…
12 | "999+"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-ka/values-ka.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ჩამოტვირთვა დასრულდა
4 | ჩამოტვირთვა
5 | მიმდინარეობს ჩამოტვირთვა
6 | ჩამოტვირთვა ვერ მოხერხდა
7 | ჩამოტვირთვები
8 | ჩამოტვირთვები დაპაუზებულია
9 | ჩამოტვირთვები ქსელს ელოდება
10 | ჩამოტვირთვები Wi-Fi-ს ელოდება
11 | მიმდინარეობს ჩამოტვირთვების ამოშლა
12 | "999+"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-th/values-th.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | การดาวน์โหลดเสร็จสมบูรณ์
4 | ดาวน์โหลด
5 | กำลังดาวน์โหลด
6 | การดาวน์โหลดล้มเหลว
7 | ดาวน์โหลด
8 | การดาวน์โหลดหยุดชั่วคราว
9 | กำลังรอเครือข่ายเพื่อดาวน์โหลด
10 | กำลังรอ Wi-Fi เพื่อดาวน์โหลด
11 | กำลังนำรายการที่ดาวน์โหลดออก
12 | "999+"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-tr/values-tr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | İndirme işlemi tamamlandı
4 | İndir
5 | İndiriliyor
6 | İndirilemedi
7 | İndirilenler
8 | İndirmeler duraklatıldı
9 | İndirmeler için ağ bekleniyor
10 | İndirmeler için kablosuz ağ bekleniyor
11 | İndirilenler kaldırılıyor
12 | "999+"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-ur/values-ur.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ڈاؤن لوڈ مکمل ہو گیا
4 | ڈاؤن لوڈ کریں
5 | ڈاؤن لوڈ ہو رہا ہے
6 | ڈاؤن لوڈ ناکام ہو گیا
7 | ڈاؤن لوڈز
8 | ڈاؤن لوڈز موقوف ہو گئے
9 | ڈاؤن لوڈز نیٹ ورک کے منتظر ہیں
10 | ڈاؤن لوڈز WiFi کے منتظر ہیں
11 | ڈاؤن لوڈز کو ہٹایا جا رہا ہے
12 | "+999"
13 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/debugAndroidTest/mergeDebugAndroidTestResources/merged.dir/values-v24/values-v24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/packageDebugAndroidTest/tmp/debugAndroidTest/zip-cache/androidResources:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/incremental/packageDebugAndroidTest/tmp/debugAndroidTest/zip-cache/androidResources
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/incremental/release/packageReleaseResources/compile-file-map.properties:
--------------------------------------------------------------------------------
1 | #Sat Jun 21 18:46:04 HKT 2025
2 |
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libflacJNI.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libflacJNI.so
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-af_values-af.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-af_values-af.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-hu_values-hu.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-hu_values-hu.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-ms_values-ms.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-ms_values-ms.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-or_values-or.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-or_values-or.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-ro_values-ro.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-ro_values-ro.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-si_values-si.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-si_values-si.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-sk_values-sk.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-sk_values-sk.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-v24_values-v24.arsc.flat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/merged_res/debugAndroidTest/mergeDebugAndroidTestResources/values-v24_values-v24.arsc.flat
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-bn.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-bn/values-bn.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-bn\\values-bn.xml",
8 | "from": {
9 | "startLines": "2,3,4,5,6,7,8,9,10",
10 | "startColumns": "4,4,4,4,4,4,4,4,4",
11 | "startOffsets": "55,123,189,256,322,397,464,596,725",
12 | "endColumns": "67,65,66,65,74,66,131,128,88",
13 | "endOffsets": "118,184,251,317,392,459,591,720,809"
14 | }
15 | },
16 | {
17 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-bn\\values-bn.xml",
18 | "from": {
19 | "startLines": "2",
20 | "startColumns": "4",
21 | "startOffsets": "55",
22 | "endColumns": "100",
23 | "endOffsets": "151"
24 | },
25 | "to": {
26 | "startLines": "11",
27 | "startColumns": "4",
28 | "startOffsets": "814",
29 | "endColumns": "100",
30 | "endOffsets": "910"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-bs.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-bs/values-bs.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-bs\\values-bs.xml",
8 | "from": {
9 | "startLines": "2,3,4,5,6,7,8,9,10",
10 | "startColumns": "4,4,4,4,4,4,4,4,4",
11 | "startOffsets": "55,130,191,256,329,408,481,581,662",
12 | "endColumns": "74,60,64,72,78,72,99,80,72",
13 | "endOffsets": "125,186,251,324,403,476,576,657,730"
14 | }
15 | },
16 | {
17 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-bs\\values-bs.xml",
18 | "from": {
19 | "startLines": "2",
20 | "startColumns": "4",
21 | "startOffsets": "55",
22 | "endColumns": "100",
23 | "endOffsets": "151"
24 | },
25 | "to": {
26 | "startLines": "11",
27 | "startColumns": "4",
28 | "startOffsets": "735",
29 | "endColumns": "100",
30 | "endOffsets": "831"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-en-rGB.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-en-rGB/values-en-rGB.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-en-rGB\\values-en-rGB.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "702",
19 | "endColumns": "100",
20 | "endOffsets": "798"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-en-rGB\\values-en-rGB.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,125,187,252,316,393,458,548,633",
29 | "endColumns": "69,61,64,63,76,64,89,84,68",
30 | "endOffsets": "120,182,247,311,388,453,543,628,697"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-iw.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-iw/values-iw.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-iw\\values-iw.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "669",
19 | "endColumns": "100",
20 | "endOffsets": "765"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-iw\\values-iw.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,120,179,246,311,385,447,527,607",
29 | "endColumns": "64,58,66,64,73,61,79,79,61",
30 | "endOffsets": "115,174,241,306,380,442,522,602,664"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-lo.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-lo/values-lo.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-lo\\values-lo.xml",
8 | "from": {
9 | "startLines": "2,3,4,5,6,7,8,9,10",
10 | "startColumns": "4,4,4,4,4,4,4,4,4",
11 | "startOffsets": "55,124,185,251,316,391,461,553,640",
12 | "endColumns": "68,60,65,64,74,69,91,86,71",
13 | "endOffsets": "119,180,246,311,386,456,548,635,707"
14 | }
15 | },
16 | {
17 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-lo\\values-lo.xml",
18 | "from": {
19 | "startLines": "2",
20 | "startColumns": "4",
21 | "startOffsets": "55",
22 | "endColumns": "100",
23 | "endOffsets": "151"
24 | },
25 | "to": {
26 | "startLines": "11",
27 | "startColumns": "4",
28 | "startOffsets": "712",
29 | "endColumns": "100",
30 | "endOffsets": "808"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-lt.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-lt/values-lt.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-lt\\values-lt.xml",
8 | "from": {
9 | "startLines": "2,3,4,5,6,7,8,9,10",
10 | "startColumns": "4,4,4,4,4,4,4,4,4",
11 | "startOffsets": "55,136,200,267,335,416,490,587,682",
12 | "endColumns": "80,63,66,67,80,73,96,94,74",
13 | "endOffsets": "131,195,262,330,411,485,582,677,752"
14 | }
15 | },
16 | {
17 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-lt\\values-lt.xml",
18 | "from": {
19 | "startLines": "2",
20 | "startColumns": "4",
21 | "startOffsets": "55",
22 | "endColumns": "100",
23 | "endOffsets": "151"
24 | },
25 | "to": {
26 | "startLines": "11",
27 | "startColumns": "4",
28 | "startOffsets": "757",
29 | "endColumns": "100",
30 | "endOffsets": "853"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-mk.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-mk/values-mk.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-mk\\values-mk.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "720",
19 | "endColumns": "100",
20 | "endOffsets": "816"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-mk\\values-mk.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,125,186,250,318,395,468,557,642",
29 | "endColumns": "69,60,63,67,76,72,88,84,77",
30 | "endOffsets": "120,181,245,313,390,463,552,637,715"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-pt/values-pt.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-pt\\values-pt.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "741",
19 | "endColumns": "100",
20 | "endOffsets": "837"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-pt\\values-pt.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,125,195,267,333,410,477,578,671",
29 | "endColumns": "69,69,71,65,76,66,100,92,69",
30 | "endOffsets": "120,190,262,328,405,472,573,666,736"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-sv.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-sv/values-sv.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-sv\\values-sv.xml",
8 | "from": {
9 | "startLines": "2,3,4,5,6,7,8,9,10",
10 | "startColumns": "4,4,4,4,4,4,4,4,4",
11 | "startOffsets": "55,128,191,255,330,411,485,579,665",
12 | "endColumns": "72,62,63,74,80,73,93,85,72",
13 | "endOffsets": "123,186,250,325,406,480,574,660,733"
14 | }
15 | },
16 | {
17 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-sv\\values-sv.xml",
18 | "from": {
19 | "startLines": "2",
20 | "startColumns": "4",
21 | "startOffsets": "55",
22 | "endColumns": "100",
23 | "endOffsets": "151"
24 | },
25 | "to": {
26 | "startLines": "11",
27 | "startColumns": "4",
28 | "startOffsets": "738",
29 | "endColumns": "100",
30 | "endOffsets": "834"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-th.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-th/values-th.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-th\\values-th.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "736",
19 | "endColumns": "100",
20 | "endOffsets": "832"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-th\\values-th.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,131,194,262,330,407,480,571,657",
29 | "endColumns": "75,62,67,67,76,72,90,85,78",
30 | "endOffsets": "126,189,257,325,402,475,566,652,731"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-uk.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-uk/values-uk.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-uk\\values-uk.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "743",
19 | "endColumns": "100",
20 | "endOffsets": "839"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-uk\\values-uk.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,129,194,262,333,413,486,579,668",
29 | "endColumns": "73,64,67,70,79,72,92,88,74",
30 | "endOffsets": "124,189,257,328,408,481,574,663,738"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/merged_res_blame_folder/debugAndroidTest/mergeDebugAndroidTestResources/out/multi-v2/values-zh-rCN.json:
--------------------------------------------------------------------------------
1 | {
2 | "logs": [
3 | {
4 | "outputFile": "androidx.media3.decoder.flac.test.decoder_flac-mergeDebugAndroidTestResources-6:/values-zh-rCN/values-zh-rCN.xml",
5 | "map": [
6 | {
7 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\054f7b3e2a5b04cf7adca0dd328408d1\\transformed\\core-1.9.0\\res\\values-zh-rCN\\values-zh-rCN.xml",
8 | "from": {
9 | "startLines": "2",
10 | "startColumns": "4",
11 | "startOffsets": "55",
12 | "endColumns": "100",
13 | "endOffsets": "151"
14 | },
15 | "to": {
16 | "startLines": "11",
17 | "startColumns": "4",
18 | "startOffsets": "616",
19 | "endColumns": "100",
20 | "endOffsets": "712"
21 | }
22 | },
23 | {
24 | "source": "C:\\Users\\SPICa27\\.gradle\\caches\\transforms-4\\41cab7c59e0ff0ec896454150715464b\\transformed\\media3-exoplayer-1.4.1\\res\\values-zh-rCN\\values-zh-rCN.xml",
25 | "from": {
26 | "startLines": "2,3,4,5,6,7,8,9,10",
27 | "startColumns": "4,4,4,4,4,4,4,4,4",
28 | "startOffsets": "55,111,167,225,278,350,404,479,557",
29 | "endColumns": "55,55,57,52,71,53,74,77,58",
30 | "endOffsets": "106,162,220,273,345,399,474,552,611"
31 | }
32 | }
33 | ]
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt:
--------------------------------------------------------------------------------
1 | 0 Warning/Error
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/nested_resources_validation_report/debugAndroidTest/generateDebugAndroidTestResources/nestedResourcesValidationReport.txt:
--------------------------------------------------------------------------------
1 | 0 Warning/Error
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacExtractor.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/FlacExtractor.class
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/package-info.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/androidx/media3/decoder/flac/package-info.class
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/androidx/media3/decoder/flac/FlacBinarySearchSeeker.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/androidx/media3/decoder/flac/FlacBinarySearchSeeker.class
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/androidx/media3/decoder/flac/FlacDecoder.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease/androidx/media3/decoder/flac/FlacDecoder.class
--------------------------------------------------------------------------------
/decoder_flac/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar
--------------------------------------------------------------------------------
/decoder_flac/build/tmp/compileDebugAndroidTestJavaWithJavac/previous-compilation-data.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/tmp/compileDebugAndroidTestJavaWithJavac/previous-compilation-data.bin
--------------------------------------------------------------------------------
/decoder_flac/build/tmp/compileDebugUnitTestJavaWithJavac/previous-compilation-data.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/build/tmp/compileDebugUnitTestJavaWithJavac/previous-compilation-data.bin
--------------------------------------------------------------------------------
/decoder_flac/proguard-rules.txt:
--------------------------------------------------------------------------------
1 | # Proguard rules specific to the Flac extension.
2 |
3 | # This prevents the names of native methods from being obfuscated.
4 | -keepclasseswithmembernames class * {
5 | native ;
6 | }
7 |
8 | # Some members of these classes are being accessed from native methods. Keep them unobfuscated.
9 | -keep class androidx.media3.decoder.flac.FlacDecoderJni {
10 | *;
11 | }
12 | -keep class androidx.media3.extractor.FlacStreamMetadata {
13 | *;
14 | }
15 | -keep class androidx.media3.extractor.metadata.flac.PictureFrame {
16 | *;
17 | }
18 |
--------------------------------------------------------------------------------
/decoder_flac/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorSeekTest.java:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorSeekTest.java
--------------------------------------------------------------------------------
/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorTest.java:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorTest.java
--------------------------------------------------------------------------------
/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacPlaybackTest.java:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacPlaybackTest.java
--------------------------------------------------------------------------------
/decoder_flac/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/decoder_flac/src/main/java/androidx/media3/decoder/flac/FlacDecoderException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package androidx.media3.decoder.flac;
17 |
18 | import androidx.media3.common.util.UnstableApi;
19 | import androidx.media3.decoder.DecoderException;
20 |
21 | /** Thrown when an Flac decoder error occurs. */
22 | @UnstableApi
23 | public final class FlacDecoderException extends DecoderException {
24 |
25 | /* package */ FlacDecoderException(String message) {
26 | super(message);
27 | }
28 |
29 | /* package */ FlacDecoderException(String message, Throwable cause) {
30 | super(message, cause);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/decoder_flac/src/main/java/androidx/media3/decoder/flac/FlacLibrary.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package androidx.media3.decoder.flac;
17 |
18 | import androidx.media3.common.MediaLibraryInfo;
19 | import androidx.media3.common.util.LibraryLoader;
20 | import androidx.media3.common.util.UnstableApi;
21 |
22 | /** Configures and queries the underlying native library. */
23 | @UnstableApi
24 | public final class FlacLibrary {
25 |
26 | private static final LibraryLoader LOADER =
27 | new LibraryLoader("flacJNI") {
28 | @Override
29 | protected void loadLibrary(String name) {
30 | System.loadLibrary(name);
31 | }
32 | };
33 |
34 | static {
35 | MediaLibraryInfo.registerModule("media3.decoder.flac");
36 | }
37 |
38 | private FlacLibrary() {}
39 |
40 | /**
41 | * Override the names of the Flac native libraries. If an application wishes to call this method,
42 | * it must do so before calling any other method defined by this class, and before instantiating
43 | * any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances.
44 | *
45 | * @param libraries The names of the Flac native libraries.
46 | */
47 | public static void setLibraries(String... libraries) {
48 | LOADER.setLibraries(libraries);
49 | }
50 |
51 | /** Returns whether the underlying library is available, loading it if necessary. */
52 | public static boolean isAvailable() {
53 | return LOADER.isAvailable();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/decoder_flac/src/main/java/androidx/media3/decoder/flac/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | @NonNullApi
17 | package androidx.media3.decoder.flac;
18 |
19 | import androidx.media3.common.util.NonNullApi;
20 |
--------------------------------------------------------------------------------
/decoder_flac/src/main/libs/arm64-v8a/libflacJNI.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/src/main/libs/arm64-v8a/libflacJNI.so
--------------------------------------------------------------------------------
/decoder_flac/src/main/libs/armeabi-v7a/libflacJNI.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/decoder_flac/src/main/libs/armeabi-v7a/libflacJNI.so
--------------------------------------------------------------------------------
/decoder_flac/src/test/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/decoder_flac/src/test/java/androidx/media3/decoder/flac/DefaultRenderersFactoryTest.java:
--------------------------------------------------------------------------------
1 | // /*
2 | // * Copyright (C) 2019 The Android Open Source Project
3 | // *
4 | // * Licensed under the Apache License, Version 2.0 (the "License");
5 | // * you may not use this file except in compliance with the License.
6 | // * You may obtain a copy of the License at
7 | // *
8 | // * http://www.apache.org/licenses/LICENSE-2.0
9 | // *
10 | // * Unless required by applicable law or agreed to in writing, software
11 | // * distributed under the License is distributed on an "AS IS" BASIS,
12 | // * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // * See the License for the specific language governing permissions and
14 | // * limitations under the License.
15 | // */
16 | // package androidx.media3.decoder.flac;
17 | //
18 | // import androidx.media3.common.C;
19 | // import androidx.media3.test.utils.DefaultRenderersFactoryAsserts;
20 | // import androidx.test.ext.junit.runners.AndroidJUnit4;
21 | // import org.junit.Test;
22 | // import org.junit.runner.RunWith;
23 | //
24 | // /** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibflacAudioRenderer}. */
25 | // @RunWith(AndroidJUnit4.class)
26 | // public final class DefaultRenderersFactoryTest {
27 | //
28 | // @Test
29 | // public void createRenderers_instantiatesFlacRenderer() {
30 | // DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
31 | // LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
32 | // }
33 | // }
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.injected.testOnly=false
25 | ksp.useKSP2=false
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | accompanistVersion = "0.36.0"
3 | adaptiveVersion = "1.2.0-alpha06"
4 | agp = "8.10.1"
5 | coilCompose = "3.0.0-rc01"
6 | datastorePreferencesVersion = "1.1.7"
7 | hiltAndroidVersion = "2.56.2"
8 | hiltNavigationComposeVersion = "1.3.0-alpha01"
9 | kotlin = "2.1.21"
10 | coreKtx = "1.16.0"
11 | junit = "4.13.2"
12 | junitVersion = "1.2.1"
13 | espressoCore = "3.6.1"
14 | lifecycleRuntimeKtx = "2.9.1"
15 | activityCompose = "1.10.1"
16 | composeBom = "2025.06.01"
17 | constraintlayout = "2.2.1"
18 | media3ExoplayerVersion = "1.4.1"
19 | mediaVersion = "1.7.0"
20 | noiseVersion = "2.0.0"
21 | timberVersion = "5.0.1"
22 | nav3Core = "1.0.0-alpha04"
23 | material3AdaptiveNav3 = "1.1.0"
24 | lifecycleViewmodelNav3 = "1.0.0-alpha02"
25 | kotlinSerialization = "2.1.21"
26 | kotlinxSerializationCore = "1.8.1"
27 |
28 | [libraries]
29 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistVersion" }
30 | androidx-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptiveVersion" }
31 | androidx-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveVersion" }
32 | androidx-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptiveVersion" }
33 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
34 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferencesVersion" }
35 | androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationComposeVersion" }
36 | androidx-media = { module = "androidx.media:media", version.ref = "mediaVersion" }
37 | androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3ExoplayerVersion" }
38 | coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
39 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidVersion" }
40 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroidVersion" }
41 | junit = { group = "junit", name = "junit", version.ref = "junit" }
42 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
43 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
44 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
45 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
46 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
47 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
48 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
49 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
50 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
51 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
52 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
53 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
54 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
55 | noise = { module = "com.github.paramsen:noise", version.ref = "noiseVersion" }
56 | timber = { module = "com.jakewharton.timber:timber", version.ref = "timberVersion" }
57 | # Core Navigation 3 libraries
58 | androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
59 | androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
60 | # Optional add-on libraries
61 | androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
62 | androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
63 | kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
64 |
65 |
66 | [plugins]
67 | android-application = { id = "com.android.application", version.ref = "agp" }
68 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
69 | jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"}
70 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
71 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 15 11:49:05 HKT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.14.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/img/Screenshot_20250621_231150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/img/Screenshot_20250621_231150.png
--------------------------------------------------------------------------------
/img/Screenshot_20250621_231231.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/img/Screenshot_20250621_231231.png
--------------------------------------------------------------------------------
/img/Screenshot_20250621_231303.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/img/Screenshot_20250621_231303.png
--------------------------------------------------------------------------------
/img/Screenshot_20250621_231331.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/img/Screenshot_20250621_231331.png
--------------------------------------------------------------------------------
/key.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangSpica27/SPICaMusic_Android/bb787990561f5ee803fa7bb4b41af7b76ef5d7e1/key.jks
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | maven { url "https://jitpack.io" }
4 | maven { url 'https://maven.aliyun.com/repository/central' }
5 | maven { url 'https://maven.aliyun.com/repository/google' }
6 | maven { url 'https://maven.aliyun.com/repository/public' }
7 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
8 | google {
9 | content {
10 | includeGroupByRegex("com\\.android.*")
11 | includeGroupByRegex("com\\.google.*")
12 | includeGroupByRegex("androidx.*")
13 | }
14 | }
15 | mavenCentral()
16 | gradlePluginPortal()
17 | }
18 | }
19 | dependencyResolutionManagement {
20 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
21 | repositories {
22 | maven { url "https://jitpack.io" }
23 | maven { url 'https://maven.aliyun.com/repository/central' }
24 | maven { url 'https://maven.aliyun.com/repository/google' }
25 | maven { url 'https://maven.aliyun.com/repository/public' }
26 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
27 | google()
28 | mavenCentral()
29 | }
30 | }
31 |
32 | rootProject.name = "SPICaMusic"
33 | include ':app'
34 | include(':decoder_flac')
35 | //include(":starrysky")
36 |
--------------------------------------------------------------------------------