├── .codespellignore ├── .gitignore ├── rust-toolchain.toml ├── .sourceheaders.toml ├── README.md ├── .github ├── dependabot.yml └── workflows │ ├── pre-commit.yml │ ├── docs.yml │ └── build.yml ├── Cargo.toml ├── src ├── lib.rs ├── util.rs ├── pitch.rs ├── format.rs ├── visualizer.rs ├── bits.rs ├── generator.rs ├── timecode.rs ├── bitstream.rs └── lfsr.rs ├── examples ├── generate.rs ├── serato.rs └── visualizer.rs ├── Cargo.lock ├── .pre-commit-config.yaml └── COPYING /.codespellignore: -------------------------------------------------------------------------------- 1 | crate 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Serato Control CD.wav 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.92" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /.sourceheaders.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | license = "MPL-2.0" 3 | copyright_holder = "Jan Holthuis " 4 | prefer_inline = true 5 | width = 99 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vinylla 2 | 3 | *Rust library for working with timecode vinyl/CD audio for DVS systems.* 4 | 5 | **Note:** This library is still in early stages of development and it's API 6 | might change at any time. 7 | 8 | ## License 9 | 10 | This library is released under the terms of the of the [Mozilla Public License 11 | (MPL) Version 2.0](https://www.mozilla.org/MPL/). 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | # Check for updated Rust packages 5 | - package-ecosystem: cargo 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | # Check for updated Rust toolchain 10 | - package-ecosystem: rust-toolchain 11 | directory: / 12 | schedule: 13 | interval: weekly 14 | # Check for updated GitHub Actions 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: weekly 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vinylla" 3 | version = "0.0.0" 4 | authors = ["Jan Holthuis "] 5 | edition = "2018" 6 | repository = "https://github.com/Holzhaus/vinylla.git" 7 | homepage = "https://github.com/Holzhaus/vinylla.git" 8 | license = "LGPL-3.0+" 9 | readme = "README.md" 10 | description = "Rust library for working with timecode vinyl/CD audio for DVS systems." 11 | keywords = ["vinyl", "control", "timecode", "dvs", "scratch"] 12 | categories = [ 13 | "multimedia::audio", 14 | ] 15 | documentation = "https://docs.rs/vinylla/" 16 | 17 | [dependencies] 18 | 19 | [dev-dependencies] 20 | hound = "3.5" 21 | sdl2 = "0.38" 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | // FIXME: Enable missing_docs 10 | //#![deny(missing_docs)] 11 | #![warn(missing_debug_implementations)] 12 | #![warn(rustdoc::broken_intra_doc_links)] 13 | #![cfg_attr(test, deny(warnings))] 14 | 15 | mod bits; 16 | mod bitstream; 17 | mod format; 18 | mod generator; 19 | mod lfsr; 20 | mod pitch; 21 | mod timecode; 22 | mod util; 23 | mod visualizer; 24 | 25 | pub use format::SERATO_CONTROL_CD_1_0_0; 26 | pub use generator::TimecodeAudioGenerator; 27 | pub use timecode::Timecode; 28 | pub use visualizer::Visualizer; 29 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pre-commit 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | pre-commit: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v6 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Install Rust toolchain 22 | run: rustup toolchain install 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v6 26 | 27 | - name: Detect code style issues 28 | uses: pre-commit/action@1b06ec171f2f6faa71ed760c4042bd969e4f8b43 29 | env: 30 | SKIP: no-commit-to-branch 31 | 32 | - name: Generate patch file 33 | if: failure() 34 | run: | 35 | git diff-index -p HEAD > "${PATCH_FILE}" 36 | [ -s "${PATCH_FILE}" ] && echo "UPLOAD_PATCH_FILE=${PATCH_FILE}" >> "${GITHUB_ENV}" 37 | env: 38 | PATCH_FILE: pre-commit.patch 39 | 40 | - name: Upload patch artifact 41 | if: failure() && env.UPLOAD_PATCH_FILE != null 42 | uses: actions/upload-artifact@v6 43 | with: 44 | name: ${{ env.UPLOAD_PATCH_FILE }} 45 | path: ${{ env.UPLOAD_PATCH_FILE }} 46 | -------------------------------------------------------------------------------- /examples/generate.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! Reads a Serato Control CD 1.0.0 WAV file and prints the decoded positions. 10 | //! 11 | //! The WAV file can be downloaded from: 12 | //! https://serato.com/controlcd/downloads/zip 13 | //! 14 | //! You can run this using: 15 | //! 16 | //! ```bash 17 | //! $ cargo run --example serato -- /path/to/Serato\ Control\ CD.wav 18 | //! ``` 19 | //! 20 | //! Note that this will panic when the end of the file is reached. 21 | 22 | use hound::{SampleFormat, WavSpec, WavWriter}; 23 | use std::env; 24 | use vinylla::{TimecodeAudioGenerator, SERATO_CONTROL_CD_1_0_0}; 25 | 26 | const SAMPLE_RATE_HZ: f64 = 44100.0; 27 | 28 | fn main() { 29 | let mut args = env::args().skip(1); 30 | let path = args.next().expect("No file given"); 31 | println!("{}", path); 32 | 33 | let spec = WavSpec { 34 | channels: 2, 35 | sample_rate: SAMPLE_RATE_HZ as u32, 36 | bits_per_sample: 16, 37 | sample_format: SampleFormat::Int, 38 | }; 39 | 40 | let mut writer = WavWriter::create(&path, spec).unwrap(); 41 | let mut generator = TimecodeAudioGenerator::new(&SERATO_CONTROL_CD_1_0_0, SAMPLE_RATE_HZ); 42 | let initial_state = generator.state(); 43 | let mut state_changed = false; 44 | 45 | loop { 46 | let (left, right) = generator.next_sample(); 47 | writer.write_sample(left).unwrap(); 48 | writer.write_sample(right).unwrap(); 49 | if !state_changed { 50 | state_changed = generator.state() != initial_state; 51 | } else if generator.state() == initial_state { 52 | break; 53 | } 54 | } 55 | writer.finalize().unwrap(); 56 | } 57 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 | 11 | [[package]] 12 | name = "cfg-if" 13 | version = "1.0.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 16 | 17 | [[package]] 18 | name = "hound" 19 | version = "3.5.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" 22 | 23 | [[package]] 24 | name = "lazy_static" 25 | version = "1.5.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 28 | 29 | [[package]] 30 | name = "libc" 31 | version = "0.2.177" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 34 | 35 | [[package]] 36 | name = "sdl2" 37 | version = "0.38.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" 40 | dependencies = [ 41 | "bitflags", 42 | "lazy_static", 43 | "libc", 44 | "sdl2-sys", 45 | ] 46 | 47 | [[package]] 48 | name = "sdl2-sys" 49 | version = "0.38.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" 52 | dependencies = [ 53 | "cfg-if", 54 | "libc", 55 | "version-compare", 56 | ] 57 | 58 | [[package]] 59 | name = "version-compare" 60 | version = "0.1.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 63 | 64 | [[package]] 65 | name = "vinylla" 66 | version = "0.0.0" 67 | dependencies = [ 68 | "hound", 69 | "sdl2", 70 | ] 71 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default_language_version: 3 | python: python3 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 7 | hooks: 8 | - id: check-case-conflict 9 | - id: check-json 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: check-toml 13 | - id: check-xml 14 | - id: check-yaml 15 | - id: destroyed-symlinks 16 | - id: detect-private-key 17 | - id: end-of-file-fixer 18 | - id: fix-byte-order-marker 19 | - id: forbid-new-submodules 20 | - id: mixed-line-ending 21 | - id: trailing-whitespace 22 | exclude: .tsv$ 23 | - repo: https://github.com/codespell-project/codespell 24 | rev: 63c8f8312b7559622c0d82815639671ae42132ac # frozen: v2.4.1 25 | hooks: 26 | - id: codespell 27 | args: [--ignore-words=.codespellignore] 28 | exclude_types: [tsv, json] 29 | - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt 30 | rev: 8d1b9cadaf854cb25bb0b0f5870e1cc66a083d6b # frozen: 0.2.3 31 | hooks: 32 | - id: yamlfmt 33 | - repo: https://github.com/gitleaks/gitleaks 34 | rev: 39fdb480a06768cc41a84ef86959c07ff33091c4 # frozen: v8.28.0 35 | hooks: 36 | - id: gitleaks 37 | - repo: https://github.com/woodruffw/zizmor-pre-commit 38 | rev: 122d24ec728f140b38330ae8b04e3c5fe8b774c5 # frozen: v1.15.2 39 | hooks: 40 | - id: zizmor 41 | - repo: https://github.com/doublify/pre-commit-rust 42 | rev: eeee35a89e69d5772bdee97db1a6a898467b686e # frozen: v1.0 43 | hooks: 44 | - id: fmt 45 | args: [--all, --] 46 | - id: clippy 47 | args: [--locked, --workspace, --all-features, --all-targets, --, -D, warnings] 48 | - repo: https://github.com/Holzhaus/sourceheaders 49 | rev: 37fab20a62cc63ebb9a8855a2ab90ec7dc56cadf # frozen: v0.0.4 50 | hooks: 51 | - id: sourceheaders 52 | 53 | ci: 54 | skip: [fmt, clippy] 55 | -------------------------------------------------------------------------------- /examples/serato.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! Reads a Serato Control CD 1.0.0 WAV file and prints the decoded positions. 10 | //! 11 | //! The WAV file can be downloaded from: 12 | //! https://serato.com/controlcd/downloads/zip 13 | //! 14 | //! You can run this using: 15 | //! 16 | //! ```bash 17 | //! $ cargo run --example serato -- /path/to/Serato\ Control\ CD.wav 18 | //! ``` 19 | //! 20 | //! Note that this will panic when the end of the file is reached. 21 | 22 | use hound::WavReader; 23 | use std::env; 24 | use vinylla::{Timecode, SERATO_CONTROL_CD_1_0_0}; 25 | 26 | fn main() { 27 | let mut args = env::args().skip(1); 28 | let path = args.next().expect("No file given"); 29 | let reverse = args.next().is_some_and(|x| x == "-r" || x == "--reverse"); 30 | println!("Reverse: {}", reverse); 31 | 32 | println!("{}", path); 33 | let mut reader = WavReader::open(&path).unwrap(); 34 | let mut timecode = Timecode::new(&SERATO_CONTROL_CD_1_0_0, 44100.0); 35 | 36 | let mut i = 0; 37 | let mut position = reader.len() / 2; 38 | loop { 39 | if reader.len() < 2 || reverse && position < 2 { 40 | return; 41 | } 42 | 43 | if reverse { 44 | reader.seek(position - 2).unwrap(); 45 | position -= 2; 46 | } else { 47 | position += 2; 48 | } 49 | let mut samples = reader.samples::().map(|x| x.unwrap()); 50 | let left = match samples.next() { 51 | None => return, 52 | Some(s) => s, 53 | }; 54 | let right = match samples.next() { 55 | None => return, 56 | Some(s) => s, 57 | }; 58 | if let Some((bit, position)) = timecode.process_channels(left, right) { 59 | println!("{:10}: Bit {} => Position {:?}", i, bit as u8, position); 60 | i += 1; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate Docs 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | docs: 13 | name: Update Docs 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write # needed because peaceiris/actions-gh-pages writes to the gh-pages branch 17 | pages: write 18 | steps: 19 | - name: Check out repository 20 | uses: actions/checkout@v6 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Print Rust version 25 | run: rustc -vV 26 | 27 | - name: Generate Documentation 28 | run: cargo doc --all-features --no-deps --document-private-items 29 | 30 | - name: Generate index file 31 | shell: python3 {0} 32 | run: | 33 | import os 34 | 35 | package = os.environ["PACKAGE_NAME"] 36 | assert package 37 | 38 | owner, sep, repository = os.environ["GITHUB_REPOSITORY"].partition("/") 39 | assert owner 40 | assert repository 41 | 42 | doc = f""" 43 | 44 | 45 | Redirecting to https://{owner}.github.io/{repository}/{package}/ 46 | 47 | 48 | 49 | 50 | """ 51 | 52 | output_dir = os.environ["OUTPUT_DIR"] 53 | assert output_dir 54 | output_dir = os.path.join(output_dir, "index.html") 55 | 56 | print(f"Writing to {output_dir}") 57 | with open(output_dir, mode="w") as f: 58 | f.write(doc) 59 | env: 60 | PACKAGE_NAME: vinylla 61 | OUTPUT_DIR: target/doc 62 | 63 | - name: Deploy to GitHub Pages 64 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e 65 | with: 66 | github_token: ${{ secrets.GITHUB_TOKEN }} 67 | publish_dir: target/doc 68 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! Helper Utilities 10 | 11 | /// Discrete-time implementation of a simple RC low-pass filter to calculate the exponential 12 | /// weighted moving average. 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub struct ExponentialWeightedMovingAverage { 15 | /// The smoothed last output. 16 | pub last_output: i32, 17 | 18 | /// The smoothing factor (commonly named α in literature). Needs to be in range 0.0 − 1.0. 19 | /// (inclusive). 20 | pub smoothing_factor: f64, 21 | } 22 | 23 | impl ExponentialWeightedMovingAverage { 24 | pub fn new(time_constant: f64, sample_rate_hz: f64) -> Self { 25 | let last_output = 0; 26 | let smoothing_factor = Self::calculate_smoothing_factor(time_constant, sample_rate_hz); 27 | ExponentialWeightedMovingAverage { 28 | last_output, 29 | smoothing_factor, 30 | } 31 | } 32 | 33 | /// Calculate the smoothing factor. 34 | /// 35 | /// Using the time constant RC and and the sample rate f_s, this calculates the 36 | /// smoothing factor α: 37 | /// 38 | /// Δ_T = 1/f_s 39 | /// α = Δ_T / (RC + Δ_T) 40 | /// 41 | /// where Δ_T is the sampling period. 42 | fn calculate_smoothing_factor(time_constant: f64, sample_rate_hz: f64) -> f64 { 43 | let sampling_period_secs = 1f64 / sample_rate_hz; 44 | sampling_period_secs / (time_constant + sampling_period_secs) 45 | } 46 | 47 | /// Calculate the difference between the current input and last output value. 48 | pub fn difference_to(&self, input: i32) -> i32 { 49 | input - self.last_output 50 | } 51 | 52 | /// Calculate the next smoothed value. 53 | /// 54 | /// This calculates the next smoothed value yᵢ using the previous smoothed value yᵢ₋₁, the 55 | /// current unsmoothed value xᵢ and the smoothing factor α: 56 | /// 57 | /// yᵢ = α ⋅ xᵢ + (1 − α) ⋅ yᵢ₋₁ 58 | /// 59 | /// To avoid unnecessary floating point calculations, the above equation can be written as: 60 | /// 61 | /// yᵢ = α ⋅ xᵢ + (1 − α) ⋅ yᵢ₋₁ 62 | /// = α ⋅ xᵢ + yᵢ₋₁ − α ⋅ yᵢ₋₁ 63 | /// = yᵢ₋₁ + α ⋅ xᵢ − α ⋅ yᵢ₋₁ 64 | /// = yᵢ₋₁ + alpha ⋅ (xᵢ − yᵢ₋₁) 65 | pub fn smoothen(&self, input: i32) -> i32 { 66 | self.last_output + (self.smoothing_factor * self.difference_to(input) as f64) as i32 67 | } 68 | 69 | /// Calculate the next smoothed value and store it. 70 | pub fn process(&mut self, input: i32) -> i32 { 71 | self.last_output = self.smoothen(input); 72 | self.last_output 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | on: [push, pull_request] 4 | permissions: {} 5 | 6 | jobs: 7 | cargo-toml-features: 8 | name: Generate Feature Combinations 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | outputs: 13 | feature-combinations: ${{ steps.cargo-toml-features.outputs.feature-combinations }} 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v6 17 | with: 18 | persist-credentials: false 19 | - name: Determine Cargo Features 20 | id: cargo-toml-features 21 | uses: Holzhaus/cargo-toml-features-action@3afa751aae4071b2d1ca1c5fa42528a351c995f4 22 | 23 | build: 24 | needs: cargo-toml-features 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: read 28 | strategy: 29 | matrix: 30 | features: ${{ fromJson(needs.cargo-toml-features.outputs.feature-combinations) }} 31 | env: 32 | CRATE_FEATURES: ${{ join(matrix.features, ',') }} 33 | steps: 34 | - name: Check out repository 35 | uses: actions/checkout@v6 36 | with: 37 | persist-credentials: false 38 | - name: Print Rust version 39 | run: rustc -vV 40 | - name: Install SDL2 41 | run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libsdl2-dev 42 | - name: Build Package 43 | run: | # zizmor: ignore[use-trusted-publishing] 44 | cargo publish --dry-run --locked --no-default-features --features "${CRATE_FEATURES}" 45 | - name: Run Tests 46 | run: cargo test --locked --no-default-features --features "${CRATE_FEATURES}" 47 | - name: Run Benchmark 48 | run: cargo bench --locked --no-default-features --features "${CRATE_FEATURES}" 49 | - name: Generate Documentation 50 | run: cargo doc --no-deps --locked --no-default-features --features "${CRATE_FEATURES}" 51 | 52 | publish: 53 | needs: build 54 | runs-on: ubuntu-latest 55 | permissions: 56 | id-token: write # Required for OIDC token exchange 57 | if: startsWith(github.ref, 'refs/tags/') 58 | steps: 59 | - name: Check out repository 60 | uses: actions/checkout@v6 61 | with: 62 | persist-credentials: false 63 | - name: Authenticate with registry 64 | id: auth 65 | uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec 66 | - name: Publish Package 67 | run: cargo publish --locked 68 | env: 69 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 70 | -------------------------------------------------------------------------------- /src/pitch.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | #[derive(Debug, Clone, Copy)] 10 | pub struct PitchDetector { 11 | samples_per_quarter_cycle: f64, 12 | samples_since_last_quarter_cycle: f64, 13 | last_primary_sample: i32, 14 | last_secondary_sample: i32, 15 | } 16 | 17 | impl PitchDetector { 18 | pub fn new(sample_rate_hz: f64, timecode_frequency_hz: f64) -> Self { 19 | let samples_per_quarter_cycle = sample_rate_hz / timecode_frequency_hz / 4.0; 20 | 21 | PitchDetector { 22 | samples_per_quarter_cycle, 23 | samples_since_last_quarter_cycle: 1.0, 24 | last_primary_sample: 0, 25 | last_secondary_sample: 0, 26 | } 27 | } 28 | 29 | pub fn update(&mut self, primary_sample: i32, secondary_sample: i32) { 30 | self.last_primary_sample = primary_sample; 31 | self.last_secondary_sample = secondary_sample; 32 | self.samples_since_last_quarter_cycle += 1.0; 33 | } 34 | 35 | pub fn update_after_zero_crossing( 36 | &mut self, 37 | primary_sample: i32, 38 | secondary_sample: i32, 39 | primary_crossed_zero: bool, 40 | ) -> f64 { 41 | // If a channel crossed zero, we now know the last sample value a (before the zero 42 | // crossing) and the current sample value b (after the crossing). 43 | // 44 | // a 45 | // \ 46 | // ───── 47 | // \ 48 | // b 49 | // 50 | // We could now assume that the zero crossing happened just now, but it's more precise to 51 | // interpolate the exact subsample position where zero was crossed. This can be done by 52 | // assuming that the zero crossing position is equal to the proportion how far both are 53 | // from zero, e.g.: 54 | // 55 | // samples_since_zero_crossing = |b|/(|b| + |a|) 56 | // 57 | // This gives a number between 0.0 (if b is almost 0, i.e. the zero crossing is close to b) 58 | // and 1.0 (if a is almost 0, i.e. the zero crossing was immediately after sampling a). 59 | let samples_since_zero_crossing = if primary_crossed_zero { 60 | let b = f64::from(primary_sample.abs()); 61 | b / (b + f64::from(self.last_primary_sample.abs())) 62 | } else { 63 | let b = f64::from(secondary_sample.abs()); 64 | b / (b + f64::from(self.last_secondary_sample.abs())) 65 | }; 66 | 67 | let samples_since_last_quarter_cycle = 68 | self.samples_since_last_quarter_cycle + 1.0 - samples_since_zero_crossing; 69 | 70 | let pitch = self.samples_per_quarter_cycle / samples_since_last_quarter_cycle; 71 | self.samples_since_last_quarter_cycle = samples_since_zero_crossing; 72 | pitch 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub struct TimecodeFormat { 11 | pub size: usize, 12 | pub seed: u32, 13 | pub taps: u32, 14 | pub signal_frequency_hz: f64, 15 | } 16 | 17 | /// Serato Control CD 1.0.0 18 | /// 19 | /// The Serato Control CD can be downloaded free of cost [from the Serato 20 | /// Website](https://serato.com/controlcd/downloads) as zipped WAV file or ISO image. 21 | pub const SERATO_CONTROL_CD_1_0_0: TimecodeFormat = TimecodeFormat { 22 | size: 20, 23 | seed: 0b1001_0001_0100_1010_1011, 24 | // LFSR feedback polynomial: 25 | // x^20 + x^18 + x^16 + x^14 + x^12 + x^10 + x^9 + x^6 + x^4 + x^3 + 1 26 | taps: 0b0011_0100_1101_0101_0101, 27 | signal_frequency_hz: 1000.0, 28 | }; 29 | 30 | #[cfg(test)] 31 | mod test { 32 | use super::*; 33 | use crate::{Timecode, TimecodeAudioGenerator}; 34 | 35 | fn test_format(format: &TimecodeFormat, sample_rate_hz: f64) { 36 | let mut generator = TimecodeAudioGenerator::new(format, sample_rate_hz); 37 | let mut timecode = Timecode::new(format, sample_rate_hz); 38 | let initial_state = generator.state(); 39 | let mut previous_timecode_state = timecode.state(); 40 | let mut state_changed = false; 41 | assert_eq!(timecode.state(), initial_state); 42 | assert_eq!(timecode.state(), generator.state()); 43 | println!( 44 | "{:0>size$b} {:0>size$b}", 45 | timecode.state(), 46 | generator.state(), 47 | size = format.size, 48 | ); 49 | 50 | // Skip the first few samples until the bit detection works properly 51 | for _ in 0..20 { 52 | let (left, right) = generator.next_sample(); 53 | timecode.process_channels(left, right); 54 | } 55 | timecode.set_state(generator.state()); 56 | 57 | loop { 58 | let (left, right) = generator.next_sample(); 59 | timecode.process_channels(left, right); 60 | if timecode.state() != previous_timecode_state { 61 | println!( 62 | "{:0>size$b} {:0>size$b}", 63 | timecode.state(), 64 | generator.state(), 65 | size = format.size, 66 | ); 67 | 68 | assert_eq!(timecode.state(), generator.state()); 69 | previous_timecode_state = timecode.state(); 70 | state_changed = true; 71 | } 72 | 73 | if state_changed && generator.state() == initial_state { 74 | break; 75 | } 76 | } 77 | } 78 | 79 | #[test] 80 | fn test_serato_control_cd_1_0_0_44100hz() { 81 | test_format(&SERATO_CONTROL_CD_1_0_0, 44100.0); 82 | } 83 | 84 | #[test] 85 | fn test_serato_control_cd_1_0_0_48000hz() { 86 | test_format(&SERATO_CONTROL_CD_1_0_0, 48000.0); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/visualizer.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | #[derive(Debug)] 10 | pub struct Visualizer { 11 | size: usize, 12 | half_size: usize, 13 | samples_drawn: usize, 14 | decay_interval: usize, 15 | #[expect(dead_code)] 16 | decay_factor: f32, 17 | } 18 | 19 | impl Visualizer { 20 | const DECAY_INTERVAL: usize = 50; 21 | const DECAY_FACTOR: f32 = 0.95; 22 | 23 | pub fn new(size: usize) -> Self { 24 | assert!(size > 10); 25 | 26 | let half_size = size / 2; 27 | assert_eq!(half_size * 2, size); 28 | 29 | Visualizer { 30 | size, 31 | half_size, 32 | samples_drawn: 0, 33 | decay_interval: Self::DECAY_INTERVAL, 34 | decay_factor: Self::DECAY_FACTOR, 35 | } 36 | } 37 | 38 | pub fn decay(&self, buffer: &mut [u8], size: usize) { 39 | let num_pixels = size * size; 40 | buffer 41 | .iter_mut() 42 | .take(num_pixels) 43 | .for_each(|x| *x = (f32::from(*x) * Self::DECAY_FACTOR) as u8); 44 | } 45 | 46 | fn normalize_sample_to_size(&self, sample: i16) -> usize { 47 | let sample = f32::from(sample) / -(i16::MIN as f32); 48 | ((self.half_size as i16) - (sample * ((self.half_size - 1) as f32)) as i16) as usize 49 | } 50 | 51 | const fn coordinate_to_index(&self, x: usize, y: usize) -> usize { 52 | x * self.size + y 53 | } 54 | 55 | pub fn draw_sample(&mut self, buffer: &mut [u8], size: usize, left: i16, right: i16) { 56 | assert_eq!(buffer.len(), size * size); 57 | 58 | if self.samples_drawn == self.decay_interval { 59 | self.decay(buffer, size); 60 | self.samples_drawn = 0; 61 | } else { 62 | self.samples_drawn += 1; 63 | } 64 | 65 | // Calculate coordinate in range [0, size - 1] 66 | let x = self.normalize_sample_to_size(left); 67 | let y = self.normalize_sample_to_size(right); 68 | 69 | // Draw pixel 70 | let index = self.coordinate_to_index(x, y); 71 | buffer[index] = u8::MAX; 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::Visualizer; 78 | 79 | #[test] 80 | fn test_normalize() { 81 | const SIZE: usize = 400; 82 | let visualizer = Visualizer::new(SIZE); 83 | 84 | for sample in i16::MIN..=i16::MAX { 85 | let coord = visualizer.normalize_sample_to_size(sample); 86 | assert!(coord < SIZE); 87 | } 88 | } 89 | 90 | #[test] 91 | fn test_coordinate() { 92 | const SIZE: usize = 400; 93 | let visualizer = Visualizer::new(SIZE); 94 | 95 | for x in 0..SIZE - 1 { 96 | for y in 0..SIZE - 1 { 97 | let index = visualizer.coordinate_to_index(x, y); 98 | assert!(index < SIZE * SIZE); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/visualizer.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! Reads a Serato Control CD 1.0.0 WAV file and show the "donut" visualizer. 10 | //! 11 | //! The WAV file can be downloaded from: 12 | //! https://serato.com/controlcd/downloads/zip 13 | //! 14 | //! You can run this using: 15 | //! 16 | //! ```bash 17 | //! $ cargo run --example visualizer -- /path/to/Serato\ Control\ CD.wav 18 | //! ``` 19 | //! 20 | //! Note that this will panic when the end of the file is reached. 21 | 22 | use hound::WavReader; 23 | use sdl2::{ 24 | event::Event, 25 | pixels::{Color, PixelFormatEnum}, 26 | render::Canvas, 27 | video::Window, 28 | }; 29 | use std::env; 30 | use vinylla::{Timecode, Visualizer, SERATO_CONTROL_CD_1_0_0}; 31 | 32 | const PIXEL_SIZE: usize = 400; 33 | 34 | fn main() { 35 | let path = env::args().nth(1).expect("No file given"); 36 | 37 | println!("File: {}", path); 38 | let mut reader = WavReader::open(&path).unwrap(); 39 | let mut samples = reader.samples::().map(|x| x.unwrap()); 40 | let mut timecode = Timecode::new(&SERATO_CONTROL_CD_1_0_0, 44100.0); 41 | 42 | // Set up SDL window and Texture that we can draw on 43 | let sdl_context = sdl2::init().unwrap(); 44 | let video_subsystem = sdl_context.video().unwrap(); 45 | //video_subsystem.gl_set_swap_interval(SwapInterval::Immediate).unwrap(); 46 | let window = video_subsystem 47 | .window("Example", PIXEL_SIZE as u32, PIXEL_SIZE as u32) 48 | .build() 49 | .unwrap(); 50 | let mut canvas: Canvas = window.into_canvas().build().unwrap(); 51 | canvas.set_draw_color(Color::RGB(0, 0, 0)); 52 | canvas.clear(); 53 | let texture_creator = canvas.texture_creator(); 54 | let mut texture = texture_creator 55 | .create_texture_static( 56 | PixelFormatEnum::RGB332, 57 | PIXEL_SIZE as u32, 58 | PIXEL_SIZE as u32, 59 | ) 60 | .unwrap(); 61 | let mut event_pump = sdl_context.event_pump().unwrap(); 62 | 63 | let mut pixels: [u8; PIXEL_SIZE * PIXEL_SIZE] = [0; PIXEL_SIZE * PIXEL_SIZE]; 64 | let mut visualizer = Visualizer::new(PIXEL_SIZE); 65 | 66 | let mut i = 0; 67 | let mut samples_read = false; 68 | 'running: loop { 69 | for event in event_pump.poll_iter() { 70 | if let Event::Quit { .. } = event { 71 | break 'running; 72 | } 73 | } 74 | 75 | let left = samples.next().expect("failed to read left sample"); 76 | let right = samples.next().expect("failed to read right sample"); 77 | if !samples_read && left == 0 && right == 0 { 78 | continue; 79 | } 80 | samples_read = true; 81 | if let Some((bit, position)) = timecode.process_channels(left, right) { 82 | println!("{:10}: Bit {} => Position {:?}", i, bit as u8, position); 83 | } 84 | 85 | visualizer.draw_sample(&mut pixels, PIXEL_SIZE, left, right); 86 | texture.update(None, &pixels, PIXEL_SIZE).unwrap(); 87 | canvas.copy(&texture, None, None).unwrap(); 88 | canvas.present(); 89 | 90 | i += 1; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bits.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! Low level bitwise operations 10 | 11 | /// Return 2^size - 1 that can be used as a bitmask 12 | pub const fn mask(size: usize) -> u32 { 13 | (1 << size) - 1 14 | } 15 | 16 | /// Shift all bits in `size`-bit integer `data` to the right and set `bit` as MSB. 17 | /// 18 | /// The LSB of `data` before the shift will be discarded. 19 | pub const fn insert_msb(size: usize, data: u32, bit: u32) -> u32 { 20 | let bit = bit & 1; 21 | (bit << (size - 1)) | (data >> 1) 22 | } 23 | 24 | /// Shift all bits in `size`-bit integer `data` to the left and set `bit` as LSB. 25 | /// 26 | /// The MSB of `data` before the shift will be discarded. 27 | pub const fn insert_lsb(size: usize, data: u32, bit: u32) -> u32 { 28 | let bit = bit & 1; 29 | (data << 1) & mask(size) | bit 30 | } 31 | 32 | /// Shift all bits in `size`-bit integer `data` to the left and use the old MSB as new LSB. 33 | #[allow(dead_code)] 34 | pub const fn rotate_left(size: usize, data: u32) -> u32 { 35 | let msb = data >> (size - 1); 36 | insert_lsb(size, data, msb) 37 | } 38 | 39 | /// Shift all bits in `size`-bit integer `data` to the right and use the old LSB as new MSB. 40 | pub const fn rotate_right(size: usize, data: u32) -> u32 { 41 | let lsb = data & 1; 42 | insert_msb(size, data, lsb) 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | 49 | #[test] 50 | fn test_mask() { 51 | assert_eq!(mask(0), 0b00000000); 52 | assert_eq!(mask(1), 0b00000001); 53 | assert_eq!(mask(5), 0b00011111); 54 | assert_eq!(mask(8), 0b11111111); 55 | } 56 | 57 | #[test] 58 | fn test_insert_msb() { 59 | assert_eq!(insert_msb(5, 0b10101, 0), 0b01010); 60 | assert_eq!(insert_msb(5, 0b10101, 1), 0b11010); 61 | 62 | assert_eq!(insert_msb(4, 0b0101, 0), 0b0010); 63 | assert_eq!(insert_msb(4, 0b0101, 1), 0b1010); 64 | 65 | assert_eq!(insert_msb(16, 0b1111000011110000, 0), 0b0111100001111000); 66 | assert_eq!(insert_msb(16, 0b1111000011110000, 1), 0b1111100001111000); 67 | } 68 | 69 | #[test] 70 | fn test_insert_lsb() { 71 | assert_eq!(insert_lsb(5, 0b10101, 0), 0b01010); 72 | assert_eq!(insert_lsb(5, 0b10101, 1), 0b01011); 73 | 74 | assert_eq!(insert_lsb(4, 0b0101, 0), 0b1010); 75 | assert_eq!(insert_lsb(4, 0b0101, 1), 0b1011); 76 | 77 | assert_eq!(insert_lsb(16, 0b1111000011110000, 0), 0b1110000111100000); 78 | assert_eq!(insert_lsb(16, 0b1111000011110000, 1), 0b1110000111100001); 79 | } 80 | 81 | #[test] 82 | fn test_rotate_left() { 83 | assert_eq!(rotate_left(5, 0b10101), 0b01011); 84 | assert_eq!(rotate_left(5, 0b01011), 0b10110); 85 | 86 | assert_eq!(rotate_left(4, 0b1101), 0b1011); 87 | assert_eq!(rotate_left(4, 0b1011), 0b0111); 88 | 89 | assert_eq!(rotate_left(16, 0b1111000011110000), 0b1110000111100001); 90 | assert_eq!(rotate_left(16, 0b1110000111100001), 0b1100001111000011); 91 | } 92 | 93 | #[test] 94 | fn test_rotate_right() { 95 | assert_eq!(rotate_right(5, 0b10101), 0b11010); 96 | assert_eq!(rotate_right(5, 0b11010), 0b01101); 97 | 98 | assert_eq!(rotate_right(4, 0b0111), 0b1011); 99 | assert_eq!(rotate_right(4, 0b1011), 0b1101); 100 | 101 | assert_eq!(rotate_right(16, 0b1111000011110000), 0b0111100001111000); 102 | assert_eq!(rotate_right(16, 0b0111100001111000), 0b0011110000111100); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/generator.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | use super::format::TimecodeFormat; 10 | use super::lfsr::FibonacciLfsr; 11 | 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct TimecodeAudioGenerator { 14 | lfsr: FibonacciLfsr, 15 | sample_rate_hz: f64, 16 | signal_frequency_hz: f64, 17 | previous_bit: bool, 18 | cycle_index: i32, 19 | index: i32, 20 | } 21 | 22 | impl TimecodeAudioGenerator { 23 | pub fn new(format: &TimecodeFormat, sample_rate_hz: f64) -> Self { 24 | let TimecodeFormat { 25 | size, 26 | seed, 27 | taps, 28 | signal_frequency_hz, 29 | } = format; 30 | 31 | let mut lfsr = FibonacciLfsr { 32 | size: *size, 33 | state: *seed, 34 | taps: *taps, 35 | }; 36 | let signal_frequency_hz = *signal_frequency_hz; 37 | lfsr.revert(); 38 | let previous_bit = (lfsr.state >> (lfsr.size - 2)) & 1 == 1; 39 | lfsr.advance(); 40 | assert_eq!(lfsr.state, *seed); 41 | 42 | Self { 43 | lfsr, 44 | sample_rate_hz, 45 | signal_frequency_hz, 46 | cycle_index: 0, 47 | previous_bit, 48 | index: 0, 49 | } 50 | } 51 | 52 | const SCALE_FACTOR_ZERO: f64 = 0.75; 53 | 54 | fn scale_sample(sample: f64) -> i16 { 55 | let sample = sample * (i16::MAX as f64) * 0.5; 56 | sample.round().trunc() as i16 57 | } 58 | 59 | fn sample_from_cycle(cycle: f64, primary_bit: bool, secondary_bit: bool) -> (f64, f64) { 60 | let angle = 2.0 * std::f64::consts::PI * cycle; 61 | let (mut primary, mut secondary) = angle.sin_cos(); 62 | 63 | if !primary_bit { 64 | primary *= Self::SCALE_FACTOR_ZERO; 65 | } 66 | 67 | if !secondary_bit { 68 | secondary *= Self::SCALE_FACTOR_ZERO; 69 | }; 70 | 71 | (primary, secondary) 72 | } 73 | 74 | pub fn next_sample(&mut self) -> (i16, i16) { 75 | let index = f64::from(self.index); 76 | 77 | let cycle = (index * self.signal_frequency_hz) / self.sample_rate_hz; 78 | let cycle_index = cycle.trunc() as i32; 79 | let cycle_position = cycle - f64::from(cycle_index); 80 | 81 | if cycle_index == self.cycle_index && cycle_position >= 0.75 { 82 | self.cycle_index = cycle_index + 1; 83 | self.previous_bit = (self.lfsr.state >> (self.lfsr.size - 1)) & 1 == 1; 84 | self.lfsr.advance(); 85 | } 86 | 87 | let secondary_bit = (self.lfsr.state >> (self.lfsr.size - 1)) == 1; 88 | let primary_bit = if cycle_position >= 0.75 { 89 | self.previous_bit 90 | } else { 91 | secondary_bit 92 | }; 93 | 94 | let (mut primary_sample, mut secondary_sample) = 95 | Self::sample_from_cycle(cycle, primary_bit, secondary_bit); 96 | 97 | if cycle < 1.0 { 98 | primary_sample *= cycle; 99 | secondary_sample *= cycle; 100 | } 101 | 102 | let primary_sample = Self::scale_sample(primary_sample); 103 | let secondary_sample = Self::scale_sample(secondary_sample); 104 | 105 | self.index += 1; 106 | (primary_sample, secondary_sample) 107 | } 108 | 109 | pub fn state(&self) -> u32 { 110 | self.lfsr.state 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod test { 116 | use super::TimecodeAudioGenerator; 117 | use crate::SERATO_CONTROL_CD_1_0_0; 118 | 119 | #[test] 120 | fn test_generator() { 121 | let mut generator = TimecodeAudioGenerator::new(&SERATO_CONTROL_CD_1_0_0, 44100.0); 122 | let initial_state = generator.state(); 123 | loop { 124 | generator.next_sample(); 125 | if generator.state() == initial_state { 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/timecode.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | use crate::{ 10 | bitstream::Bitstream, format::TimecodeFormat, pitch::PitchDetector, 11 | util::ExponentialWeightedMovingAverage, 12 | }; 13 | use std::cmp; 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 16 | pub enum WaveCycleStatus { 17 | Positive, 18 | Negative, 19 | } 20 | 21 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 22 | pub enum TimecodeDirection { 23 | Forwards, 24 | Backwards, 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct TimecodeChannel { 29 | ewma: ExponentialWeightedMovingAverage, 30 | wave_cycle_status: WaveCycleStatus, 31 | peak_threshold: i32, 32 | } 33 | 34 | const TIME_CONSTANT: f64 = 0.0001; 35 | 36 | const fn sample_to_i32(sample: i16) -> i32 { 37 | (sample as i32) << 16 38 | } 39 | 40 | impl TimecodeChannel { 41 | const INITIAL_PEAK_THRESHOLD: i32 = 0; 42 | 43 | pub fn new(sample_rate_hz: f64) -> Self { 44 | let ewma = ExponentialWeightedMovingAverage::new(TIME_CONSTANT, sample_rate_hz); 45 | 46 | let wave_cycle_status = WaveCycleStatus::Positive; 47 | let peak_threshold = Self::INITIAL_PEAK_THRESHOLD; 48 | 49 | TimecodeChannel { 50 | ewma, 51 | wave_cycle_status, 52 | peak_threshold, 53 | } 54 | } 55 | 56 | /// Returns true if the wave has crossed zero. 57 | pub fn has_crossed_zero(&self, sample: i32) -> bool { 58 | match self.wave_cycle_status { 59 | WaveCycleStatus::Negative => sample > self.ewma.last_output, 60 | WaveCycleStatus::Positive => sample < self.ewma.last_output, 61 | } 62 | } 63 | 64 | /// Process a sample and detect zero crossing. 65 | pub fn process_sample(&mut self, sample: i32) -> bool { 66 | let crossed_zero = self.has_crossed_zero(sample); 67 | if crossed_zero { 68 | self.wave_cycle_status = match self.wave_cycle_status { 69 | WaveCycleStatus::Negative => WaveCycleStatus::Positive, 70 | WaveCycleStatus::Positive => WaveCycleStatus::Negative, 71 | }; 72 | } 73 | 74 | self.ewma.process(sample); 75 | 76 | crossed_zero 77 | } 78 | 79 | /// Reads a bit from the sample and adjust the threshold. 80 | pub fn bit_from_sample(&mut self, sample: i32) -> bool { 81 | let sample = self.ewma.difference_to(sample).abs(); 82 | self.peak_threshold = cmp::max(sample, self.peak_threshold); 83 | let threshold = (f64::from(self.peak_threshold) * 0.9).trunc() as i32; 84 | sample > threshold 85 | } 86 | } 87 | 88 | #[derive(Debug)] 89 | pub struct Timecode { 90 | bitstream: Bitstream, 91 | primary_channel: TimecodeChannel, 92 | secondary_channel: TimecodeChannel, 93 | direction: TimecodeDirection, 94 | pitch: PitchDetector, 95 | } 96 | 97 | impl Timecode { 98 | pub fn new(format: &TimecodeFormat, sample_rate_hz: f64) -> Self { 99 | let TimecodeFormat { 100 | size, 101 | seed, 102 | taps, 103 | signal_frequency_hz, 104 | } = format; 105 | 106 | let bitstream = Bitstream::new(*size, *seed, *taps); 107 | let primary_channel = TimecodeChannel::new(sample_rate_hz); 108 | let secondary_channel = TimecodeChannel::new(sample_rate_hz); 109 | 110 | let pitch = PitchDetector::new(sample_rate_hz, *signal_frequency_hz); 111 | 112 | Self { 113 | bitstream, 114 | primary_channel, 115 | secondary_channel, 116 | direction: TimecodeDirection::Forwards, 117 | pitch, 118 | } 119 | } 120 | /// Returns the current state of the bitstream 121 | pub fn state(&self) -> u32 { 122 | self.bitstream.state() 123 | } 124 | 125 | pub fn set_state(&mut self, state: u32) { 126 | self.bitstream.set_state(state); 127 | } 128 | 129 | pub fn process_channels( 130 | &mut self, 131 | primary_sample: i16, 132 | secondary_sample: i16, 133 | ) -> Option<(bool, Option)> { 134 | let primary_sample = sample_to_i32(primary_sample); 135 | let secondary_sample = sample_to_i32(secondary_sample); 136 | let primary_crossed_zero = self.primary_channel.process_sample(primary_sample); 137 | let secondary_crossed_zero = self.secondary_channel.process_sample(secondary_sample); 138 | 139 | // Detect the playback direction of the timecode. 140 | // 141 | // Assuming the primary channel crossed zero: 142 | // ──╮ ╭───╮ ╭(4)╮ If both the primary wave and the secondary 143 | // │ (2) │ │ │ wave are negative (1) or both are positive 144 | // ───────────────────── (2), then the timecode is playing forwards, 145 | // (1) │ │ │ │ otherwise it's playing backwards. 146 | // ╰───╯ ╰(3)╯ ╰── 147 | // Assuming the secondary channel crossed zero: 148 | // ╮ ╭(2)╮ ╭───╮ ╭ If the primary wave is negative and the 149 | // │ │ │ (3) │ │ secondary wave is positive (3) or if the 150 | // ───────────────────── primary wave is positive and the secondary 151 | // │ │ │ │ (4) │ wave is positive (4), the timecode is playing 152 | // ╰(1)╯ ╰───╯ ╰───╯ forwards, otherwise it's playing backwards. 153 | // 154 | if primary_crossed_zero { 155 | self.direction = if self.primary_channel.wave_cycle_status 156 | == self.secondary_channel.wave_cycle_status 157 | { 158 | TimecodeDirection::Forwards 159 | } else { 160 | TimecodeDirection::Backwards 161 | } 162 | } else if secondary_crossed_zero { 163 | self.direction = if self.primary_channel.wave_cycle_status 164 | != self.secondary_channel.wave_cycle_status 165 | { 166 | TimecodeDirection::Forwards 167 | } else { 168 | TimecodeDirection::Backwards 169 | } 170 | } 171 | 172 | // The timecode has a frequency of 1000 Hz and the sample rate is 44100 Hz. 173 | // This means a cycle at full playback rate takes 44.1 samples to complete. 174 | // 175 | // ⇤ Cycle ⇥ 176 | // ──╮┋ ╭───╮┋ ╭ 177 | // │┋ │ │┋ │ 178 | // ───┋──2───4┋─── 179 | // │┋ │ │┋ │ 180 | // ╰┋──╯ ╰┋──╯ 181 | // ┋ ┋ 182 | // ╮ ┋╭───╮ ┋╭── 183 | // │ ┋│ │ ┋│ 184 | // ───┋1───3──┋─── 185 | // │ ┋│ │ ┋│ 186 | // ╰──┋╯ ╰──┋╯ 187 | // 188 | // For each cycle, the wave for each channel crosses zero 2 times, so there are 4 zero 189 | // crossings per cycle total. This means there should be 4 zero crossings per 44.1 samples 190 | // if the record is playing with full speed. With the record is played back with double 191 | // speed, it takes 22.05 samples to complete a cycle (in other words: to detect 4 zero 192 | // crossings), and if it plays with half speed, it takes 88.2 samples. 193 | // 194 | // This means we can count the number of samples of the last current cycle, and then 195 | // calculate the pitch as 44.1 / number_of_samples_of_this_cycle. 196 | // 197 | // To get faster responses, we can simply count the number of samples per quarter cycle 198 | // (i.e. per single zero crossing) then calculate: 199 | // pitch = 11.025 / number_of_samples_since_previous_zero_crossing 200 | if primary_crossed_zero || secondary_crossed_zero { 201 | self.pitch.update_after_zero_crossing( 202 | primary_sample, 203 | secondary_sample, 204 | primary_crossed_zero, 205 | ); 206 | } else { 207 | self.pitch.update(primary_sample, secondary_sample); 208 | } 209 | 210 | // Read a bit from the timecode. 211 | // 212 | // The timecode waveform has a constant frequency with a variable 213 | // amplitude. The variations in the amplitude encode the binary data 214 | // stream. The primary channel's amplitude is read as a bit when 215 | // the secondary channel's waveform crosses 0 and the primary 216 | // channel's waveform is positive. Peaks with a larger amplitude 217 | // are bit 1 (diagram positions 1 and 3) and peaks with a lower 218 | // amplitude are bit 0 (diagram position 2). 219 | // 220 | // "1" "1" 221 | // ╭───╮ "0" ╭───╮ 222 | // │ │ ╭───╮ │ │ 223 | // ───(1)─────(2)─────(3)─── primary channel 224 | // │ ╰───╯ │ │ │ 225 | // ──╯ ╰───╯ ╰── 226 | // 227 | // ╭───╮ ╭───╮ ╭ 228 | // │ │ ╭───╮ │ │ │ 229 | // ───(1)─────(2)─────(3)─── secondary channel 230 | // │ ╰───╯ │ │ │ │ 231 | // ╯ ╰───╯ ╰───╯ 232 | // 233 | if secondary_crossed_zero 234 | && self.primary_channel.wave_cycle_status == WaveCycleStatus::Positive 235 | { 236 | let bit = self.primary_channel.bit_from_sample(primary_sample); 237 | if self.direction == TimecodeDirection::Forwards { 238 | self.bitstream.process_bit(bit as u32); 239 | } else { 240 | self.bitstream.process_bit_backward(bit as u32); 241 | } 242 | return Some((bit, self.bitstream.position())); 243 | } 244 | 245 | None 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/bitstream.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! The [`Bitstream` struct](Bitstream) processes bits and maps them to positions. 10 | 11 | use crate::{bits, lfsr::FibonacciLfsr}; 12 | use std::collections::HashMap; 13 | 14 | /// Maps a bitstream to a position in the underlying lookup table. 15 | /// 16 | /// The [`Bitstream` struct](Bitstream) uses an n-bit LFSR to populate a lookup table (LUT), which 17 | /// can then be used to retrieve a position for some n-bit sequence. 18 | #[derive(Debug)] 19 | pub struct Bitstream { 20 | lookup_table: HashMap, 21 | size: usize, 22 | bitstream: u32, 23 | valid_bits: usize, 24 | } 25 | 26 | impl Bitstream { 27 | /// Create a timecode bitstream using a LFSR with length `capacity`. 28 | pub fn new(size: usize, seed: u32, taps: u32) -> Self { 29 | // Precompute lookup table 30 | let capacity = 2u32.pow(size as u32) - 1; 31 | let mut lfsr = FibonacciLfsr { 32 | size, 33 | state: seed, 34 | taps, 35 | }; 36 | let mut lookup_table = HashMap::with_capacity(capacity as usize); 37 | for i in 0..capacity { 38 | lookup_table.insert(lfsr.state, i); 39 | lfsr.advance(); 40 | } 41 | 42 | Self { 43 | lookup_table, 44 | size, 45 | bitstream: seed, 46 | valid_bits: size, 47 | } 48 | } 49 | 50 | /// Process a single bit in forwards direction. 51 | /// 52 | /// If the positions before and after inserting the bit are not consecutive, the bitstream 53 | /// is marked as invalid. Processing more bits will let the bitstream become valid again. 54 | pub fn process_bit(&mut self, bit: u32) { 55 | let prev_position = self.position(); 56 | self.bitstream = bits::insert_msb(self.size, self.bitstream, bit); 57 | if let Some(prev_position) = prev_position { 58 | let next_position = self.position(); 59 | if let Some(next_position) = next_position { 60 | if prev_position + 1 != next_position { 61 | // Discard all previously processed bits 62 | self.valid_bits = 0 63 | } 64 | } 65 | } 66 | self.valid_bits += 1; 67 | } 68 | 69 | /// Process a single bit in backwards direction. 70 | /// 71 | /// If the positions before and after inserting the bit are not consecutive, the bitstream 72 | /// is marked as invalid. Processing more bits will let the bitstream become valid again. 73 | pub fn process_bit_backward(&mut self, bit: u32) { 74 | let prev_position = self.position(); 75 | self.bitstream = bits::insert_lsb(self.size, self.bitstream, bit); 76 | if let Some(prev_position) = prev_position { 77 | let next_position = self.position(); 78 | if let Some(next_position) = next_position { 79 | if prev_position != next_position + 1 { 80 | // Discard all previously processed bits 81 | self.valid_bits = 0; 82 | } 83 | } 84 | } 85 | self.valid_bits += 1; 86 | } 87 | 88 | /// Returns `true` if the position is considered valid. 89 | pub fn is_valid(&self) -> bool { 90 | self.valid_bits >= self.size 91 | } 92 | 93 | /// Returns the current state of the bitstream 94 | pub fn state(&self) -> u32 { 95 | self.bitstream 96 | } 97 | 98 | pub fn set_state(&mut self, state: u32) { 99 | self.bitstream = state; 100 | } 101 | 102 | /// Retrieve the Position from the current bitstream. 103 | /// 104 | /// Returns None if the bitstream is considered invalid. 105 | pub fn position(&self) -> Option { 106 | if !self.is_valid() { 107 | return None; 108 | } 109 | 110 | self.lookup_table 111 | .get(&self.bitstream) 112 | .map(ToOwned::to_owned) 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod test { 118 | use super::Bitstream; 119 | 120 | #[test] 121 | fn test_lookup_table() { 122 | let mut timecode = Bitstream::new(8, 0b00000001, 0b00011101); 123 | assert_eq!(timecode.position(), Some(0)); 124 | assert_eq!(timecode.valid_bits, 8); 125 | 126 | // old state: 0b00000001 127 | // taps: 0b00011101 128 | // next input: 0b00000001.count_ones() mod 2 = 1 129 | timecode.process_bit(1); 130 | // new state: 0b10000000 131 | 132 | assert_eq!(timecode.position(), Some(1)); 133 | assert_eq!(timecode.valid_bits, 9); 134 | 135 | // old state: 0b10000000 136 | // taps: 0b00011101 137 | // next input: 0b00000000.count_ones() mod 2 = 0 138 | timecode.process_bit(0); 139 | // new state: 0b01000000 140 | 141 | assert_eq!(timecode.position(), Some(2)); 142 | assert_eq!(timecode.valid_bits, 10); 143 | 144 | // old state: 0b01000000 145 | // taps: 0b00011101 146 | // next input: 0b00000000.count_ones() mod 2 = 0 147 | timecode.process_bit(0); 148 | // new state: 0b00100000 149 | 150 | assert_eq!(timecode.position(), Some(3)); 151 | assert_eq!(timecode.valid_bits, 11); 152 | 153 | // old state: 0b00100000 154 | // taps: 0b00011101 155 | // next input: 0b00000000.count_ones() mod 2 = 0 156 | timecode.process_bit(0); 157 | // new state: 0b00011000 158 | 159 | assert_eq!(timecode.position(), Some(4)); 160 | assert_eq!(timecode.valid_bits, 12); 161 | 162 | // old state: 0b00010000 163 | // taps: 0b00011101 164 | // next input: 0b00010000.count_ones() mod 2 = 1 165 | // 166 | // Here, we simulate skipping, resulting in an invalid bitstream until at least 8 bits were 167 | // processed. Hence, we push 0 even though the next bit is expected to be 0. 168 | timecode.process_bit(0); 169 | assert_eq!(timecode.position(), None); 170 | assert_eq!(timecode.valid_bits, 1); 171 | 172 | timecode.process_bit(0); 173 | assert_eq!(timecode.position(), None); 174 | assert_eq!(timecode.valid_bits, 2); 175 | 176 | timecode.process_bit(1); 177 | assert_eq!(timecode.position(), None); 178 | assert_eq!(timecode.valid_bits, 3); 179 | 180 | timecode.process_bit(1); 181 | assert_eq!(timecode.position(), None); 182 | assert_eq!(timecode.valid_bits, 4); 183 | 184 | timecode.process_bit(0); 185 | assert_eq!(timecode.position(), None); 186 | assert_eq!(timecode.valid_bits, 5); 187 | 188 | timecode.process_bit(0); 189 | assert_eq!(timecode.position(), None); 190 | assert_eq!(timecode.valid_bits, 6); 191 | 192 | timecode.process_bit(1); 193 | assert_eq!(timecode.position(), None); 194 | assert_eq!(timecode.valid_bits, 7); 195 | 196 | timecode.process_bit(1); 197 | 198 | // At this point, 8 consecutive bits were processed, so bitstream is valid again 199 | assert_eq!(timecode.bitstream, 0b11001100); 200 | assert_eq!(timecode.position(), Some(182)); 201 | assert_eq!(timecode.valid_bits, 8); 202 | 203 | // old state: 0b11001100 204 | // taps: 0b00011101 205 | // next input: 0b00001100.count_ones() mod 2 = 0 206 | timecode.process_bit(0); 207 | // new state: 0b01100110 208 | 209 | assert_eq!(timecode.position(), Some(183)); 210 | assert_eq!(timecode.valid_bits, 9); 211 | 212 | // old state: 0b01100110 213 | // taps: 0b00011101 214 | // next input: 0b00000100.count_ones() mod 2 = 1 215 | timecode.process_bit(1); 216 | // new state: 0b10110011 217 | 218 | assert_eq!(timecode.position(), Some(184)); 219 | assert_eq!(timecode.valid_bits, 10); 220 | 221 | // old state: 0b10110011 222 | // taps: 0b00011101 223 | // next input: 0b00010001.count_ones() mod 2 = 0 224 | timecode.process_bit(0); 225 | // new state: 0b01011001 226 | 227 | assert_eq!(timecode.position(), Some(185)); 228 | assert_eq!(timecode.valid_bits, 11); 229 | 230 | timecode.process_bit_backward(1); 231 | assert_eq!(timecode.position(), Some(184)); 232 | assert_eq!(timecode.valid_bits, 12); 233 | 234 | timecode.process_bit_backward(0); 235 | assert_eq!(timecode.position(), Some(183)); 236 | assert_eq!(timecode.valid_bits, 13); 237 | 238 | timecode.process_bit(1); 239 | assert_eq!(timecode.position(), Some(184)); 240 | assert_eq!(timecode.valid_bits, 14); 241 | } 242 | 243 | #[test] 244 | fn test_exactly_1_bit_produces_consecutive_positions() { 245 | // At any point in time, you can either process a 1 or a 0. 246 | // 247 | // Let a be the position before processing bit x and b be the position after processing 248 | // bit x. Then a and b should be consecutive positions for exactly one x ∈ {0, 1}. 249 | 250 | // Process bit 0 and check if positions are consecutive 251 | let mut timecode0 = Bitstream::new(8, 0b11110000, 0b00011101); 252 | let position0_a = timecode0.position(); 253 | timecode0.process_bit(0); 254 | let position0_b = timecode0.position(); 255 | let consecutive0 = if let (Some(a), Some(b)) = (position0_a, position0_b) { 256 | a + 1 == b 257 | } else { 258 | false 259 | }; 260 | 261 | // Now do the same for bit 1 262 | let mut timecode1 = Bitstream::new(8, 0b11110000, 0b00011101); 263 | let position1_a = timecode1.position(); 264 | timecode1.process_bit(1); 265 | let position1_b = timecode1.position(); 266 | let consecutive1 = if let (Some(a), Some(b)) = (position1_a, position1_b) { 267 | a + 1 == b 268 | } else { 269 | false 270 | }; 271 | 272 | assert_ne!(consecutive0, consecutive1); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/lfsr.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Jan Holthuis et al. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy 4 | // of the MPL was not distributed with this file, You can obtain one at 5 | // http://mozilla.org/MPL/2.0/. 6 | // 7 | // SPDX-License-Identifier: MPL-2.0 8 | 9 | //! Implementation of a Fibonacci Linear Feedback Shift Register (LFSR). 10 | //! 11 | //! An n-bit LFSR generates a bitstream from an n-bit state. For each cycle, the bits at certain 12 | //! positions of the current state are XOR'ed and the result is fed back into the register. The 13 | //! rightmost bit of the state that is "pushed out" of the register is the output bit. 14 | //! 15 | //! # Description 16 | //! 17 | //! *Note: Let a = n. We use a instead of n here because there is no subscript n in Unicode). Also, 18 | //! this generic description may look complicated and daunting, but keep reading. There's an 19 | //! example below that will make it clearer.* 20 | //! 21 | //! An LFSR can be described by the register's bit length (a) and the bit positions that influence 22 | //! the next feedback bit. These bit positions are called "taps" and can be written as vector p = 23 | //! (pₐ₋₁, ..., p₃, p₂, p₁, p₀) where each element can either be 0 or 1 (mathematically speaking: ∀ 24 | //! x ∈ ℕ: pₓ ∈ {0, 1}). 25 | //! 26 | //! ```text 27 | //! MSB LSB 28 | //! ┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ 29 | //! ┌──▶│ sₐ₋₁├┬──▶ ... ─▶│ s₃├┬▶│ s₂├┬▶│ s₁├┬▶│ s₀├┬───▶ output bit 30 | //! │ └─────┘│ └───┘│ └───┘│ └───┘│ └───┘│ 31 | //! │ ▼ ▼ ▼ ▼ ▼ 32 | //! │sₐ ⊗ ◀─pₐ₋₁ ⊗ ◀─p₃ ⊗ ◀─p₂ ⊗ ◀─p₁ ⊗ ◀─p₀ 33 | //! │ │ │ │ │ │ 34 | //! │ ▼ ▼ ▼ ▼ │ 35 | //! └─────────╴⊕ ◀─ ... ◀──────⊕ ◀────⊕ ◀────⊕ ◀────┘ 36 | //! ``` 37 | //! 38 | //! The LFSR state is a-bit vector s = (sₐ₋₁, ..., s₃, s₂, s₁, s₀). 39 | //! 40 | //! After the first clock cycle, the internal state is shifted, such that s = (sₐ, ..., s₄, s₃, s₂, 41 | //! s₁) and s₀ becomes the output bit. The feedback bit can be calculated as: 42 | //! 43 | //! ```text 44 | //! sₐ ≡ pₐ₋₁ × sₐ₋₁ + ... + p₃ × s₃ + p₂ × s₂ + p₁ × s₁ + p₁ × s₁ + p₀ × s₀ mod 2 45 | //! ``` 46 | //! 47 | //! It's important that the taps `p` are a property of the LFSR. That property doesn't change, so 48 | //! the next feedback bit is calculated like this: 49 | //! 50 | //! ```text 51 | //! sₐ₊₁ ≡ pₐ₋₁ × sₐ + ... + p₃ × s₄ + p₂ × s₃ + p₁ × s₂ + p₁ × s₂ + p₀ × s₁ mod 2 52 | //! ``` 53 | //! 54 | //! # Example 55 | //! 56 | //! Let's consider an 8-bit LFSR with taps p = (0, 0, 0, 1, 1, 1, 0, 1). This information 57 | //! suffices to draw it as: 58 | //! 59 | //! ```text 60 | //! MSB LSB 61 | //! ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ 62 | //! ┌──▶│ s₇├─▶│ s₆├─▶│ s₅├─▶│ s₄├┬▶│ s₃├┬▶│ s₂├┬▶│ s₁├─▶│ s₀├┬───▶ output bit 63 | //! │ └───┘ └───┘ └───┘ └───┘│ └───┘│ └───┘│ └───┘ └───┘│ 64 | //! │s₈ │ │ │ │ 65 | //! │ ▼ ▼ ▼ │ 66 | //! └─────────────────────────────⊕ ◀────⊕ ◀────⊕ ◀───────────┘ 67 | //! ``` 68 | //! 69 | //! We can now calculate the output bit as: 70 | //! 71 | //! ```text 72 | //! s₈ ≡ p₇ × s₇ + p₆ × s₆ + p₅ × s₅ + p₄ × s₄ + p₃ × s₃ + p₂ × s₂ + p₁ × s₁ + p₀ × s₀ mod 2 73 | //! ≡ 0 × s₇ + 0 × s₆ + 0 × s₅ + 1 × s₄ + 1 × s₃ + 1 × s₂ + 0 × s₁ + 1 × s₀ mod 2 74 | //! ≡ s₄ + s₃ + s₂ + s₀ mod 2 75 | //! ≡ s₄ ⊕ s₃ ⊕ s₂ ⊕ s₀ 76 | //! ``` 77 | //! 78 | //! Let the initial state s = (1, 1, 0, 0, 1, 0, 0, 1). 79 | //! 80 | //! ```text 81 | //! MSB LSB 82 | //! s₇ s₆ s₅ s₄ s₃ s₂ s₁ s₀ 83 | //! ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ 84 | //! ┌──▶│ 1 ├─▶│ 1 ├─▶│ 0 ├─▶│ 0 ├┬▶│ 1 ├┬▶│ 0 ├┬▶│ 0 ├─▶│ 1 ├┬───▶ s₀ 85 | //! │ └───┘ └───┘ └───┘ └───┘│ └───┘│ └───┘│ └───┘ └───┘│ 86 | //! │s₈ │ │ │ │ 87 | //! │ ▼ ▼ ▼ │ 88 | //! └─────────────────────────────⊕ ◀────⊕ ◀────⊕ ◀───────────┘ 89 | //! ``` 90 | //! 91 | //! The first clock cycle now shifts the register to the right using feedback bit s₈. 92 | //! That bit can be calculated using the equation above: 93 | //! 94 | //! ```text 95 | //! s₈ ≡ s₄ ⊕ s₃ ⊕ s₂ ⊕ s₀ 96 | //! ≡ 0 ⊕ 1 ⊕ 0 ⊕ 1 97 | //! ≡ 0 98 | //! ``` 99 | //! 100 | //! The output bit is the bit that gets "pushed out" of the register, i.e. s₀ = 0. 101 | //! 102 | //! After the first clock the LFSR has state s = (0, 1, 1, 0, 0, 1, 0, 0) and looks like this: 103 | //! 104 | //! ```text 105 | //! MSB LSB 106 | //! s₈ s₇ s₆ s₅ s₄ s₃ s₂ s₁ 107 | //! ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ 108 | //! ┌──▶│ 0 ├─▶│ 1 ├─▶│ 1 ├─▶│ 0 ├┬▶│ 0 ├┬▶│ 1 ├┬▶│ 0 ├─▶│ 0 ├┬───▶ s₁ 109 | //! │ └───┘ └───┘ └───┘ └───┘│ └───┘│ └───┘│ └───┘ └───┘│ 110 | //! │s₉ │ │ │ │ 111 | //! │ ▼ ▼ ▼ │ 112 | //! └─────────────────────────────⊕ ◀────⊕ ◀────⊕ ◀───────────┘ 113 | //! ``` 114 | //! 115 | //! For the second clock cycle, the output bit is s₁ = 0 and we can calculate the feedback bit s₉ as: 116 | //! 117 | //! ```text 118 | //! s₉ ≡ s₅ ⊕ s₄ ⊕ s₃ ⊕ s₁ 119 | //! ≡ 0 ⊕ 0 ⊕ 1 ⊕ 0 120 | //! ≡ 1 121 | //! ``` 122 | //! 123 | //! After the second clock the LFSR has state s = (1, 0, 1, 1, 0, 0, 1, 0) and looks like this: 124 | //! 125 | //! ```text 126 | //! MSB LSB 127 | //! s₉ s₈ s₇ s₆ s₅ s₄ s₃ s₂ 128 | //! ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ 129 | //! ┌──▶│ 1 ├─▶│ 0 ├─▶│ 1 ├─▶│ 1 ├┬▶│ 0 ├┬▶│ 0 ├┬▶│ 1 ├─▶│ 0 ├┬───▶ s₂ 130 | //! │ └───┘ └───┘ └───┘ └───┘│ └───┘│ └───┘│ └───┘ └───┘│ 131 | //! │s₁₀ │ │ │ │ 132 | //! │ ▼ ▼ ▼ │ 133 | //! └─────────────────────────────⊕ ◀────⊕ ◀────⊕ ◀───────────┘ 134 | //! ``` 135 | //! 136 | //! ## Feedback Polynomial 137 | //! 138 | //! Mathematicians love polynomials, so instead of using the size and the taps to describe an LFSR, 139 | //! they often use a polynomial: 140 | //! 141 | //! ```text 142 | //! P(x) = p₀ × xᵃ + p₁ × xᵃ⁻¹ + p₂ × xᵃ⁻² + ... + pₐ₋₁ × x¹ + x⁰ 143 | //! ``` 144 | //! 145 | //! So for the 8-bit LFSR in the example, we had these taps: 146 | //! 147 | //! ```text 148 | //! p = (p₇, p₆, p₅, p₄, p₃, p₂, p₁, p₀) 149 | //! = (0, 0, 0, 1, 1, 1, 0, 1) 150 | //! ``` 151 | //! 152 | //! Therefore, the feedback polynomial of that LFSR is: 153 | //! 154 | //! ```text 155 | //! P(x) = p₀ × x⁸ + p₁ × x⁷ + p₂ × x⁶ + p₃ × x⁵ + p₄ × x⁴ + p₅ × x³ + p₆ × x² + p₇ × x¹ + x⁰ 156 | //! = 1 × x⁸ + 0 × x⁷ + 1 × x⁶ + 1 × x⁵ + 1 × x⁴ + 0 × x³ + 0 × x² + 0 × x¹ + x⁰ 157 | //! = x⁸ + x⁶ + x⁵ + x⁴ + x⁰ 158 | //! = x⁸ + x⁶ + x⁵ + x⁴ + 1 159 | //! ``` 160 | //! 161 | use super::bits; 162 | 163 | /// Fibonacci Linear Feedback Shift Register (LFSR) 164 | #[derive(Debug, Clone, PartialEq)] 165 | pub struct FibonacciLfsr { 166 | pub size: usize, 167 | pub state: u32, 168 | pub taps: u32, 169 | } 170 | 171 | impl FibonacciLfsr { 172 | /// Return the next LFSR state (without making any changes). 173 | pub const fn next_state(&self) -> u32 { 174 | let next_bit = (self.state & self.taps).count_ones() & 1; 175 | bits::insert_msb(self.size, self.state, next_bit) 176 | } 177 | 178 | /// Return the previous LFSR state (without making any changes). 179 | pub const fn previous_state(&self) -> u32 { 180 | let taps = bits::rotate_right(self.size, self.taps); 181 | let previous_bit = (self.state & taps).count_ones() & 1; 182 | bits::insert_lsb(self.size, self.state, previous_bit) 183 | } 184 | 185 | /// Advance the LFSR state and return it. 186 | pub fn advance(&mut self) -> u32 { 187 | self.state = self.next_state(); 188 | self.state 189 | } 190 | 191 | /// Revert the LFSR state and return it. 192 | pub fn revert(&mut self) -> u32 { 193 | self.state = self.previous_state(); 194 | self.state 195 | } 196 | 197 | ///// Returns the maximum period length for the register size 198 | //pub fn max_period(size: usize) -> usize { 199 | // assert!(size < (u32::MAX as usize)); 200 | // 2_usize.pow(size as u32) - 1 201 | //} 202 | } 203 | 204 | #[cfg(test)] 205 | mod tests { 206 | use super::FibonacciLfsr; 207 | 208 | fn find_lfsr_period(size: usize, seed: u32, taps: u32) -> Option { 209 | let mut lfsr = FibonacciLfsr { 210 | size, 211 | state: seed, 212 | taps, 213 | }; 214 | let mut period: usize = 0; 215 | let last_state = lfsr.state; 216 | while period < usize::MAX { 217 | let state = lfsr.advance(); 218 | period += 1; 219 | if lfsr.state == seed { 220 | return Some(period); 221 | } 222 | assert_eq!(lfsr.state, state); 223 | assert_ne!(lfsr.state, last_state); 224 | } 225 | None 226 | } 227 | 228 | #[test] 229 | fn test_maximal_length_lfsrs() { 230 | // Test a bunch of maximum length LFSRs (i. e. b-bit LFSRs that generate an bitstream with 231 | // a 2^n - 1 period). 232 | let configurations = [ 233 | (2, 0b11), // x^2 + x + 1 234 | (3, 0b011), // x^3 + x^2 + 1 235 | (4, 0b0011), // x^4 + x^3 + 1 236 | (5, 0b00101), // x^5 + x^3 + 1 237 | (6, 0b000011), // x^6 + x^5 + 1 238 | (7, 0b0000011), // x^7 + x^6 + 1 239 | (8, 0b00011101), // x^8 + x^6 + x^5 + x^4 + 1 240 | (9, 0b000010001), // x^9 + x^5 + 1 241 | (10, 0b0000001001), // x^10 + x^7 + 1 242 | (11, 0b00000000101), // x^11 + x^9 + 1 243 | (12, 0b000100000111), // x^12 + x^11 + x^10 + x^4 + 1 244 | ]; 245 | 246 | let seed = 1; 247 | for &(size, taps) in configurations.iter() { 248 | let expected_period = 2_usize.pow(size as u32) - 1; 249 | let actual_period = 250 | find_lfsr_period(size, seed, taps).expect("Failed to find LFSR period!"); 251 | assert_eq!(expected_period, actual_period, "Unexpected LFSR period!"); 252 | } 253 | } 254 | 255 | #[test] 256 | fn test_lfsr_advance_and_revert() { 257 | let mut lfsr = FibonacciLfsr { 258 | state: 0b10101, 259 | size: 5, 260 | taps: 0b00101, 261 | }; 262 | assert_eq!(lfsr.state, 0b10101); 263 | 264 | assert_eq!(lfsr.advance(), 0b01010); 265 | assert_eq!(lfsr.advance(), 0b00101); 266 | assert_eq!(lfsr.advance(), 0b00010); 267 | assert_eq!(lfsr.advance(), 0b00001); 268 | assert_eq!(lfsr.advance(), 0b10000); 269 | assert_eq!(lfsr.advance(), 0b01000); 270 | assert_eq!(lfsr.advance(), 0b00100); 271 | assert_eq!(lfsr.advance(), 0b10010); 272 | assert_eq!(lfsr.advance(), 0b01001); 273 | assert_eq!(lfsr.advance(), 0b10100); 274 | assert_eq!(lfsr.advance(), 0b11010); 275 | assert_eq!(lfsr.advance(), 0b01101); 276 | assert_eq!(lfsr.advance(), 0b00110); 277 | assert_eq!(lfsr.advance(), 0b10011); 278 | assert_eq!(lfsr.advance(), 0b11001); 279 | assert_eq!(lfsr.advance(), 0b11100); 280 | assert_eq!(lfsr.advance(), 0b11110); 281 | assert_eq!(lfsr.advance(), 0b11111); 282 | assert_eq!(lfsr.advance(), 0b01111); 283 | assert_eq!(lfsr.advance(), 0b00111); 284 | assert_eq!(lfsr.advance(), 0b00011); 285 | assert_eq!(lfsr.advance(), 0b10001); 286 | assert_eq!(lfsr.advance(), 0b11000); 287 | assert_eq!(lfsr.advance(), 0b01100); 288 | assert_eq!(lfsr.advance(), 0b10110); 289 | assert_eq!(lfsr.advance(), 0b11011); 290 | assert_eq!(lfsr.advance(), 0b11101); 291 | assert_eq!(lfsr.advance(), 0b01110); 292 | assert_eq!(lfsr.advance(), 0b10111); 293 | assert_eq!(lfsr.advance(), 0b01011); 294 | assert_eq!(lfsr.advance(), 0b10101); 295 | 296 | assert_eq!(lfsr.revert(), 0b01011); 297 | assert_eq!(lfsr.revert(), 0b10111); 298 | assert_eq!(lfsr.revert(), 0b01110); 299 | assert_eq!(lfsr.revert(), 0b11101); 300 | assert_eq!(lfsr.revert(), 0b11011); 301 | assert_eq!(lfsr.revert(), 0b10110); 302 | assert_eq!(lfsr.revert(), 0b01100); 303 | assert_eq!(lfsr.revert(), 0b11000); 304 | assert_eq!(lfsr.revert(), 0b10001); 305 | assert_eq!(lfsr.revert(), 0b00011); 306 | assert_eq!(lfsr.revert(), 0b00111); 307 | assert_eq!(lfsr.revert(), 0b01111); 308 | assert_eq!(lfsr.revert(), 0b11111); 309 | assert_eq!(lfsr.revert(), 0b11110); 310 | assert_eq!(lfsr.revert(), 0b11100); 311 | assert_eq!(lfsr.revert(), 0b11001); 312 | assert_eq!(lfsr.revert(), 0b10011); 313 | assert_eq!(lfsr.revert(), 0b00110); 314 | assert_eq!(lfsr.revert(), 0b01101); 315 | assert_eq!(lfsr.revert(), 0b11010); 316 | assert_eq!(lfsr.revert(), 0b10100); 317 | assert_eq!(lfsr.revert(), 0b01001); 318 | assert_eq!(lfsr.revert(), 0b10010); 319 | assert_eq!(lfsr.revert(), 0b00100); 320 | assert_eq!(lfsr.revert(), 0b01000); 321 | assert_eq!(lfsr.revert(), 0b10000); 322 | assert_eq!(lfsr.revert(), 0b00001); 323 | assert_eq!(lfsr.revert(), 0b00010); 324 | assert_eq!(lfsr.revert(), 0b00101); 325 | assert_eq!(lfsr.revert(), 0b01010); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------