diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d2a7f7dc..af14e4e88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,8 @@ jobs: runs-on: windows-latest env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + permissions: + contents: write steps: - name: Checkout branch uses: actions/checkout@v4 @@ -57,35 +59,86 @@ jobs: path: build/windows/x64/runner/Release/ name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-windows - release-windows: + - name: Build Zip for Release + uses: thedoctor0/zip-release@master + if: startsWith(github.ref, 'refs/tags/v') + with: + type: "zip" + filename: Rune-${{ github.ref_name }}-windows-amd64.zip + directory: build/windows/x64/runner/Release/ + + - name: Release + uses: ncipollo/release-action@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + artifacts: "build/windows/x64/runner/Release/Rune-${{ github.ref_name }}-windows-amd64.zip" + allowUpdates: true + replacesArtifacts: false + omitBodyDuringUpdate: true + makeLatest: true + + build-windows-msix: + runs-on: windows-latest + env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" permissions: contents: write - needs: build-windows - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest steps: - - name: Download artifact - uses: actions/download-artifact@v4 + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 with: - pattern: Rune-*-windows - path: artifacts + channel: "stable" + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: "26.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install the Rust dependencies + run: cargo install 'flutter_rust_bridge_codegen' rinf protoc-gen-prost + + - name: Activate Protobuf + run: flutter pub global activate protoc_plugin + + - name: Flutter pub get + run: flutter pub get + + - name: Generate message files + run: rinf message + + - name: Build MSIX + run: dart run msix:create --release - uses: benjlevesque/short-sha@v3.0 id: short-sha with: length: 7 - - name: Build Zip for Release - uses: thedoctor0/zip-release@master + - name: Upload MSIX artifact + uses: actions/upload-artifact@v4 with: - type: "zip" - filename: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-windows-amd64.zip - directory: artifacts + path: build/windows/x64/runner/Release/rune.msix + name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-windows-msix + + - name: Rename MSIX + if: startsWith(github.ref, 'refs/tags/v') + run: | + mv build/windows/x64/runner/Release/rune.msix "build/windows/x64/runner/Release/Rune-${{ github.ref_name }}-windows-amd64.msix" - name: Release uses: ncipollo/release-action@v1 + if: startsWith(github.ref, 'refs/tags/v') with: - artifacts: "artifacts/*.zip" + artifacts: "build/windows/x64/runner/Release/Rune-${{ github.ref_name }}-windows-amd64.msix" allowUpdates: true replacesArtifacts: false omitBodyDuringUpdate: true @@ -93,6 +146,8 @@ jobs: build-linux: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout branch uses: actions/checkout@v4 @@ -144,9 +199,29 @@ jobs: path: build/linux/x64/release/bundle/ name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-linux + - name: Build Zip for Release + uses: thedoctor0/zip-release@master + if: startsWith(github.ref, 'refs/tags/v') + with: + type: "zip" + filename: Rune-${{ github.ref_name }}-linux-amd64.zip + directory: build/linux/x64/release/bundle/ + + - name: Release + uses: ncipollo/release-action@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + artifacts: "build/linux/x64/release/bundle/Rune-${{ github.ref_name }}-linux-amd64.zip" + allowUpdates: true + replacesArtifacts: false + omitBodyDuringUpdate: true + makeLatest: true + build-steam-sniper: runs-on: ubuntu-latest container: registry.gitlab.steamos.cloud/steamrt/sniper/sdk:beta + permissions: + contents: write steps: - name: Checkout branch uses: actions/checkout@v4 @@ -209,76 +284,28 @@ jobs: path: build/linux/x64/release/bundle/ name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-steam-sniper - release-linux: - permissions: - contents: write - needs: build-linux - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v4 - with: - pattern: Rune-*-linux - path: artifacts - - - uses: benjlevesque/short-sha@v3.0 - id: short-sha - with: - length: 7 - - name: Build Zip for Release uses: thedoctor0/zip-release@master + if: startsWith(github.ref, 'refs/tags/v') with: type: "zip" - filename: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-linux-amd64.zip - directory: artifacts + filename: Rune-${{ github.ref_name }}-steam-sniper-amd64.zip + directory: build/linux/x64/release/bundle/ - name: Release uses: ncipollo/release-action@v1 + if: startsWith(github.ref, 'refs/tags/v') with: + artifacts: "build/linux/x64/release/bundle/Rune-${{ github.ref_name }}-steam-sniper-amd64.zip" allowUpdates: true - artifacts: "artifacts/*.zip" - replacesArtifacts: false - omitBodyDuringUpdate: true - makeLatest: true - - release-steam-sniper: - permissions: - contents: write - needs: build-steam-sniper - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v4 - with: - pattern: Rune-*-steam-sniper - path: artifacts - - - uses: benjlevesque/short-sha@v3.0 - id: short-sha - with: - length: 7 - - - name: Build Zip for Release - uses: thedoctor0/zip-release@master - with: - type: "zip" - filename: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-steam-sniper-amd64.zip - directory: artifacts - - - name: Release - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifacts: "artifacts/*.zip" replacesArtifacts: false omitBodyDuringUpdate: true makeLatest: true build-macos: runs-on: macos-latest + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 @@ -356,11 +383,26 @@ jobs: install_name_tool -change /opt/homebrew/opt/lmdb/lib/liblmdb.dylib @executable_path/../Frameworks/liblmdb.dylib Rune.app/Contents/MacOS/Rune working-directory: build/macos/Build/Products/Release + - name: Rename DMG + run: | + mv temp_macos/*.dmg "temp_macos/Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS.dmg" + + - name: Rename ZIP + run: | + mv temp_macos/*.zip "temp_macos/Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS.zip" + - name: Upload artifact macOS DMG uses: actions/upload-artifact@v4 with: - path: "temp_macos/*.dmg" - name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS + path: temp_macos/Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS.dmg + name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS.dmg + + - name: Upload artifact macOS ZIP + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + path: temp_macos/Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS.zip + name: Rune-${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && format('-{0}', steps.short-sha.outputs.sha) || '' }}-macOS.zip - name: Clean up if: ${{ always() }} @@ -372,8 +414,29 @@ jobs: fi rm -f .env + - name: Release DMG + uses: ncipollo/release-action@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + artifacts: "temp_macos/Rune-${{ github.ref_name }}-macOS.dmg" + allowUpdates: true + replacesArtifacts: false + omitBodyDuringUpdate: true + makeLatest: true + + - name: Release ZIP + uses: ncipollo/release-action@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + artifacts: "temp_macos/Rune-${{ github.ref_name }}-macOS.zip" + allowUpdates: true + replacesArtifacts: false + omitBodyDuringUpdate: true + makeLatest: true + build-and-release-mac-app-store: runs-on: macos-latest + if: startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout repository uses: actions/checkout@v4 @@ -473,25 +536,3 @@ jobs: fi rm -f .env rm -f $RUNNER_TEMP/*.p8 - - release-macos: - permissions: - contents: write - needs: build-macos - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v4 - with: - pattern: Rune-*-macOS - path: artifacts - - - name: Release - uses: ncipollo/release-action@v1 - with: - artifacts: "artifacts/**/*.dmg" - allowUpdates: true - replacesArtifacts: false - omitBodyDuringUpdate: true - makeLatest: true diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 000000000..9a95949c2 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,111 @@ +name: Lint + +on: + pull_request: + branches: + - master + workflow_dispatch: + +permissions: + checks: write + pull-requests: write + +jobs: + rust-analyze: + name: Rust Analyze + runs-on: ubuntu-latest + + steps: + - name: Check Out + uses: actions/checkout@v4 + + - name: Setup Flutter Toolchain + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt, clippy + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y liblmdb0 jq alsa-base alsa-source librust-alsa-sys-dev libasound2-dev liblmdb-dev clang cmake ninja-build pkg-config libgtk-3-dev dpkg-dev libayatana-appindicator3-dev libnotify-dev + + - uses: Swatinem/rust-cache@v2 + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: "26.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install the Rust dependencies + run: cargo install 'flutter_rust_bridge_codegen' rinf protoc-gen-prost + + - name: Activate Protobuf + run: flutter pub global activate protoc_plugin + + - name: Flutter pub get + run: flutter pub get + + - name: Generate message files + run: rinf message + + - name: Run cargo fmt + run: cargo fmt -- --check + + - name: Run cargo clippy + run: | + rustup override set stable + cargo clippy -- -D warnings + + flutter-analyze: + name: Flutter analyze + runs-on: ubuntu-latest + + steps: + - name: Check Out + uses: actions/checkout@v4 + + - name: Setup Flutter Toolchain + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Setup Rust Toolchain + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: "26.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install the Rust dependencies + run: cargo install 'flutter_rust_bridge_codegen' rinf protoc-gen-prost + + - name: Activate Protobuf + run: flutter pub global activate protoc_plugin + + - name: Flutter pub get + run: flutter pub get + + - name: Generate message files + run: rinf message + + - name: Analyze Flutter + run: | + flutter analyze . + + - name: Dart Flutter + run: | + dart analyze . + + + + diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..8f572f175 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +ignore = ["native/hub/src/messages"] diff --git a/Cargo.lock b/Cargo.lock index b6a8d42e2..9b660a20c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3302,7 +3302,7 @@ dependencies = [ "once_cell", "rand", "raw-window-handle 0.5.2", - "rodio 0.20.0", + "rodio", "rustfft", "souvlaki", "tokio", @@ -3895,28 +3895,15 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" dependencies = [ "claxon", "cpal", "hound", "lewton", "symphonia", - "thiserror 1.0.65", -] - -[[package]] -name = "rodio" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b4839fa8524afa1829ee630ee1ef27b7bbe5bf0cde773456941c69060a4f74" -dependencies = [ - "cpal", - "hound", - "lewton", - "symphonia", ] [[package]] @@ -3965,7 +3952,7 @@ dependencies = [ "pathdiff", "playback", "prettytable", - "rodio 0.19.0", + "rodio", "rust_decimal", "serde", "serde_json", diff --git a/Justfile b/Justfile index 5d24694b7..cb14be260 100644 --- a/Justfile +++ b/Justfile @@ -1,3 +1,11 @@ +set dotenv-load + +lint: + cargo fmt -- --check + cargo clippy -- -D warnings + flutter analyze . + dart analyze . + macos-ci-all: macos-ci-clean macos-ci-install ./scripts/macos_2_build.sh ./scripts/macos_3_prepare_before_sign.sh diff --git a/analysis/src/analyzer/cpu_sub_analyzer.rs b/analysis/src/analyzer/cpu_sub_analyzer.rs index 25ed4b2c1..e855aadee 100644 --- a/analysis/src/analyzer/cpu_sub_analyzer.rs +++ b/analysis/src/analyzer/cpu_sub_analyzer.rs @@ -52,8 +52,9 @@ impl SubAnalyzer for CpuSubAnalyzer { core_analyzer.total_zcr += zcr(resampled_chunk); core_analyzer.total_energy += energy(resampled_chunk); - let start_idx = self.batch_cache_buffer_count * core_analyzer.window_size; - let buffer_slice = &mut self.fft_input_buffer[start_idx..start_idx + core_analyzer.window_size]; + let start_idx = self.batch_cache_buffer_count * core_analyzer.window_size; + let buffer_slice = + &mut self.fft_input_buffer[start_idx..start_idx + core_analyzer.window_size]; for (i, sample) in buffer_slice.iter_mut().enumerate() { *sample = resampled_chunk[i] * self.hanning_window[i]; } diff --git a/analysis/src/analyzer/gpu_sub_analyzer.rs b/analysis/src/analyzer/gpu_sub_analyzer.rs index aac98cb67..2d230d6b9 100644 --- a/analysis/src/analyzer/gpu_sub_analyzer.rs +++ b/analysis/src/analyzer/gpu_sub_analyzer.rs @@ -43,7 +43,8 @@ impl SubAnalyzer for GpuSubAnalyzer { core_analyzer.total_energy += energy(resampled_chunk); let start_idx = self.batch_cache_buffer_count * core_analyzer.window_size; - let buffer_slice = &mut self.batch_fft_buffer[start_idx..start_idx + core_analyzer.window_size]; + let buffer_slice = + &mut self.batch_fft_buffer[start_idx..start_idx + core_analyzer.window_size]; for (i, sample) in buffer_slice.iter_mut().enumerate() { *sample = Complex::new(resampled_chunk[i] * self.hanning_window[i], 0.0); } diff --git a/analysis/src/shared_utils/audio_metadata_reader.rs b/analysis/src/shared_utils/audio_metadata_reader.rs index a5c16dce8..b58c49f1f 100644 --- a/analysis/src/shared_utils/audio_metadata_reader.rs +++ b/analysis/src/shared_utils/audio_metadata_reader.rs @@ -36,14 +36,20 @@ pub fn get_format(file_path: &str) -> Result> { } pub fn get_codec_information(track: &Track) -> Result<(u32, f64), symphonia::core::errors::Error> { - let sample_rate = track - .codec_params - .sample_rate - .ok_or_else(|| symphonia::core::errors::Error::Unsupported("No sample rate found"))?; - let duration = track - .codec_params - .n_frames - .ok_or_else(|| symphonia::core::errors::Error::Unsupported("No duration found"))?; + let sample_rate = + track + .codec_params + .sample_rate + .ok_or(symphonia::core::errors::Error::Unsupported( + "No sample rate found", + ))?; + let duration = + track + .codec_params + .n_frames + .ok_or(symphonia::core::errors::Error::Unsupported( + "No duration found", + ))?; let time_base = track .codec_params diff --git a/analysis/src/shared_utils/computing_device.rs b/analysis/src/shared_utils/computing_device.rs index fe5b80032..013d31f0a 100644 --- a/analysis/src/shared_utils/computing_device.rs +++ b/analysis/src/shared_utils/computing_device.rs @@ -23,4 +23,4 @@ impl From<&str> for ComputingDevice { _ => ComputingDevice::Gpu, } } -} \ No newline at end of file +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7fb5e052b..ad993d1d8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,7 +9,7 @@ path = "src/lib.rs" [dependencies] futures = "0.3.30" -rodio = { version = "0.19.0", features = ["symphonia-all"] } +rodio = { version = "0.20.1", features = ["symphonia-all"] } tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] } database = { path = "../database" } metadata = { path = "../metadata" } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index ae8429c1e..1760852d3 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,5 +1,5 @@ pub mod analysis; pub mod index; +pub mod mix; pub mod playback; pub mod recommend; -pub mod mix; diff --git a/cli/src/main.rs b/cli/src/main.rs index 7e507d006..9ba759345 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -54,9 +54,13 @@ enum Commands { /// Play audio files in the library Play { - /// Currently, we only support play audio files randomly + /// The mode to play audio files #[arg()] mode: Option, + + /// The ID of the file to play (used with playById mode) + #[arg(short, long)] + id: Option, }, /// Recommend music @@ -170,7 +174,13 @@ async fn main() { index_audio_library(&main_db).await; } Commands::Analyze { computing_device } => { - analyze_audio_library(computing_device.as_str().into(), &main_db, &analysis_db, &path).await; + analyze_audio_library( + computing_device.as_str().into(), + &main_db, + &analysis_db, + &path, + ) + .await; } Commands::Info { file_ids } => { match get_metadata_summary_by_file_ids(&main_db, file_ids.to_vec()).await { @@ -205,13 +215,22 @@ async fn main() { } } } - Commands::Play { mode } => { - if mode.as_deref() == Some("random") { + // In the main function, update the match statement for Commands::Play + Commands::Play { mode, id } => match mode.as_deref() { + Some("random") => { play_random(&main_db, &canonicalized_path).await; - } else { + } + Some("id") => { + if let Some(file_id) = id { + play_by_id(&main_db, &canonicalized_path, *file_id).await; + } else { + error!("File ID is required for playById mode."); + } + } + _ => { info!("Mode not implemented!"); } - } + }, Commands::Recommend { item_id, file_path, @@ -263,4 +282,4 @@ async fn main() { } }, } -} \ No newline at end of file +} diff --git a/cli/src/playback.rs b/cli/src/playback.rs index 2013182bc..ca69b89ba 100644 --- a/cli/src/playback.rs +++ b/cli/src/playback.rs @@ -1,24 +1,35 @@ use dunce::canonicalize; +use futures::future::join_all; use log::{debug, error, info}; use std::path::Path; use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; use tokio::task; -use database::actions::file::get_random_files; +use database::actions::file::{get_file_by_id, get_random_files}; use database::connection::MainDbConnection; use playback::player::Player; -pub async fn play_random(main_db: &MainDbConnection, canonicalized_path: &Path) { +async fn play_files(main_db: &MainDbConnection, canonicalized_path: &Path, file_ids: Vec) { let player = Player::new(None); let player = Arc::new(Mutex::new(player)); - let files = match get_random_files(main_db, 30).await { - Ok(files) => files, - Err(e) => { - error!("Failed to get random files by: {}", e); - return; + let file_futures = file_ids.into_iter().map(|id| async move { + match get_file_by_id(main_db, id).await { + Ok(file) => Some(file), + Err(e) => { + error!("Failed to get file by id {}: {}", id, e); + None + } } - }; + }); + + let files: Vec = join_all(file_futures) + .await + .into_iter() + .filter_map(|file| file.flatten()) + .collect(); player.lock().unwrap().add_to_playlist( files @@ -52,4 +63,22 @@ pub async fn play_random(main_db: &MainDbConnection, canonicalized_path: &Path) ); } }); + + thread::sleep(Duration::from_millis(30000)); +} + +pub async fn play_random(main_db: &MainDbConnection, canonicalized_path: &Path) { + match get_random_files(main_db, 30).await { + Ok(files) => { + let file_ids = files.into_iter().map(|file| file.id).collect(); + play_files(main_db, canonicalized_path, file_ids).await; + } + Err(e) => { + error!("Failed to get random files: {}", e); + } + } +} + +pub async fn play_by_id(main_db: &MainDbConnection, canonicalized_path: &Path, id: i32) { + play_files(main_db, canonicalized_path, vec![id]).await; } diff --git a/database/src/actions/analysis.rs b/database/src/actions/analysis.rs index c53b871eb..9667e2671 100644 --- a/database/src/actions/analysis.rs +++ b/database/src/actions/analysis.rs @@ -420,7 +420,7 @@ pub async fn get_percentile( }; // .with_context(|| "Unable to get analysis value")?; - Ok(result.unwrap_or_default() as f32) + Ok(result.unwrap_or_default()) } pub async fn get_percentile_analysis_result( diff --git a/database/src/actions/file.rs b/database/src/actions/file.rs index 5513b64d4..ee6ba9614 100644 --- a/database/src/actions/file.rs +++ b/database/src/actions/file.rs @@ -5,7 +5,10 @@ use anyhow::Result; use metadata::describe::FileDescription; use rust_decimal::prelude::ToPrimitive; use sea_orm::entity::prelude::*; -use sea_orm::{ColumnTrait, EntityTrait, FromQueryResult, Order, QueryFilter, QueryOrder, QuerySelect, QueryTrait}; +use sea_orm::{ + ColumnTrait, EntityTrait, FromQueryResult, Order, QueryFilter, QueryOrder, QuerySelect, + QueryTrait, +}; use migration::{Func, SimpleExpr}; diff --git a/database/src/actions/index.rs b/database/src/actions/index.rs index d4d74889e..88ccd6e56 100644 --- a/database/src/actions/index.rs +++ b/database/src/actions/index.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, Error}; +use anyhow::{Error, Result}; use log::{error, info}; use sea_orm::{prelude::*, ActiveValue}; use sea_orm::{DatabaseConnection, Set, TransactionTrait}; @@ -167,10 +167,8 @@ pub async fn index_audio_library( let producer = async { loop { // Fetch the next batch of files - let files: Vec = cursor - .first(batch_size.try_into()?) - .all(main_db) - .await?; + let files: Vec = + cursor.first(batch_size.try_into()?).all(main_db).await?; if files.is_empty() { info!("No more files to process. Exiting loop."); diff --git a/database/src/actions/metadata.rs b/database/src/actions/metadata.rs index bdc1ed90a..97b95b2f5 100644 --- a/database/src/actions/metadata.rs +++ b/database/src/actions/metadata.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use regex::Regex; use anyhow::{bail, Context, Result}; use log::{debug, error, info}; +use regex::Regex; use rust_decimal::prelude::{FromPrimitive, ToPrimitive}; use sea_orm::entity::prelude::*; use sea_orm::{ActiveValue, ColumnTrait, EntityTrait, QueryFilter}; diff --git a/database/src/actions/recommendation.rs b/database/src/actions/recommendation.rs index 626b62a04..0dc41abe2 100644 --- a/database/src/actions/recommendation.rs +++ b/database/src/actions/recommendation.rs @@ -4,6 +4,7 @@ use std::num::NonZeroUsize; use anyhow::{bail, Context, Result}; use arroy::distances::Euclidean; use arroy::{Reader, Writer}; +use log::error; use rand::rngs::StdRng; use rand::SeedableRng; use rust_decimal::prelude::ToPrimitive; @@ -71,12 +72,20 @@ pub fn get_recommendation_by_parameter( let results = reader .nns_by_vector(&rtxn, &feature_vector, n, Some(search_k), None) - .with_context(|| "Failed to get recommendation by parameter")?; - - if results.is_empty() { - bail!("No results found for the given parameter") - } else { - Ok(results) + .with_context(|| "Failed to get recommendation by parameter"); + + match results { + Ok(results) => { + if results.is_empty() { + bail!("No results found for the given parameter") + } else { + Ok(results) + } + } + Err(e) => { + error!("{:#?}", e); + Ok(vec![]) + } } } diff --git a/database/src/connection.rs b/database/src/connection.rs index 4064c85cf..27bc5b82a 100644 --- a/database/src/connection.rs +++ b/database/src/connection.rs @@ -131,7 +131,7 @@ pub fn get_storage_info(lib_path: &str, db_path: Option<&str>) -> Result signal) { final newStatus = signal.message; if (!_isPlaybackStatusEqual(_playbackStatus, newStatus)) { + final bool isNewTrack = _playbackStatus.id != newStatus.id; + _playbackStatus.state = newStatus.state; _playbackStatus.progressSeconds = newStatus.progressSeconds; _playbackStatus.progressPercentage = newStatus.progressPercentage; @@ -59,9 +61,7 @@ class PlaybackStatusProvider with ChangeNotifier { _playbackStatus.ready = newStatus.ready; _playbackStatus.coverArtPath = newStatus.coverArtPath; - final bool isNewTrack = _playbackStatus.id != newStatus.id; - - if (isNewTrack && newStatus.state != "Stopped") { + if (isNewTrack) { ThemeColorManager().handleCoverArtColorChange(newStatus.id); SettingsManager().setValue(lastQueueIndexKey, newStatus.index); } diff --git a/lib/screens/settings_theme/settings_theme.dart b/lib/screens/settings_theme/settings_theme.dart index 07b53228b..1a5c8c785 100644 --- a/lib/screens/settings_theme/settings_theme.dart +++ b/lib/screens/settings_theme/settings_theme.dart @@ -10,12 +10,12 @@ import '../../utils/settings_manager.dart'; import '../../utils/update_color_mode.dart'; import '../../utils/theme_color_manager.dart'; import '../../utils/settings_page_padding.dart'; +import '../../widgets/tile/tile.dart'; import '../../widgets/settings/settings_box_toggle.dart'; import '../../widgets/settings/settings_block_title.dart'; import '../../widgets/settings/settings_box_combo_box.dart'; -import '../../widgets/tile/tile.dart'; -import '../../widgets/unavailable_page_on_band.dart'; import '../../widgets/navigation_bar/page_content_frame.dart'; +import '../../widgets/unavailable_page_on_band.dart'; import 'constants/colors.dart'; import 'constants/window_sizes.dart'; diff --git a/lib/utils/close_manager.dart b/lib/utils/close_manager.dart index 0e6d9bbfd..51abd903f 100644 --- a/lib/utils/close_manager.dart +++ b/lib/utils/close_manager.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:flutter/services.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:flutter_window_close/flutter_window_close.dart'; @@ -40,7 +41,12 @@ class CloseManager { close() { forceClose = true; - appWindow.close(); + + if (Platform.isMacOS) { + SystemNavigator.pop(); + } else { + appWindow.close(); + } } } diff --git a/lib/utils/theme_color_manager.dart b/lib/utils/theme_color_manager.dart index 5c0cfc089..a8049111e 100644 --- a/lib/utils/theme_color_manager.dart +++ b/lib/utils/theme_color_manager.dart @@ -38,6 +38,8 @@ class ThemeColorManager { // If dynamic colors are enabled and a song is currently playing, apply the cover color immediately if (_currentCoverArtId != null) { await handleCoverArtColorChange(_currentCoverArtId!); + } else { + appTheme.updateThemeColor(_userSelectedColor); } } else { // If dynamic colors are disabled, decide which color to use based on user settings @@ -52,6 +54,12 @@ class ThemeColorManager { // If dynamic colors are not enabled, update the theme based on user selection if (!_isDynamicColorEnabled) { appTheme.updateThemeColor(color); + } else { + if (_currentCoverArtId != null) { + handleCoverArtColorChange(_currentCoverArtId!); + } else { + appTheme.updateThemeColor(color); + } } } diff --git a/lib/widgets/playback_controller/fft_visualize.dart b/lib/widgets/playback_controller/fft_visualize.dart index 156519d1a..f8553c0aa 100644 --- a/lib/widgets/playback_controller/fft_visualize.dart +++ b/lib/widgets/playback_controller/fft_visualize.dart @@ -86,7 +86,7 @@ class FFTVisualizeState extends State _ticker = createTicker((Duration elapsed) { final now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastUpdateTime > 42) { + if (now - _lastUpdateTime > 168) { if (mounted) { final reduced = _currentFftValues.reduce((a, b) => a + b); if (!_hasData && reduced < 1e-2) { diff --git a/lib/widgets/title_bar/window_frame_for_windows.dart b/lib/widgets/title_bar/window_frame_for_windows.dart index 5f96ff7ab..fc0b4707d 100644 --- a/lib/widgets/title_bar/window_frame_for_windows.dart +++ b/lib/widgets/title_bar/window_frame_for_windows.dart @@ -1,12 +1,11 @@ import 'dart:io'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:flutter_fullscreen/flutter_fullscreen.dart'; import '../../main.dart'; +import '../../providers/full_screen.dart'; import '../../utils/router/navigation.dart'; import '../../utils/navigation/utils/escape_from_search.dart'; import 'drag_move_window_area.dart'; @@ -25,29 +24,7 @@ class WindowFrameForWindows extends StatefulWidget { State createState() => _WindowFrameForWindowsState(); } -class _WindowFrameForWindowsState extends State - with FullScreenListener { - bool isFullScreen = FullScreen.isFullScreen; - - @override - void initState() { - super.initState(); - FullScreen.addListener(this); - } - - @override - dispose() { - super.dispose(); - FullScreen.removeListener(this); - } - - @override - void onFullScreenChanged(bool enabled, SystemUiMode? systemUiMode) { - setState(() { - isFullScreen = enabled; - }); - } - +class _WindowFrameForWindowsState extends State { @override Widget build(BuildContext context) { if (!Platform.isWindows) { @@ -57,6 +34,8 @@ class _WindowFrameForWindowsState extends State final path = Provider.of(context).path; Provider.of(context); + final fullScreen = Provider.of(context); + final isSearch = path == '/search'; return RuneStack( @@ -84,7 +63,7 @@ class _WindowFrameForWindowsState extends State Expanded( child: DragMoveWindowArea(), ), - if (isFullScreen) + if (fullScreen.isFullScreen) WindowIconButton( onPressed: () { if (isSearch) { @@ -100,10 +79,10 @@ class _WindowFrameForWindowsState extends State ), ), ), - if (isFullScreen) + if (fullScreen.isFullScreen) WindowIconButton( onPressed: () { - FullScreen.setFullScreen(false); + fullScreen.setFullScreen(false); }, child: Center( child: Icon( @@ -112,7 +91,7 @@ class _WindowFrameForWindowsState extends State ), ), ), - if (!isFullScreen) + if (!fullScreen.isFullScreen) activeBreakpoint == DeviceType.zune || activeBreakpoint == DeviceType.belt ? Container() @@ -131,7 +110,7 @@ class _WindowFrameForWindowsState extends State ), ), ), - if (!isFullScreen) + if (!fullScreen.isFullScreen) WindowIconButton( onPressed: () async { appWindow.minimize(); @@ -145,7 +124,7 @@ class _WindowFrameForWindowsState extends State ), ), ), - if (!isFullScreen) + if (!fullScreen.isFullScreen) MouseRegion( onEnter: (event) async { // await platform.invokeMethod('maximumButtonEnter'); @@ -171,7 +150,7 @@ class _WindowFrameForWindowsState extends State ), ), ), - if (!isFullScreen) + if (!fullScreen.isFullScreen) WindowIconButton( onPressed: () { appWindow.close(); @@ -185,7 +164,7 @@ class _WindowFrameForWindowsState extends State ), ), ), - if (!isFullScreen) + if (!fullScreen.isFullScreen) appWindow.isMaximized ? SizedBox(width: 2) : SizedBox(width: 7), diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 3a5472549..270f583e5 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -737,10 +737,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = LG57TUQ726; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 078a5e81c..2851779bc 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -2,7 +2,8 @@ use lazy_static::lazy_static; use regex::Regex; lazy_static! { - static ref SPLITTERS: Vec<&'static str> = vec![", ", "; ", " × ", " x ", " / ", " ft.", " ft. ", " feat. " , " & "]; + static ref SPLITTERS: Vec<&'static str> = + vec![", ", "; ", " × ", " x ", " / ", " ft.", " ft. ", " feat. ", " & "]; static ref WHITELIST: Vec<&'static str> = vec![]; static ref SPLITTERS_REGEX: Regex = { let splitters_pattern = SPLITTERS diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 5bbe4beb0..a9ee8d430 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -1,6 +1,6 @@ +pub mod artist; +pub mod cover_art; pub mod crc; +pub mod describe; pub mod reader; pub mod scanner; -pub mod artist; -pub mod describe; -pub mod cover_art; \ No newline at end of file diff --git a/metadata/src/reader.rs b/metadata/src/reader.rs index 0f0886a38..313a71a2f 100644 --- a/metadata/src/reader.rs +++ b/metadata/src/reader.rs @@ -183,7 +183,11 @@ pub fn string_to_standard_tag_key(s: &str) -> Option { STRING_TO_STANDARD_TAG_KEY.get(s).cloned() } -fn push_tags(revision: &MetadataRevision, metadata_list: &mut Vec<(String, String)>, field_blacklist: &[&str]) { +fn push_tags( + revision: &MetadataRevision, + metadata_list: &mut Vec<(String, String)>, + field_blacklist: &[&str], +) { for tag in revision.tags() { let std_key = match tag.std_key { Some(standard_key) => standard_tag_key_to_string(standard_key), @@ -207,7 +211,10 @@ fn push_tags(revision: &MetadataRevision, metadata_list: &mut Vec<(String, Strin } } -pub fn get_metadata(file_path: &str, field_blacklist: Option>) -> Result> { +pub fn get_metadata( + file_path: &str, + field_blacklist: Option>, +) -> Result> { if !Path::new(file_path).exists() { bail!("File not found"); } @@ -228,13 +235,13 @@ pub fn get_metadata(file_path: &str, field_blacklist: Option>) -> Resu let meta_opts: MetadataOptions = Default::default(); // Probe the media source. - let mut probed = symphonia::default::get_probe() - .format(&hint, mss, &fmt_opts, &meta_opts)?; + let mut probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts)?; let mut format = probed.format; let mut metadata_list = Vec::new(); - let blacklist = field_blacklist.unwrap_or(vec!["encoded_by", "encoder", "comment", "description"]); + let blacklist = + field_blacklist.unwrap_or(vec!["encoded_by", "encoder", "comment", "description"]); if let Some(metadata_rev) = format.metadata().current() { push_tags(metadata_rev, &mut metadata_list, &blacklist); @@ -243,4 +250,4 @@ pub fn get_metadata(file_path: &str, field_blacklist: Option>) -> Resu } Ok(metadata_list) -} \ No newline at end of file +} diff --git a/migration/src/m20230701_000001_create_media_files_table.rs b/migration/src/m20230701_000001_create_media_files_table.rs index 025b68907..253beb0e9 100644 --- a/migration/src/m20230701_000001_create_media_files_table.rs +++ b/migration/src/m20230701_000001_create_media_files_table.rs @@ -34,11 +34,7 @@ impl MigrationTrait for Migration { .not_null(), ) .col(ColumnDef::new(MediaFiles::CoverArtId).integer().null()) - .col( - ColumnDef::new(MediaFiles::SampleRate) - .integer() - .not_null(), - ) + .col(ColumnDef::new(MediaFiles::SampleRate).integer().not_null()) .col(ColumnDef::new(MediaFiles::Duration).double().not_null()) .foreign_key( ForeignKey::create() diff --git a/migration/src/m20230728_000008_create_media_cover_art_table.rs b/migration/src/m20230728_000008_create_media_cover_art_table.rs index b67c7c3f9..facfa6d79 100644 --- a/migration/src/m20230728_000008_create_media_cover_art_table.rs +++ b/migration/src/m20230728_000008_create_media_cover_art_table.rs @@ -22,8 +22,16 @@ impl MigrationTrait for Migration { .auto_increment() .primary_key(), ) - .col(ColumnDef::new(MediaCoverArt::FileHash).char_len(64).not_null()) - .col(ColumnDef::new(MediaCoverArt::Binary).var_binary(16777216).not_null()) + .col( + ColumnDef::new(MediaCoverArt::FileHash) + .char_len(64) + .not_null(), + ) + .col( + ColumnDef::new(MediaCoverArt::Binary) + .var_binary(16777216) + .not_null(), + ) .to_owned(), ) .await @@ -42,5 +50,5 @@ pub enum MediaCoverArt { Id, FileHash, Binary, - PrimaryColor + PrimaryColor, } diff --git a/migration/src/m20230806_000009_create_artists_table.rs b/migration/src/m20230806_000009_create_artists_table.rs index 8cd5c5d72..7cadafaff 100644 --- a/migration/src/m20230806_000009_create_artists_table.rs +++ b/migration/src/m20230806_000009_create_artists_table.rs @@ -28,11 +28,7 @@ impl MigrationTrait for Migration { .not_null() .unique_key(), ) - .col( - ColumnDef::new(Artists::Group) - .string() - .not_null(), - ) + .col(ColumnDef::new(Artists::Group).string().not_null()) .to_owned(), ) .await diff --git a/migration/src/m20230806_000011_create_albums_table.rs b/migration/src/m20230806_000011_create_albums_table.rs index 9c53e90fc..e97bca26c 100644 --- a/migration/src/m20230806_000011_create_albums_table.rs +++ b/migration/src/m20230806_000011_create_albums_table.rs @@ -28,11 +28,7 @@ impl MigrationTrait for Migration { .not_null() .unique_key(), ) - .col( - ColumnDef::new(Albums::Group) - .string() - .not_null(), - ) + .col(ColumnDef::new(Albums::Group).string().not_null()) .to_owned(), ) .await diff --git a/migration/src/m20231029_000017_create_search_index.rs b/migration/src/m20231029_000017_create_search_index.rs index b00601fbe..c8386ea23 100644 --- a/migration/src/m20231029_000017_create_search_index.rs +++ b/migration/src/m20231029_000017_create_search_index.rs @@ -16,7 +16,7 @@ impl MigrationTrait for Migration { // ID is only for making sea-orm happy db.execute_unprepared( - "CREATE VIRTUAL TABLE search_index USING fts5(id, key, entry_type, doc);" + "CREATE VIRTUAL TABLE search_index USING fts5(id, key, entry_type, doc);", ) .await?; diff --git a/native/hub/Cargo.toml b/native/hub/Cargo.toml index e9340ec38..cb0301e06 100644 --- a/native/hub/Cargo.toml +++ b/native/hub/Cargo.toml @@ -30,7 +30,7 @@ num_cpus = "1.16.0" anyhow = { version = "1.0.89", features = ["backtrace"] } futures = "0.3.30" async-trait = "0.1.83" -sysinfo = { version = "0.31.4", features = ["windows"] } +sysinfo = { version = "0.31.4", features = ["windows", "apple-app-store"] } tracing-appender = "0.2.3" chrono = "0.4.38" windows = { version = "0.58.0", features = ["Services", "Services_Store" ] } diff --git a/native/hub/build.rs b/native/hub/build.rs index 6a65f62fe..f39ed85b1 100644 --- a/native/hub/build.rs +++ b/native/hub/build.rs @@ -19,7 +19,7 @@ fn main() -> Result<()> { .add_instructions(&rustc)? .emit()?; - #[cfg(target_os = "macos")] + #[cfg(target_os = "macos")] apple_bridge::build_apple_bridge_library(); Ok(()) diff --git a/native/hub/src/directory.rs b/native/hub/src/directory.rs index 7519ba036..fed69faef 100644 --- a/native/hub/src/directory.rs +++ b/native/hub/src/directory.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use anyhow::{Context, Result}; use rinf::DartSignal; - use database::actions::directory::get_directory_tree; use database::actions::directory::DirectoryTree; use database::connection::MainDbConnection; diff --git a/native/hub/src/lib.rs b/native/hub/src/lib.rs index 04125fdfd..b00b4d93d 100644 --- a/native/hub/src/lib.rs +++ b/native/hub/src/lib.rs @@ -1,4 +1,5 @@ mod analyze; +mod apple_bridge; mod collection; mod connection; mod cover_art; @@ -18,7 +19,6 @@ mod sfx; mod stat; mod system; mod utils; -mod apple_bridge; use std::sync::Arc; diff --git a/native/hub/src/license.rs b/native/hub/src/license.rs index 5385c0038..deda5db29 100644 --- a/native/hub/src/license.rs +++ b/native/hub/src/license.rs @@ -44,7 +44,7 @@ pub async fn check_store_license() -> Result, &'sta let bundle_id = bundle_id(); let mut is_active = false; let mut is_trial = false; - + if bundle_id == "ci.not.rune.appstore" { is_active = true; is_trial = false; diff --git a/native/hub/src/sfx.rs b/native/hub/src/sfx.rs index 637619e4d..8b30cac5c 100644 --- a/native/hub/src/sfx.rs +++ b/native/hub/src/sfx.rs @@ -11,6 +11,9 @@ pub async fn sfx_play_request( sfx_player: Arc>, dart_signal: DartSignal, ) -> Result<()> { - sfx_player.lock().await.load(dart_signal.message.path.into()); + sfx_player + .lock() + .await + .load(dart_signal.message.path.into()); Ok(()) } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index adde6fa6d..bcff9d552 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -18,7 +18,7 @@ tokio = { version = "1.40.0", features = [ "rt-multi-thread", "rt", ] } -rodio = { version = "0.20.0", default-features = false, features = [ +rodio = { version = "0.20.1", default-features = false, features = [ "vorbis", "wav", "symphonia-mp3", diff --git a/playback/src/android_utils.rs b/playback/src/android_utils.rs index 583765ad6..c149690de 100644 --- a/playback/src/android_utils.rs +++ b/playback/src/android_utils.rs @@ -8,7 +8,7 @@ use jni::{ sys::{jint, JNI_VERSION_1_6}, JavaVM, }; -use ndk_context::{release_android_context, initialize_android_context}; +use ndk_context::{initialize_android_context, release_android_context}; /// Invalid JNI version constant, signifying JNI_OnLoad failure. const INVALID_JNI_VERSION: jint = 0; @@ -45,7 +45,7 @@ pub extern "system" fn Java_ci_not_rune_MainActivity_initializeContext( if let Some(jvm) = JVM { // Converting context to raw pointer let context_ptr = context.into_raw() as *mut c_void; - + initialize_android_context(jvm, context_ptr); } } diff --git a/playback/src/buffered.rs b/playback/src/buffered.rs new file mode 100644 index 000000000..ec083e0c3 --- /dev/null +++ b/playback/src/buffered.rs @@ -0,0 +1,290 @@ +/** + * These code comes from: https://github.com/RustAudio/rodio/blob/de0ffdc9050ed6a0b4ecf1ce7ad981829c97e82e/src/source/buffered.rs#L12 + * Licensed under the MIT License. + */ +use std::cmp; +use std::mem; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use rodio::source::SeekError; +use rodio::{Sample, Source}; + +/// Internal function that builds a `RuneBuffered` object. +#[inline] +pub fn rune_buffered(input: I) -> RuneBuffered +where + I: Source, + I::Item: Sample, +{ + let total_duration = input.total_duration(); + let first_frame = extract(input); + + RuneBuffered { + current_frame: first_frame, + position_in_frame: 0, + total_duration, + } +} + +/// Iterator that at the same time extracts data from the iterator and stores it in a buffer. +pub struct RuneBuffered +where + I: Source, + I::Item: Sample, +{ + /// Immutable reference to the next frame of data. Cannot be `Frame::Input`. + current_frame: Arc>, + + /// The position in number of samples of this iterator inside `current_frame`. + position_in_frame: usize, + + /// Obtained once at creation and never modified again. + total_duration: Option, +} + +enum Frame +where + I: Source, + I::Item: Sample, +{ + /// Data that has already been extracted from the iterator. Also contains a pointer to the + /// next frame. + Data(FrameData), + + /// No more data. + End, + + /// Unextracted data. The `Option` should never be `None` and is only here for easier data + /// processing. + Input(Mutex>), +} + +struct FrameData +where + I: Source, + I::Item: Sample, +{ + data: Vec, + channels: u16, + rate: u32, + next: Mutex>>, +} + +impl Drop for FrameData +where + I: Source, + I::Item: Sample, +{ + fn drop(&mut self) { + // This is necessary to prevent stack overflows deallocating long chains of the mutually + // recursive `Frame` and `FrameData` types. This iteratively traverses as much of the + // chain as needs to be deallocated, and repeatedly "pops" the head off the list. This + // solves the problem, as when the time comes to actually deallocate the `FrameData`, + // the `next` field will contain a `Frame::End`, or an `Arc` with additional references, + // so the depth of recursive drops will be bounded. + while let Ok(arc_next) = self.next.get_mut() { + if let Some(next_ref) = Arc::get_mut(arc_next) { + // This allows us to own the next Frame. + let next = mem::replace(next_ref, Frame::End); + if let Frame::Data(next_data) = next { + // Swap the current FrameData with the next one, allowing the current one + // to go out of scope. + *self = next_data; + } else { + break; + } + } else { + break; + } + } + } +} + +/// Builds a frame from the input iterator. +fn extract(mut input: I) -> Arc> +where + I: Source, + I::Item: Sample, +{ + let frame_len = input.current_frame_len(); + + if frame_len == Some(0) { + return Arc::new(Frame::End); + } + + let channels = input.channels(); + let rate = input.sample_rate(); + let data: Vec = input + .by_ref() + .take(cmp::min(frame_len.unwrap_or(32768), 32768)) + .collect(); + + if data.is_empty() { + return Arc::new(Frame::End); + } + + Arc::new(Frame::Data(FrameData { + data, + channels, + rate, + next: Mutex::new(Arc::new(Frame::Input(Mutex::new(Some(input))))), + })) +} + +impl RuneBuffered +where + I: Source, + I::Item: Sample, +{ + /// Advances to the next frame. + fn next_frame(&mut self) { + let next_frame = { + let mut next_frame_ptr = match &*self.current_frame { + Frame::Data(FrameData { next, .. }) => next.lock().unwrap(), + _ => unreachable!(), + }; + + let next_frame = match &**next_frame_ptr { + Frame::Data(_) => next_frame_ptr.clone(), + Frame::End => next_frame_ptr.clone(), + Frame::Input(input) => { + let input = input.lock().unwrap().take().unwrap(); + extract(input) + } + }; + + *next_frame_ptr = next_frame.clone(); + next_frame + }; + + self.current_frame = next_frame; + self.position_in_frame = 0; + } + + /// Returns the current samples for all channels at the current position + /// Returns None if we're at the end of the stream or the position is invalid + pub fn current_samples(&self) -> Option> { + match &*self.current_frame { + Frame::Data(FrameData { data, channels, .. }) => { + let channels = *channels as usize; + let base_pos = self.position_in_frame * channels; + + if base_pos + channels <= data.len() { + Some(data[base_pos..base_pos + channels].to_vec()) + } else { + None + } + } + Frame::End => None, + Frame::Input(_) => None, + } + } +} + +impl Iterator for RuneBuffered +where + I: Source, + I::Item: Sample, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + let current_sample; + let advance_frame; + + match &*self.current_frame { + Frame::Data(FrameData { data, .. }) => { + current_sample = Some(data[self.position_in_frame]); + self.position_in_frame += 1; + advance_frame = self.position_in_frame >= data.len(); + } + + Frame::End => { + current_sample = None; + advance_frame = false; + } + + Frame::Input(_) => unreachable!(), + }; + + if advance_frame { + self.next_frame(); + } + + current_sample + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // TODO: + (0, None) + } +} + +// TODO: uncomment when `size_hint` is fixed +/*impl ExactSizeIterator for Amplify where I: Source + ExactSizeIterator, I::Item: Sample { +}*/ + +impl Source for RuneBuffered +where + I: Source, + I::Item: Sample, +{ + #[inline] + fn current_frame_len(&self) -> Option { + match &*self.current_frame { + Frame::Data(FrameData { data, .. }) => Some(data.len() - self.position_in_frame), + Frame::End => Some(0), + Frame::Input(_) => unreachable!(), + } + } + + #[inline] + fn channels(&self) -> u16 { + match *self.current_frame { + Frame::Data(FrameData { channels, .. }) => channels, + Frame::End => 1, + Frame::Input(_) => unreachable!(), + } + } + + #[inline] + fn sample_rate(&self) -> u32 { + match *self.current_frame { + Frame::Data(FrameData { rate, .. }) => rate, + Frame::End => 44100, + Frame::Input(_) => unreachable!(), + } + } + + #[inline] + fn total_duration(&self) -> Option { + self.total_duration + } + + /// Can not support seek, in the end state we lose the underlying source + /// which makes seeking back impossible. + #[inline] + fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { + Err(SeekError::NotSupported { + underlying_source: std::any::type_name::(), + }) + } +} + +impl Clone for RuneBuffered +where + I: Source, + I::Item: Sample, +{ + #[inline] + fn clone(&self) -> RuneBuffered { + RuneBuffered { + current_frame: self.current_frame.clone(), + position_in_frame: self.position_in_frame, + total_duration: self.total_duration, + } + } +} diff --git a/playback/src/dummy_souvlaki.rs b/playback/src/dummy_souvlaki.rs index 835be1eae..6ee49c49b 100644 --- a/playback/src/dummy_souvlaki.rs +++ b/playback/src/dummy_souvlaki.rs @@ -1,5 +1,4 @@ /// A dummy implementation of the media controls. - use std::time::Duration; /// The metadata of a media item. diff --git a/playback/src/internal.rs b/playback/src/internal.rs index 9d5d93a6e..f7816198e 100644 --- a/playback/src/internal.rs +++ b/playback/src/internal.rs @@ -10,8 +10,10 @@ use tokio::sync::mpsc; use tokio::time::{interval, sleep_until, Duration, Instant}; use tokio_util::sync::CancellationToken; +use crate::buffered::rune_buffered; use crate::output_stream::{RuneOutputStream, RuneOutputStreamHandle}; use crate::realtime_fft::RealTimeFFT; +use crate::shared_sample::SharedSource; use crate::strategies::{ AddMode, PlaybackStrategy, RepeatAllStrategy, RepeatOneStrategy, SequentialStrategy, ShuffleStrategy, UpdateReason, @@ -146,6 +148,24 @@ fn try_new_sink(stream: &RuneOutputStreamHandle) -> Result { Ok(sink) } +impl Source for SharedSource { + fn current_frame_len(&self) -> Option { + self.inner.lock().unwrap().current_frame_len() + } + + fn channels(&self) -> u16 { + self.inner.lock().unwrap().channels() + } + + fn sample_rate(&self) -> u32 { + self.inner.lock().unwrap().sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.lock().unwrap().total_duration() + } +} + pub(crate) struct PlayerInternal { commands: mpsc::UnboundedReceiver, event_sender: mpsc::UnboundedSender, @@ -321,7 +341,8 @@ impl PlayerInternal { return Ok(()); } - let source = source.unwrap(); + let source = SharedSource::new(rune_buffered(source.unwrap())); + let source_for_fft = Arc::clone(&source.inner); let (stream, stream_handle) = RuneOutputStream::try_default_with_callback({ let error_sender = self.stream_error_sender.clone(); @@ -351,14 +372,19 @@ impl PlayerInternal { }); sink.set_volume(self.volume); - sink.append( - source.periodic_access(Duration::from_millis(16), move |sample| { - let data: Vec = sample.take(sample.channels() as usize).collect(); - if fft_tx.send(data).is_err() { - error!("Failed to send FFT data"); + sink.append(source.periodic_access( + Duration::from_millis(12), + move |_sample: &mut SharedSource| { + if let Ok(guard) = source_for_fft.lock() { + let data: Option> = guard.current_samples(); + if let Some(data) = data { + if fft_tx.send(data).is_err() { + error!("Failed to send FFT data"); + } + } } - }), - ); + }, + )); if !play { sink.pause(); diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 48cbe55d1..6b10929ed 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -1,13 +1,15 @@ mod internal; -mod sfx_internal; mod realtime_fft; +mod sfx_internal; +mod shared_sample; mod simple_channel; -pub mod strategies; -pub mod output_stream; +pub mod buffered; pub mod controller; +pub mod output_stream; pub mod player; pub mod sfx_player; +pub mod strategies; #[cfg(target_os = "android")] mod dummy_souvlaki; diff --git a/playback/src/output_stream.rs b/playback/src/output_stream.rs index 1bfceb0c7..bf091c321 100644 --- a/playback/src/output_stream.rs +++ b/playback/src/output_stream.rs @@ -28,7 +28,11 @@ impl RuneOutputStream { let default_config = device .default_output_config() .map_err(StreamError::DefaultStreamConfigError)?; - RuneOutputStream::try_from_device_config_with_callback(device, default_config, error_callback) + RuneOutputStream::try_from_device_config_with_callback( + device, + default_config, + error_callback, + ) } pub fn try_from_device_config_with_callback( diff --git a/playback/src/shared_sample.rs b/playback/src/shared_sample.rs new file mode 100644 index 000000000..c7515c656 --- /dev/null +++ b/playback/src/shared_sample.rs @@ -0,0 +1,27 @@ +use std::fs::File; +use std::io::BufReader; +use std::sync::{Arc, Mutex}; + +use rodio::Decoder; + +use crate::buffered::RuneBuffered; + +pub struct SharedSource { + pub inner: Arc>>>>, +} + +impl SharedSource { + pub fn new(source: RuneBuffered>>) -> Self { + Self { + inner: Arc::new(Mutex::new(source)), + } + } +} + +impl Iterator for SharedSource { + type Item = >> as Iterator>::Item; + + fn next(&mut self) -> Option { + self.inner.lock().unwrap().next() + } +} diff --git a/pubspec.lock b/pubspec.lock index a27453aba..c5e1ab4b3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1148,8 +1148,8 @@ packages: dependency: "direct main" description: path: "." - ref: bc64829aa789a890d6dde5f4d960d7dcd3152341 - resolved-ref: bc64829aa789a890d6dde5f4d960d7dcd3152341 + ref: e0fd539785d2f656a8efd76376c6f2225848b9be + resolved-ref: e0fd539785d2f656a8efd76376c6f2225848b9be url: "https://github.com/Losses/system_tray.git" source: git version: "2.0.3" diff --git a/pubspec.yaml b/pubspec.yaml index 063334a1a..087d9e77f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,7 +77,7 @@ dependencies: system_tray: git: url: https://github.com/Losses/system_tray.git - ref: bc64829aa789a890d6dde5f4d960d7dcd3152341 + ref: e0fd539785d2f656a8efd76376c6f2225848b9be dev_dependencies: build_runner: ^2.4.11 @@ -162,4 +162,5 @@ msix_config: msix_version: 1.0.1.0 store: true capabilities: removableStorage - logo_path: ./assets/source/microsoft_store/installer_icon.png \ No newline at end of file + logo_path: ./assets/source/microsoft_store/installer_icon.png + debug: false \ No newline at end of file diff --git a/scripts/macos_5_codesign_and_notarize.sh b/scripts/macos_5_codesign_and_notarize.sh index 6c10b5165..61314eebd 100755 --- a/scripts/macos_5_codesign_and_notarize.sh +++ b/scripts/macos_5_codesign_and_notarize.sh @@ -35,3 +35,7 @@ echo "Notarize: ----------------------------" xcrun notarytool submit "Rune.zip" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD" --wait xcrun stapler staple "Rune.app" + +rm -rf "Rune.zip" + +/usr/bin/ditto -c -k --keepParent --sequesterRsrc "Rune.app" "Rune.zip" \ No newline at end of file diff --git a/scripts/macos_6_create_dmg.sh b/scripts/macos_6_create_dmg.sh index 6e1a9cdb5..2419be44f 100755 --- a/scripts/macos_6_create_dmg.sh +++ b/scripts/macos_6_create_dmg.sh @@ -6,11 +6,16 @@ cd "$(dirname "$0")" cd .. cd temp_macos +# if $REF_NAME exists +if [ -n "$REF_NAME" ]; then + REF_NAME="-$REF_NAME" +fi + create-dmg \ - --volname "Rune-$REF_NAME-$SHA-macOS" \ + --volname "Rune$REF_NAME-macOS" \ --window-pos 200 120 \ --window-size 800 450 \ --icon-size 100 \ --app-drop-link 600 185 \ - "Rune-$REF_NAME-$SHA-macOS.dmg" \ + "Rune$REF_NAME-macOS.dmg" \ Rune.app \ No newline at end of file