26.0.1
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Markdown personal settings
|
||||
/markdown.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/capi-types/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/cdda-interface/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/cdda-paranoia/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/cli/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/cli/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/core/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/core/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/core/examples" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/cdrustanoia.iml" filepath="$PROJECT_DIR$/.idea/cdrustanoia.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+5
-5
@@ -10,7 +10,7 @@ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "cdda_interface"
|
||||
version = "0.1.0"
|
||||
version = "26.0.1"
|
||||
dependencies = [
|
||||
"cdrustanoia_capi_types",
|
||||
"cdrustanoia_core",
|
||||
@@ -19,7 +19,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdda_paranoia"
|
||||
version = "0.1.0"
|
||||
version = "26.0.1"
|
||||
dependencies = [
|
||||
"cdda_interface",
|
||||
"cdrustanoia_capi_types",
|
||||
@@ -29,14 +29,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdrustanoia_capi_types"
|
||||
version = "0.1.0"
|
||||
version = "26.0.1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cdrustanoia_cli"
|
||||
version = "0.1.0"
|
||||
version = "26.0.1"
|
||||
dependencies = [
|
||||
"cdrustanoia_core",
|
||||
"libc",
|
||||
@@ -45,7 +45,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdrustanoia_core"
|
||||
version = "0.1.0"
|
||||
version = "26.0.1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tempfile",
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
version = "26.0.1"
|
||||
edition = "2021"
|
||||
license = "LGPL-2.1-or-later"
|
||||
|
||||
@@ -20,4 +20,5 @@ cdrustanoia_capi_types = { path = "crates/capi-types" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
|
||||
@@ -58,6 +58,20 @@ cdrustanoia -r -- "-3" out.raw
|
||||
Use `cdrustanoia --help` for the full option and span documentation. The man
|
||||
page is the original cdparanoia man page.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The core crate contains a small synthetic benchmark for the paranoia read path
|
||||
and the sample index:
|
||||
|
||||
```sh
|
||||
cargo run --release -p cdrustanoia_core --example bench_paranoia -- 5000
|
||||
```
|
||||
|
||||
It reports index setup/query times, a comparison with the old `HashMap`
|
||||
index, and overall synthetic read throughput. The image backend is
|
||||
memory-backed, so the absolute numbers are useful for regression tracking
|
||||
rather than as a prediction of physical drive speed.
|
||||
|
||||
## C API
|
||||
|
||||
The two C libraries export the traditional symbols and use the original
|
||||
|
||||
@@ -17,6 +17,13 @@ pub struct BackendHolder {
|
||||
pub drive: Box<dyn Drive>,
|
||||
}
|
||||
|
||||
fn ffi_guard<T>(default: T, f: impl FnOnce() -> T) -> T {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
|
||||
Ok(value) => value,
|
||||
Err(_) => default,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn cstr(s: &str) -> *mut c_char {
|
||||
let clean: String = s.chars().filter(|c| *c != '\0').collect();
|
||||
match CString::new(clean) {
|
||||
@@ -212,6 +219,16 @@ pub unsafe extern "C" fn cdda_identify(
|
||||
device: *const c_char,
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
ffi_guard(ptr::null_mut(), || {
|
||||
cdda_identify_impl(device, messagedest, messages)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn cdda_identify_impl(
|
||||
device: *const c_char,
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
if !messages.is_null() {
|
||||
*messages = ptr::null_mut();
|
||||
@@ -234,6 +251,16 @@ pub unsafe extern "C" fn cdda_identify_cooked(
|
||||
device: *const c_char,
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
ffi_guard(ptr::null_mut(), || {
|
||||
cdda_identify_cooked_impl(device, messagedest, messages)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn cdda_identify_cooked_impl(
|
||||
device: *const c_char,
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
if !messages.is_null() {
|
||||
*messages = ptr::null_mut();
|
||||
@@ -257,6 +284,17 @@ pub unsafe extern "C" fn cdda_identify_scsi(
|
||||
ioctl_device: *const c_char,
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
ffi_guard(ptr::null_mut(), || {
|
||||
cdda_identify_scsi_impl(generic_device, ioctl_device, messagedest, messages)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn cdda_identify_scsi_impl(
|
||||
generic_device: *const c_char,
|
||||
ioctl_device: *const c_char,
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
if !messages.is_null() {
|
||||
*messages = ptr::null_mut();
|
||||
@@ -289,6 +327,15 @@ pub unsafe extern "C" fn cdda_identify_scsi(
|
||||
pub unsafe extern "C" fn cdda_find_a_cdrom(
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
ffi_guard(ptr::null_mut(), || {
|
||||
cdda_find_a_cdrom_impl(messagedest, messages)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn cdda_find_a_cdrom_impl(
|
||||
messagedest: c_int,
|
||||
messages: *mut *mut c_char,
|
||||
) -> *mut CdromDrive {
|
||||
if !messages.is_null() {
|
||||
*messages = ptr::null_mut();
|
||||
@@ -301,6 +348,10 @@ pub unsafe extern "C" fn cdda_find_a_cdrom(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int {
|
||||
ffi_guard(-100, || cdda_open_impl(d))
|
||||
}
|
||||
|
||||
unsafe fn cdda_open_impl(d: *mut CdromDrive) -> c_int {
|
||||
if d.is_null() {
|
||||
return -100;
|
||||
}
|
||||
@@ -343,6 +394,10 @@ pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn cdda_close(d: *mut CdromDrive) -> c_int {
|
||||
ffi_guard(0, || cdda_close_impl(d))
|
||||
}
|
||||
|
||||
unsafe fn cdda_close_impl(d: *mut CdromDrive) -> c_int {
|
||||
if d.is_null() {
|
||||
return 0;
|
||||
}
|
||||
@@ -357,6 +412,18 @@ pub unsafe extern "C" fn cdda_read_timed(
|
||||
beginsector: c_long,
|
||||
sectors: c_long,
|
||||
ms: *mut c_int,
|
||||
) -> c_long {
|
||||
ffi_guard(-1, || {
|
||||
cdda_read_timed_impl(d, buffer, beginsector, sectors, ms)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn cdda_read_timed_impl(
|
||||
d: *mut CdromDrive,
|
||||
buffer: *mut c_void,
|
||||
beginsector: c_long,
|
||||
sectors: c_long,
|
||||
ms: *mut c_int,
|
||||
) -> c_long {
|
||||
if !ms.is_null() {
|
||||
*ms = -1;
|
||||
@@ -422,6 +489,10 @@ pub unsafe extern "C" fn cdda_read(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn cdda_speed_set(d: *mut CdromDrive, speed: c_int) -> c_int {
|
||||
ffi_guard(-405, || cdda_speed_set_impl(d, speed))
|
||||
}
|
||||
|
||||
unsafe fn cdda_speed_set_impl(d: *mut CdromDrive, speed: c_int) -> c_int {
|
||||
match holder(d) {
|
||||
Some(h) => {
|
||||
let ret = h.drive.speed_set(speed);
|
||||
@@ -569,3 +640,81 @@ pub unsafe extern "C" fn cdda_track_bitmap(
|
||||
// Keep the c_long import used on all targets.
|
||||
#[allow(dead_code)]
|
||||
fn _typecheck(_: c_long) {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cdrustanoia_core::device::{DiscToc, InterfaceKind, ReadError, TimedRead};
|
||||
|
||||
struct PanicDrive {
|
||||
toc: DiscToc,
|
||||
}
|
||||
|
||||
impl Drive for PanicDrive {
|
||||
fn toc(&self) -> &DiscToc {
|
||||
&self.toc
|
||||
}
|
||||
fn toc_mut(&mut self) -> &mut DiscToc {
|
||||
&mut self.toc
|
||||
}
|
||||
fn opened(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn open_disc(&mut self) -> Result<(), String> {
|
||||
panic!("intentional test panic");
|
||||
}
|
||||
fn read_audio(
|
||||
&mut self,
|
||||
_buffer: Option<&mut [u8]>,
|
||||
_begin: i64,
|
||||
_sectors: i64,
|
||||
) -> Result<usize, ReadError> {
|
||||
Err(ReadError::closed())
|
||||
}
|
||||
fn read_timed(
|
||||
&mut self,
|
||||
_buffer: Option<&mut [u8]>,
|
||||
_begin: i64,
|
||||
_sectors: i64,
|
||||
) -> TimedRead {
|
||||
TimedRead {
|
||||
result: Err(ReadError::closed()),
|
||||
milliseconds: -1,
|
||||
}
|
||||
}
|
||||
fn speed_set(&mut self, _speed: i32) -> i32 {
|
||||
-405
|
||||
}
|
||||
fn close(&mut self) {}
|
||||
fn drive_model(&self) -> &str {
|
||||
"panic drive"
|
||||
}
|
||||
fn bigendianp(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
fn set_bigendianp(&mut self, _value: i32) {}
|
||||
fn nsectors(&self) -> i64 {
|
||||
1
|
||||
}
|
||||
fn set_nsectors(&mut self, _value: i64) {}
|
||||
fn interface(&self) -> InterfaceKind {
|
||||
InterfaceKind::SgIo
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_api_catches_panics() {
|
||||
unsafe {
|
||||
let d = make_drive(
|
||||
"panic",
|
||||
None,
|
||||
Box::new(PanicDrive {
|
||||
toc: DiscToc::default(),
|
||||
}),
|
||||
);
|
||||
assert!(!d.is_null());
|
||||
assert_eq!(cdda_open(d), -100);
|
||||
assert_eq!(cdda_close(d), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ use std::time::Instant;
|
||||
|
||||
pub type ParanoiaCallback = Option<unsafe extern "C" fn(c_long, c_int)>;
|
||||
|
||||
fn ffi_guard<T>(default: T, f: impl FnOnce() -> T) -> T {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
|
||||
Ok(value) => value,
|
||||
Err(_) => default,
|
||||
}
|
||||
}
|
||||
|
||||
struct CAbiDrive {
|
||||
d: *mut CdromDrive,
|
||||
toc: DiscToc,
|
||||
@@ -190,6 +197,10 @@ pub unsafe extern "C" fn paranoia_version() -> *const c_char {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void {
|
||||
ffi_guard(ptr::null_mut(), || paranoia_init_impl(d))
|
||||
}
|
||||
|
||||
unsafe fn paranoia_init_impl(d: *mut CdromDrive) -> *mut c_void {
|
||||
if d.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
@@ -201,6 +212,10 @@ pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn paranoia_free(p: *mut c_void) {
|
||||
ffi_guard((), || paranoia_free_impl(p))
|
||||
}
|
||||
|
||||
unsafe fn paranoia_free_impl(p: *mut c_void) {
|
||||
if !p.is_null() {
|
||||
drop(Box::from_raw(p as *mut Paranoia));
|
||||
}
|
||||
@@ -246,6 +261,16 @@ pub unsafe extern "C" fn paranoia_read_limited(
|
||||
p: *mut c_void,
|
||||
callback: ParanoiaCallback,
|
||||
max_retries: c_int,
|
||||
) -> *mut i16 {
|
||||
ffi_guard(ptr::null_mut(), || {
|
||||
paranoia_read_limited_impl(p, callback, max_retries)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn paranoia_read_limited_impl(
|
||||
p: *mut c_void,
|
||||
callback: ParanoiaCallback,
|
||||
max_retries: c_int,
|
||||
) -> *mut i16 {
|
||||
let p = match p_ref(p) {
|
||||
Some(p) => p,
|
||||
|
||||
+38
-33
@@ -22,19 +22,20 @@ use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
const MANUAL: &str = r#"cdparanoia III release 10.2 (September 11, 2008)
|
||||
const MANUAL: &str = r#"cdrustanoia 26.0.1 (2026-11-03)
|
||||
based on the great cdparanoia by Monty <monty@xiph.org> and Xiph.Org
|
||||
|
||||
(C) 2008 Monty <monty@xiph.org> and Xiph.Org
|
||||
(C) 2026 Martin Wohlert <martin.wohlert@b-root-force.de>
|
||||
|
||||
Report bugs to martin.wohlert@b-root-force.de
|
||||
|
||||
Report bugs to paranoia@xiph.org
|
||||
http://www.xiph.org/paranoia/
|
||||
USAGE:
|
||||
cdparanoia [options] <span> [outfile]
|
||||
cdrustanoia [options] <span> [outfile]
|
||||
|
||||
OPTIONS:
|
||||
-A --analyze-drive : run and log a complete analysis of drive
|
||||
caching, timing and reading behavior;
|
||||
verifies that cdparanoia is correctly
|
||||
verifies that cdrustanoia is correctly
|
||||
modelling a sprcific drive's cache and
|
||||
read behavior. Implies -vQL
|
||||
-v --verbose : extra verbose operation
|
||||
@@ -42,10 +43,10 @@ OPTIONS:
|
||||
-e --stderr-progress : force output of progress information to
|
||||
stderr (for wrapper scripts)
|
||||
-l --log-summary [<file>] : save result summary to file, default
|
||||
filename cdparanoia.log
|
||||
filename cdrustanoia.log
|
||||
-L --log-debug [<file>] : save detailed device autosense and
|
||||
debugging output to file, default
|
||||
filename cdparanoia.log
|
||||
filename cdrustanoia.log
|
||||
-V --version : print version info and quit
|
||||
-Q --query : autosense drive, query disc and quit
|
||||
-B --batch : 'batch' mode (saves each track to a
|
||||
@@ -77,7 +78,7 @@ OPTIONS:
|
||||
force use of the old SG kernel
|
||||
interface. -g cannot be used with -k.
|
||||
-S --force-read-speed <n> : read from device at specified speed; by
|
||||
default, cdparanoia sets drive to full
|
||||
default, cdrustanoia sets drive to full
|
||||
speed.
|
||||
-t --toc-offset <n> : Add <n> sectors to the values reported
|
||||
when addressing tracks. May be negative
|
||||
@@ -154,16 +155,14 @@ the shell...
|
||||
A few examples, protected from the shell:
|
||||
A) query only with exhaustive search for a drive and full reporting
|
||||
of autosense:
|
||||
cdparanoia -vsQ
|
||||
cdrustanoia -vsQ
|
||||
|
||||
B) extract up to and including track 3, putting each track in a seperate
|
||||
file:
|
||||
cdparanoia -B -- "-3"
|
||||
cdrustanoia -B -- "-3"
|
||||
|
||||
C) extract from track 1, time 0:30.12 to 1:10.00:
|
||||
cdparanoia "1[:30.12]-1[1:10]"
|
||||
|
||||
Submit bug reports to paranoia@xiph.org
|
||||
cdrustanoia "1[:30.12]-1[1:10]"
|
||||
|
||||
"#;
|
||||
|
||||
@@ -283,17 +282,6 @@ fn display_toc(d: &dyn Drive, log: &mut Log) {
|
||||
log.report(format_args!(" "));
|
||||
}
|
||||
|
||||
fn read_sector(
|
||||
p: &mut Paranoia,
|
||||
progress: &mut Progress,
|
||||
log: &mut Log,
|
||||
max_retries: i32,
|
||||
) -> Option<Vec<i16>> {
|
||||
let mut callback = |pos: i64, func: i32| progress.callback(pos, func, log);
|
||||
p.read_limited(&mut callback, max_retries)
|
||||
.map(|s| s.to_vec())
|
||||
}
|
||||
|
||||
fn samples_as_bytes(samples: &[i16]) -> &[u8] {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
@@ -708,7 +696,11 @@ of the machine.\n\nMore information about /dev/cdrom:"
|
||||
|
||||
progress.skipped_flag = false;
|
||||
while cursor <= batch_last {
|
||||
let sector = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
|
||||
let sector = {
|
||||
let mut callback = |pos: i64, func: i32| progress.callback(pos, func, &mut log);
|
||||
p.read_limited(&mut callback, args.max_retries)
|
||||
};
|
||||
let data = match sector {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
progress.skipped_flag = true;
|
||||
@@ -725,23 +717,32 @@ of the machine.\n\nMore information about /dev/cdrom:"
|
||||
progress.skipped_flag = false;
|
||||
cursor += 1;
|
||||
|
||||
let mut data = sector;
|
||||
if target_big != host_big {
|
||||
for s in &mut data {
|
||||
for s in data.iter_mut() {
|
||||
*s = swap16(*s);
|
||||
}
|
||||
}
|
||||
progress.callback(cursor * 1176 - 1, -2, &mut log);
|
||||
|
||||
let data_bytes = samples_as_bytes(&data);
|
||||
let data_bytes = samples_as_bytes(data);
|
||||
if let Err(e) = out.write_all(&data_bytes[offset_skip.min(data_bytes.len())..]) {
|
||||
log.report(format_args!("Error writing output: {}", e));
|
||||
return 1;
|
||||
}
|
||||
offset_skip = 0;
|
||||
|
||||
if target_big != host_big {
|
||||
for s in data.iter_mut() {
|
||||
*s = swap16(*s);
|
||||
}
|
||||
}
|
||||
|
||||
if sample_offset != 0 && cursor > batch_last {
|
||||
let extra = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
|
||||
let extra = {
|
||||
let mut callback = |pos: i64, func: i32| progress.callback(pos, func, &mut log);
|
||||
p.read_limited(&mut callback, args.max_retries)
|
||||
};
|
||||
let extra = match extra {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
progress.skipped_flag = true;
|
||||
@@ -756,14 +757,18 @@ of the machine.\n\nMore information about /dev/cdrom:"
|
||||
break;
|
||||
}
|
||||
progress.skipped_flag = false;
|
||||
let mut extra = extra;
|
||||
if target_big != host_big {
|
||||
for s in &mut extra {
|
||||
for s in extra.iter_mut() {
|
||||
*s = swap16(*s);
|
||||
}
|
||||
}
|
||||
let eb = samples_as_bytes(&extra);
|
||||
let eb = samples_as_bytes(extra);
|
||||
offset_buffer[..eb.len().min(2352)].copy_from_slice(&eb[..eb.len().min(2352)]);
|
||||
if target_big != host_big {
|
||||
for s in extra.iter_mut() {
|
||||
*s = swap16(*s);
|
||||
}
|
||||
}
|
||||
offset_buffer_used = (sample_offset as usize * 4).min(2352);
|
||||
progress.callback(cursor * 1176, -2, &mut log);
|
||||
if let Err(e) = out.write_all(&offset_buffer[..offset_buffer_used]) {
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "cdrustanoia_core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license = "LGPL-2.1-or-later"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Small benchmark harness for the paranoia read path and sample index.
|
||||
//!
|
||||
//! Run with:
|
||||
//! cargo run --release --example bench_paranoia
|
||||
//! cargo run --release --example bench_paranoia -- 5000
|
||||
|
||||
use cdrustanoia_core::device::image::ImageDrive;
|
||||
use cdrustanoia_core::device::samples_to_bytes;
|
||||
use cdrustanoia_core::paranoia::sort::SortIndex;
|
||||
use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
fn make_samples(sectors: usize) -> Vec<i16> {
|
||||
let mut out = Vec::with_capacity(sectors * 1176);
|
||||
let mut x = 0x1234_5678_9abc_def0u64;
|
||||
for i in 0..sectors * 1176 {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
let v = if i % 5000 < 700 {
|
||||
0
|
||||
} else {
|
||||
(x as i16).wrapping_add((i as i16).wrapping_mul(11))
|
||||
};
|
||||
out.push(v);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn bench_index(samples: &[i16]) {
|
||||
let mut index = SortIndex::new(samples.len());
|
||||
let started = Instant::now();
|
||||
index.setup(samples, 0, 0, samples.len() as i64);
|
||||
let setup = started.elapsed();
|
||||
|
||||
let mut found = 0usize;
|
||||
let started = Instant::now();
|
||||
for i in 0..10_000i64 {
|
||||
let post = (i * 7919) % samples.len() as i64;
|
||||
let value = samples[post as usize];
|
||||
if index.getmatch(samples, post, 512, value).is_some() {
|
||||
found += 1;
|
||||
while index.nextmatch(samples).is_some() {
|
||||
found += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let queries = started.elapsed();
|
||||
println!(
|
||||
"SortIndex: setup={:?} 10k queries={:?} matches={} ({} samples)",
|
||||
setup,
|
||||
queries,
|
||||
found,
|
||||
samples.len()
|
||||
);
|
||||
}
|
||||
|
||||
struct OldIndex {
|
||||
buckets: HashMap<i16, Vec<u32>>,
|
||||
size: usize,
|
||||
cursor: Option<(i16, usize, usize)>,
|
||||
}
|
||||
|
||||
impl OldIndex {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
buckets: HashMap::new(),
|
||||
size: 0,
|
||||
cursor: None,
|
||||
}
|
||||
}
|
||||
fn setup(&mut self, samples: &[i16]) {
|
||||
self.buckets.clear();
|
||||
self.size = samples.len();
|
||||
for (i, &sample) in samples.iter().enumerate() {
|
||||
self.buckets.entry(sample).or_default().push(i as u32);
|
||||
}
|
||||
}
|
||||
fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option<usize> {
|
||||
let post = post.clamp(0, self.size as i64);
|
||||
let lo = (post - overlap).max(0) as usize;
|
||||
let hi = ((post + overlap).min(self.size as i64)).max(0) as usize;
|
||||
self.cursor = None;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
let idx = positions.partition_point(|p| (*p as usize) < lo);
|
||||
if idx >= positions.len() {
|
||||
return None;
|
||||
}
|
||||
let pos = positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some((value, idx + 1, hi));
|
||||
Some(pos)
|
||||
}
|
||||
fn nextmatch(&mut self) -> Option<usize> {
|
||||
let (value, idx, hi) = self.cursor?;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
if idx >= positions.len() {
|
||||
self.cursor = None;
|
||||
return None;
|
||||
}
|
||||
let pos = positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
self.cursor = None;
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some((value, idx + 1, hi));
|
||||
Some(pos)
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_old_index(samples: &[i16]) {
|
||||
let mut index = OldIndex::new();
|
||||
let started = Instant::now();
|
||||
index.setup(samples);
|
||||
let setup = started.elapsed();
|
||||
let mut found = 0usize;
|
||||
let started = Instant::now();
|
||||
for i in 0..10_000i64 {
|
||||
let post = (i * 7919) % samples.len() as i64;
|
||||
let value = samples[post as usize];
|
||||
if index.getmatch(post, 512, value).is_some() {
|
||||
found += 1;
|
||||
while index.nextmatch().is_some() {
|
||||
found += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let queries = started.elapsed();
|
||||
println!(
|
||||
"Old HashMap index: setup={:?} 10k queries={:?} matches={}",
|
||||
setup, queries, found
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sectors: usize = std::env::args()
|
||||
.nth(1)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
let samples = make_samples(sectors);
|
||||
bench_index(&samples);
|
||||
bench_old_index(&samples);
|
||||
|
||||
let bytes = samples_to_bytes(&samples);
|
||||
let drive = ImageDrive::from_bytes(bytes);
|
||||
let mut p = Paranoia::new(Box::new(drive));
|
||||
p.modeset(PARANOIA_MODE_FULL);
|
||||
let mut cb = |_: i64, _: i32| {};
|
||||
let started = Instant::now();
|
||||
let mut got = 0usize;
|
||||
for _ in 0..sectors {
|
||||
let sector = p.read(&mut cb).expect("read sector");
|
||||
got += sector.len();
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
let bytes_read = (got * 2) as f64;
|
||||
println!(
|
||||
"Read: {} sectors, {} samples, {:?}, {:.1} MB/s",
|
||||
sectors,
|
||||
got,
|
||||
elapsed,
|
||||
bytes_read / elapsed.as_secs_f64() / 1_000_000.0
|
||||
);
|
||||
assert_eq!(got, sectors * 1176);
|
||||
}
|
||||
@@ -192,7 +192,13 @@ impl ImageDrive {
|
||||
|
||||
fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec<i16> {
|
||||
let words = sectors.max(0) * CD_FRAMEWORDS;
|
||||
let mut out = Vec::with_capacity(words as usize);
|
||||
let mut out = vec![0i16; words as usize];
|
||||
self.read_words_into(begin_sector, sectors, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn read_words_into(&mut self, begin_sector: i64, sectors: i64, out: &mut [i16]) {
|
||||
let words = sectors.max(0) * CD_FRAMEWORDS;
|
||||
let mut jitter = 0i64;
|
||||
if self.faults.jitter_words > 0 {
|
||||
self.read_count = self.read_count.wrapping_add(1);
|
||||
@@ -211,10 +217,9 @@ impl ImageDrive {
|
||||
}
|
||||
}
|
||||
let start = begin_sector * CD_FRAMEWORDS + jitter;
|
||||
for w in 0..words {
|
||||
out.push(self.mapped_sample(start + w).unwrap_or(0));
|
||||
for (w, slot) in out.iter_mut().enumerate().take(words as usize) {
|
||||
*slot = self.mapped_sample(start + w as i64).unwrap_or(0);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +286,32 @@ impl Drive for ImageDrive {
|
||||
self.last_begin = begin;
|
||||
Ok(sectors as usize)
|
||||
}
|
||||
fn read_audio_samples(
|
||||
&mut self,
|
||||
buffer: &mut [i16],
|
||||
begin: i64,
|
||||
sectors: i64,
|
||||
) -> Result<usize, ReadError> {
|
||||
if !self.opened {
|
||||
return Err(ReadError::closed());
|
||||
}
|
||||
if sectors <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut sectors = sectors;
|
||||
if self.faults.max_read_sectors > 0 {
|
||||
sectors = sectors.min(self.faults.max_read_sectors);
|
||||
}
|
||||
let out_len = (sectors * CD_FRAMEWORDS) as usize;
|
||||
self.read_words_into(begin, sectors, buffer);
|
||||
if super::host_big_endian() {
|
||||
for sample in buffer.iter_mut().take(out_len) {
|
||||
*sample = super::swap16(*sample);
|
||||
}
|
||||
}
|
||||
self.last_begin = begin;
|
||||
Ok(sectors as usize)
|
||||
}
|
||||
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
|
||||
let start = Instant::now();
|
||||
let previous_begin = self.last_begin;
|
||||
|
||||
@@ -178,6 +178,7 @@ pub struct LinuxDrive {
|
||||
fua: bool,
|
||||
read_cmd: ReadCommand,
|
||||
enable_mode: EnableMode,
|
||||
last_milliseconds: i32,
|
||||
}
|
||||
|
||||
impl LinuxDrive {
|
||||
@@ -199,6 +200,7 @@ impl LinuxDrive {
|
||||
fua: false,
|
||||
read_cmd: ReadCommand::D8,
|
||||
enable_mode: EnableMode::Dummy,
|
||||
last_milliseconds: 0,
|
||||
};
|
||||
d.inquiry()?;
|
||||
d.check_atapi();
|
||||
@@ -238,6 +240,7 @@ impl LinuxDrive {
|
||||
fua: false,
|
||||
read_cmd: ReadCommand::Read28,
|
||||
enable_mode: EnableMode::Dummy,
|
||||
last_milliseconds: 0,
|
||||
};
|
||||
Ok(d)
|
||||
}
|
||||
@@ -561,7 +564,9 @@ impl LinuxDrive {
|
||||
duration: 0,
|
||||
info: 0,
|
||||
};
|
||||
let started = Instant::now();
|
||||
let rc = unsafe { libc::ioctl(self.fd, SG_IO, &mut hdr as *mut SgIoHdr) };
|
||||
self.last_milliseconds = started.elapsed().as_millis() as i32;
|
||||
if rc < 0 {
|
||||
return Err(ScsiError::io(io::Error::last_os_error()));
|
||||
}
|
||||
@@ -819,7 +824,9 @@ impl LinuxDrive {
|
||||
nframes: requested as c_int,
|
||||
buf: buf.as_mut_ptr(),
|
||||
};
|
||||
let started = Instant::now();
|
||||
let rc = unsafe { libc::ioctl(self.fd, CDROMREADAUDIO, &mut arg as *mut _) };
|
||||
self.last_milliseconds = started.elapsed().as_millis() as i32;
|
||||
if rc == 0 {
|
||||
return Ok(requested as usize);
|
||||
}
|
||||
@@ -1211,12 +1218,11 @@ impl Drive for LinuxDrive {
|
||||
self.read_raw(buffer, begin, sectors)
|
||||
}
|
||||
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
|
||||
let started = Instant::now();
|
||||
self.last_milliseconds = -1;
|
||||
let result = self.read_audio(buffer, begin, sectors);
|
||||
let elapsed = started.elapsed().as_millis() as i32;
|
||||
TimedRead {
|
||||
result,
|
||||
milliseconds: elapsed,
|
||||
milliseconds: self.last_milliseconds,
|
||||
}
|
||||
}
|
||||
fn speed_set(&mut self, speed: i32) -> i32 {
|
||||
|
||||
@@ -275,6 +275,31 @@ pub trait Drive {
|
||||
begin: i64,
|
||||
sectors: i64,
|
||||
) -> Result<usize, ReadError>;
|
||||
|
||||
/// Read audio directly into a host-endian `i16` buffer.
|
||||
///
|
||||
/// This is a byte-view over the same memory; it lets the paranoia engine
|
||||
/// read straight into the `CBlock` vector without an intermediate
|
||||
/// `Vec<u8>` plus `bytes_to_samples()` copy. Implementations of
|
||||
/// `read_audio()` already promise host endian PCM data.
|
||||
fn read_audio_samples(
|
||||
&mut self,
|
||||
buffer: &mut [i16],
|
||||
begin: i64,
|
||||
sectors: i64,
|
||||
) -> Result<usize, ReadError> {
|
||||
// SAFETY: `i16` has no padding and the byte view covers exactly the
|
||||
// initialized elements of `buffer`. The view lives only for this
|
||||
// call, and all bytes are written by the backend before it returns.
|
||||
let bytes = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
buffer.as_mut_ptr().cast::<u8>(),
|
||||
std::mem::size_of_val(buffer),
|
||||
)
|
||||
};
|
||||
self.read_audio(Some(bytes), begin, sectors)
|
||||
}
|
||||
|
||||
/// Low-level read without host-endian conversion, matching the original
|
||||
/// `cdrom_drive::read_audio` function pointer.
|
||||
fn read_audio_raw(
|
||||
|
||||
@@ -15,8 +15,8 @@ pub mod callback;
|
||||
pub mod device;
|
||||
pub mod paranoia;
|
||||
|
||||
pub const VERSIONNUM: &str = "10.2";
|
||||
pub const VERSION: &str = "cdparanoia III release 10.2 (September 11, 2008)";
|
||||
pub const VERSIONNUM: &str = "26.0.1";
|
||||
pub const VERSION: &str = "cdrustanoia 26.0.1 (2026-11-03)";
|
||||
|
||||
pub fn paranoia_version() -> &'static str {
|
||||
VERSIONNUM
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
//! c_block/v_fragment/root data structures and vector operations.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const FLAGS_EDGE: u8 = 0x1;
|
||||
pub const FLAGS_UNREAD: u8 = 0x2;
|
||||
pub const FLAGS_VERIFIED: u8 = 0x4;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CBlock {
|
||||
pub vector: Vec<i16>,
|
||||
pub vector: Arc<Vec<i16>>,
|
||||
pub flags: Option<Vec<u8>>,
|
||||
pub begin: i64,
|
||||
pub size: i64,
|
||||
@@ -22,7 +24,7 @@ impl Default for CBlock {
|
||||
impl CBlock {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vector: Vec::new(),
|
||||
vector: Arc::new(Vec::new()),
|
||||
flags: None,
|
||||
begin: 0,
|
||||
size: 0,
|
||||
@@ -33,7 +35,7 @@ impl CBlock {
|
||||
pub fn from_parts(vector: Vec<i16>, begin: i64, lastsector: bool) -> Self {
|
||||
let size = vector.len() as i64;
|
||||
Self {
|
||||
vector,
|
||||
vector: Arc::new(vector),
|
||||
flags: None,
|
||||
begin,
|
||||
size,
|
||||
@@ -62,7 +64,7 @@ impl CBlock {
|
||||
return;
|
||||
}
|
||||
let pos = pos as usize;
|
||||
self.vector.splice(pos..pos, data.iter().copied());
|
||||
Arc::make_mut(&mut self.vector).splice(pos..pos, data.iter().copied());
|
||||
self.size += data.len() as i64;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
let n = data.len();
|
||||
@@ -87,7 +89,7 @@ impl CBlock {
|
||||
}
|
||||
let pos = cutpos as usize;
|
||||
let n = cutsize as usize;
|
||||
self.vector.drain(pos..pos + n);
|
||||
Arc::make_mut(&mut self.vector).drain(pos..pos + n);
|
||||
self.size -= cutsize;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
let pos = pos.min(flags.len());
|
||||
@@ -109,11 +111,11 @@ impl CBlock {
|
||||
}
|
||||
let p = pos as usize;
|
||||
let n = size as usize;
|
||||
self.vector[p..p + n].copy_from_slice(&data[..n]);
|
||||
Arc::make_mut(&mut self.vector)[p..p + n].copy_from_slice(&data[..n]);
|
||||
}
|
||||
|
||||
pub fn append(&mut self, data: &[i16]) {
|
||||
self.vector.extend_from_slice(data);
|
||||
Arc::make_mut(&mut self.vector).extend_from_slice(data);
|
||||
self.size += data.len() as i64;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
flags.extend(std::iter::repeat_n(0, data.len()));
|
||||
@@ -125,7 +127,7 @@ impl CBlock {
|
||||
return;
|
||||
}
|
||||
let n = (cut as usize).min(self.vector.len());
|
||||
self.vector.drain(..n);
|
||||
Arc::make_mut(&mut self.vector).drain(..n);
|
||||
self.size -= n as i64;
|
||||
self.begin += n as i64;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
@@ -148,7 +150,10 @@ impl CBlock {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VFragment {
|
||||
pub vector: Vec<i16>,
|
||||
/// Shared backing data. Cache blocks and all fragments carved from them
|
||||
/// reference the same allocation; no per-fragment copy is made.
|
||||
pub data: Arc<Vec<i16>>,
|
||||
pub offset: usize,
|
||||
pub begin: i64,
|
||||
pub size: i64,
|
||||
pub lastsector: bool,
|
||||
@@ -158,6 +163,10 @@ impl VFragment {
|
||||
pub fn end(&self) -> i64 {
|
||||
self.begin + self.size
|
||||
}
|
||||
|
||||
pub fn vector(&self) -> &[i16] {
|
||||
&self.data[self.offset..self.offset + self.size as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
+174
-111
@@ -8,6 +8,7 @@ use crate::device::{Drive, CD_FRAMESIZE_RAW, CD_FRAMEWORDS};
|
||||
use block::*;
|
||||
use sort::SortIndex;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const MIN_WORDS_OVERLAP: i64 = 64;
|
||||
pub const MIN_WORDS_SEARCH: i64 = 64;
|
||||
@@ -61,6 +62,8 @@ pub struct Paranoia {
|
||||
pub dynoverlap: i64,
|
||||
pub dyndrift: i64,
|
||||
stage2_adjust_pending: bool,
|
||||
sample_pool: Vec<Vec<i16>>,
|
||||
flag_pool: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -329,7 +332,7 @@ fn try_sort_sync(
|
||||
// Search for a matching sample within dynoverlap.
|
||||
let search_post = post - a_begin;
|
||||
if sort
|
||||
.getmatch(search_post, *dynoverlap, b.vector[b_idx as usize])
|
||||
.getmatch(a_vector, search_post, *dynoverlap, b.vector[b_idx as usize])
|
||||
.is_some()
|
||||
{
|
||||
loop {
|
||||
@@ -356,7 +359,7 @@ fn try_sort_sync(
|
||||
*end = bend;
|
||||
return true;
|
||||
}
|
||||
if !sort.advance() {
|
||||
if !sort.advance(a_vector) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -732,7 +735,7 @@ fn iterate_stage2(
|
||||
callback(v.begin, PARANOIA_CB_VERIFY);
|
||||
|
||||
let mut fbv = v.begin.max(root.begin - *dynoverlap);
|
||||
while fbv < v.end() && v.vector[(fbv - v.begin) as usize] == 0 {
|
||||
while fbv < v.end() && v.vector()[(fbv - v.begin) as usize] == 0 {
|
||||
fbv += 1;
|
||||
}
|
||||
if fbv == v.end() {
|
||||
@@ -744,7 +747,7 @@ fn iterate_stage2(
|
||||
.min(v.end());
|
||||
let searchend = (fev + *dynoverlap).min(root.end());
|
||||
let searchbegin = (fbv - *dynoverlap).max(root.begin);
|
||||
sort.setup(&v.vector, v.begin, fbv, fev);
|
||||
sort.setup(v.vector(), v.begin, fbv, fev);
|
||||
|
||||
let mut j = searchbegin;
|
||||
while j < searchend {
|
||||
@@ -759,7 +762,7 @@ fn iterate_stage2(
|
||||
let mut offset = 0;
|
||||
if try_sort_sync(
|
||||
sort,
|
||||
&v.vector,
|
||||
v.vector(),
|
||||
v.begin,
|
||||
v.size,
|
||||
None,
|
||||
@@ -827,7 +830,7 @@ fn silence_match(
|
||||
return false;
|
||||
}
|
||||
let mut j = 0;
|
||||
while j < end && v.vector[j as usize] == 0 {
|
||||
while j < end && v.vector()[j as usize] == 0 {
|
||||
j += 1;
|
||||
}
|
||||
if j < MIN_SILENCE_BOUNDARY {
|
||||
@@ -849,7 +852,7 @@ fn silence_match(
|
||||
if v.end() > root.end() {
|
||||
let voff = begin - v.begin;
|
||||
root.remove(begin - root.begin, root.size - (begin - root.begin));
|
||||
root.append(&v.vector[voff as usize..]);
|
||||
root.append(&v.vector()[voff as usize..]);
|
||||
}
|
||||
add_stage2_value(stage2, 0, stage2_pending);
|
||||
} else if j < begin {
|
||||
@@ -859,7 +862,7 @@ fn silence_match(
|
||||
*silence_begin - root.begin,
|
||||
root.size - (*silence_begin - root.begin),
|
||||
);
|
||||
root.append(&v.vector[voff as usize..]);
|
||||
root.append(&v.vector()[voff as usize..]);
|
||||
}
|
||||
add_stage2_value(stage2, end - begin, stage2_pending);
|
||||
} else {
|
||||
@@ -907,7 +910,7 @@ fn stage2_each(
|
||||
callback: &mut dyn FnMut(i64, i32),
|
||||
) -> MergeOutcome {
|
||||
let dyno = *dynoverlap / 2 * 2;
|
||||
if v.size <= 0 || v.vector.is_empty() {
|
||||
if v.size <= 0 || v.vector().is_empty() {
|
||||
return MergeOutcome::Discarded;
|
||||
}
|
||||
|
||||
@@ -931,7 +934,11 @@ fn stage2_each(
|
||||
while begin + offset > 0 && begin > 0 {
|
||||
let begin_l = begin + offset;
|
||||
if scratch.is_none() {
|
||||
scratch = Some(CBlock::from_parts(v.vector.clone(), v.begin, v.lastsector));
|
||||
scratch = Some(CBlock::from_parts(
|
||||
v.vector().to_vec(),
|
||||
v.begin,
|
||||
v.lastsector,
|
||||
));
|
||||
}
|
||||
let (mut match_a, mut match_b, mut match_c) = (0, 0, 0);
|
||||
{
|
||||
@@ -1021,7 +1028,11 @@ fn stage2_each(
|
||||
let begin_l = begin + offset;
|
||||
let end_l = end + offset;
|
||||
if scratch.is_none() {
|
||||
scratch = Some(CBlock::from_parts(v.vector.clone(), v.begin, v.lastsector));
|
||||
scratch = Some(CBlock::from_parts(
|
||||
v.vector().to_vec(),
|
||||
v.begin,
|
||||
v.lastsector,
|
||||
));
|
||||
}
|
||||
let (mut match_a, mut match_b, mut match_c) = (0, 0, 0);
|
||||
{
|
||||
@@ -1130,7 +1141,7 @@ fn stage2_each(
|
||||
}
|
||||
let data: Vec<i16> = match scratch.as_ref() {
|
||||
Some(l) => l.vector[(end + offset) as usize..size_b as usize].to_vec(),
|
||||
None => v.vector[(end + offset) as usize..size_b as usize].to_vec(),
|
||||
None => v.vector()[(end + offset) as usize..size_b as usize].to_vec(),
|
||||
};
|
||||
if !data.is_empty() {
|
||||
root.append(&data);
|
||||
@@ -1183,6 +1194,8 @@ impl Paranoia {
|
||||
dynoverlap: MAX_SECTOR_OVERLAP * CD_FRAMEWORDS,
|
||||
dyndrift: 0,
|
||||
stage2_adjust_pending: false,
|
||||
sample_pool: Vec::new(),
|
||||
flag_pool: Vec::new(),
|
||||
};
|
||||
p.firstlast();
|
||||
p
|
||||
@@ -1212,6 +1225,41 @@ impl Paranoia {
|
||||
ret
|
||||
}
|
||||
|
||||
fn take_sample_buf(&mut self, len: usize) -> Vec<i16> {
|
||||
let mut buf = self.sample_pool.pop().unwrap_or_default();
|
||||
buf.clear();
|
||||
buf.resize(len, 0);
|
||||
buf
|
||||
}
|
||||
|
||||
fn take_flag_buf(&mut self, len: usize) -> Vec<u8> {
|
||||
let mut buf = self.flag_pool.pop().unwrap_or_default();
|
||||
buf.clear();
|
||||
buf.resize(len, 0);
|
||||
buf
|
||||
}
|
||||
|
||||
fn recycle_block(&mut self, mut block: CBlock) {
|
||||
if let Some(mut flags) = block.flags.take() {
|
||||
if self.flag_pool.len() < 2 {
|
||||
flags.clear();
|
||||
self.flag_pool.push(flags);
|
||||
}
|
||||
}
|
||||
if let Ok(mut vector) = Arc::try_unwrap(block.vector) {
|
||||
if self.sample_pool.len() < 2 {
|
||||
vector.clear();
|
||||
self.sample_pool.push(vector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recycle_root(&mut self) {
|
||||
if let Some(block) = self.root.vector.take() {
|
||||
self.recycle_block(block);
|
||||
}
|
||||
}
|
||||
|
||||
fn firstlast(&mut self) {
|
||||
let tracks = self.d.tracks();
|
||||
let curtrack = self.d.sector_gettrack(self.cursor);
|
||||
@@ -1249,7 +1297,7 @@ impl Paranoia {
|
||||
if self.d.sector_gettrack(sector) < 0 {
|
||||
return -1;
|
||||
}
|
||||
self.root.vector = None;
|
||||
self.recycle_root();
|
||||
self.root.lastsector = false;
|
||||
self.root.returned_limit = 0;
|
||||
self.root.silence_flag = false;
|
||||
@@ -1264,19 +1312,23 @@ impl Paranoia {
|
||||
self.root.returned_limit = 0;
|
||||
self.dyndrift = 0;
|
||||
self.root.lastsector = false;
|
||||
self.root.vector = None;
|
||||
self.recycle_root();
|
||||
self.root.silence_flag = false;
|
||||
self.resetcache();
|
||||
}
|
||||
|
||||
fn resetcache(&mut self) {
|
||||
self.cache.clear();
|
||||
self.fragments.clear();
|
||||
while let Some(block) = self.cache.pop_front() {
|
||||
self.recycle_block(block);
|
||||
}
|
||||
}
|
||||
|
||||
fn recover_cache(&mut self) {
|
||||
while self.cache.len() > self.cache_limit {
|
||||
self.cache.pop_back();
|
||||
if let Some(block) = self.cache.pop_back() {
|
||||
self.recycle_block(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1338,7 +1390,7 @@ impl Paranoia {
|
||||
beginword: i64,
|
||||
_endword: i64,
|
||||
callback: &mut dyn FnMut(i64, i32),
|
||||
) -> Option<CBlock> {
|
||||
) -> bool {
|
||||
let totaltoread = self.cdcache_size;
|
||||
let sectatonce = self.d.nsectors().max(1);
|
||||
let driftcomp = (self.dyndrift as f64 / CD_FRAMEWORDS as f64 + 0.5) as i64;
|
||||
@@ -1370,14 +1422,15 @@ impl Paranoia {
|
||||
|
||||
let mut new = CBlock::new();
|
||||
new.flags = if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 {
|
||||
Some(vec![0u8; (totaltoread * CD_FRAMEWORDS) as usize])
|
||||
Some(self.take_flag_buf((totaltoread * CD_FRAMEWORDS) as usize))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.cache.push_front(new);
|
||||
self.recover_cache();
|
||||
|
||||
let mut raw = vec![0u8; (totaltoread * CD_FRAMESIZE_RAW as i64) as usize];
|
||||
let word_count = (totaltoread * CD_FRAMEWORDS) as usize;
|
||||
let mut samples = self.take_sample_buf(word_count);
|
||||
let mut sofar = 0i64;
|
||||
let mut firstread = -1i64;
|
||||
let mut anyflag = false;
|
||||
@@ -1402,12 +1455,12 @@ impl Paranoia {
|
||||
if firstread < 0 {
|
||||
firstread = adjread;
|
||||
}
|
||||
let start = (sofar * CD_FRAMESIZE_RAW as i64) as usize;
|
||||
let end = start + (secread * CD_FRAMESIZE_RAW as i64) as usize;
|
||||
let end = end.min(raw.len());
|
||||
let start = (sofar * CD_FRAMEWORDS) as usize;
|
||||
let end = start + (secread * CD_FRAMEWORDS) as usize;
|
||||
let end = end.min(samples.len());
|
||||
let result = self
|
||||
.d
|
||||
.read_audio(Some(&mut raw[start..end]), adjread, secread);
|
||||
.read_audio_samples(&mut samples[start..end], adjread, secread);
|
||||
let thisread = match result {
|
||||
Ok(n) => {
|
||||
let mut n = n as i64;
|
||||
@@ -1418,8 +1471,10 @@ impl Paranoia {
|
||||
}
|
||||
Err(e) => {
|
||||
if e.errno == libc::ENOMEDIUM {
|
||||
self.cache.pop_front();
|
||||
return None;
|
||||
if let Some(block) = self.cache.pop_front() {
|
||||
self.recycle_block(block);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
0
|
||||
}
|
||||
@@ -1428,15 +1483,13 @@ impl Paranoia {
|
||||
if thisread >= 0 {
|
||||
callback((adjread + thisread) * CD_FRAMEWORDS, PARANOIA_CB_READERR);
|
||||
}
|
||||
let start = (sofar + thisread) as usize * CD_FRAMESIZE_RAW;
|
||||
let end = (sofar + secread) as usize * CD_FRAMESIZE_RAW;
|
||||
if start < raw.len() {
|
||||
let end = end.min(raw.len());
|
||||
raw[start..end].fill(0);
|
||||
let start = ((sofar + thisread) * CD_FRAMEWORDS) as usize;
|
||||
let end = ((sofar + secread) * CD_FRAMEWORDS) as usize;
|
||||
if start < samples.len() {
|
||||
let end = end.min(samples.len());
|
||||
samples[start..end].fill(0);
|
||||
}
|
||||
if let Some(flags) = self.cache.front_mut().and_then(|c| c.flags.as_mut()) {
|
||||
let start = ((sofar + thisread) * CD_FRAMEWORDS) as usize;
|
||||
let end = ((sofar + secread) * CD_FRAMEWORDS) as usize;
|
||||
if start < flags.len() {
|
||||
let end = end.min(flags.len());
|
||||
flags[start..end].fill(FLAGS_UNREAD);
|
||||
@@ -1476,23 +1529,20 @@ impl Paranoia {
|
||||
|
||||
if anyflag {
|
||||
let words = (sofar * CD_FRAMEWORDS) as usize;
|
||||
let bytes = (sofar * CD_FRAMESIZE_RAW as i64) as usize;
|
||||
raw.truncate(bytes);
|
||||
let samples = crate::device::bytes_to_samples(&raw);
|
||||
let mut flags = self.cache.front().and_then(|c| c.flags.clone());
|
||||
if let Some(f) = flags.as_mut() {
|
||||
f.truncate(words);
|
||||
samples.truncate(words);
|
||||
let block = self.cache.front_mut().unwrap();
|
||||
if let Some(flags) = block.flags.as_mut() {
|
||||
flags.truncate(words);
|
||||
}
|
||||
let mut new = self.cache.pop_front().unwrap();
|
||||
new.vector = samples;
|
||||
new.flags = flags;
|
||||
new.begin = firstread * CD_FRAMEWORDS - self.dyndrift;
|
||||
new.size = words as i64;
|
||||
self.cache.push_front(new);
|
||||
Some(self.cache.front().unwrap().clone())
|
||||
block.vector = Arc::new(samples);
|
||||
block.begin = firstread * CD_FRAMEWORDS - self.dyndrift;
|
||||
block.size = words as i64;
|
||||
true
|
||||
} else {
|
||||
self.cache.pop_front();
|
||||
None
|
||||
if let Some(block) = self.cache.pop_front() {
|
||||
self.recycle_block(block);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1535,9 +1585,9 @@ impl Paranoia {
|
||||
let frag_end = new.begin + (end + OVERLAP_ADJ).min(new.size);
|
||||
let lastsector = end + OVERLAP_ADJ >= new.size && new.lastsector;
|
||||
let start_idx = (frag_begin - new.begin) as usize;
|
||||
let end_idx = (frag_end - new.begin) as usize;
|
||||
self.fragments.push(Some(VFragment {
|
||||
vector: new.vector[start_idx..end_idx].to_vec(),
|
||||
data: Arc::clone(&new.vector),
|
||||
offset: start_idx,
|
||||
begin: frag_begin,
|
||||
size: frag_end - frag_begin,
|
||||
lastsector,
|
||||
@@ -1586,7 +1636,7 @@ impl Paranoia {
|
||||
}
|
||||
if let Some(id) = initialized {
|
||||
let v = self.fragments[id].take().unwrap();
|
||||
let c = CBlock::from_parts(v.vector, v.begin, v.lastsector);
|
||||
let c = CBlock::from_parts(v.vector().to_vec(), v.begin, v.lastsector);
|
||||
self.root.lastsector = v.lastsector;
|
||||
self.root.returned_limit = beginword;
|
||||
self.root.silence_flag = false;
|
||||
@@ -1726,7 +1776,8 @@ impl Paranoia {
|
||||
if v.begin < av {
|
||||
// Drop fragments that would move before absolute zero.
|
||||
*v = VFragment {
|
||||
vector: Vec::new(),
|
||||
data: Arc::new(Vec::new()),
|
||||
offset: 0,
|
||||
begin: 0,
|
||||
size: -1,
|
||||
lastsector: false,
|
||||
@@ -1754,29 +1805,38 @@ impl Paranoia {
|
||||
}
|
||||
|
||||
fn trim(&mut self, beginword: i64) {
|
||||
let mut free_root = false;
|
||||
if let Some(root) = self.root.vector.as_mut() {
|
||||
let target = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS;
|
||||
let rbegin = root.begin;
|
||||
let rend = root.end();
|
||||
if rbegin > beginword {
|
||||
self.root.vector = None;
|
||||
self.root.returned_limit = -1;
|
||||
self.root.lastsector = false;
|
||||
return;
|
||||
}
|
||||
if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword {
|
||||
free_root = true;
|
||||
} else if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword {
|
||||
if target + MIN_WORDS_OVERLAP > rend {
|
||||
self.root.vector = None;
|
||||
self.root.returned_limit = -1;
|
||||
self.root.lastsector = false;
|
||||
return;
|
||||
free_root = true;
|
||||
} else {
|
||||
let offset = target - rbegin;
|
||||
root.remove_front(offset);
|
||||
}
|
||||
let offset = target - rbegin;
|
||||
root.remove_front(offset);
|
||||
}
|
||||
}
|
||||
if free_root {
|
||||
self.recycle_root();
|
||||
self.root.returned_limit = -1;
|
||||
self.root.lastsector = false;
|
||||
return;
|
||||
}
|
||||
let cutoff = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS;
|
||||
self.cache.retain(|c| c.end() >= cutoff);
|
||||
let mut kept = VecDeque::with_capacity(self.cache.len());
|
||||
while let Some(block) = self.cache.pop_front() {
|
||||
if block.end() >= cutoff {
|
||||
kept.push_back(block);
|
||||
} else {
|
||||
self.recycle_block(block);
|
||||
}
|
||||
}
|
||||
self.cache = kept;
|
||||
}
|
||||
|
||||
fn end_case(&mut self, endword: i64) {
|
||||
@@ -1856,7 +1916,8 @@ impl Paranoia {
|
||||
gend = (gend + OVERLAP_ADJ).min(cend);
|
||||
if self.root.is_empty() {
|
||||
let c = &self.cache[idx];
|
||||
self.root.vector = Some(CBlock::from_parts(c.vector.clone(), cbegin, false));
|
||||
self.root.vector =
|
||||
Some(CBlock::from_parts(c.vector.as_ref().clone(), cbegin, false));
|
||||
} else {
|
||||
let c = &self.cache[idx];
|
||||
let data: Vec<i16> =
|
||||
@@ -1904,7 +1965,8 @@ impl Paranoia {
|
||||
if let Some(root) = self.root.vector.as_mut() {
|
||||
if root.begin > beginword {
|
||||
let add = (root.begin - beginword) as usize;
|
||||
root.vector.splice(0..0, std::iter::repeat_n(0i16, add));
|
||||
Arc::make_mut(&mut root.vector)
|
||||
.splice(0..0, std::iter::repeat_n(0i16, add));
|
||||
root.size += add as i64;
|
||||
root.begin = beginword;
|
||||
}
|
||||
@@ -1938,63 +2000,63 @@ impl Paranoia {
|
||||
let satisfied = !(self.root.begin() == -1
|
||||
|| self.root.begin() > beginword
|
||||
|| self.root.end() < endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS);
|
||||
if std::env::var_os("CDR_NEG").is_some() {
|
||||
eprintln!(
|
||||
"neg loop cursor={} root={:?}..{:?} need={} satisfied={} retry={}",
|
||||
self.cursor,
|
||||
self.root.begin(),
|
||||
self.root.end(),
|
||||
need_more,
|
||||
satisfied,
|
||||
retry_count
|
||||
);
|
||||
}
|
||||
if satisfied {
|
||||
break;
|
||||
}
|
||||
|
||||
let new = self.read_c_block(beginword, endword, callback);
|
||||
if std::env::var_os("CDR_NEG").is_some() {
|
||||
eprintln!(
|
||||
" new={:?} frags={}",
|
||||
new.as_ref().map(|b| (b.begin, b.size)),
|
||||
self.fragments.iter().filter(|f| f.is_some()).count()
|
||||
);
|
||||
}
|
||||
if let Some(mut new) = new {
|
||||
if self.read_c_block(beginword, endword, callback) {
|
||||
if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 {
|
||||
if self.enable & PARANOIA_MODE_VERIFY != 0 {
|
||||
// stage1() expects the new block at the head.
|
||||
// stage1() expects the new block at the head and
|
||||
// leaves it there when it returns.
|
||||
self.stage1(callback);
|
||||
} else {
|
||||
let mut begin = 0i64;
|
||||
while begin < new.size {
|
||||
while begin < new.size
|
||||
&& new.flags.as_ref().unwrap()[begin as usize] & FLAGS_EDGE != 0
|
||||
{
|
||||
begin += 1;
|
||||
// Build fragments from the block already at the
|
||||
// cache head. Collect first so the cache borrow is
|
||||
// released before touching self.fragments.
|
||||
let mut new_frags = Vec::new();
|
||||
{
|
||||
let new = self.cache.front().unwrap();
|
||||
if let Some(flags) = new.flags.as_ref() {
|
||||
let mut begin = 0i64;
|
||||
while begin < new.size {
|
||||
while begin < new.size
|
||||
&& flags[begin as usize] & FLAGS_EDGE != 0
|
||||
{
|
||||
begin += 1;
|
||||
}
|
||||
let mut end = begin + 1;
|
||||
while end < new.size && flags[end as usize] & FLAGS_EDGE == 0 {
|
||||
end += 1;
|
||||
}
|
||||
let frag_begin = new.begin + begin;
|
||||
let frag_end = new.begin + end;
|
||||
let lastsector = new.lastsector && new.begin + end == new.end();
|
||||
new_frags.push(VFragment {
|
||||
data: Arc::clone(&new.vector),
|
||||
offset: begin as usize,
|
||||
begin: frag_begin,
|
||||
size: frag_end - frag_begin,
|
||||
lastsector,
|
||||
});
|
||||
begin = end;
|
||||
}
|
||||
} else {
|
||||
new_frags.push(VFragment {
|
||||
data: Arc::clone(&new.vector),
|
||||
offset: 0,
|
||||
begin: new.begin,
|
||||
size: new.size,
|
||||
lastsector: new.lastsector,
|
||||
});
|
||||
}
|
||||
let mut end = begin + 1;
|
||||
while end < new.size
|
||||
&& new.flags.as_ref().unwrap()[end as usize] & FLAGS_EDGE == 0
|
||||
{
|
||||
end += 1;
|
||||
}
|
||||
let frag_begin = new.begin + begin;
|
||||
let frag_end = new.begin + end;
|
||||
let lastsector = new.lastsector && new.begin + end == new.end();
|
||||
self.fragments.push(Some(VFragment {
|
||||
vector: new.vector[begin as usize..end as usize].to_vec(),
|
||||
begin: frag_begin,
|
||||
size: frag_end - frag_begin,
|
||||
lastsector,
|
||||
}));
|
||||
begin = end;
|
||||
}
|
||||
self.fragments.extend(new_frags.into_iter().map(Some));
|
||||
}
|
||||
} else {
|
||||
let new = self.cache.pop_front().unwrap();
|
||||
self.root.vector = Some(CBlock {
|
||||
vector: std::mem::take(&mut new.vector),
|
||||
vector: new.vector,
|
||||
flags: None,
|
||||
begin: new.begin,
|
||||
size: new.size,
|
||||
@@ -2038,6 +2100,7 @@ impl Paranoia {
|
||||
if end > root.vector.len() {
|
||||
return None;
|
||||
}
|
||||
Some(&mut root.vector[start..end])
|
||||
let vector = Arc::make_mut(&mut root.vector);
|
||||
Some(&mut vector[start..end])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,315 @@
|
||||
//! Sample-value index, a safe Rust equivalent of isort.c.
|
||||
//! Sample-value index for paranoia.
|
||||
//!
|
||||
//! This is a Rust equivalent of the original `isort.c`, with two strategies:
|
||||
//!
|
||||
//! * small search ranges (stage 2) use a direct linear scan, avoiding the
|
||||
//! cost of building an index for a few hundred samples;
|
||||
//! * large ranges (stage 1) use a compact CSR-style index with one bucket
|
||||
//! per 16-bit sample value.
|
||||
//!
|
||||
//! The CSR index has no per-value `Vec` allocations and no hashing, which is
|
||||
//! important because stage 1 indexes up to 1200 CD sectors at a time.
|
||||
|
||||
use std::collections::HashMap;
|
||||
const BUCKETS: usize = 65536;
|
||||
const LINEAR_THRESHOLD: usize = 4096;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Empty,
|
||||
Linear,
|
||||
Csr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Cursor {
|
||||
Linear {
|
||||
value: i16,
|
||||
next: usize,
|
||||
hi: usize,
|
||||
current: usize,
|
||||
},
|
||||
Csr {
|
||||
key: usize,
|
||||
next: usize,
|
||||
hi: usize,
|
||||
current: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct SortIndex {
|
||||
buckets: HashMap<i16, Vec<u32>>,
|
||||
mode: Mode,
|
||||
pub begin: i64,
|
||||
pub size: usize,
|
||||
cursor: Option<(i16, usize, usize)>, // value, next bucket index, hi
|
||||
index_lo: usize,
|
||||
index_hi: usize,
|
||||
|
||||
// CSR payload: positions grouped by sample value.
|
||||
positions: Vec<u32>,
|
||||
starts: Vec<u32>,
|
||||
write: Vec<u32>,
|
||||
|
||||
cursor: Option<Cursor>,
|
||||
}
|
||||
|
||||
impl SortIndex {
|
||||
pub fn new(_maxsize: usize) -> Self {
|
||||
Self {
|
||||
buckets: HashMap::new(),
|
||||
mode: Mode::Empty,
|
||||
begin: 0,
|
||||
size: 0,
|
||||
index_lo: 0,
|
||||
index_hi: 0,
|
||||
positions: Vec::new(),
|
||||
starts: Vec::new(),
|
||||
write: Vec::new(),
|
||||
cursor: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) {
|
||||
self.buckets.clear();
|
||||
self.begin = begin_abs;
|
||||
self.size = samples.len();
|
||||
self.cursor = None;
|
||||
let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len());
|
||||
let hi = ((sorthi - begin_abs).max(0) as usize).min(samples.len());
|
||||
self.index_lo = lo;
|
||||
self.index_hi = hi;
|
||||
|
||||
if lo >= hi {
|
||||
self.mode = Mode::Empty;
|
||||
return;
|
||||
}
|
||||
for (i, sample) in samples.iter().enumerate().take(hi).skip(lo) {
|
||||
self.buckets.entry(*sample).or_default().push(i as u32);
|
||||
|
||||
if hi - lo <= LINEAR_THRESHOLD {
|
||||
self.mode = Mode::Linear;
|
||||
return;
|
||||
}
|
||||
|
||||
self.mode = Mode::Csr;
|
||||
if self.starts.is_empty() {
|
||||
self.starts = vec![0u32; BUCKETS + 1];
|
||||
self.write = vec![0u32; BUCKETS];
|
||||
} else {
|
||||
self.starts.fill(0);
|
||||
}
|
||||
let range_len = hi - lo;
|
||||
self.positions.resize(range_len, 0);
|
||||
|
||||
for &sample in &samples[lo..hi] {
|
||||
self.starts[(sample as u16) as usize + 1] += 1;
|
||||
}
|
||||
for i in 1..=BUCKETS {
|
||||
self.starts[i] += self.starts[i - 1];
|
||||
}
|
||||
self.write.copy_from_slice(&self.starts[..BUCKETS]);
|
||||
|
||||
for (offset, &sample) in samples[lo..hi].iter().enumerate() {
|
||||
let key = (sample as u16) as usize;
|
||||
let slot = self.write[key] as usize;
|
||||
self.positions[slot] = (lo + offset) as u32;
|
||||
self.write[key] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent of sort_getmatch(). Returns the first matching relative
|
||||
/// position, and leaves a cursor for sort_nextmatch().
|
||||
pub fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option<usize> {
|
||||
pub fn getmatch(
|
||||
&mut self,
|
||||
samples: &[i16],
|
||||
post: i64,
|
||||
overlap: i64,
|
||||
value: i16,
|
||||
) -> Option<usize> {
|
||||
self.cursor = None;
|
||||
let post = post.clamp(0, self.size as i64);
|
||||
let lo = (post - overlap).max(0) as usize;
|
||||
let hi = ((post + overlap).min(self.size as i64)).max(0) as usize;
|
||||
self.cursor = None;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
let idx = positions.partition_point(|p| (*p as usize) < lo);
|
||||
if idx >= positions.len() {
|
||||
return None;
|
||||
|
||||
match self.mode {
|
||||
Mode::Empty => None,
|
||||
Mode::Linear => {
|
||||
let start = lo.max(self.index_lo);
|
||||
let end = hi.min(self.index_hi);
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
if let Some(offset) = samples[start..end]
|
||||
.iter()
|
||||
.position(|&sample| sample == value)
|
||||
{
|
||||
let pos = start + offset;
|
||||
self.cursor = Some(Cursor::Linear {
|
||||
value,
|
||||
next: pos + 1,
|
||||
hi: end,
|
||||
current: pos,
|
||||
});
|
||||
return Some(pos);
|
||||
}
|
||||
None
|
||||
}
|
||||
Mode::Csr => {
|
||||
let key = (value as u16) as usize;
|
||||
let start = self.starts[key] as usize;
|
||||
let end = self.starts[key + 1] as usize;
|
||||
let idx =
|
||||
start + self.positions[start..end].partition_point(|p| (*p as usize) < lo);
|
||||
if idx >= end {
|
||||
return None;
|
||||
}
|
||||
let pos = self.positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some(Cursor::Csr {
|
||||
key,
|
||||
next: idx + 1,
|
||||
hi,
|
||||
current: pos,
|
||||
});
|
||||
Some(pos)
|
||||
}
|
||||
}
|
||||
let pos = positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some((value, idx + 1, hi));
|
||||
Some(pos)
|
||||
}
|
||||
|
||||
pub fn nextmatch(&mut self) -> Option<usize> {
|
||||
let (value, idx, hi) = self.cursor?;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
if idx >= positions.len() {
|
||||
self.cursor = None;
|
||||
return None;
|
||||
pub fn nextmatch(&mut self, samples: &[i16]) -> Option<usize> {
|
||||
match self.cursor.take()? {
|
||||
Cursor::Linear {
|
||||
value, next, hi, ..
|
||||
} => {
|
||||
if next >= hi {
|
||||
return None;
|
||||
}
|
||||
if let Some(offset) = samples[next..hi].iter().position(|&sample| sample == value) {
|
||||
let pos = next + offset;
|
||||
self.cursor = Some(Cursor::Linear {
|
||||
value,
|
||||
next: pos + 1,
|
||||
hi,
|
||||
current: pos,
|
||||
});
|
||||
return Some(pos);
|
||||
}
|
||||
None
|
||||
}
|
||||
Cursor::Csr { key, next, hi, .. } => {
|
||||
let end = self.starts[key + 1] as usize;
|
||||
if next >= end {
|
||||
return None;
|
||||
}
|
||||
let pos = self.positions[next] as usize;
|
||||
if pos >= hi {
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some(Cursor::Csr {
|
||||
key,
|
||||
next: next + 1,
|
||||
hi,
|
||||
current: pos,
|
||||
});
|
||||
Some(pos)
|
||||
}
|
||||
}
|
||||
let pos = positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
self.cursor = None;
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some((value, idx + 1, hi));
|
||||
Some(pos)
|
||||
}
|
||||
|
||||
/// Position most recently returned by getmatch()/advance().
|
||||
pub fn cursor_position(&self) -> Option<i64> {
|
||||
let (value, idx, _hi) = self.cursor?;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
if idx == 0 || idx > positions.len() {
|
||||
return None;
|
||||
match self.cursor? {
|
||||
Cursor::Linear { current, .. } | Cursor::Csr { current, .. } => Some(current as i64),
|
||||
}
|
||||
Some(positions[idx - 1] as i64)
|
||||
}
|
||||
|
||||
/// Advance to the next match. Returns false when exhausted.
|
||||
pub fn advance(&mut self) -> bool {
|
||||
self.nextmatch().is_some()
|
||||
pub fn advance(&mut self, samples: &[i16]) -> bool {
|
||||
self.nextmatch(samples).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn naive_matches(
|
||||
samples: &[i16],
|
||||
index_lo: usize,
|
||||
index_hi: usize,
|
||||
post: i64,
|
||||
overlap: i64,
|
||||
value: i16,
|
||||
) -> Vec<usize> {
|
||||
let post = post.clamp(0, samples.len() as i64);
|
||||
let lo = (post - overlap).max(0) as usize;
|
||||
let hi = ((post + overlap).min(samples.len() as i64)).max(0) as usize;
|
||||
(lo.max(index_lo)..hi.min(index_hi))
|
||||
.filter(|&i| samples[i] == value)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_matches(
|
||||
index: &mut SortIndex,
|
||||
samples: &[i16],
|
||||
post: i64,
|
||||
overlap: i64,
|
||||
value: i16,
|
||||
) -> Vec<usize> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(first) = index.getmatch(samples, post, overlap, value) {
|
||||
out.push(first);
|
||||
while let Some(next) = index.nextmatch(samples) {
|
||||
out.push(next);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adaptive_index_matches_naive_search() {
|
||||
let mut x = 0x1234_5678u64;
|
||||
let mut samples = Vec::with_capacity(12000);
|
||||
for i in 0..12000 {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
let value = if i % 7 == 0 {
|
||||
0
|
||||
} else {
|
||||
((x as i32) % 9 - 4) as i16
|
||||
};
|
||||
samples.push(value);
|
||||
}
|
||||
|
||||
let mut index = SortIndex::new(samples.len());
|
||||
for (lo, hi) in [(0usize, samples.len()), (300, 900), (1000, 1400)] {
|
||||
index.setup(&samples, 0, lo as i64, hi as i64);
|
||||
let expected_mode = if hi - lo <= LINEAR_THRESHOLD {
|
||||
Mode::Linear
|
||||
} else {
|
||||
Mode::Csr
|
||||
};
|
||||
assert_eq!(index.mode, expected_mode);
|
||||
for post in [0i64, 1, 500, 1500, 9000, 11999] {
|
||||
for overlap in [0i64, 1, 64, 512] {
|
||||
for value in -4i16..=4 {
|
||||
let expected = naive_matches(&samples, lo, hi, post, overlap, value);
|
||||
let got = collect_matches(&mut index, &samples, post, overlap, value);
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"lo={lo} hi={hi} post={post} overlap={overlap} value={value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_bucket_is_ordered() {
|
||||
let samples = vec![0i16; 20000];
|
||||
let mut index = SortIndex::new(samples.len());
|
||||
index.setup(&samples, 0, 0, samples.len() as i64);
|
||||
assert_eq!(index.mode, Mode::Csr);
|
||||
let got = collect_matches(&mut index, &samples, 10000, 200, 0);
|
||||
let expected = (9800..10200).collect::<Vec<_>>();
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user