first draft
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
target/
|
||||||
|
*.log
|
||||||
|
cdda.*
|
||||||
|
track*.cdda.*
|
||||||
|
cdparanoia-III-10.2
|
||||||
Generated
+150
@@ -0,0 +1,150 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cdda_interface"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cdrustanoia_capi_types",
|
||||||
|
"cdrustanoia_core",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cdda_paranoia"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cdda_interface",
|
||||||
|
"cdrustanoia_capi_types",
|
||||||
|
"cdrustanoia_core",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cdrustanoia_capi_types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cdrustanoia_cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cdrustanoia_core",
|
||||||
|
"libc",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cdrustanoia_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.191"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"crates/core",
|
||||||
|
"crates/cli",
|
||||||
|
"crates/capi-types",
|
||||||
|
"crates/cdda-interface",
|
||||||
|
"crates/cdda-paranoia",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "LGPL-2.1-or-later"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
cdrustanoia_core = { path = "crates/core" }
|
||||||
|
cdrustanoia_capi_types = { path = "crates/capi-types" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
+343
@@ -0,0 +1,343 @@
|
|||||||
|
The cdparanoia command line tool versions 10.1 and later are
|
||||||
|
distributed under the GNU General Public Licence v2.0 or, at your
|
||||||
|
option, any later version of the GPL.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
The 10.1 and later release versions of the cdda-interface and
|
||||||
|
cdda-paranoia libraries are distributed the GNU Lesser General Public
|
||||||
|
License v2.1 or, at your option, any later version of the GNU LGPL.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms of the
|
||||||
|
ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library. It is
|
||||||
|
safest to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
PREFIX ?= /usr/local
|
||||||
|
DESTDIR ?=
|
||||||
|
CARGO ?= cargo
|
||||||
|
CC ?= cc
|
||||||
|
INSTALL ?= install
|
||||||
|
|
||||||
|
BINDIR = $(DESTDIR)$(PREFIX)/bin
|
||||||
|
LIBDIR = $(DESTDIR)$(PREFIX)/lib
|
||||||
|
INCLUDEDIR = $(DESTDIR)$(PREFIX)/include
|
||||||
|
MANDIR = $(DESTDIR)$(PREFIX)/share/man/man1
|
||||||
|
|
||||||
|
.PHONY: all build test check install uninstall clean
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(CARGO) build --release --workspace
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(CARGO) test --workspace
|
||||||
|
|
||||||
|
check:
|
||||||
|
$(CARGO) fmt --all -- --check
|
||||||
|
$(CARGO) clippy --workspace --all-targets -- -D warnings
|
||||||
|
$(CARGO) test --workspace
|
||||||
|
$(CC) -std=gnu11 -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-function -Iinclude -c tests/capi_compile.c -o target/capi_compile.o
|
||||||
|
|
||||||
|
install: build
|
||||||
|
$(INSTALL) -d -m 0755 $(BINDIR)
|
||||||
|
$(INSTALL) -m 0755 target/release/cdrustanoia $(BINDIR)/cdrustanoia
|
||||||
|
ln -sf cdrustanoia $(BINDIR)/cdparanoia
|
||||||
|
$(INSTALL) -d -m 0755 $(MANDIR)
|
||||||
|
$(INSTALL) -m 0644 man/cdparanoia.1 $(MANDIR)/cdparanoia.1
|
||||||
|
$(INSTALL) -m 0644 man/cdrustanoia.1 $(MANDIR)/cdrustanoia.1
|
||||||
|
$(INSTALL) -d -m 0755 $(INCLUDEDIR)
|
||||||
|
$(INSTALL) -m 0644 include/cdda_interface.h $(INCLUDEDIR)/cdda_interface.h
|
||||||
|
$(INSTALL) -m 0644 include/cdda_paranoia.h $(INCLUDEDIR)/cdda_paranoia.h
|
||||||
|
$(INSTALL) -m 0644 include/utils.h $(INCLUDEDIR)/utils.h
|
||||||
|
$(INSTALL) -d -m 0755 $(LIBDIR)
|
||||||
|
$(INSTALL) -m 0755 target/release/libcdda_interface.so $(LIBDIR)/libcdda_interface.so.0.10.2
|
||||||
|
$(INSTALL) -m 0755 target/release/libcdda_paranoia.so $(LIBDIR)/libcdda_paranoia.so.0.10.2
|
||||||
|
ln -sf libcdda_interface.so.0.10.2 $(LIBDIR)/libcdda_interface.so.0
|
||||||
|
ln -sf libcdda_interface.so.0 $(LIBDIR)/libcdda_interface.so
|
||||||
|
ln -sf libcdda_paranoia.so.0.10.2 $(LIBDIR)/libcdda_paranoia.so.0
|
||||||
|
ln -sf libcdda_paranoia.so.0 $(LIBDIR)/libcdda_paranoia.so
|
||||||
|
$(INSTALL) -m 0644 target/release/libcdda_interface.a $(LIBDIR)/libcdda_interface.a
|
||||||
|
$(INSTALL) -m 0644 target/release/libcdda_paranoia.a $(LIBDIR)/libcdda_paranoia.a
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(BINDIR)/cdrustanoia $(BINDIR)/cdparanoia
|
||||||
|
rm -f $(MANDIR)/cdparanoia.1 $(MANDIR)/cdrustanoia.1
|
||||||
|
rm -f $(INCLUDEDIR)/cdda_interface.h $(INCLUDEDIR)/cdda_paranoia.h $(INCLUDEDIR)/utils.h
|
||||||
|
rm -f $(LIBDIR)/libcdda_interface.so* $(LIBDIR)/libcdda_paranoia.so*
|
||||||
|
rm -f $(LIBDIR)/libcdda_interface.a $(LIBDIR)/libcdda_paranoia.a
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(CARGO) clean
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# cdrustanoia
|
||||||
|
|
||||||
|
`cdrustanoia` is a Rust port of cdparanoia III 10.2 for Linux. It is intended
|
||||||
|
to be a drop-in replacement for the `cdparanoia` command line tool and for the
|
||||||
|
`libcdda_interface` / `libcdda_paranoia` C libraries.
|
||||||
|
|
||||||
|
The port includes:
|
||||||
|
|
||||||
|
- the cdparanoia command line interface and span grammar,
|
||||||
|
- the two-stage verification, overlap, drift and rift-reconstruction engine,
|
||||||
|
- the Linux SG_IO and cooked-ioctl CD-ROM backends,
|
||||||
|
- TOC query and drive/endianness probing,
|
||||||
|
- WAV, AIFF, AIFF-C and raw output,
|
||||||
|
- batch mode, stdout output, sample/TOC offsets, logging and progress display,
|
||||||
|
- source- and ABI-compatible C libraries and headers,
|
||||||
|
- a synthetic image backend and fault-injection tests that do not need a CD
|
||||||
|
drive.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
The binaries are written to `target/release/cdrustanoia` and
|
||||||
|
`target/release/cdparanoia`. Both run the same code; `cdparanoia` exists so
|
||||||
|
existing scripts and packages can invoke the expected name.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo make install PREFIX=/usr/local
|
||||||
|
# or
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs:
|
||||||
|
|
||||||
|
- `bin/cdrustanoia` and the `bin/cdparanoia` symlink,
|
||||||
|
- `man/man1/cdparanoia.1` and `cdrustanoia.1`,
|
||||||
|
- `include/cdda_interface.h`, `cdda_paranoia.h` and `utils.h`,
|
||||||
|
- `lib/libcdda_interface.{so,a}` and `lib/libcdda_paranoia.{so,a}`,
|
||||||
|
- the original `.so.0` SONAME symlinks.
|
||||||
|
|
||||||
|
`DESTDIR` is supported for package builds.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The command line is compatible with cdparanoia 10.2:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cdrustanoia -Q
|
||||||
|
cdrustanoia -B
|
||||||
|
cdrustanoia "1[:30.12]-1[1:10]" track.wav
|
||||||
|
cdrustanoia -r -- "-3" out.raw
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `cdrustanoia --help` for the full option and span documentation. The man
|
||||||
|
page is the original cdparanoia man page.
|
||||||
|
|
||||||
|
## C API
|
||||||
|
|
||||||
|
The two C libraries export the traditional symbols and use the original
|
||||||
|
public structure layout. The headers are installed under `include/`; link
|
||||||
|
programs the same way as with cdparanoia:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cc app.c -lcdda_paranoia -lcdda_interface
|
||||||
|
```
|
||||||
|
|
||||||
|
The public `cdrom_drive` layout, `TOC` layout, function-pointer fields, error
|
||||||
|
codes and `paranoia_*` entry points match cdparanoia 10.2 on 64-bit Linux.
|
||||||
|
|
||||||
|
## Testing without a CD drive
|
||||||
|
|
||||||
|
Set `CDRUSTANOIA_TEST_IMAGE` to a raw CDDA image (2352-byte sectors). A
|
||||||
|
sidecar file named `<image>.<ext>.toc` may contain one line per track:
|
||||||
|
|
||||||
|
```text
|
||||||
|
start_sector end_sector flags_hex
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 99 0
|
||||||
|
100 199 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use the normal command line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CDRUSTANOIA_TEST_IMAGE=/tmp/disc.raw ./target/debug/cdparanoia -Q
|
||||||
|
CDRUSTANOIA_TEST_IMAGE=/tmp/disc.raw ./target/debug/cdparanoia -B
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend is intentionally not advertised in `--help`; it exists for tests
|
||||||
|
and development.
|
||||||
|
|
||||||
|
Run the automated suite with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite covers span parsing, output headers, command-line parsing, the
|
||||||
|
paranoia engine (including jitter and fragmented reads), the C ABI layout, and
|
||||||
|
the C libraries.
|
||||||
|
|
||||||
|
## Hardware checklist
|
||||||
|
|
||||||
|
The SG_IO and cooked ioctl paths cannot be exercised in a machine without a
|
||||||
|
CD-ROM drive. When hardware is available, verify:
|
||||||
|
|
||||||
|
1. `cdrustanoia -Q` lists the TOC.
|
||||||
|
2. `cdrustanoia -v -Q` shows SG_IO or cooked autosense.
|
||||||
|
3. `cdrustanoia -B` produces `trackNN.cdda.wav` and the audio matches the disc.
|
||||||
|
4. A deliberately scratched disc produces the expected `:-(`/`;-(` progress
|
||||||
|
and either reconstructs audio or removes the aborted file with `-X`.
|
||||||
|
5. `-A` completes and writes `cdparanoia.log`.
|
||||||
|
6. `-e` produces the `##:` callback lines for wrapper scripts.
|
||||||
|
7. A small C program using `cdda_identify`, `cdda_open`, `paranoia_init`,
|
||||||
|
`paranoia_read` and `cdda_close` works.
|
||||||
|
|
||||||
|
## Compatibility notes
|
||||||
|
|
||||||
|
- Linux is the only supported platform, matching cdparanoia 10.2.
|
||||||
|
- Modern SG_IO and legacy cooked ioctl access are implemented. The obsolete
|
||||||
|
`/dev/sg` SG2/SG3 packet transport is not; `-g` uses SG_IO on the named
|
||||||
|
generic device where the kernel supports it.
|
||||||
|
- `-i/--output-info` is accepted but is a no-op, matching the 10.2 source.
|
||||||
|
- 64-bit Linux ABI is the primary target.
|
||||||
|
- The `-A` cache analysis is a compact functional port; its timing
|
||||||
|
recommendations should be treated as advisory.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
The port is derived from cdparanoia III 10.2. The command line program is
|
||||||
|
GPL-2.0-or-later; the libraries are LGPL-2.1-or-later, following the original
|
||||||
|
distribution. See `LICENSE-GPL-2.0`, `LICENSE-LGPL-2.1` and the copyright
|
||||||
|
notices in the original headers.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdrustanoia_capi_types"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cdrustanoia_capi_types"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libc.workspace = true
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//! C ABI-compatible public structures shared by the two cdda libraries.
|
||||||
|
|
||||||
|
use libc::{c_char, c_int, c_long, c_void, sigset_t};
|
||||||
|
|
||||||
|
pub const MAXTRK: usize = 100;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
pub struct Toc {
|
||||||
|
pub b_flags: u8,
|
||||||
|
pub b_track: u8,
|
||||||
|
pub dw_start_sector: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type EnableCddaFn = Option<unsafe extern "C" fn(*mut CdromDrive, c_int) -> c_int>;
|
||||||
|
pub type ReadTocFn = Option<unsafe extern "C" fn(*mut CdromDrive) -> c_int>;
|
||||||
|
pub type ReadAudioFn =
|
||||||
|
Option<unsafe extern "C" fn(*mut CdromDrive, *mut c_void, c_long, c_long) -> c_long>;
|
||||||
|
pub type SetSpeedFn = Option<unsafe extern "C" fn(*mut CdromDrive, c_int) -> c_int>;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct CddaPrivateData {
|
||||||
|
pub holder: *mut c_void,
|
||||||
|
pub last_milliseconds: c_int,
|
||||||
|
pub clock: c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct CdromDrive {
|
||||||
|
pub opened: c_int,
|
||||||
|
pub cdda_device_name: *mut c_char,
|
||||||
|
pub ioctl_device_name: *mut c_char,
|
||||||
|
pub cdda_fd: c_int,
|
||||||
|
pub ioctl_fd: c_int,
|
||||||
|
pub drive_model: *mut c_char,
|
||||||
|
pub drive_type: c_int,
|
||||||
|
pub interface: c_int,
|
||||||
|
pub bigendianp: c_int,
|
||||||
|
pub nsectors: c_int,
|
||||||
|
pub cd_extra: c_int,
|
||||||
|
pub tracks: c_int,
|
||||||
|
pub disc_toc: [Toc; MAXTRK],
|
||||||
|
pub audio_first_sector: c_long,
|
||||||
|
pub audio_last_sector: c_long,
|
||||||
|
pub errordest: c_int,
|
||||||
|
pub messagedest: c_int,
|
||||||
|
pub errorbuf: *mut c_char,
|
||||||
|
pub messagebuf: *mut c_char,
|
||||||
|
pub enable_cdda: EnableCddaFn,
|
||||||
|
pub read_toc: ReadTocFn,
|
||||||
|
pub read_audio: ReadAudioFn,
|
||||||
|
pub set_speed: SetSpeedFn,
|
||||||
|
pub error_retry: c_int,
|
||||||
|
pub report_all: c_int,
|
||||||
|
pub is_atapi: c_int,
|
||||||
|
pub is_mmc: c_int,
|
||||||
|
pub private: *mut CddaPrivateData,
|
||||||
|
pub reserved: *mut c_void,
|
||||||
|
pub inqbytes: [u8; 4],
|
||||||
|
pub density: u8,
|
||||||
|
pub orgdens: u8,
|
||||||
|
pub orgsize: u32,
|
||||||
|
pub bigbuff: c_long,
|
||||||
|
pub adjust_ssize: c_int,
|
||||||
|
pub fua: c_int,
|
||||||
|
pub lun: c_int,
|
||||||
|
pub sigset: sigset_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CdromDrive {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
opened: 0,
|
||||||
|
cdda_device_name: std::ptr::null_mut(),
|
||||||
|
ioctl_device_name: std::ptr::null_mut(),
|
||||||
|
cdda_fd: -1,
|
||||||
|
ioctl_fd: -1,
|
||||||
|
drive_model: std::ptr::null_mut(),
|
||||||
|
drive_type: 0,
|
||||||
|
interface: 0,
|
||||||
|
bigendianp: -1,
|
||||||
|
nsectors: -1,
|
||||||
|
cd_extra: 0,
|
||||||
|
tracks: 0,
|
||||||
|
disc_toc: [Toc::default(); MAXTRK],
|
||||||
|
audio_first_sector: 0,
|
||||||
|
audio_last_sector: 0,
|
||||||
|
errordest: 0,
|
||||||
|
messagedest: 0,
|
||||||
|
errorbuf: std::ptr::null_mut(),
|
||||||
|
messagebuf: std::ptr::null_mut(),
|
||||||
|
enable_cdda: None,
|
||||||
|
read_toc: None,
|
||||||
|
read_audio: None,
|
||||||
|
set_speed: None,
|
||||||
|
error_retry: 0,
|
||||||
|
report_all: 0,
|
||||||
|
is_atapi: 0,
|
||||||
|
is_mmc: 0,
|
||||||
|
private: std::ptr::null_mut(),
|
||||||
|
reserved: std::ptr::null_mut(),
|
||||||
|
inqbytes: [0; 4],
|
||||||
|
density: 0,
|
||||||
|
orgdens: 0,
|
||||||
|
orgsize: 0,
|
||||||
|
bigbuff: 0,
|
||||||
|
adjust_ssize: 1,
|
||||||
|
fua: 0,
|
||||||
|
lun: 0,
|
||||||
|
sigset: unsafe { std::mem::zeroed() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const INTERFACE_GENERIC_SCSI: c_int = 0;
|
||||||
|
pub const INTERFACE_COOKED_IOCTL: c_int = 1;
|
||||||
|
pub const INTERFACE_TEST: c_int = 2;
|
||||||
|
pub const INTERFACE_SGIO_SCSI: c_int = 3;
|
||||||
|
pub const INTERFACE_SGIO_SCSI_BUGGY1: c_int = 4;
|
||||||
|
|
||||||
|
pub const CDDA_MESSAGE_FORGETIT: c_int = 0;
|
||||||
|
pub const CDDA_MESSAGE_PRINTIT: c_int = 1;
|
||||||
|
pub const CDDA_MESSAGE_LOGIT: c_int = 2;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn abi_layout_matches_cdparanoia() {
|
||||||
|
assert_eq!(size_of::<Toc>(), 8);
|
||||||
|
assert_eq!(offset_of!(Toc, b_flags), 0);
|
||||||
|
assert_eq!(offset_of!(Toc, b_track), 1);
|
||||||
|
assert_eq!(offset_of!(Toc, dw_start_sector), 4);
|
||||||
|
assert_eq!(size_of::<CdromDrive>(), 1136);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, opened), 0);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, cdda_device_name), 8);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, cdda_fd), 24);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, drive_model), 32);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, drive_type), 40);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, interface), 44);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, bigendianp), 48);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, nsectors), 52);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, cd_extra), 56);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, tracks), 60);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, disc_toc), 64);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, audio_first_sector), 864);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, errordest), 880);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, errorbuf), 888);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, error_retry), 936);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, report_all), 940);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, is_atapi), 944);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, is_mmc), 948);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, private), 952);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, reserved), 960);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, inqbytes), 968);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, density), 972);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, bigbuff), 984);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, adjust_ssize), 992);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, fua), 996);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, lun), 1000);
|
||||||
|
assert_eq!(offset_of!(CdromDrive, sigset), 1008);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdda_interface"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cdda_interface"
|
||||||
|
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cdrustanoia_core.workspace = true
|
||||||
|
cdrustanoia_capi_types.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_interface.so.0");
|
||||||
|
}
|
||||||
@@ -0,0 +1,571 @@
|
|||||||
|
#![allow(
|
||||||
|
clippy::missing_safety_doc,
|
||||||
|
clippy::unnecessary_cast,
|
||||||
|
clippy::field_reassign_with_default
|
||||||
|
)]
|
||||||
|
|
||||||
|
//! C ABI-compatible libcdda_interface implementation.
|
||||||
|
|
||||||
|
use cdrustanoia_capi_types::*;
|
||||||
|
use cdrustanoia_core::device::discover;
|
||||||
|
use cdrustanoia_core::device::{DiscToc, Drive, InterfaceKind, ReadError};
|
||||||
|
use libc::{c_char, c_int, c_long, c_void};
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
pub struct BackendHolder {
|
||||||
|
pub drive: Box<dyn Drive>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn cstr(s: &str) -> *mut c_char {
|
||||||
|
let clean: String = s.chars().filter(|c| *c != '\0').collect();
|
||||||
|
match CString::new(clean) {
|
||||||
|
Ok(c) => libc::strdup(c.as_ptr()),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn set_error(d: *mut CdromDrive, message: &str) {
|
||||||
|
if d.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !(*d).errorbuf.is_null() {
|
||||||
|
libc::free((*d).errorbuf as *mut c_void);
|
||||||
|
}
|
||||||
|
(*d).errorbuf = cstr(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn holder<'a>(d: *mut CdromDrive) -> Option<&'a mut BackendHolder> {
|
||||||
|
if d.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let private = (*d).private;
|
||||||
|
if private.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let h = (*private).holder as *mut BackendHolder;
|
||||||
|
if h.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&mut *h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn make_drive(
|
||||||
|
device: &str,
|
||||||
|
ioctl_device: Option<&str>,
|
||||||
|
backend: Box<dyn Drive>,
|
||||||
|
) -> *mut CdromDrive {
|
||||||
|
let mut d = Box::new(CdromDrive::empty());
|
||||||
|
d.drive_type = 0;
|
||||||
|
d.interface = match backend.interface() {
|
||||||
|
InterfaceKind::SgIo => INTERFACE_SGIO_SCSI,
|
||||||
|
InterfaceKind::Cooked => INTERFACE_COOKED_IOCTL,
|
||||||
|
InterfaceKind::Image => INTERFACE_TEST,
|
||||||
|
};
|
||||||
|
d.bigendianp = backend.bigendianp();
|
||||||
|
d.nsectors = backend.nsectors() as c_int;
|
||||||
|
d.cdda_device_name = cstr(device);
|
||||||
|
d.ioctl_device_name = cstr(ioctl_device.unwrap_or(device));
|
||||||
|
d.drive_model = cstr(backend.drive_model());
|
||||||
|
d.enable_cdda = Some(cb_enable_cdda);
|
||||||
|
d.read_toc = Some(cb_read_toc);
|
||||||
|
d.read_audio = Some(cb_read_audio);
|
||||||
|
d.set_speed = Some(cb_set_speed);
|
||||||
|
d.error_retry = 1;
|
||||||
|
d.report_all = 0;
|
||||||
|
d.adjust_ssize = 1;
|
||||||
|
d.lun = 0;
|
||||||
|
let private = Box::new(CddaPrivateData {
|
||||||
|
holder: Box::into_raw(Box::new(BackendHolder { drive: backend })) as *mut c_void,
|
||||||
|
last_milliseconds: -1,
|
||||||
|
clock: 0,
|
||||||
|
});
|
||||||
|
d.private = Box::into_raw(private);
|
||||||
|
Box::into_raw(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn free_drive(d: *mut CdromDrive) {
|
||||||
|
if d.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let private = (*d).private;
|
||||||
|
if !private.is_null() {
|
||||||
|
let holder = (*private).holder as *mut BackendHolder;
|
||||||
|
if !holder.is_null() {
|
||||||
|
drop(Box::from_raw(holder));
|
||||||
|
}
|
||||||
|
drop(Box::from_raw(private));
|
||||||
|
(*d).private = ptr::null_mut();
|
||||||
|
}
|
||||||
|
if !(*d).cdda_device_name.is_null() {
|
||||||
|
libc::free((*d).cdda_device_name as *mut c_void);
|
||||||
|
}
|
||||||
|
if !(*d).ioctl_device_name.is_null() {
|
||||||
|
libc::free((*d).ioctl_device_name as *mut c_void);
|
||||||
|
}
|
||||||
|
if !(*d).drive_model.is_null() {
|
||||||
|
libc::free((*d).drive_model as *mut c_void);
|
||||||
|
}
|
||||||
|
if !(*d).errorbuf.is_null() {
|
||||||
|
libc::free((*d).errorbuf as *mut c_void);
|
||||||
|
}
|
||||||
|
if !(*d).messagebuf.is_null() {
|
||||||
|
libc::free((*d).messagebuf as *mut c_void);
|
||||||
|
}
|
||||||
|
drop(Box::from_raw(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn cb_enable_cdda(_d: *mut CdromDrive, _onoff: c_int) -> c_int {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn cb_read_toc(d: *mut CdromDrive) -> c_int {
|
||||||
|
if d.is_null() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
(*d).tracks
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn read_into(
|
||||||
|
d: *mut CdromDrive,
|
||||||
|
p: *mut c_void,
|
||||||
|
begin: c_long,
|
||||||
|
sectors: c_long,
|
||||||
|
) -> Result<usize, ReadError> {
|
||||||
|
let h = holder(d).ok_or_else(ReadError::closed)?;
|
||||||
|
if p.is_null() {
|
||||||
|
h.drive.read_audio_raw(None, begin as i64, sectors as i64)
|
||||||
|
} else {
|
||||||
|
let len = (sectors.max(0) as usize) * cdrustanoia_core::device::CD_FRAMESIZE_RAW;
|
||||||
|
let buf = std::slice::from_raw_parts_mut(p as *mut u8, len);
|
||||||
|
h.drive
|
||||||
|
.read_audio_raw(Some(buf), begin as i64, sectors as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn cb_read_audio(
|
||||||
|
d: *mut CdromDrive,
|
||||||
|
p: *mut c_void,
|
||||||
|
begin: c_long,
|
||||||
|
sectors: c_long,
|
||||||
|
) -> c_long {
|
||||||
|
match read_into(d, p, begin, sectors) {
|
||||||
|
Ok(n) => n as c_long,
|
||||||
|
Err(e) => {
|
||||||
|
set_errno(e.errno);
|
||||||
|
match e.errno {
|
||||||
|
libc::ENOMEDIUM => -404,
|
||||||
|
libc::ENOMEM => -300,
|
||||||
|
_ => -7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn cb_set_speed(d: *mut CdromDrive, speed: c_int) -> c_int {
|
||||||
|
match holder(d) {
|
||||||
|
Some(h) => h.drive.speed_set(speed),
|
||||||
|
None => -405,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_errno(value: c_int) {
|
||||||
|
unsafe {
|
||||||
|
*libc::__errno_location() = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toc_from_drive(d: &CdromDrive) -> DiscToc {
|
||||||
|
let mut toc = DiscToc::default();
|
||||||
|
toc.tracks = d.tracks;
|
||||||
|
toc.cd_extra = d.cd_extra;
|
||||||
|
for i in 0..MAXTRK {
|
||||||
|
toc.entries[i].flags = d.disc_toc[i].b_flags;
|
||||||
|
toc.entries[i].track = d.disc_toc[i].b_track;
|
||||||
|
toc.entries[i].start_sector = d.disc_toc[i].dw_start_sector as i64;
|
||||||
|
}
|
||||||
|
toc
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn fill_toc(d: *mut CdromDrive) {
|
||||||
|
let h = match holder(d) {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let toc = h.drive.toc().clone();
|
||||||
|
(*d).tracks = toc.tracks;
|
||||||
|
(*d).cd_extra = toc.cd_extra;
|
||||||
|
for i in 0..MAXTRK {
|
||||||
|
(*d).disc_toc[i].b_flags = toc.entries[i].flags;
|
||||||
|
(*d).disc_toc[i].b_track = toc.entries[i].track;
|
||||||
|
(*d).disc_toc[i].dw_start_sector = toc.entries[i].start_sector as i32;
|
||||||
|
}
|
||||||
|
(*d).audio_first_sector = toc.disc_firstsector() as c_long;
|
||||||
|
(*d).audio_last_sector = toc.disc_lastsector() as c_long;
|
||||||
|
(*d).bigendianp = h.drive.bigendianp();
|
||||||
|
(*d).nsectors = h.drive.nsectors() as c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_identify(
|
||||||
|
device: *const c_char,
|
||||||
|
messagedest: c_int,
|
||||||
|
messages: *mut *mut c_char,
|
||||||
|
) -> *mut CdromDrive {
|
||||||
|
if !messages.is_null() {
|
||||||
|
*messages = ptr::null_mut();
|
||||||
|
}
|
||||||
|
if device.is_null() {
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let device = match std::ffi::CStr::from_ptr(device).to_str() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return ptr::null_mut(),
|
||||||
|
};
|
||||||
|
match discover::identify_device(device, messagedest == CDDA_MESSAGE_PRINTIT) {
|
||||||
|
Ok(drive) => make_drive(device, Some(device), drive),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_identify_cooked(
|
||||||
|
device: *const c_char,
|
||||||
|
messagedest: c_int,
|
||||||
|
messages: *mut *mut c_char,
|
||||||
|
) -> *mut CdromDrive {
|
||||||
|
if !messages.is_null() {
|
||||||
|
*messages = ptr::null_mut();
|
||||||
|
}
|
||||||
|
if device.is_null() {
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let device = match std::ffi::CStr::from_ptr(device).to_str() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return ptr::null_mut(),
|
||||||
|
};
|
||||||
|
match discover::identify_cooked(device, messagedest == CDDA_MESSAGE_PRINTIT) {
|
||||||
|
Ok(drive) => make_drive(device, Some(device), drive),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_identify_scsi(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
let generic = if generic_device.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
std::ffi::CStr::from_ptr(generic_device).to_str().ok()
|
||||||
|
};
|
||||||
|
let ioctl = if ioctl_device.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
std::ffi::CStr::from_ptr(ioctl_device).to_str().ok()
|
||||||
|
};
|
||||||
|
let verbose = messagedest == CDDA_MESSAGE_PRINTIT;
|
||||||
|
if let Some(dev) = generic {
|
||||||
|
if let Ok(drive) = discover::identify_sgio(dev, verbose) {
|
||||||
|
return make_drive(dev, ioctl, drive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(dev) = ioctl {
|
||||||
|
if let Ok(drive) = discover::identify_device(dev, verbose) {
|
||||||
|
return make_drive(dev, Some(dev), drive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_find_a_cdrom(
|
||||||
|
messagedest: c_int,
|
||||||
|
messages: *mut *mut c_char,
|
||||||
|
) -> *mut CdromDrive {
|
||||||
|
if !messages.is_null() {
|
||||||
|
*messages = ptr::null_mut();
|
||||||
|
}
|
||||||
|
match discover::find_a_cdrom(messagedest == CDDA_MESSAGE_PRINTIT) {
|
||||||
|
Some(drive) => make_drive("auto", None, drive),
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int {
|
||||||
|
if d.is_null() {
|
||||||
|
return -100;
|
||||||
|
}
|
||||||
|
if (*d).opened != 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if let Some(h) = holder(d) {
|
||||||
|
if let Err(e) = h.drive.open_disc() {
|
||||||
|
set_errno(libc::EIO);
|
||||||
|
set_error(d, &format!("Unable to open disc: {}\n", e));
|
||||||
|
return if e.contains("illegal number of tracks") {
|
||||||
|
-3
|
||||||
|
} else if e.contains("contents header") {
|
||||||
|
-4
|
||||||
|
} else if e.contains("contents entry") {
|
||||||
|
-5
|
||||||
|
} else if e.contains("lead-out") {
|
||||||
|
-2
|
||||||
|
} else if e.contains("CDDA") || e.contains("command set") {
|
||||||
|
-6
|
||||||
|
} else {
|
||||||
|
-9
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fill_toc(d);
|
||||||
|
if (*d).tracks < 1 {
|
||||||
|
(*d).opened = 0;
|
||||||
|
return -9;
|
||||||
|
}
|
||||||
|
for i in 0..(*d).tracks as usize {
|
||||||
|
if (*d).disc_toc[i].dw_start_sector < 0 || (*d).disc_toc[i + 1].dw_start_sector == 0 {
|
||||||
|
(*d).opened = 0;
|
||||||
|
return -9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*d).opened = 1;
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_close(d: *mut CdromDrive) -> c_int {
|
||||||
|
if d.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
free_drive(d);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_read_timed(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
set_errno(libc::EBADF);
|
||||||
|
set_error(d, "400: Device not open\n");
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
if sectors <= 0 {
|
||||||
|
return sectors;
|
||||||
|
}
|
||||||
|
let h = match holder(d) {
|
||||||
|
Some(h) => h,
|
||||||
|
None => {
|
||||||
|
set_errno(libc::EBADF);
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (*d).bigendianp >= 0 {
|
||||||
|
h.drive.set_bigendianp((*d).bigendianp);
|
||||||
|
}
|
||||||
|
if (*d).nsectors > 0 {
|
||||||
|
h.drive.set_nsectors((*d).nsectors as i64);
|
||||||
|
}
|
||||||
|
let timed = if buffer.is_null() {
|
||||||
|
h.drive.read_timed(None, beginsector as i64, sectors as i64)
|
||||||
|
} else {
|
||||||
|
let len = (sectors as usize) * cdrustanoia_core::device::CD_FRAMESIZE_RAW;
|
||||||
|
let buf = std::slice::from_raw_parts_mut(buffer as *mut u8, len);
|
||||||
|
h.drive
|
||||||
|
.read_timed(Some(buf), beginsector as i64, sectors as i64)
|
||||||
|
};
|
||||||
|
if !ms.is_null() {
|
||||||
|
*ms = timed.milliseconds;
|
||||||
|
}
|
||||||
|
if (*d).bigendianp < 0 {
|
||||||
|
(*d).bigendianp = h.drive.bigendianp();
|
||||||
|
}
|
||||||
|
match timed.result {
|
||||||
|
Ok(n) => n as c_long,
|
||||||
|
Err(e) => {
|
||||||
|
set_errno(e.errno);
|
||||||
|
set_error(d, &format!("SCSI read error: {}\n", e.message));
|
||||||
|
match e.errno {
|
||||||
|
libc::ENOMEDIUM => -404,
|
||||||
|
libc::ENOMEM => -300,
|
||||||
|
_ => -7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_read(
|
||||||
|
d: *mut CdromDrive,
|
||||||
|
buffer: *mut c_void,
|
||||||
|
beginsector: c_long,
|
||||||
|
sectors: c_long,
|
||||||
|
) -> c_long {
|
||||||
|
cdda_read_timed(d, buffer, beginsector, sectors, ptr::null_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_speed_set(d: *mut CdromDrive, speed: c_int) -> c_int {
|
||||||
|
match holder(d) {
|
||||||
|
Some(h) => {
|
||||||
|
let ret = h.drive.speed_set(speed);
|
||||||
|
if ret == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
-405
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => -405,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_verbose_set(
|
||||||
|
d: *mut CdromDrive,
|
||||||
|
err_action: c_int,
|
||||||
|
mes_action: c_int,
|
||||||
|
) {
|
||||||
|
if !d.is_null() {
|
||||||
|
(*d).errordest = err_action;
|
||||||
|
(*d).messagedest = mes_action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_messages(d: *mut CdromDrive) -> *mut c_char {
|
||||||
|
if d.is_null() {
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let ret = (*d).messagebuf;
|
||||||
|
(*d).messagebuf = ptr::null_mut();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_errors(d: *mut CdromDrive) -> *mut c_char {
|
||||||
|
if d.is_null() {
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let ret = (*d).errorbuf;
|
||||||
|
(*d).errorbuf = ptr::null_mut();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn cdda_version() -> *const c_char {
|
||||||
|
c"10.2".as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_tracks(d: *mut CdromDrive) -> c_long {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
(*d).tracks as c_long
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_firstsector(d: *mut CdromDrive, track: c_int) -> c_long {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_firstsector(track) as c_long
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_lastsector(d: *mut CdromDrive, track: c_int) -> c_long {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_lastsector(track) as c_long
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_disc_firstsector(d: *mut CdromDrive) -> c_long {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).disc_firstsector() as c_long
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_disc_lastsector(d: *mut CdromDrive) -> c_long {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).disc_lastsector() as c_long
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_sector_gettrack(d: *mut CdromDrive, sector: c_long) -> c_int {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).sector_gettrack(sector as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_channels(d: *mut CdromDrive, track: c_int) -> c_int {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_channels(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_audiop(d: *mut CdromDrive, track: c_int) -> c_int {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_audiop(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_copyp(d: *mut CdromDrive, track: c_int) -> c_int {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_copyp(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_preemp(d: *mut CdromDrive, track: c_int) -> c_int {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_preemp(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn cdda_track_bitmap(
|
||||||
|
d: *mut CdromDrive,
|
||||||
|
track: c_int,
|
||||||
|
bit: c_int,
|
||||||
|
set: c_int,
|
||||||
|
clear: c_int,
|
||||||
|
) -> c_int {
|
||||||
|
if d.is_null() || (*d).opened == 0 {
|
||||||
|
return -400;
|
||||||
|
}
|
||||||
|
toc_from_drive(&*d).track_bitmap(track, bit as u8, set, clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the c_long import used on all targets.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn _typecheck(_: c_long) {}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdda_paranoia"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cdda_paranoia"
|
||||||
|
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cdrustanoia_core.workspace = true
|
||||||
|
cdrustanoia_capi_types.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cdda_interface = { path = "../cdda-interface" }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_paranoia.so.0");
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
#![allow(
|
||||||
|
clippy::missing_safety_doc,
|
||||||
|
clippy::unnecessary_cast,
|
||||||
|
clippy::field_reassign_with_default
|
||||||
|
)]
|
||||||
|
|
||||||
|
//! C ABI-compatible libcdda_paranoia implementation.
|
||||||
|
|
||||||
|
use cdrustanoia_capi_types::*;
|
||||||
|
use cdrustanoia_core::device::{
|
||||||
|
host_big_endian, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead,
|
||||||
|
CD_FRAMESIZE_RAW, MAXTRK,
|
||||||
|
};
|
||||||
|
use cdrustanoia_core::paranoia::Paranoia;
|
||||||
|
use libc::{c_char, c_int, c_long, c_void};
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::ptr;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub type ParanoiaCallback = Option<unsafe extern "C" fn(c_long, c_int)>;
|
||||||
|
|
||||||
|
struct CAbiDrive {
|
||||||
|
d: *mut CdromDrive,
|
||||||
|
toc: DiscToc,
|
||||||
|
model: String,
|
||||||
|
bigendianp: i32,
|
||||||
|
nsectors: i64,
|
||||||
|
interface: InterfaceKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn toc_from_d(d: *mut CdromDrive) -> DiscToc {
|
||||||
|
let mut toc = DiscToc::default();
|
||||||
|
if d.is_null() {
|
||||||
|
return toc;
|
||||||
|
}
|
||||||
|
toc.tracks = (*d).tracks;
|
||||||
|
toc.cd_extra = (*d).cd_extra;
|
||||||
|
for i in 0..MAXTRK {
|
||||||
|
toc.entries[i].flags = (*d).disc_toc[i].b_flags;
|
||||||
|
toc.entries[i].track = (*d).disc_toc[i].b_track;
|
||||||
|
toc.entries[i].start_sector = (*d).disc_toc[i].dw_start_sector as i64;
|
||||||
|
}
|
||||||
|
toc
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CAbiDrive {
|
||||||
|
unsafe fn new(d: *mut CdromDrive) -> Self {
|
||||||
|
let model = if (*d).drive_model.is_null() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
CStr::from_ptr((*d).drive_model)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
let interface = match (*d).interface {
|
||||||
|
INTERFACE_COOKED_IOCTL => InterfaceKind::Cooked,
|
||||||
|
INTERFACE_TEST => InterfaceKind::Image,
|
||||||
|
_ => InterfaceKind::SgIo,
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
d,
|
||||||
|
toc: toc_from_d(d),
|
||||||
|
model,
|
||||||
|
bigendianp: (*d).bigendianp,
|
||||||
|
nsectors: (*d).nsectors as i64,
|
||||||
|
interface,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
self.toc = toc_from_d(self.d);
|
||||||
|
self.bigendianp = (*self.d).bigendianp;
|
||||||
|
self.nsectors = (*self.d).nsectors as i64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drive for CAbiDrive {
|
||||||
|
fn toc(&self) -> &DiscToc {
|
||||||
|
&self.toc
|
||||||
|
}
|
||||||
|
fn toc_mut(&mut self) -> &mut DiscToc {
|
||||||
|
&mut self.toc
|
||||||
|
}
|
||||||
|
fn opened(&self) -> bool {
|
||||||
|
unsafe { !self.d.is_null() && (*self.d).opened != 0 }
|
||||||
|
}
|
||||||
|
fn read_audio(
|
||||||
|
&mut self,
|
||||||
|
mut buffer: Option<&mut [u8]>,
|
||||||
|
begin: i64,
|
||||||
|
sectors: i64,
|
||||||
|
) -> Result<usize, ReadError> {
|
||||||
|
unsafe {
|
||||||
|
if self.d.is_null() || (*self.d).opened == 0 {
|
||||||
|
return Err(ReadError::closed());
|
||||||
|
}
|
||||||
|
let f = (*self.d)
|
||||||
|
.read_audio
|
||||||
|
.ok_or_else(|| ReadError::new(libc::EIO, "No read function"))?;
|
||||||
|
let ptr = match buffer.as_mut() {
|
||||||
|
Some(b) => b.as_mut_ptr() as *mut c_void,
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
};
|
||||||
|
let ret = f(self.d, ptr, begin as c_long, sectors as c_long);
|
||||||
|
if ret < 0 {
|
||||||
|
let errno = match ret {
|
||||||
|
-404 => libc::ENOMEDIUM,
|
||||||
|
-300 => libc::ENOMEM,
|
||||||
|
_ => libc::EIO,
|
||||||
|
};
|
||||||
|
return Err(ReadError::new(errno, "SCSI read error"));
|
||||||
|
}
|
||||||
|
let n = ret as usize;
|
||||||
|
if let Some(buf) = buffer.as_mut() {
|
||||||
|
let be = (*self.d).bigendianp;
|
||||||
|
if n > 0 && be >= 0 && (be == 1) != host_big_endian() {
|
||||||
|
let len = (n * CD_FRAMESIZE_RAW).min(buf.len());
|
||||||
|
swap_raw16(&mut buf[..len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
|
||||||
|
let started = Instant::now();
|
||||||
|
let result = self.read_audio(buffer, begin, sectors);
|
||||||
|
TimedRead {
|
||||||
|
result,
|
||||||
|
milliseconds: started.elapsed().as_millis() as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn speed_set(&mut self, speed: i32) -> i32 {
|
||||||
|
unsafe {
|
||||||
|
if self.d.is_null() {
|
||||||
|
return -405;
|
||||||
|
}
|
||||||
|
match (*self.d).set_speed {
|
||||||
|
Some(f) => f(self.d, speed as c_int),
|
||||||
|
None => -405,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn close(&mut self) {
|
||||||
|
// The paranoia library never closes the caller-owned drive.
|
||||||
|
}
|
||||||
|
fn drive_model(&self) -> &str {
|
||||||
|
&self.model
|
||||||
|
}
|
||||||
|
fn bigendianp(&self) -> i32 {
|
||||||
|
self.bigendianp
|
||||||
|
}
|
||||||
|
fn set_bigendianp(&mut self, value: i32) {
|
||||||
|
self.bigendianp = value;
|
||||||
|
unsafe {
|
||||||
|
if !self.d.is_null() {
|
||||||
|
(*self.d).bigendianp = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn nsectors(&self) -> i64 {
|
||||||
|
self.nsectors
|
||||||
|
}
|
||||||
|
fn set_nsectors(&mut self, value: i64) {
|
||||||
|
self.nsectors = value;
|
||||||
|
unsafe {
|
||||||
|
if !self.d.is_null() {
|
||||||
|
(*self.d).nsectors = value as c_int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn interface(&self) -> InterfaceKind {
|
||||||
|
self.interface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn p_ref<'a>(p: *mut c_void) -> Option<&'a mut Paranoia> {
|
||||||
|
if p.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&mut *(p as *mut Paranoia))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_version() -> *const c_char {
|
||||||
|
c"10.2".as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void {
|
||||||
|
if d.is_null() {
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let mut adapter = CAbiDrive::new(d);
|
||||||
|
adapter.refresh();
|
||||||
|
let p = Paranoia::new(Box::new(adapter));
|
||||||
|
Box::into_raw(Box::new(p)) as *mut c_void
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_free(p: *mut c_void) {
|
||||||
|
if !p.is_null() {
|
||||||
|
drop(Box::from_raw(p as *mut Paranoia));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_modeset(p: *mut c_void, mode: c_int) {
|
||||||
|
if let Some(p) = p_ref(p) {
|
||||||
|
p.modeset(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_seek(p: *mut c_void, seek: c_long, mode: c_int) -> c_long {
|
||||||
|
match p_ref(p) {
|
||||||
|
Some(p) => p.seek(seek as i64, mode) as c_long,
|
||||||
|
None => -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_overlapset(p: *mut c_void, overlap: c_long) {
|
||||||
|
if let Some(p) = p_ref(p) {
|
||||||
|
p.overlapset(overlap as i64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_cachemodel_size(p: *mut c_void, sectors: c_int) -> c_int {
|
||||||
|
match p_ref(p) {
|
||||||
|
Some(p) => p.cachemodel_size(sectors as i64) as c_int,
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_read(p: *mut c_void, callback: ParanoiaCallback) -> *mut i16 {
|
||||||
|
paranoia_read_limited(p, callback, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn paranoia_read_limited(
|
||||||
|
p: *mut c_void,
|
||||||
|
callback: ParanoiaCallback,
|
||||||
|
max_retries: c_int,
|
||||||
|
) -> *mut i16 {
|
||||||
|
let p = match p_ref(p) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return ptr::null_mut(),
|
||||||
|
};
|
||||||
|
let mut cb = |pos: i64, func: i32| {
|
||||||
|
if let Some(f) = callback {
|
||||||
|
f(pos as c_long, func as c_int);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match p.read_limited(&mut cb, max_retries) {
|
||||||
|
Some(slice) => slice.as_mut_ptr(),
|
||||||
|
None => {
|
||||||
|
let errno = if p.drive().opened() {
|
||||||
|
libc::EIO
|
||||||
|
} else {
|
||||||
|
libc::EBADF
|
||||||
|
};
|
||||||
|
*libc::__errno_location() = errno;
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cdrustanoia_core::device::image::ImageDrive;
|
||||||
|
use cdrustanoia_core::device::samples_to_bytes;
|
||||||
|
use cdrustanoia_core::device::CD_FRAMEWORDS;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn c_api_query_and_read() {
|
||||||
|
unsafe {
|
||||||
|
let mut samples = Vec::new();
|
||||||
|
for i in 0..(3 * 1176) {
|
||||||
|
samples.push(((i * 37) as i16).wrapping_mul(3));
|
||||||
|
}
|
||||||
|
let bytes = samples_to_bytes(&samples);
|
||||||
|
let img = ImageDrive::from_bytes(bytes);
|
||||||
|
let d = cdda_interface::make_drive("test", None, Box::new(img));
|
||||||
|
assert_eq!(cdda_interface::cdda_open(d), 0);
|
||||||
|
assert_eq!(cdda_interface::cdda_tracks(d), 1);
|
||||||
|
assert_eq!(cdda_interface::cdda_disc_firstsector(d), 0);
|
||||||
|
assert_eq!(cdda_interface::cdda_disc_lastsector(d), 2);
|
||||||
|
|
||||||
|
let mut raw = vec![0u8; CD_FRAMESIZE_RAW];
|
||||||
|
let n = cdda_interface::cdda_read(d, raw.as_mut_ptr() as *mut c_void, 0, 1);
|
||||||
|
assert_eq!(n, 1);
|
||||||
|
assert_eq!(raw, samples_to_bytes(&samples[..1176]));
|
||||||
|
|
||||||
|
let p = paranoia_init(d);
|
||||||
|
assert!(!p.is_null());
|
||||||
|
paranoia_modeset(p, cdrustanoia_core::paranoia::PARANOIA_MODE_FULL);
|
||||||
|
paranoia_seek(p, 0, 0);
|
||||||
|
for sector in 0..3 {
|
||||||
|
let ptr = paranoia_read(p, None);
|
||||||
|
assert!(!ptr.is_null());
|
||||||
|
let got = std::slice::from_raw_parts(ptr, CD_FRAMEWORDS as usize);
|
||||||
|
assert_eq!(got, &samples[sector * 1176..(sector + 1) * 1176]);
|
||||||
|
}
|
||||||
|
paranoia_free(p);
|
||||||
|
cdda_interface::cdda_close(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdrustanoia_cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license = "GPL-2.0-or-later"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cdrustanoia"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cdparanoia"
|
||||||
|
path = "src/bin_cdparanoia.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cdrustanoia_cli"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cdrustanoia_core.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
//! Drive cache/timing analysis (`-A`), based on cachetest.c.
|
||||||
|
//!
|
||||||
|
//! This is a compact but functional port: it measures seek/read timing,
|
||||||
|
//! estimates the random-access cache size, checks cache transfer speed, and
|
||||||
|
//! verifies that a backward seek flushes the cache. Output wording follows
|
||||||
|
//! the original where it matters to users.
|
||||||
|
|
||||||
|
use crate::log::Log;
|
||||||
|
use crate::{log_c, print_c, report_c};
|
||||||
|
use cdrustanoia_core::device::{Drive, CD_FRAMESIZE_RAW};
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const MIN_SEEK_MS: i32 = 6;
|
||||||
|
|
||||||
|
fn read_timed(d: &mut dyn Drive, lba: i64, len: i64) -> (i64, i32) {
|
||||||
|
let t = d.read_timed(None, lba, len);
|
||||||
|
match t.result {
|
||||||
|
Ok(n) => (n as i64, t.milliseconds),
|
||||||
|
Err(e) => {
|
||||||
|
if e.errno == libc::ENOMEDIUM {
|
||||||
|
(-404, -1)
|
||||||
|
} else {
|
||||||
|
(-1, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, len: i64, initial_seek: bool) -> i64 {
|
||||||
|
log_c!(log, "\n");
|
||||||
|
let mut sofar = 0i64;
|
||||||
|
let mut sum = 0f64;
|
||||||
|
let mut sumsq = 0f64;
|
||||||
|
let mut latency = 0i32;
|
||||||
|
let mut i = 0;
|
||||||
|
while sofar < len {
|
||||||
|
let toread = if i == 0 && initial_seek {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
len - sofar
|
||||||
|
};
|
||||||
|
let (ret, x) = read_timed(d, lba + sofar, toread);
|
||||||
|
if ret <= 0 {
|
||||||
|
return if ret == -404 { -404 } else { -1 };
|
||||||
|
}
|
||||||
|
let x = x.clamp(0, 9999);
|
||||||
|
log_c!(log, "{}:{}:{} ", lba + sofar, ret, x);
|
||||||
|
sofar += ret;
|
||||||
|
if i != 0 || !initial_seek {
|
||||||
|
sum += x as f64;
|
||||||
|
sumsq += (x * x) as f64 / ret as f64;
|
||||||
|
} else {
|
||||||
|
latency = x;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let denom = (len - 1).max(1) as f64;
|
||||||
|
let mean = sum / denom;
|
||||||
|
let stddev = ((sumsq / denom - mean * mean).max(0.0)).sqrt();
|
||||||
|
if initial_seek {
|
||||||
|
print_c!(
|
||||||
|
log,
|
||||||
|
"{:4}ms seek, {:.2}ms/sec read [{:.1}x]",
|
||||||
|
latency,
|
||||||
|
mean,
|
||||||
|
1000.0 / 75.0 / mean.max(0.001)
|
||||||
|
);
|
||||||
|
log_c!(
|
||||||
|
log,
|
||||||
|
"\n\tInitial seek latency ({} sectors): {}ms",
|
||||||
|
len,
|
||||||
|
latency
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log_c!(
|
||||||
|
log,
|
||||||
|
"\n\tAverage read latency: {:.2}ms/sector (raw speed: {:.1}x)",
|
||||||
|
mean,
|
||||||
|
1000.0 / 75.0 / mean.max(0.001)
|
||||||
|
);
|
||||||
|
log_c!(
|
||||||
|
log,
|
||||||
|
"\n\tRead latency standard deviation: {:.2}ms/sector",
|
||||||
|
stddev
|
||||||
|
);
|
||||||
|
sum as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retime_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, readahead: i64, oldmean: f64) -> f64 {
|
||||||
|
let mut sectors = 2000i64;
|
||||||
|
if sectors as f64 * oldmean > 5000.0 {
|
||||||
|
sectors = (5000.0 / oldmean.max(0.001)) as i64;
|
||||||
|
}
|
||||||
|
let readahead = readahead * 10 / 9;
|
||||||
|
if readahead > sectors {
|
||||||
|
sectors = readahead;
|
||||||
|
}
|
||||||
|
let total = time_drive(d, log, lba, sectors, true);
|
||||||
|
if total <= 0 {
|
||||||
|
return oldmean;
|
||||||
|
}
|
||||||
|
let newmean = total as f64 / sectors as f64;
|
||||||
|
log_c!(
|
||||||
|
log,
|
||||||
|
"\n\tOld mean={:.2}ms/sec, New mean={:.2}ms/sec\n",
|
||||||
|
oldmean,
|
||||||
|
newmean
|
||||||
|
);
|
||||||
|
newmean.max(oldmean)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_assignments)]
|
||||||
|
pub fn analyze_cache(d: &mut dyn Drive, log: &mut Log, speed: i32) -> i32 {
|
||||||
|
let speed = if speed <= 0 { -1 } else { speed };
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\n=================== Checking drive cache/timing behavior ===================\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Longest contiguous audio stretch.
|
||||||
|
let mut firstsector = -1i64;
|
||||||
|
let mut lastsector = -1i64;
|
||||||
|
let mut firsttest = -1i64;
|
||||||
|
let mut lasttest = -1i64;
|
||||||
|
for i in 1..=d.tracks() {
|
||||||
|
if d.track_audiop(i) {
|
||||||
|
if firsttest == -1 {
|
||||||
|
firsttest = d.track_firstsector(i);
|
||||||
|
}
|
||||||
|
lasttest = d.track_lastsector(i);
|
||||||
|
if lasttest - firsttest > lastsector - firstsector {
|
||||||
|
firstsector = firsttest;
|
||||||
|
lastsector = lasttest;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
firsttest = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if firstsector == -1 {
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\n\tNo audio on disc; Cannot determine timing behavior..."
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avail = lastsector - firstsector + 1;
|
||||||
|
let current = (1000i64).min((avail / 2).max(1));
|
||||||
|
let mut offset = firstsector + 10;
|
||||||
|
if offset + current > lastsector {
|
||||||
|
offset = firstsector;
|
||||||
|
}
|
||||||
|
|
||||||
|
report_c!(log, "\nSeek/read timing:\n");
|
||||||
|
let total = time_drive(d, log, offset, current, true);
|
||||||
|
if total == -404 {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
let mspersector = total as f64 / current as f64;
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\n\tAverage access speed: {:.2}ms/sector [{:.1}x]",
|
||||||
|
mspersector,
|
||||||
|
1000.0 / 75.0 / mspersector.max(0.001)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
report_c!(log, "\n\nAnalyzing cache behavior...\n");
|
||||||
|
|
||||||
|
// Find the end of the drive's random-access cache by preloading a
|
||||||
|
// linear run and timing a read of its first sector.
|
||||||
|
let mut cache_size = 0i64;
|
||||||
|
let mut c = 1i64;
|
||||||
|
while c <= 1200 {
|
||||||
|
let (r1, _) = read_timed(d, offset + c, 1);
|
||||||
|
if r1 <= 0 {
|
||||||
|
offset += c + 100;
|
||||||
|
if offset + c > lastsector {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (r2, ms) = read_timed(d, offset, 1);
|
||||||
|
if r2 <= 0 {
|
||||||
|
offset += c + 100;
|
||||||
|
if offset + c > lastsector {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ms >= MIN_SEEK_MS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cache_size = c;
|
||||||
|
c += 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cache_size == 0 {
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\tDrive does not cache nonlinear access \n"
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\tApproximate random access cache size: {} sector(s) \n",
|
||||||
|
cache_size
|
||||||
|
);
|
||||||
|
let mut warn = 0;
|
||||||
|
if cache_size > 1200 {
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\nWARNING: This drive appears to be caching more sectors of\n\
|
||||||
|
readahead than Paranoia can currently handle!\n"
|
||||||
|
);
|
||||||
|
warn = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if speed == -1 {
|
||||||
|
log_c!(log, "\tAttempting to reset read speed to full... ");
|
||||||
|
} else {
|
||||||
|
log_c!(log, "\tAttempting to reset read speed to {}... ", speed);
|
||||||
|
}
|
||||||
|
if d.speed_set(speed) != 0 {
|
||||||
|
log_c!(log, "failed.\n");
|
||||||
|
} else {
|
||||||
|
log_c!(log, "drive said OK\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache transfer speed and backseek flushing.
|
||||||
|
let readsize = (cache_size - 8).max(1).min(cache_size);
|
||||||
|
if readsize >= 7 {
|
||||||
|
report_c!(log, "\tTesting cache transfer speed...");
|
||||||
|
let (_, _) = read_timed(d, offset, readsize);
|
||||||
|
let mut elapsed = 0i64;
|
||||||
|
let mut sectors = 0i64;
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
while start.elapsed().as_millis() < 1000 {
|
||||||
|
let (n, ms) = read_timed(d, offset, readsize);
|
||||||
|
if n <= 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
elapsed += ms as i64;
|
||||||
|
sectors += n;
|
||||||
|
}
|
||||||
|
let cachems = if sectors > 0 {
|
||||||
|
elapsed as f64 / sectors as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\t Cache read speed: {:.2}ms/sector [{}x]\n",
|
||||||
|
cachems,
|
||||||
|
(1000.0 / 75.0 / cachems.max(0.001)) as i64
|
||||||
|
);
|
||||||
|
|
||||||
|
report_c!(log, "\tTesting that backseek flushes cache...");
|
||||||
|
let seekpos = (offset + cache_size + 20000).min((lastsector - 150).max(offset + 1));
|
||||||
|
let (r, _) = read_timed(d, seekpos, 1);
|
||||||
|
let mut readms = 0.0;
|
||||||
|
if r > 0 {
|
||||||
|
let total = time_drive(d, log, offset + 1, readsize, true);
|
||||||
|
if total > 0 {
|
||||||
|
readms = total as f64 / readsize as f64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\t Access speed after backseek: {:.2}ms/sector [{}x]\n",
|
||||||
|
readms,
|
||||||
|
(1000.0 / 75.0 / readms.max(0.001)) as i64
|
||||||
|
);
|
||||||
|
if readms > 0.0 && readms * 2.0 < cachems.max(0.001) {
|
||||||
|
report_c!(
|
||||||
|
log,
|
||||||
|
"\tWARNING: Read timing after backseek faster than expected!\n\
|
||||||
|
\t It's possible/likely that this drive is not\n\
|
||||||
|
\t flushing the readahead cache on backward seeks!\n\n"
|
||||||
|
);
|
||||||
|
warn = 1;
|
||||||
|
} else if readms > 0.0 {
|
||||||
|
report_c!(log, "\tBackseek flushes the cache as expected\n");
|
||||||
|
}
|
||||||
|
let _ = retime_drive(d, log, offset, readsize, cachems.max(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A short pause mirrors the original's readahead settling delay.
|
||||||
|
sleep(Duration::from_millis(50));
|
||||||
|
let _ = CD_FRAMESIZE_RAW;
|
||||||
|
warn
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
//! GNU getopt-compatible command line parsing for cdparanoia 10.2.
|
||||||
|
|
||||||
|
use crate::output::OutputFormat;
|
||||||
|
use cdrustanoia_core::paranoia::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Args {
|
||||||
|
pub analyze: bool,
|
||||||
|
pub verbose: bool,
|
||||||
|
pub quiet: bool,
|
||||||
|
pub stderr_progress: bool,
|
||||||
|
pub log_summary: Option<Option<String>>,
|
||||||
|
pub log_debug: Option<Option<String>>,
|
||||||
|
pub version: bool,
|
||||||
|
pub query: bool,
|
||||||
|
pub batch: bool,
|
||||||
|
pub search: bool,
|
||||||
|
pub help: bool,
|
||||||
|
pub output_format: OutputFormat,
|
||||||
|
pub force_endian: Option<i32>,
|
||||||
|
pub force_sectors: Option<i32>,
|
||||||
|
pub force_overlap: Option<i32>,
|
||||||
|
pub force_device: Option<String>,
|
||||||
|
pub force_cooked_device: Option<String>,
|
||||||
|
pub force_generic_device: Option<String>,
|
||||||
|
pub force_speed: Option<i32>,
|
||||||
|
pub toc_offset: i32,
|
||||||
|
pub toc_bias: bool,
|
||||||
|
pub sample_offset: i32,
|
||||||
|
pub max_retries: i32,
|
||||||
|
pub paranoia_mode: i32,
|
||||||
|
pub abort_on_skip: bool,
|
||||||
|
pub info_file: Option<String>,
|
||||||
|
pub span: Option<String>,
|
||||||
|
pub outfile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Args {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
analyze: false,
|
||||||
|
verbose: false,
|
||||||
|
quiet: false,
|
||||||
|
stderr_progress: false,
|
||||||
|
log_summary: None,
|
||||||
|
log_debug: None,
|
||||||
|
version: false,
|
||||||
|
query: false,
|
||||||
|
batch: false,
|
||||||
|
search: false,
|
||||||
|
help: false,
|
||||||
|
output_format: OutputFormat::Wav,
|
||||||
|
force_endian: None,
|
||||||
|
force_sectors: None,
|
||||||
|
force_overlap: None,
|
||||||
|
force_device: None,
|
||||||
|
force_cooked_device: None,
|
||||||
|
force_generic_device: None,
|
||||||
|
force_speed: None,
|
||||||
|
toc_offset: 0,
|
||||||
|
toc_bias: false,
|
||||||
|
sample_offset: 0,
|
||||||
|
max_retries: 20,
|
||||||
|
paranoia_mode: PARANOIA_MODE_FULL ^ PARANOIA_MODE_NEVERSKIP,
|
||||||
|
abort_on_skip: false,
|
||||||
|
info_file: None,
|
||||||
|
span: None,
|
||||||
|
outfile: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atoi(s: &str) -> i32 {
|
||||||
|
let s = s.trim_start();
|
||||||
|
let mut chars = s.chars().peekable();
|
||||||
|
let mut sign = 1i64;
|
||||||
|
if let Some(c) = chars.peek() {
|
||||||
|
if *c == '-' {
|
||||||
|
sign = -1;
|
||||||
|
chars.next();
|
||||||
|
} else if *c == '+' {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut value = 0i64;
|
||||||
|
let mut any = false;
|
||||||
|
for c in chars {
|
||||||
|
if let Some(d) = c.to_digit(10) {
|
||||||
|
any = true;
|
||||||
|
value = value.saturating_mul(10).saturating_add(d as i64);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !any {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
(sign * value) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum Short {
|
||||||
|
NoArg(char),
|
||||||
|
Required(char),
|
||||||
|
Optional(char),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_kind(c: char) -> Option<Short> {
|
||||||
|
match c {
|
||||||
|
'e' | 's' | 'c' | 'C' | 'p' | 'r' | 'R' | 'w' | 'a' | 'f' | 'v' | 'q' | 'V' | 'Q' | 'h'
|
||||||
|
| 'Z' | 'Y' | 'X' | 'W' | 'B' | 'F' | 'T' | 'A' => Some(Short::NoArg(c)),
|
||||||
|
'n' | 'o' | 'O' | 'd' | 'g' | 'k' | 'S' | 'i' | 't' => Some(Short::Required(c)),
|
||||||
|
'z' | 'l' | 'L' => Some(Short::Optional(c)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn long_kind(name: &str) -> Option<(&'static str, Short)> {
|
||||||
|
let table: &[(&str, Short)] = &[
|
||||||
|
("stderr-progress", Short::NoArg('e')),
|
||||||
|
("search-for-drive", Short::NoArg('s')),
|
||||||
|
("force-cdrom-little-endian", Short::NoArg('c')),
|
||||||
|
("force-cdrom-big-endian", Short::NoArg('C')),
|
||||||
|
("force-default-sectors", Short::Required('n')),
|
||||||
|
("force-search-overlap", Short::Required('o')),
|
||||||
|
("force-cdrom-device", Short::Required('d')),
|
||||||
|
("force-cooked-device", Short::Required('k')),
|
||||||
|
("force-generic-device", Short::Required('g')),
|
||||||
|
("force-read-speed", Short::Required('S')),
|
||||||
|
("sample-offset", Short::Required('O')),
|
||||||
|
("toc-offset", Short::Required('t')),
|
||||||
|
("toc-bias", Short::NoArg('T')),
|
||||||
|
("output-raw", Short::NoArg('p')),
|
||||||
|
("output-raw-little-endian", Short::NoArg('r')),
|
||||||
|
("output-raw-big-endian", Short::NoArg('R')),
|
||||||
|
("output-wav", Short::NoArg('w')),
|
||||||
|
("output-aiff", Short::NoArg('f')),
|
||||||
|
("output-aifc", Short::NoArg('a')),
|
||||||
|
("batch", Short::NoArg('B')),
|
||||||
|
("verbose", Short::NoArg('v')),
|
||||||
|
("quiet", Short::NoArg('q')),
|
||||||
|
("version", Short::NoArg('V')),
|
||||||
|
("query", Short::NoArg('Q')),
|
||||||
|
("help", Short::NoArg('h')),
|
||||||
|
("analyze-drive", Short::NoArg('A')),
|
||||||
|
("disable-paranoia", Short::NoArg('Z')),
|
||||||
|
("disable-extra-paranoia", Short::NoArg('Y')),
|
||||||
|
("abort-on-skip", Short::NoArg('X')),
|
||||||
|
("disable-fragmentation", Short::NoArg('F')),
|
||||||
|
("output-info", Short::Required('i')),
|
||||||
|
("never-skip", Short::Optional('z')),
|
||||||
|
("log-summary", Short::Optional('l')),
|
||||||
|
("log-debug", Short::Optional('L')),
|
||||||
|
];
|
||||||
|
if let Some((_, kind)) = table.iter().find(|(n, _)| *n == name) {
|
||||||
|
return Some(("", *kind));
|
||||||
|
}
|
||||||
|
let matches: Vec<&(&str, Short)> = table.iter().filter(|(n, _)| n.starts_with(name)).collect();
|
||||||
|
if matches.len() == 1 {
|
||||||
|
Some(("", matches[0].1))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(args: &mut Args, opt: Short, value: Option<String>) -> Result<(), String> {
|
||||||
|
let c = match opt {
|
||||||
|
Short::NoArg(c) | Short::Required(c) | Short::Optional(c) => c,
|
||||||
|
};
|
||||||
|
match c {
|
||||||
|
'A' => {
|
||||||
|
args.analyze = true;
|
||||||
|
args.query = true;
|
||||||
|
args.log_debug = Some(None);
|
||||||
|
args.verbose = true;
|
||||||
|
}
|
||||||
|
'v' => {
|
||||||
|
args.verbose = true;
|
||||||
|
args.quiet = false;
|
||||||
|
}
|
||||||
|
'q' => {
|
||||||
|
args.verbose = false;
|
||||||
|
args.quiet = true;
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
args.stderr_progress = true;
|
||||||
|
eprintln!("Sending all callbacks to stderr for wrapper script");
|
||||||
|
}
|
||||||
|
'l' => {
|
||||||
|
args.log_summary = Some(value);
|
||||||
|
}
|
||||||
|
'L' => {
|
||||||
|
args.log_debug = Some(value);
|
||||||
|
}
|
||||||
|
'V' => args.version = true,
|
||||||
|
'Q' => args.query = true,
|
||||||
|
's' => args.search = true,
|
||||||
|
'h' => args.help = true,
|
||||||
|
'p' => args.output_format = OutputFormat::RawHost,
|
||||||
|
'r' => args.output_format = OutputFormat::RawLittle,
|
||||||
|
'R' => args.output_format = OutputFormat::RawBig,
|
||||||
|
'w' => args.output_format = OutputFormat::Wav,
|
||||||
|
'f' => args.output_format = OutputFormat::Aiff,
|
||||||
|
'a' => args.output_format = OutputFormat::Aifc,
|
||||||
|
'c' => args.force_endian = Some(0),
|
||||||
|
'C' => args.force_endian = Some(1),
|
||||||
|
'n' => args.force_sectors = Some(atoi(value.as_deref().unwrap_or(""))),
|
||||||
|
'o' => args.force_overlap = Some(atoi(value.as_deref().unwrap_or(""))),
|
||||||
|
'd' => args.force_device = value,
|
||||||
|
'g' => {
|
||||||
|
if args.force_cooked_device.is_some() {
|
||||||
|
return Err("-g option incompatable with -k".to_string());
|
||||||
|
}
|
||||||
|
args.force_cooked_device = None;
|
||||||
|
args.force_generic_device = value;
|
||||||
|
}
|
||||||
|
'k' => {
|
||||||
|
if args.force_generic_device.is_some() || args.force_device.is_some() {
|
||||||
|
return Err("-k option incompatable with -d and -g".to_string());
|
||||||
|
}
|
||||||
|
args.force_cooked_device = value;
|
||||||
|
}
|
||||||
|
'S' => args.force_speed = Some(atoi(value.as_deref().unwrap_or(""))),
|
||||||
|
't' => args.toc_offset = atoi(value.as_deref().unwrap_or("")),
|
||||||
|
'T' => args.toc_bias = true,
|
||||||
|
'O' => args.sample_offset = atoi(value.as_deref().unwrap_or("")),
|
||||||
|
'z' => {
|
||||||
|
if let Some(v) = value {
|
||||||
|
args.max_retries = atoi(&v);
|
||||||
|
args.paranoia_mode &= !PARANOIA_MODE_NEVERSKIP;
|
||||||
|
} else {
|
||||||
|
args.paranoia_mode |= PARANOIA_MODE_NEVERSKIP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'Z' => args.paranoia_mode = PARANOIA_MODE_DISABLE,
|
||||||
|
'Y' => {
|
||||||
|
args.paranoia_mode |= PARANOIA_MODE_OVERLAP;
|
||||||
|
args.paranoia_mode &= !PARANOIA_MODE_VERIFY;
|
||||||
|
}
|
||||||
|
'X' => args.abort_on_skip = true,
|
||||||
|
'W' => args.paranoia_mode &= !PARANOIA_MODE_REPAIR,
|
||||||
|
'F' => args.paranoia_mode &= !PARANOIA_MODE_FRAGMENT,
|
||||||
|
'B' => args.batch = true,
|
||||||
|
'i' => args.info_file = value,
|
||||||
|
_ => return Err(format!("Unknown option -{}", c)),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(argv: &[String]) -> Result<Args, String> {
|
||||||
|
let mut args = Args::default();
|
||||||
|
let mut positionals = Vec::new();
|
||||||
|
let mut i = 1usize;
|
||||||
|
let mut stop = false;
|
||||||
|
while i < argv.len() {
|
||||||
|
let arg = &argv[i];
|
||||||
|
if stop || arg == "-" || !arg.starts_with('-') {
|
||||||
|
positionals.push(arg.clone());
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if arg == "--" {
|
||||||
|
stop = true;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(long) = arg.strip_prefix("--") {
|
||||||
|
let (name, inline) = match long.split_once('=') {
|
||||||
|
Some((n, v)) => (n, Some(v.to_string())),
|
||||||
|
None => (long, None),
|
||||||
|
};
|
||||||
|
let (_, kind) =
|
||||||
|
long_kind(name).ok_or_else(|| format!("unrecognized option '--{}'", name))?;
|
||||||
|
match kind {
|
||||||
|
Short::NoArg(c) => {
|
||||||
|
if inline.is_some() {
|
||||||
|
return Err(format!("option '--{}' doesn't allow an argument", name));
|
||||||
|
}
|
||||||
|
apply(&mut args, Short::NoArg(c), None)?;
|
||||||
|
if args.help || args.version {
|
||||||
|
return Ok(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Short::Required(c) => {
|
||||||
|
let value = match inline {
|
||||||
|
Some(v) => Some(v),
|
||||||
|
None => {
|
||||||
|
i += 1;
|
||||||
|
if i >= argv.len() {
|
||||||
|
return Err(format!("option '--{}' requires an argument", name));
|
||||||
|
}
|
||||||
|
Some(argv[i].clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
apply(&mut args, Short::Required(c), value)?;
|
||||||
|
}
|
||||||
|
Short::Optional(c) => apply(&mut args, Short::Optional(c), inline)?,
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chars: Vec<char> = arg[1..].chars().collect();
|
||||||
|
let mut j = 0usize;
|
||||||
|
while j < chars.len() {
|
||||||
|
let c = chars[j];
|
||||||
|
let kind = short_kind(c).ok_or_else(|| format!("invalid option -- '{}'", c))?;
|
||||||
|
match kind {
|
||||||
|
Short::NoArg(c) => {
|
||||||
|
apply(&mut args, Short::NoArg(c), None)?;
|
||||||
|
if args.help || args.version {
|
||||||
|
return Ok(args);
|
||||||
|
}
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
Short::Required(c) => {
|
||||||
|
let rest: String = chars[j + 1..].iter().collect();
|
||||||
|
let value = if !rest.is_empty() {
|
||||||
|
rest
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
if i >= argv.len() {
|
||||||
|
return Err(format!("option requires an argument -- '{}'", c));
|
||||||
|
}
|
||||||
|
argv[i].clone()
|
||||||
|
};
|
||||||
|
apply(&mut args, Short::Required(c), Some(value))?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Short::Optional(c) => {
|
||||||
|
let rest: String = chars[j + 1..].iter().collect();
|
||||||
|
apply(
|
||||||
|
&mut args,
|
||||||
|
Short::Optional(c),
|
||||||
|
if rest.is_empty() { None } else { Some(rest) },
|
||||||
|
)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !positionals.is_empty() {
|
||||||
|
args.span = Some(positionals[0].clone());
|
||||||
|
}
|
||||||
|
if positionals.len() > 1 {
|
||||||
|
args.outfile = Some(positionals[1].clone());
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::output::OutputFormat;
|
||||||
|
|
||||||
|
fn p(args: &[&str]) -> Result<Args, String> {
|
||||||
|
let mut v = vec!["cdparanoia".to_string()];
|
||||||
|
v.extend(args.iter().map(|s| s.to_string()));
|
||||||
|
parse(&v)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clustered_flags() {
|
||||||
|
let a = p(&["-vQ"]).unwrap();
|
||||||
|
assert!(a.verbose && !a.quiet && a.query);
|
||||||
|
let a = p(&["-qv"]).unwrap();
|
||||||
|
assert!(a.verbose && !a.quiet);
|
||||||
|
let a = p(&["-vq"]).unwrap();
|
||||||
|
assert!(a.quiet && !a.verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optional_arguments_are_attached_only() {
|
||||||
|
let a = p(&["-lfoo.log"]).unwrap();
|
||||||
|
assert_eq!(a.log_summary, Some(Some("foo.log".to_string())));
|
||||||
|
let a = p(&["-l", "foo.log"]).unwrap();
|
||||||
|
assert_eq!(a.log_summary, Some(None));
|
||||||
|
assert_eq!(a.span.as_deref(), Some("foo.log"));
|
||||||
|
let a = p(&["--log-summary=bar.log"]).unwrap();
|
||||||
|
assert_eq!(a.log_summary, Some(Some("bar.log".to_string())));
|
||||||
|
let a = p(&["--log-summary", "bar.log"]).unwrap();
|
||||||
|
assert_eq!(a.log_summary, Some(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn never_skip_modes() {
|
||||||
|
let a = p(&["-z", "1"]).unwrap();
|
||||||
|
assert_ne!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0);
|
||||||
|
assert_eq!(a.span.as_deref(), Some("1"));
|
||||||
|
let a = p(&["-z5", "1"]).unwrap();
|
||||||
|
assert_eq!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0);
|
||||||
|
assert_eq!(a.max_retries, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn end_of_options() {
|
||||||
|
let a = p(&["--", "-3", "out.raw"]).unwrap();
|
||||||
|
assert_eq!(a.span.as_deref(), Some("-3"));
|
||||||
|
assert_eq!(a.outfile.as_deref(), Some("out.raw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_formats_last_wins() {
|
||||||
|
assert_eq!(
|
||||||
|
p(&["-w", "-r", "1"]).unwrap().output_format,
|
||||||
|
OutputFormat::RawLittle
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
p(&["-p", "-a", "1"]).unwrap().output_format,
|
||||||
|
OutputFormat::Aifc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incompatible_devices() {
|
||||||
|
assert!(p(&["-g", "/dev/sg0", "-k", "/dev/sr0", "1"]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fn main() {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
std::process::exit(cdrustanoia_cli::run(&argv));
|
||||||
|
}
|
||||||
@@ -0,0 +1,806 @@
|
|||||||
|
//! cdrustanoia command line driver, ported from cdparanoia 10.2 main.c.
|
||||||
|
|
||||||
|
mod analyze;
|
||||||
|
pub mod args;
|
||||||
|
mod log;
|
||||||
|
mod output;
|
||||||
|
mod progress;
|
||||||
|
mod span;
|
||||||
|
|
||||||
|
use crate::log::Log;
|
||||||
|
use crate::output::{write_header, BufferedWriter, OutputFormat};
|
||||||
|
use crate::progress::Progress;
|
||||||
|
use crate::span::parse_span;
|
||||||
|
use args::Args;
|
||||||
|
use cdrustanoia_core::device::discover::{find_a_cdrom, identify, identify_cooked, identify_sgio};
|
||||||
|
use cdrustanoia_core::device::{host_big_endian, swap16, Drive};
|
||||||
|
use cdrustanoia_core::paranoia::*;
|
||||||
|
use cdrustanoia_core::{VERSION, VERSIONNUM};
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
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)
|
||||||
|
|
||||||
|
(C) 2008 Monty <monty@xiph.org> and Xiph.Org
|
||||||
|
|
||||||
|
Report bugs to paranoia@xiph.org
|
||||||
|
http://www.xiph.org/paranoia/
|
||||||
|
USAGE:
|
||||||
|
cdparanoia [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
|
||||||
|
modelling a sprcific drive's cache and
|
||||||
|
read behavior. Implies -vQL
|
||||||
|
-v --verbose : extra verbose operation
|
||||||
|
-q --quiet : quiet operation
|
||||||
|
-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
|
||||||
|
-L --log-debug [<file>] : save detailed device autosense and
|
||||||
|
debugging output to file, default
|
||||||
|
filename cdparanoia.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
|
||||||
|
seperate file.
|
||||||
|
-s --search-for-drive : do an exhaustive search for drive
|
||||||
|
-h --help : print help
|
||||||
|
|
||||||
|
-p --output-raw : output raw 16 bit PCM in host byte
|
||||||
|
order
|
||||||
|
-r --output-raw-little-endian : output raw 16 bit little-endian PCM
|
||||||
|
-R --output-raw-big-endian : output raw 16 bit big-endian PCM
|
||||||
|
-w --output-wav : output as WAV file (default)
|
||||||
|
-f --output-aiff : output as AIFF file
|
||||||
|
-a --output-aifc : output as AIFF-C file
|
||||||
|
|
||||||
|
-c --force-cdrom-little-endian : force treating drive as little endian
|
||||||
|
-C --force-cdrom-big-endian : force treating drive as big endian
|
||||||
|
-n --force-default-sectors <n> : force default number of sectors in read
|
||||||
|
to n sectors
|
||||||
|
-o --force-search-overlap <n> : force minimum overlap search during
|
||||||
|
verification to n sectors
|
||||||
|
-d --force-cdrom-device <dev> : use specified device; disallow
|
||||||
|
autosense
|
||||||
|
-k --force-cooked-device <dev> : use specified cdrom device and force
|
||||||
|
use of the old 'cooked ioctl' kernel
|
||||||
|
interface. -k cannot be used with -d
|
||||||
|
or -g.
|
||||||
|
-g --force-generic-device <dev> : use specified generic scsi device and
|
||||||
|
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
|
||||||
|
speed.
|
||||||
|
-t --toc-offset <n> : Add <n> sectors to the values reported
|
||||||
|
when addressing tracks. May be negative
|
||||||
|
-T --toc-bias : Assume that the beginning offset of
|
||||||
|
track 1 as reported in the TOC will be
|
||||||
|
addressed as LBA 0. Necessary for some
|
||||||
|
Toshiba drives to get track boundaries
|
||||||
|
correct
|
||||||
|
-O --sample-offset <n> : Add <n> samples to the offset when
|
||||||
|
reading data. May be negative.
|
||||||
|
-z --never-skip[=n] : never accept any less than perfect
|
||||||
|
data reconstruction (don't allow 'V's)
|
||||||
|
but if [n] is given, skip after [n]
|
||||||
|
retries without progress.
|
||||||
|
-Z --disable-paranoia : disable all paranoia checking
|
||||||
|
-Y --disable-extra-paranoia : only do cdda2wav-style overlap checking
|
||||||
|
-X --abort-on-skip : abort on imperfect reads/skips
|
||||||
|
|
||||||
|
OUTPUT SMILIES:
|
||||||
|
:-) Normal operation, low/no jitter
|
||||||
|
:-| Normal operation, considerable jitter
|
||||||
|
:-/ Read drift
|
||||||
|
:-P Unreported loss of streaming in atomic read operation
|
||||||
|
8-| Finding read problems at same point during reread; hard to correct
|
||||||
|
:-0 SCSI/ATAPI transport error
|
||||||
|
:-( Scratch detected
|
||||||
|
;-( Gave up trying to perform a correction
|
||||||
|
8-X Aborted (as per -X) due to a scratch/skip
|
||||||
|
:^D Finished extracting
|
||||||
|
|
||||||
|
PROGRESS BAR SYMBOLS:
|
||||||
|
<space> No corrections needed
|
||||||
|
- Jitter correction required
|
||||||
|
+ Unreported loss of streaming/other error in read
|
||||||
|
! Errors are getting through stage 1 but corrected in stage2
|
||||||
|
e SCSI/ATAPI transport error (corrected)
|
||||||
|
V Uncorrected error/skip
|
||||||
|
|
||||||
|
SPAN ARGUMENT:
|
||||||
|
The span argument may be a simple track number or a offset/span
|
||||||
|
specification. The syntax of an offset/span takes the rough form:
|
||||||
|
|
||||||
|
1[ww:xx:yy.zz]-2[aa:bb:cc.dd]
|
||||||
|
|
||||||
|
Here, 1 and 2 are track numbers; the numbers in brackets provide a
|
||||||
|
finer grained offset within a particular track. [aa:bb:cc.dd] is in
|
||||||
|
hours/minutes/seconds/sectors format. Zero fields need not be
|
||||||
|
specified: [::20], [:20], [20], [20.], etc, would be interpreted as
|
||||||
|
twenty seconds, [10:] would be ten minutes, [.30] would be thirty
|
||||||
|
sectors (75 sectors per second).
|
||||||
|
|
||||||
|
When only a single offset is supplied, it is interpreted as a starting
|
||||||
|
offset and ripping will continue to the end of he track. If a single
|
||||||
|
offset is preceeded or followed by a hyphen, the implicit missing
|
||||||
|
offset is taken to be the start or end of the disc, respectively. Thus:
|
||||||
|
|
||||||
|
1:[20.35] Specifies ripping from track 1, second 20, sector 35 to
|
||||||
|
the end of track 1.
|
||||||
|
|
||||||
|
1:[20.35]- Specifies ripping from 1[20.35] to the end of the disc
|
||||||
|
|
||||||
|
-2 Specifies ripping from the beginning of the disc up to
|
||||||
|
(and including) track 2
|
||||||
|
|
||||||
|
-2:[30.35] Specifies ripping from the beginning of the disc up to
|
||||||
|
2:[30.35]
|
||||||
|
|
||||||
|
2-4 Specifies ripping from the beginning of track two to the
|
||||||
|
end of track 4.
|
||||||
|
|
||||||
|
Don't forget to protect square brackets and preceeding hyphens from
|
||||||
|
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
|
||||||
|
|
||||||
|
B) extract up to and including track 3, putting each track in a seperate
|
||||||
|
file:
|
||||||
|
cdparanoia -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
|
||||||
|
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn usage(out: &mut dyn Write) {
|
||||||
|
let _ = out.write_all(MANUAL.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_log_name(name: &str) -> Result<Box<dyn Write>, String> {
|
||||||
|
if name == "-" {
|
||||||
|
Ok(Box::new(io::stdout()))
|
||||||
|
} else {
|
||||||
|
OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.mode(0o666)
|
||||||
|
.open(name)
|
||||||
|
.map(|f| Box::new(f) as Box<dyn Write>)
|
||||||
|
.map_err(|e| format!("Cannot open log file {}: {}", name, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logs(args: &Args, argv: &[String]) -> Result<Log, String> {
|
||||||
|
let mut log = Log::new();
|
||||||
|
log.quiet = args.quiet;
|
||||||
|
|
||||||
|
let summary_name = args
|
||||||
|
.log_summary
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string()));
|
||||||
|
let report_name = args
|
||||||
|
.log_debug
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string()));
|
||||||
|
|
||||||
|
match (&summary_name, &report_name) {
|
||||||
|
(Some(s), Some(r)) if s == r && s != "-" => {
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.mode(0o666)
|
||||||
|
.open(s)
|
||||||
|
.map_err(|e| format!("Cannot open log file {}: {}", s, e))?;
|
||||||
|
let clone = file
|
||||||
|
.try_clone()
|
||||||
|
.map_err(|e| format!("Cannot duplicate log file {}: {}", s, e))?;
|
||||||
|
log.set_summary_writer(Box::new(file));
|
||||||
|
log.set_report_writer(Box::new(clone));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(name) = &summary_name {
|
||||||
|
log.set_summary_writer(open_log_name(name)?);
|
||||||
|
}
|
||||||
|
if let Some(name) = &report_name {
|
||||||
|
log.set_report_writer(open_log_name(name)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.summary_file.is_some() {
|
||||||
|
let line = argv.join(" ");
|
||||||
|
log.summary(format_args!("{}", line));
|
||||||
|
if summary_name.as_deref() != report_name.as_deref() {
|
||||||
|
log.summary(format_args!("{}\n", VERSION));
|
||||||
|
log.summary(format_args!("Using cdda library version: {}", VERSIONNUM));
|
||||||
|
log.summary(format_args!(
|
||||||
|
"Using paranoia library version: {}",
|
||||||
|
VERSIONNUM
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if log.report_file.is_some() && summary_name.as_deref() != report_name.as_deref() {
|
||||||
|
let line = argv.join(" ");
|
||||||
|
log.report(format_args!("{}", line));
|
||||||
|
}
|
||||||
|
log.flush();
|
||||||
|
Ok(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_toc(d: &dyn Drive, log: &mut Log) {
|
||||||
|
let mut s = String::new();
|
||||||
|
s.push_str("\nTable of contents (audio tracks only):\n");
|
||||||
|
s.push_str("track length begin copy pre ch\n");
|
||||||
|
s.push_str("===========================================================");
|
||||||
|
let mut audiolen = 0i64;
|
||||||
|
for i in 1..=d.tracks() {
|
||||||
|
if d.track_audiop(i) {
|
||||||
|
let sec = d.track_firstsector(i);
|
||||||
|
let off = d.track_lastsector(i) - sec + 1;
|
||||||
|
s.push_str(&format!(
|
||||||
|
"\n{:3}. {:7} [{:02}:{:02}.{:02}] {:7} [{:02}:{:02}.{:02}] {} {} {}",
|
||||||
|
i,
|
||||||
|
off,
|
||||||
|
off / (60 * 75),
|
||||||
|
(off / 75) % 60,
|
||||||
|
off % 75,
|
||||||
|
sec,
|
||||||
|
sec / (60 * 75),
|
||||||
|
(sec / 75) % 60,
|
||||||
|
sec % 75,
|
||||||
|
if d.track_copyp(i) { " OK" } else { " no" },
|
||||||
|
if d.track_preemp(i) { " yes" } else { " no" },
|
||||||
|
if d.track_channels(i) == 2 { " 2" } else { " 4" }
|
||||||
|
));
|
||||||
|
audiolen += off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push_str(&format!(
|
||||||
|
"\nTOTAL {:7} [{:02}:{:02}.{:02}] (audio only)",
|
||||||
|
audiolen,
|
||||||
|
audiolen / (60 * 75),
|
||||||
|
(audiolen / 75) % 60,
|
||||||
|
audiolen % 75
|
||||||
|
));
|
||||||
|
log.report(format_args!("{}", s));
|
||||||
|
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(
|
||||||
|
samples.as_ptr() as *const u8,
|
||||||
|
std::mem::size_of_val(samples),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_output_name(
|
||||||
|
template: &Option<String>,
|
||||||
|
batch: bool,
|
||||||
|
track: i32,
|
||||||
|
format: OutputFormat,
|
||||||
|
) -> Option<String> {
|
||||||
|
match template {
|
||||||
|
None => {
|
||||||
|
if batch {
|
||||||
|
Some(format!("track{:02}.{}", track, format.default_extension()))
|
||||||
|
} else {
|
||||||
|
Some(format.default_extension().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(name) if name == "-" => None,
|
||||||
|
Some(name) => {
|
||||||
|
let (prefix, file) = match name.rfind('/') {
|
||||||
|
Some(pos) => (&name[..pos + 1], &name[pos + 1..]),
|
||||||
|
None => ("", name.as_str()),
|
||||||
|
};
|
||||||
|
let mut out = if batch {
|
||||||
|
format!("{}track{:02}.{}", prefix, track, file)
|
||||||
|
} else {
|
||||||
|
format!("{}{}", prefix, file)
|
||||||
|
};
|
||||||
|
if file.is_empty() {
|
||||||
|
out.push_str(format.default_extension());
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_output(name: Option<&str>, template_stdout: bool) -> Result<File, String> {
|
||||||
|
if template_stdout {
|
||||||
|
let fd = unsafe { libc::dup(libc::STDOUT_FILENO) };
|
||||||
|
if fd < 0 {
|
||||||
|
return Err("Cannot duplicate stdout".to_string());
|
||||||
|
}
|
||||||
|
return Ok(unsafe { File::from_raw_fd(fd) });
|
||||||
|
}
|
||||||
|
let name = name.ok_or_else(|| "Invalid output file".to_string())?;
|
||||||
|
OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.mode(0o666)
|
||||||
|
.open(name)
|
||||||
|
.map_err(|e| format!("Cannot open specified output file {}: {}", name, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(argv: &[String]) -> i32 {
|
||||||
|
let args = match args::parse(argv) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
usage(&mut io::stderr());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.help {
|
||||||
|
usage(&mut io::stdout());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if args.version {
|
||||||
|
eprintln!("{}\n", VERSION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut log = match setup_logs(&args, argv) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let span_arg = args.span.clone();
|
||||||
|
if span_arg.is_none() && !args.query && !args.batch {
|
||||||
|
usage(&mut io::stderr());
|
||||||
|
let _ = io::stderr().flush();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.report(format_args!("{}\n", VERSION));
|
||||||
|
if args.verbose {
|
||||||
|
log.report(format_args!("Using cdda library version: {}", VERSIONNUM));
|
||||||
|
log.report(format_args!(
|
||||||
|
"Using paranoia library version: {}",
|
||||||
|
VERSIONNUM
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let drive_result: Result<Box<dyn Drive>, String> = if let Some(dev) = &args.force_cooked_device
|
||||||
|
{
|
||||||
|
identify_cooked(dev, args.verbose)
|
||||||
|
} else if let Some(dev) = &args.force_generic_device {
|
||||||
|
match identify_sgio(dev, args.verbose) {
|
||||||
|
Ok(d) => Ok(d),
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(spec) = &args.force_device {
|
||||||
|
identify(spec, args.verbose, false).map_err(|_| e)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(dev) = &args.force_device {
|
||||||
|
identify(dev, args.verbose, false)
|
||||||
|
} else if args.search {
|
||||||
|
find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string())
|
||||||
|
} else if Path::new("/dev/cdrom").symlink_metadata().is_err() {
|
||||||
|
find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string())
|
||||||
|
} else {
|
||||||
|
match identify("/dev/cdrom", args.verbose, false) {
|
||||||
|
Ok(d) => Ok(d),
|
||||||
|
Err(_) => {
|
||||||
|
if !args.verbose {
|
||||||
|
log.report_c(format_args!(
|
||||||
|
"\n/dev/cdrom exists but isn't accessible. By default,\n\
|
||||||
|
cdparanoia stops searching for an accessible drive here.\n\
|
||||||
|
Consider using -sv to force a more complete autosense\n\
|
||||||
|
of the machine.\n\nMore information about /dev/cdrom:"
|
||||||
|
));
|
||||||
|
let _ = identify("/dev/cdrom", true, false);
|
||||||
|
log.report(format_args!("\n"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Err("Unable to open cdrom drive".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut drive = match drive_result {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
if !args.verbose {
|
||||||
|
log.report(format_args!(
|
||||||
|
"\nUnable to open cdrom drive; -v will give more information."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = drive.open_disc() {
|
||||||
|
if args.verbose {
|
||||||
|
log.report(format_args!("\nDisc open failed: {}", e));
|
||||||
|
}
|
||||||
|
log.report(format_args!(
|
||||||
|
"\nUnable to open disc. Is there an audio CD in the drive?"
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if !drive.opened() || drive.tracks() < 1 {
|
||||||
|
log.report(format_args!(
|
||||||
|
"\nUnable to open disc. Is there an audio CD in the drive?"
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(endian) = args.force_endian {
|
||||||
|
drive.set_bigendianp(endian);
|
||||||
|
log.report(format_args!(
|
||||||
|
"Forcing CDROM sense to {}-endian; ignoring preset and autosense",
|
||||||
|
if endian == 0 { "little" } else { "big" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(sectors) = args.force_sectors {
|
||||||
|
if !(1..=100).contains(§ors) {
|
||||||
|
log.report(format_args!(
|
||||||
|
"Default sector read size must be 1<= n <= 100"
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
log.report(format_args!(
|
||||||
|
"Forcing default to read {} sectors; ignoring preset and autosense",
|
||||||
|
sectors
|
||||||
|
));
|
||||||
|
drive.set_nsectors(sectors as i64);
|
||||||
|
}
|
||||||
|
if let Some(overlap) = args.force_overlap {
|
||||||
|
if !(0..=75).contains(&overlap) {
|
||||||
|
log.report(format_args!("Search overlap sectors must be 0<= n <=75"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
log.report(format_args!(
|
||||||
|
"Forcing search overlap to {} sectors; ignoring autosense",
|
||||||
|
overlap
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let speed = match args.force_speed {
|
||||||
|
Some(0) | None => -1,
|
||||||
|
Some(v) => v,
|
||||||
|
};
|
||||||
|
if speed != -1 {
|
||||||
|
log.report(format_args!("\nAttempting to set speed to {}x... ", speed));
|
||||||
|
} else if args.verbose {
|
||||||
|
log.report(format_args!("\nAttempting to set cdrom to full speed... "));
|
||||||
|
}
|
||||||
|
if drive.speed_set(speed) != 0 {
|
||||||
|
if args.verbose || speed != -1 {
|
||||||
|
log.report(format_args!(
|
||||||
|
"\tCDROM speed set FAILED. Continuing anyway..."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if args.verbose {
|
||||||
|
log.report(format_args!("\tdrive returned OK."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.analyze {
|
||||||
|
let warn = analyze::analyze_cache(drive.as_mut(), &mut log, speed);
|
||||||
|
if warn == 0 {
|
||||||
|
log.report_c(format_args!("\nDrive tests OK with Paranoia.\n\n"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
log.report_c(format_args!(
|
||||||
|
"\nWARNING! PARANOIA MAY NOT BE TRUSTWORTHY WITH THIS DRIVE!\n\
|
||||||
|
\nThe Paranoia library may not model this CDROM drive's cache\
|
||||||
|
\ncorrectly according to this analysis run. Analysis is not\
|
||||||
|
\nalways accurate (it can be fooled by machine load or random\
|
||||||
|
\nkernel latencies), but if a failed result happens more often\
|
||||||
|
\nthan one time in twenty on an unloaded machine, please mail\
|
||||||
|
\nthe cdparanoia.log file produced by this failed analysis to\
|
||||||
|
\nparanoia-dev@xiph.org to assist developers in extending\
|
||||||
|
\nParanoia to handle this CDROM properly.\n\n"
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.query || args.verbose {
|
||||||
|
display_toc(drive.as_ref(), &mut log);
|
||||||
|
}
|
||||||
|
if args.query {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut toc_offset = args.toc_offset as i64;
|
||||||
|
let mut sample_offset = args.sample_offset;
|
||||||
|
if sample_offset != 0 {
|
||||||
|
toc_offset += (sample_offset / 588) as i64;
|
||||||
|
sample_offset %= 588;
|
||||||
|
if sample_offset < 0 {
|
||||||
|
sample_offset += 588;
|
||||||
|
toc_offset -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args.toc_bias {
|
||||||
|
toc_offset = -drive.track_firstsector(1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let tracks = drive.tracks();
|
||||||
|
let toc = drive.toc_mut();
|
||||||
|
for i in 0..=tracks as usize {
|
||||||
|
toc.entries[i].start_sector += toc_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if drive.nsectors() == 1 {
|
||||||
|
log.report(format_args!(
|
||||||
|
"WARNING: The autosensed/selected sectors per read value is\n\
|
||||||
|
one sector, making it very unlikely Paranoia can \n\
|
||||||
|
work.\n\n\
|
||||||
|
Attempting to continue...\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (first_sector, last_sector) = match &span_arg {
|
||||||
|
Some(span) => match parse_span(drive.toc(), span) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
log.report(format_args!("{}", e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => (drive.disc_firstsector(), drive.disc_lastsector()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let track1 = drive.sector_gettrack(first_sector);
|
||||||
|
let track2 = drive.sector_gettrack(last_sector);
|
||||||
|
if track1 < 0 || track2 < 0 {
|
||||||
|
log.report(format_args!("Error parsing span argument"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for i in track1..=track2 {
|
||||||
|
if !drive.track_audiop(i) {
|
||||||
|
log.report(format_args!(
|
||||||
|
"Selected span contains non audio tracks. Aborting.\n"
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let off1 = first_sector - drive.track_firstsector(track1);
|
||||||
|
let off2 = last_sector - drive.track_firstsector(track2);
|
||||||
|
log.report(format_args!(
|
||||||
|
"Ripping from sector {:7} (track {:2} [{}:{:02}.{:02}])\n\t to sector {:7} (track {:2} [{}:{:02}.{:02}])",
|
||||||
|
first_sector,
|
||||||
|
track1,
|
||||||
|
off1 / (60 * 75),
|
||||||
|
(off1 / 75) % 60,
|
||||||
|
off1 % 75,
|
||||||
|
last_sector,
|
||||||
|
track2,
|
||||||
|
off2 / (60 * 75),
|
||||||
|
(off2 / 75) % 60,
|
||||||
|
off2 % 75
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut p = Paranoia::new(drive);
|
||||||
|
p.modeset(args.paranoia_mode);
|
||||||
|
if let Some(overlap) = args.force_overlap {
|
||||||
|
p.overlapset(overlap as i64);
|
||||||
|
}
|
||||||
|
p.seek(first_sector, 0);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::seteuid(libc::getuid());
|
||||||
|
libc::setegid(libc::getgid());
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample_offset != 0 {
|
||||||
|
let tracks = p.drive().tracks() as usize;
|
||||||
|
if let Some(e) = p.drive_mut().toc_mut().entries.get_mut(tracks) {
|
||||||
|
e.start_sector += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let host_big = host_big_endian();
|
||||||
|
let target_big = args.output_format.output_big_endian(host_big);
|
||||||
|
let mut offset_buffer = [0u8; 2352];
|
||||||
|
let mut offset_buffer_used = 0usize;
|
||||||
|
let mut offset_skip = (sample_offset * 4) as usize;
|
||||||
|
let mut cursor = first_sector;
|
||||||
|
let mut progress = Progress::default();
|
||||||
|
progress.callscript = args.stderr_progress;
|
||||||
|
progress.abort_on_skip = args.abort_on_skip;
|
||||||
|
|
||||||
|
while cursor <= last_sector {
|
||||||
|
let (batch_first, batch_last, batch_track) = if args.batch {
|
||||||
|
let bt = p.drive().sector_gettrack(cursor);
|
||||||
|
if bt < 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut bl = p.drive().track_lastsector(bt);
|
||||||
|
if bl > last_sector {
|
||||||
|
bl = last_sector;
|
||||||
|
}
|
||||||
|
(cursor, bl, bt)
|
||||||
|
} else {
|
||||||
|
(first_sector, last_sector, -1)
|
||||||
|
};
|
||||||
|
|
||||||
|
progress.callbegin = batch_first;
|
||||||
|
progress.callend = batch_last;
|
||||||
|
progress.reset_batch();
|
||||||
|
|
||||||
|
let template_stdout = args.outfile.as_deref() == Some("-");
|
||||||
|
let outfile_name =
|
||||||
|
make_output_name(&args.outfile, args.batch, batch_track, args.output_format);
|
||||||
|
if template_stdout {
|
||||||
|
if args.batch {
|
||||||
|
log.report(format_args!(
|
||||||
|
"Are you sure you wanted 'batch' (-B) output with stdout?"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
log.report(format_args!("outputting to stdout"));
|
||||||
|
} else {
|
||||||
|
log.report(format_args!(
|
||||||
|
"outputting to {}",
|
||||||
|
outfile_name.as_deref().unwrap_or("")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(name) = &outfile_name {
|
||||||
|
log.summary(format_args!("outputting to {}", name));
|
||||||
|
} else {
|
||||||
|
log.summary(format_args!("outputting to stdout"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = match open_output(outfile_name.as_deref(), template_stdout) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
log.report(format_args!("{}", e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut out = BufferedWriter::new(file);
|
||||||
|
let bytes = ((batch_last - batch_first + 1) * 2352) as u32;
|
||||||
|
if let Err(e) = write_header(&mut out, args.output_format, bytes) {
|
||||||
|
log.report(format_args!("Error writing output: {}", e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset_buffer_used > 0 {
|
||||||
|
cursor += 1;
|
||||||
|
if let Err(e) = out.write_all(&offset_buffer[offset_buffer_used..]) {
|
||||||
|
log.report(format_args!("Error writing output: {}", e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.skipped_flag = false;
|
||||||
|
while cursor <= batch_last {
|
||||||
|
let sector = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
progress.skipped_flag = true;
|
||||||
|
log.report(format_args!(
|
||||||
|
"\nparanoia_read: Unrecoverable error, bailing."
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if progress.skipped_flag && args.abort_on_skip {
|
||||||
|
cursor = batch_last + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
progress.skipped_flag = false;
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
let mut data = sector;
|
||||||
|
if target_big != host_big {
|
||||||
|
for s in &mut data {
|
||||||
|
*s = swap16(*s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress.callback(cursor * 1176 - 1, -2, &mut log);
|
||||||
|
|
||||||
|
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 sample_offset != 0 && cursor > batch_last {
|
||||||
|
let extra = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
progress.skipped_flag = true;
|
||||||
|
log.report(format_args!(
|
||||||
|
"\nparanoia_read: Unrecoverable error reading through \n\
|
||||||
|
\t\t\tsample_offset shift\n\tat end of track, bailing."
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if progress.skipped_flag && args.abort_on_skip {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
progress.skipped_flag = false;
|
||||||
|
let mut extra = extra;
|
||||||
|
if target_big != host_big {
|
||||||
|
for s in &mut extra {
|
||||||
|
*s = swap16(*s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let eb = samples_as_bytes(&extra);
|
||||||
|
offset_buffer[..eb.len().min(2352)].copy_from_slice(&eb[..eb.len().min(2352)]);
|
||||||
|
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]) {
|
||||||
|
log.report(format_args!("Error writing output: {}", e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.callback(cursor * 1176 - 1, -1, &mut log);
|
||||||
|
if let Err(e) = out.close() {
|
||||||
|
log.report(format_args!("Error writing output: {}", e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if progress.skipped_flag {
|
||||||
|
if let Some(name) = &outfile_name {
|
||||||
|
log.report(format_args!("\nRemoving aborted file: {}", name));
|
||||||
|
let _ = std::fs::remove_file(name);
|
||||||
|
}
|
||||||
|
if batch_track != -1 {
|
||||||
|
let next_track = batch_track + 1;
|
||||||
|
let next = p.drive().track_firstsector(next_track);
|
||||||
|
if next >= 0 {
|
||||||
|
cursor = next;
|
||||||
|
p.seek(cursor, 0);
|
||||||
|
} else {
|
||||||
|
cursor = last_sector + 1;
|
||||||
|
}
|
||||||
|
offset_skip = (sample_offset * 4) as usize;
|
||||||
|
offset_buffer_used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.report(format_args!("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.report(format_args!("Done.\n\n"));
|
||||||
|
log.flush();
|
||||||
|
0
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//! Report/log destination handling, matching report.h's macros plus the
|
||||||
|
//! separate summary and debug streams from main.c.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
pub struct Log {
|
||||||
|
pub quiet: bool,
|
||||||
|
pub summary_file: Option<Box<dyn Write>>,
|
||||||
|
pub report_file: Option<Box<dyn Write>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Log {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Log {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
quiet: false,
|
||||||
|
summary_file: None,
|
||||||
|
report_file: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_summary_writer(&mut self, writer: Box<dyn Write>) {
|
||||||
|
self.summary_file = Some(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_report_writer(&mut self, writer: Box<dyn Write>) {
|
||||||
|
self.report_file = Some(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report(&mut self, args: std::fmt::Arguments<'_>) {
|
||||||
|
if !self.quiet {
|
||||||
|
let _ = writeln!(io::stderr(), "{}", args);
|
||||||
|
}
|
||||||
|
if let Some(f) = self.report_file.as_mut() {
|
||||||
|
let _ = writeln!(f, "{}", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_c(&mut self, args: std::fmt::Arguments<'_>) {
|
||||||
|
if !self.quiet {
|
||||||
|
let _ = write!(io::stderr(), "{}", args);
|
||||||
|
}
|
||||||
|
if let Some(f) = self.report_file.as_mut() {
|
||||||
|
let _ = write!(f, "{}", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_c(&mut self, args: std::fmt::Arguments<'_>) {
|
||||||
|
if !self.quiet {
|
||||||
|
let _ = write!(io::stderr(), "{}", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_c(&mut self, args: std::fmt::Arguments<'_>) {
|
||||||
|
if let Some(f) = self.report_file.as_mut() {
|
||||||
|
let _ = write!(f, "{}", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summary(&mut self, args: std::fmt::Arguments<'_>) {
|
||||||
|
if let Some(f) = self.summary_file.as_mut() {
|
||||||
|
let _ = writeln!(f, "{}", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_summary_bytes(&mut self, data: &[u8]) {
|
||||||
|
if let Some(f) = self.summary_file.as_mut() {
|
||||||
|
let _ = f.write_all(data);
|
||||||
|
let _ = f.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) {
|
||||||
|
if let Some(f) = self.summary_file.as_mut() {
|
||||||
|
let _ = f.flush();
|
||||||
|
}
|
||||||
|
if let Some(f) = self.report_file.as_mut() {
|
||||||
|
let _ = f.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! report {
|
||||||
|
($log:expr, $($arg:tt)*) => {
|
||||||
|
$log.report(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! report_c {
|
||||||
|
($log:expr, $($arg:tt)*) => {
|
||||||
|
$log.report_c(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! print_c {
|
||||||
|
($log:expr, $($arg:tt)*) => {
|
||||||
|
$log.print_c(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_c {
|
||||||
|
($log:expr, $($arg:tt)*) => {
|
||||||
|
$log.log_c(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! summary {
|
||||||
|
($log:expr, $($arg:tt)*) => {
|
||||||
|
$log.summary(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fn main() {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
std::process::exit(cdrustanoia_cli::run(&argv));
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
//! WAV/AIFF/AIFC headers and buffered output, ported from header.c and
|
||||||
|
//! buffering_write.c.
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
pub const OUTBUF_SZ: usize = 32 * 1024;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum OutputFormat {
|
||||||
|
RawHost,
|
||||||
|
RawLittle,
|
||||||
|
RawBig,
|
||||||
|
Wav,
|
||||||
|
Aiff,
|
||||||
|
Aifc,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputFormat {
|
||||||
|
pub fn default_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "cdda.raw",
|
||||||
|
OutputFormat::Wav => "cdda.wav",
|
||||||
|
OutputFormat::Aifc => "cdda.aifc",
|
||||||
|
OutputFormat::Aiff => "cdda.aiff",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "raw",
|
||||||
|
OutputFormat::Wav => "wav",
|
||||||
|
OutputFormat::Aifc => "aifc",
|
||||||
|
OutputFormat::Aiff => "aiff",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_big_endian(self, host_big: bool) -> bool {
|
||||||
|
match self {
|
||||||
|
OutputFormat::RawHost => host_big,
|
||||||
|
OutputFormat::RawLittle | OutputFormat::Wav => false,
|
||||||
|
OutputFormat::RawBig | OutputFormat::Aiff | OutputFormat::Aifc => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put_num<W: Write>(w: &mut W, num: u32, big_endian: bool, bytes: usize) -> io::Result<()> {
|
||||||
|
let mut tmp = [0u8; 4];
|
||||||
|
if big_endian {
|
||||||
|
let b = num.to_be_bytes();
|
||||||
|
for (i, slot) in tmp.iter_mut().take(bytes).enumerate() {
|
||||||
|
*slot = b[4 - bytes + i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let b = num.to_le_bytes();
|
||||||
|
tmp[..bytes].copy_from_slice(&b[..bytes]);
|
||||||
|
}
|
||||||
|
w.write_all(&tmp[..bytes])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_wav<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
|
||||||
|
w.write_all(b"RIFF")?;
|
||||||
|
put_num(w, bytes.wrapping_add(44).wrapping_sub(8), false, 4)?;
|
||||||
|
w.write_all(b"WAVEfmt ")?;
|
||||||
|
put_num(w, 16, false, 4)?;
|
||||||
|
put_num(w, 1, false, 2)?;
|
||||||
|
put_num(w, 2, false, 2)?;
|
||||||
|
put_num(w, 44100, false, 4)?;
|
||||||
|
put_num(w, 44100 * 2 * 2, false, 4)?;
|
||||||
|
put_num(w, 4, false, 2)?;
|
||||||
|
put_num(w, 16, false, 2)?;
|
||||||
|
w.write_all(b"data")?;
|
||||||
|
put_num(w, bytes, false, 4)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_aiff<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
|
||||||
|
let frames = bytes / 4;
|
||||||
|
w.write_all(b"FORM")?;
|
||||||
|
put_num(w, bytes.wrapping_add(54).wrapping_sub(8), true, 4)?;
|
||||||
|
w.write_all(b"AIFF")?;
|
||||||
|
w.write_all(b"COMM")?;
|
||||||
|
put_num(w, 18, true, 4)?;
|
||||||
|
put_num(w, 2, true, 2)?;
|
||||||
|
put_num(w, frames, true, 4)?;
|
||||||
|
put_num(w, 16, true, 2)?;
|
||||||
|
w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?;
|
||||||
|
w.write_all(b"SSND")?;
|
||||||
|
put_num(w, bytes.wrapping_add(8), true, 4)?;
|
||||||
|
put_num(w, 0, true, 4)?;
|
||||||
|
put_num(w, 0, true, 4)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_aifc<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
|
||||||
|
let frames = bytes / 4;
|
||||||
|
w.write_all(b"FORM")?;
|
||||||
|
put_num(w, bytes.wrapping_add(86).wrapping_sub(8), true, 4)?;
|
||||||
|
w.write_all(b"AIFC")?;
|
||||||
|
w.write_all(b"FVER")?;
|
||||||
|
put_num(w, 4, true, 4)?;
|
||||||
|
put_num(w, 2726318400, true, 4)?;
|
||||||
|
w.write_all(b"COMM")?;
|
||||||
|
put_num(w, 38, true, 4)?;
|
||||||
|
put_num(w, 2, true, 2)?;
|
||||||
|
put_num(w, frames, true, 4)?;
|
||||||
|
put_num(w, 16, true, 2)?;
|
||||||
|
w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?;
|
||||||
|
w.write_all(b"NONE")?;
|
||||||
|
put_num(w, 14, true, 1)?;
|
||||||
|
w.write_all(b"not compressed")?;
|
||||||
|
put_num(w, 0, true, 1)?;
|
||||||
|
w.write_all(b"SSND")?;
|
||||||
|
put_num(w, bytes.wrapping_add(8), true, 4)?;
|
||||||
|
put_num(w, 0, true, 4)?;
|
||||||
|
put_num(w, 0, true, 4)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_header<W: Write>(w: &mut W, format: OutputFormat, bytes: u32) -> io::Result<()> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Wav => write_wav(w, bytes),
|
||||||
|
OutputFormat::Aiff => write_aiff(w, bytes),
|
||||||
|
OutputFormat::Aifc => write_aifc(w, bytes),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Buffered writer matching the original's 32 KiB write coalescing.
|
||||||
|
pub struct BufferedWriter {
|
||||||
|
file: File,
|
||||||
|
buf: Vec<u8>,
|
||||||
|
closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferedWriter {
|
||||||
|
pub fn new(file: File) -> Self {
|
||||||
|
Self {
|
||||||
|
file,
|
||||||
|
buf: Vec::with_capacity(OUTBUF_SZ),
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_all(&mut self, mut data: &[u8]) -> io::Result<()> {
|
||||||
|
while !data.is_empty() {
|
||||||
|
let space = OUTBUF_SZ.saturating_sub(self.buf.len());
|
||||||
|
if space == 0 {
|
||||||
|
self.flush_buf()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let n = space.min(data.len());
|
||||||
|
self.buf.extend_from_slice(&data[..n]);
|
||||||
|
data = &data[n..];
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_buf(&mut self) -> io::Result<()> {
|
||||||
|
if !self.buf.is_empty() {
|
||||||
|
self.file.write_all(&self.buf)?;
|
||||||
|
self.buf.clear();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.flush_buf()?;
|
||||||
|
self.file.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(mut self) -> io::Result<()> {
|
||||||
|
self.closed = true;
|
||||||
|
self.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for BufferedWriter {
|
||||||
|
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
|
||||||
|
self.write_all(data)?;
|
||||||
|
Ok(data.len())
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
BufferedWriter::flush(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BufferedWriter {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.closed {
|
||||||
|
let _ = self.flush_buf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wav_header_size() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
write_wav(&mut v, 2352).unwrap();
|
||||||
|
assert_eq!(v.len(), 44);
|
||||||
|
assert_eq!(&v[..4], b"RIFF");
|
||||||
|
assert_eq!(&v[8..16], b"WAVEfmt ");
|
||||||
|
assert_eq!(&v[36..40], b"data");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aiff_header_size() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
write_aiff(&mut v, 2352).unwrap();
|
||||||
|
assert_eq!(v.len(), 54);
|
||||||
|
assert_eq!(&v[..4], b"FORM");
|
||||||
|
assert_eq!(&v[8..12], b"AIFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aifc_header_size() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
write_aifc(&mut v, 2352).unwrap();
|
||||||
|
assert_eq!(v.len(), 86);
|
||||||
|
assert_eq!(&v[8..12], b"AIFC");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
//! Callback and progress display, ported from the callback in main.c.
|
||||||
|
|
||||||
|
use crate::log::Log;
|
||||||
|
use cdrustanoia_core::callback::*;
|
||||||
|
use cdrustanoia_core::device::CD_FRAMEWORDS;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub const CALLBACK_STRINGS: [&str; 16] = [
|
||||||
|
"wrote",
|
||||||
|
"finished",
|
||||||
|
"read",
|
||||||
|
"verify",
|
||||||
|
"jitter",
|
||||||
|
"correction",
|
||||||
|
"scratch",
|
||||||
|
"scratch repair",
|
||||||
|
"skip",
|
||||||
|
"drift",
|
||||||
|
"backoff",
|
||||||
|
"overlap",
|
||||||
|
"dropped",
|
||||||
|
"duped",
|
||||||
|
"transport error",
|
||||||
|
"cache error",
|
||||||
|
];
|
||||||
|
|
||||||
|
const GRAPH: usize = 30;
|
||||||
|
|
||||||
|
pub struct Progress {
|
||||||
|
pub callbegin: i64,
|
||||||
|
pub callend: i64,
|
||||||
|
pub callscript: bool,
|
||||||
|
pub abort_on_skip: bool,
|
||||||
|
pub skipped_flag: bool,
|
||||||
|
c_sector: i64,
|
||||||
|
v_sector: i64,
|
||||||
|
dispcache: [u8; GRAPH],
|
||||||
|
last: i32,
|
||||||
|
lasttime: i64,
|
||||||
|
heartbeat: u8,
|
||||||
|
slevel: i32,
|
||||||
|
slast: i32,
|
||||||
|
stimeout: i32,
|
||||||
|
cacheerr: i32,
|
||||||
|
overlap: i32,
|
||||||
|
printit: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Progress {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
callbegin: 0,
|
||||||
|
callend: 0,
|
||||||
|
callscript: false,
|
||||||
|
abort_on_skip: false,
|
||||||
|
skipped_flag: false,
|
||||||
|
c_sector: 0,
|
||||||
|
v_sector: 0,
|
||||||
|
dispcache: [b' '; GRAPH],
|
||||||
|
last: 0,
|
||||||
|
lasttime: 0,
|
||||||
|
heartbeat: b' ',
|
||||||
|
slevel: 0,
|
||||||
|
slast: 0,
|
||||||
|
stimeout: 0,
|
||||||
|
cacheerr: 0,
|
||||||
|
overlap: 0,
|
||||||
|
printit: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Progress {
|
||||||
|
pub fn reset_batch(&mut self) {
|
||||||
|
self.c_sector = 0;
|
||||||
|
self.v_sector = 0;
|
||||||
|
self.dispcache = [b' '; GRAPH];
|
||||||
|
self.last = 0;
|
||||||
|
self.lasttime = 0;
|
||||||
|
self.heartbeat = b' ';
|
||||||
|
self.slevel = 0;
|
||||||
|
self.slast = 0;
|
||||||
|
self.stimeout = 0;
|
||||||
|
self.overlap = 0;
|
||||||
|
self.skipped_flag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_tenths() -> i64 {
|
||||||
|
match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(d) => d.as_secs() as i64 * 10 + (d.subsec_micros() / 100_000) as i64,
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callback(&mut self, inpos: i64, function: i32, log: &mut Log) {
|
||||||
|
if self.callscript {
|
||||||
|
let name = if (-2..=13).contains(&function) {
|
||||||
|
CALLBACK_STRINGS[(function + 2) as usize]
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let _ = writeln!(io::stderr(), "##: {} [{}] @ {}", function, name, inpos);
|
||||||
|
} else if function == PARANOIA_CB_CACHEERR {
|
||||||
|
if self.cacheerr == 0 {
|
||||||
|
let _ = write!(
|
||||||
|
io::stderr(),
|
||||||
|
"\rWARNING: The CDROM drive appears to be seeking impossibly quickly.\n\
|
||||||
|
This could be due to timer bugs, a drive that really is improbably fast,\n\
|
||||||
|
or, most likely, a bug in cdparanoia's cache modelling.\n\n\
|
||||||
|
Please consider using the -A option to perform an analysis run, then mail\n\
|
||||||
|
the cdparanoia.log file produced by the analysis to paranoia-dev@xiph.org\n\
|
||||||
|
to assist developers in correcting the problem.\n\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.cacheerr += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if function == PARANOIA_CB_SKIP {
|
||||||
|
// Keep -X functional even when progress is redirected or quiet.
|
||||||
|
self.skipped_flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.quiet {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let osector = inpos;
|
||||||
|
let sector = inpos / CD_FRAMEWORDS;
|
||||||
|
|
||||||
|
if self.printit == -1 {
|
||||||
|
self.printit = if unsafe { libc::isatty(2) } == 1 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.printit == 1 {
|
||||||
|
let position = if self.callend != self.callbegin {
|
||||||
|
((sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64
|
||||||
|
* GRAPH as f64) as i32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let aheadposition = if self.callend != self.callbegin {
|
||||||
|
((self.c_sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64
|
||||||
|
* GRAPH as f64) as i32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if function == -2 {
|
||||||
|
self.v_sector = sector;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if function == -1 {
|
||||||
|
self.last = 8;
|
||||||
|
self.heartbeat = b'*';
|
||||||
|
self.slevel = 0;
|
||||||
|
self.v_sector = sector;
|
||||||
|
} else if position < GRAPH as i32 && position >= 0 {
|
||||||
|
let p = position as usize;
|
||||||
|
match function {
|
||||||
|
PARANOIA_CB_VERIFY => {
|
||||||
|
if self.stimeout >= 30 {
|
||||||
|
if self.overlap as i64 > CD_FRAMEWORDS {
|
||||||
|
self.slevel = 2;
|
||||||
|
} else {
|
||||||
|
self.slevel = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_READ => {
|
||||||
|
if sector > self.c_sector {
|
||||||
|
self.c_sector = sector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_FIXUP_EDGE => {
|
||||||
|
if self.stimeout >= 5 {
|
||||||
|
if self.overlap as i64 > CD_FRAMEWORDS {
|
||||||
|
self.slevel = 2;
|
||||||
|
} else {
|
||||||
|
self.slevel = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.dispcache[p] == b' ' {
|
||||||
|
self.dispcache[p] = b'-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_FIXUP_ATOM => {
|
||||||
|
if self.slevel < 3 || self.stimeout > 5 {
|
||||||
|
self.slevel = 3;
|
||||||
|
}
|
||||||
|
if self.dispcache[p] == b' ' || self.dispcache[p] == b'-' {
|
||||||
|
self.dispcache[p] = b'+';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_READERR => {
|
||||||
|
self.slevel = 6;
|
||||||
|
if self.dispcache[p] != b'V' && self.dispcache[p] != b'C' {
|
||||||
|
self.dispcache[p] = b'e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_CACHEERR => {
|
||||||
|
self.slevel = 8;
|
||||||
|
self.dispcache[p] = b'C';
|
||||||
|
}
|
||||||
|
PARANOIA_CB_SKIP => {
|
||||||
|
self.slevel = 8;
|
||||||
|
if self.dispcache[p] != b'C' {
|
||||||
|
self.dispcache[p] = b'V';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_OVERLAP => {
|
||||||
|
self.overlap = osector as i32;
|
||||||
|
}
|
||||||
|
PARANOIA_CB_SCRATCH => {
|
||||||
|
self.slevel = 7;
|
||||||
|
}
|
||||||
|
PARANOIA_CB_DRIFT => {
|
||||||
|
if self.slevel < 4 || self.stimeout > 5 {
|
||||||
|
self.slevel = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARANOIA_CB_FIXUP_DROPPED | PARANOIA_CB_FIXUP_DUPED => {
|
||||||
|
self.slevel = 5;
|
||||||
|
if matches!(self.dispcache[p], b' ' | b'-' | b'+') {
|
||||||
|
self.dispcache[p] = b'!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut smilie = "= :-)";
|
||||||
|
match self.slevel {
|
||||||
|
0 => {
|
||||||
|
smilie = if self.skipped_flag { " 8-X" } else { " :^D" };
|
||||||
|
}
|
||||||
|
1 => smilie = " :-)",
|
||||||
|
2 => smilie = " :-|",
|
||||||
|
3 => smilie = " :-P",
|
||||||
|
4 => smilie = " :-/",
|
||||||
|
5 => smilie = " 8-|",
|
||||||
|
6 => smilie = " :-0",
|
||||||
|
7 => smilie = " :-(",
|
||||||
|
8 => {
|
||||||
|
smilie = " ;-(";
|
||||||
|
self.skipped_flag = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let test = Self::now_tenths();
|
||||||
|
if self.lasttime != test || function == -1 || self.slast != self.slevel {
|
||||||
|
if self.lasttime != test || function == -1 {
|
||||||
|
self.last += 1;
|
||||||
|
self.lasttime = test;
|
||||||
|
if self.last > 7 {
|
||||||
|
self.last = 0;
|
||||||
|
}
|
||||||
|
self.stimeout += 1;
|
||||||
|
self.heartbeat = match self.last {
|
||||||
|
0 => b' ',
|
||||||
|
1 | 7 => b'.',
|
||||||
|
2 | 6 => b'o',
|
||||||
|
3 | 5 => b'0',
|
||||||
|
4 => b'O',
|
||||||
|
_ => self.heartbeat,
|
||||||
|
};
|
||||||
|
if function == -1 {
|
||||||
|
self.heartbeat = b'*';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.slast != self.slevel {
|
||||||
|
self.stimeout = 0;
|
||||||
|
}
|
||||||
|
self.slast = self.slevel;
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> =
|
||||||
|
if self.abort_on_skip && self.skipped_flag && function != -1 {
|
||||||
|
format!(
|
||||||
|
"\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ",
|
||||||
|
" ...aborting; please wait... ",
|
||||||
|
self.v_sector,
|
||||||
|
self.overlap as i64 / CD_FRAMEWORDS,
|
||||||
|
smilie,
|
||||||
|
self.heartbeat as char
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
} else {
|
||||||
|
let disp = String::from_utf8_lossy(&self.dispcache).to_string();
|
||||||
|
if self.v_sector == 0 {
|
||||||
|
format!(
|
||||||
|
"\r (== PROGRESS == [{}| ...... {:02} ] =={} {} ==) ",
|
||||||
|
disp,
|
||||||
|
self.overlap as i64 / CD_FRAMEWORDS,
|
||||||
|
smilie,
|
||||||
|
self.heartbeat as char
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ",
|
||||||
|
disp,
|
||||||
|
self.v_sector,
|
||||||
|
self.overlap as i64 / CD_FRAMEWORDS,
|
||||||
|
smilie,
|
||||||
|
self.heartbeat as char
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if aheadposition >= 0
|
||||||
|
&& aheadposition < GRAPH as i32
|
||||||
|
&& function != -1
|
||||||
|
&& buffer.len() > 19 + aheadposition as usize
|
||||||
|
{
|
||||||
|
buffer[19 + aheadposition as usize] = b'>';
|
||||||
|
}
|
||||||
|
let _ = io::stderr().write_all(&buffer);
|
||||||
|
|
||||||
|
if function == -1 {
|
||||||
|
let mut report_bytes = buffer[1..].to_vec();
|
||||||
|
report_bytes.extend_from_slice(b"\n\n");
|
||||||
|
log.write_summary_bytes(&report_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if function == -1 {
|
||||||
|
self.dispcache = [b' '; GRAPH];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skip_flag_survives_redirected_output() {
|
||||||
|
let mut p = Progress::default();
|
||||||
|
let mut log = Log::new();
|
||||||
|
log.quiet = true;
|
||||||
|
p.callback(0, PARANOIA_CB_SKIP, &mut log);
|
||||||
|
assert!(p.skipped_flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
//! Span argument parsing, a direct port of `parse_offset()` from main.c.
|
||||||
|
|
||||||
|
use cdrustanoia_core::device::DiscToc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SpanError(pub String);
|
||||||
|
|
||||||
|
impl std::fmt::Display for SpanError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for SpanError {}
|
||||||
|
|
||||||
|
fn err(msg: impl Into<String>) -> SpanError {
|
||||||
|
SpanError(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
struct TimeParts {
|
||||||
|
hours: Option<i64>,
|
||||||
|
minutes: Option<i64>,
|
||||||
|
seconds: Option<i64>,
|
||||||
|
sectors: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_num(s: &str, original: &str) -> Result<i64, SpanError> {
|
||||||
|
let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||||
|
if digits.is_empty() {
|
||||||
|
Ok(0)
|
||||||
|
} else {
|
||||||
|
digits
|
||||||
|
.parse::<i64>()
|
||||||
|
.map_err(|_| err(format!("Error parsing span argument: {original}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_time(time: &str) -> Result<TimeParts, SpanError> {
|
||||||
|
let mut parts = TimeParts::default();
|
||||||
|
let mut rest = time;
|
||||||
|
|
||||||
|
if let Some(pos) = rest.rfind('.') {
|
||||||
|
parts.sectors = Some(parse_num(&rest[pos + 1..], time)?);
|
||||||
|
rest = &rest[..pos];
|
||||||
|
if rest.contains('.') {
|
||||||
|
return Err(err("Error parsing span argument"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fields: Vec<&str> = rest.split(':').collect();
|
||||||
|
if fields.len() > 3 {
|
||||||
|
return Err(err("Error parsing span argument"));
|
||||||
|
}
|
||||||
|
// The original walks right-to-left: rightmost field is seconds, then
|
||||||
|
// minutes, then hours.
|
||||||
|
match fields.len() {
|
||||||
|
1 => {
|
||||||
|
parts.seconds = Some(parse_num(fields[0], time)?);
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
parts.seconds = Some(parse_num(fields[1], time)?);
|
||||||
|
parts.minutes = Some(parse_num(fields[0], time)?);
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
parts.seconds = Some(parse_num(fields[2], time)?);
|
||||||
|
parts.minutes = Some(parse_num(fields[1], time)?);
|
||||||
|
parts.hours = Some(parse_num(fields[0], time)?);
|
||||||
|
}
|
||||||
|
_ => return Err(err("Error parsing span argument")),
|
||||||
|
}
|
||||||
|
Ok(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_time(parts: &TimeParts) -> bool {
|
||||||
|
parts.seconds.is_some() || parts.sectors.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port of `parse_offset`. `begin` is the beginning of a span, if known.
|
||||||
|
pub fn parse_offset(
|
||||||
|
toc: &DiscToc,
|
||||||
|
offset: Option<&str>,
|
||||||
|
begin: Option<i64>,
|
||||||
|
) -> Result<i64, SpanError> {
|
||||||
|
let offset = match offset {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return Ok(-1),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (track_text, time_text) = if let Some(close) = offset.find(']') {
|
||||||
|
let before = &offset[..close];
|
||||||
|
if let Some(open) = before.find('[') {
|
||||||
|
(&before[..open], Some(&before[open + 1..]))
|
||||||
|
} else {
|
||||||
|
return Err(err("Error parsing span argument"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(offset, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let digit_count = track_text
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| c.is_ascii_digit())
|
||||||
|
.count();
|
||||||
|
let track = if digit_count > 0 {
|
||||||
|
let t = track_text[..digit_count]
|
||||||
|
.parse::<i64>()
|
||||||
|
.map_err(|_| err("Error parsing span argument"))?;
|
||||||
|
if t < 0 || t > toc.tracks as i64 {
|
||||||
|
return Err(err(format!("Track #{t} does not exist.")));
|
||||||
|
}
|
||||||
|
Some(t)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts = match time_text {
|
||||||
|
Some(t) if !t.is_empty() => parse_time(t)?,
|
||||||
|
Some(_) => TimeParts::default(),
|
||||||
|
None => TimeParts::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ret = match track {
|
||||||
|
None => {
|
||||||
|
if !has_time(&parts) {
|
||||||
|
return Ok(-1);
|
||||||
|
}
|
||||||
|
begin.unwrap_or_else(|| toc.disc_firstsector())
|
||||||
|
}
|
||||||
|
Some(t) => {
|
||||||
|
if !has_time(&parts) {
|
||||||
|
if begin.is_none() {
|
||||||
|
return Ok(toc.track_firstsector(t as i32));
|
||||||
|
}
|
||||||
|
return Ok(toc.track_lastsector(t as i32));
|
||||||
|
}
|
||||||
|
toc.track_firstsector(t as i32)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ret = ret;
|
||||||
|
let mut add = |v: i64, scale: i64| -> Result<(), SpanError> {
|
||||||
|
ret = ret
|
||||||
|
.checked_add(
|
||||||
|
v.checked_mul(scale)
|
||||||
|
.ok_or_else(|| err("Error parsing span argument"))?,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| err("Error parsing span argument"))?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
if let Some(v) = parts.sectors {
|
||||||
|
add(v, 1)?;
|
||||||
|
}
|
||||||
|
if let Some(v) = parts.seconds {
|
||||||
|
add(v, 75)?;
|
||||||
|
}
|
||||||
|
if let Some(v) = parts.minutes {
|
||||||
|
add(v, 60 * 75)?;
|
||||||
|
}
|
||||||
|
if let Some(v) = parts.hours {
|
||||||
|
add(v, 60 * 60 * 75)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(t) = track {
|
||||||
|
if toc.sector_gettrack(ret) != t as i32 {
|
||||||
|
return Err(err(
|
||||||
|
"Time/sector offset goes beyond end of specified track.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ret > toc.disc_lastsector() {
|
||||||
|
return Err(err("Time/sector offset goes beyond end of disc."));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a complete span expression, returning inclusive first/last sectors.
|
||||||
|
pub fn parse_span(toc: &DiscToc, span: &str) -> Result<(i64, i64), SpanError> {
|
||||||
|
let first_hyphen = span.find('-');
|
||||||
|
let last_hyphen = span.rfind('-');
|
||||||
|
if first_hyphen != last_hyphen {
|
||||||
|
return Err(err("Error parsing span argument"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (part1, part2, had_hyphen) = match first_hyphen {
|
||||||
|
Some(pos) => (&span[..pos], Some(&span[pos + 1..]), true),
|
||||||
|
None => (span, None, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut first = parse_offset(toc, Some(part1), None)?;
|
||||||
|
let mut last = if first == -1 {
|
||||||
|
parse_offset(toc, part2, Some(toc.disc_firstsector()))?
|
||||||
|
} else {
|
||||||
|
parse_offset(toc, part2, Some(first))?
|
||||||
|
};
|
||||||
|
|
||||||
|
if first == -1 {
|
||||||
|
if last == -1 {
|
||||||
|
return Err(err("Error parsing span argument"));
|
||||||
|
}
|
||||||
|
first = toc.disc_firstsector();
|
||||||
|
} else if last == -1 {
|
||||||
|
if had_hyphen {
|
||||||
|
last = toc.disc_lastsector();
|
||||||
|
} else {
|
||||||
|
let track = toc.sector_gettrack(first);
|
||||||
|
last = toc.track_lastsector(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((first, last))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cdrustanoia_core::device::TocEntry;
|
||||||
|
|
||||||
|
fn toc() -> DiscToc {
|
||||||
|
let mut t = DiscToc::default();
|
||||||
|
t.entries[0] = TocEntry {
|
||||||
|
flags: 0,
|
||||||
|
track: 1,
|
||||||
|
start_sector: 0,
|
||||||
|
};
|
||||||
|
t.entries[1] = TocEntry {
|
||||||
|
flags: 0,
|
||||||
|
track: 2,
|
||||||
|
start_sector: 75000,
|
||||||
|
};
|
||||||
|
t.entries[2] = TocEntry {
|
||||||
|
flags: 0,
|
||||||
|
track: 3,
|
||||||
|
start_sector: 150000,
|
||||||
|
};
|
||||||
|
t.entries[3] = TocEntry {
|
||||||
|
flags: 0x04,
|
||||||
|
track: 0xAA,
|
||||||
|
start_sector: 225000,
|
||||||
|
};
|
||||||
|
t.tracks = 3;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_track() {
|
||||||
|
assert_eq!(parse_span(&toc(), "2").unwrap(), (75000, 149999));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ranges() {
|
||||||
|
assert_eq!(parse_span(&toc(), "2-3").unwrap(), (75000, 224999));
|
||||||
|
assert_eq!(parse_span(&toc(), "-2").unwrap(), (0, 149999));
|
||||||
|
assert_eq!(parse_span(&toc(), "2-").unwrap(), (75000, 224999));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn times() {
|
||||||
|
assert_eq!(parse_span(&toc(), "1[1:00]").unwrap(), (4500, 74999));
|
||||||
|
assert_eq!(parse_span(&toc(), "2[::20]").unwrap(), (76500, 149999));
|
||||||
|
assert_eq!(parse_span(&toc(), "2[.30]").unwrap(), (75030, 149999));
|
||||||
|
assert_eq!(parse_span(&toc(), "2[10:]").unwrap(), (120000, 149999));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad() {
|
||||||
|
assert!(parse_span(&toc(), "4").is_err());
|
||||||
|
assert!(parse_span(&toc(), "1-2-3").is_err());
|
||||||
|
assert!(parse_span(&toc(), "2[1:00:00:00]").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use cdrustanoia_core::device::samples_to_bytes;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn end_to_end_image_rip() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let image = dir.path().join("disc.raw");
|
||||||
|
let out = dir.path().join("out.raw");
|
||||||
|
let sectors = 24usize;
|
||||||
|
let mut samples = Vec::with_capacity(sectors * 1176);
|
||||||
|
for i in 0..sectors * 1176 {
|
||||||
|
samples.push(((i as i32 * 7919) % 65536 - 32768) as i16);
|
||||||
|
}
|
||||||
|
std::fs::write(&image, samples_to_bytes(&samples)).unwrap();
|
||||||
|
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &image);
|
||||||
|
|
||||||
|
let argv = vec![
|
||||||
|
"cdparanoia".to_string(),
|
||||||
|
"-q".to_string(),
|
||||||
|
"-r".to_string(),
|
||||||
|
"1".to_string(),
|
||||||
|
out.to_string_lossy().into_owned(),
|
||||||
|
];
|
||||||
|
assert_eq!(cdrustanoia_cli::run(&argv), 0);
|
||||||
|
let got = std::fs::read(&out).unwrap();
|
||||||
|
assert_eq!(got, samples_to_bytes(&samples));
|
||||||
|
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
|
||||||
|
|
||||||
|
// Multi-track batch mode.
|
||||||
|
let multi = dir.path().join("multi.raw");
|
||||||
|
let mut multi_samples = Vec::new();
|
||||||
|
for i in 0..(48 * 1176) {
|
||||||
|
multi_samples.push((((i as i64 * 104729) % 65536) as i32 - 32768) as i16);
|
||||||
|
}
|
||||||
|
std::fs::write(&multi, samples_to_bytes(&multi_samples)).unwrap();
|
||||||
|
std::fs::write(dir.path().join("multi.raw.toc"), "0 23 0\n24 47 0\n").unwrap();
|
||||||
|
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &multi);
|
||||||
|
let argv = vec![
|
||||||
|
"cdparanoia".to_string(),
|
||||||
|
"-q".to_string(),
|
||||||
|
"-B".to_string(),
|
||||||
|
"1-2".to_string(),
|
||||||
|
format!("{}/", dir.path().display()),
|
||||||
|
];
|
||||||
|
assert_eq!(cdrustanoia_cli::run(&argv), 0);
|
||||||
|
let t1 = std::fs::read(dir.path().join("track01.cdda.wav")).unwrap();
|
||||||
|
let t2 = std::fs::read(dir.path().join("track02.cdda.wav")).unwrap();
|
||||||
|
assert_eq!(&t1[44..], &samples_to_bytes(&multi_samples[..24 * 1176]));
|
||||||
|
assert_eq!(&t2[44..], &samples_to_bytes(&multi_samples[24 * 1176..]));
|
||||||
|
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdrustanoia_core"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license = "LGPL-2.1-or-later"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libc.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//! Paranoia callback event codes, from the LGPL cdda_paranoia.h API.
|
||||||
|
|
||||||
|
pub const PARANOIA_CB_READ: i32 = 0;
|
||||||
|
pub const PARANOIA_CB_VERIFY: i32 = 1;
|
||||||
|
pub const PARANOIA_CB_FIXUP_EDGE: i32 = 2;
|
||||||
|
pub const PARANOIA_CB_FIXUP_ATOM: i32 = 3;
|
||||||
|
pub const PARANOIA_CB_SCRATCH: i32 = 4;
|
||||||
|
pub const PARANOIA_CB_REPAIR: i32 = 5;
|
||||||
|
pub const PARANOIA_CB_SKIP: i32 = 6;
|
||||||
|
pub const PARANOIA_CB_DRIFT: i32 = 7;
|
||||||
|
pub const PARANOIA_CB_BACKOFF: i32 = 8;
|
||||||
|
pub const PARANOIA_CB_OVERLAP: i32 = 9;
|
||||||
|
pub const PARANOIA_CB_FIXUP_DROPPED: i32 = 10;
|
||||||
|
pub const PARANOIA_CB_FIXUP_DUPED: i32 = 11;
|
||||||
|
pub const PARANOIA_CB_READERR: i32 = 12;
|
||||||
|
pub const PARANOIA_CB_CACHEERR: i32 = 13;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
//! CD-ROM device discovery, mirroring scan_devices.c's search order.
|
||||||
|
|
||||||
|
use crate::device::image::ImageDrive;
|
||||||
|
use crate::device::linux::LinuxDrive;
|
||||||
|
use crate::device::{Drive, InterfaceKind};
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::FileTypeExt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn image_override() -> Option<ImageDrive> {
|
||||||
|
let path = std::env::var_os("CDRUSTANOIA_TEST_IMAGE")?;
|
||||||
|
ImageDrive::open_path(Path::new(&path)).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_sgio(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
|
||||||
|
if verbose {
|
||||||
|
eprintln!("\tTesting {} for SCSI/MMC interface", device);
|
||||||
|
}
|
||||||
|
let drive = LinuxDrive::sg_io(device)?;
|
||||||
|
if verbose {
|
||||||
|
eprintln!("\t\tSG_IO device: {}", device);
|
||||||
|
eprintln!("\nCDROM model sensed sensed: {}", drive.drive_model());
|
||||||
|
}
|
||||||
|
Ok(Box::new(drive))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_cooked(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
|
||||||
|
if verbose {
|
||||||
|
eprintln!("\tTesting {} for cooked ioctl() interface", device);
|
||||||
|
}
|
||||||
|
let drive = LinuxDrive::cooked(device)?;
|
||||||
|
if verbose {
|
||||||
|
eprintln!("\t\tCDROM sensed: {}", drive.drive_model());
|
||||||
|
}
|
||||||
|
Ok(Box::new(drive))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_device(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
|
||||||
|
if let Some(img) = image_override() {
|
||||||
|
return Ok(Box::new(img));
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
eprintln!("Checking {} for cdrom...", device);
|
||||||
|
}
|
||||||
|
let meta = fs::metadata(device).map_err(|e| format!("Could not stat {}: {}", device, e))?;
|
||||||
|
let ft = meta.file_type();
|
||||||
|
if !ft.is_block_device() && !ft.is_char_device() {
|
||||||
|
return Err(format!("{} is not a block or character device", device));
|
||||||
|
}
|
||||||
|
let sg_err = match identify_sgio(device, verbose) {
|
||||||
|
Ok(d) => return Ok(d),
|
||||||
|
Err(e) => e,
|
||||||
|
};
|
||||||
|
match identify_cooked(device, verbose) {
|
||||||
|
Ok(d) => Ok(d),
|
||||||
|
Err(cooked_err) => Err(format!(
|
||||||
|
"Unable to identify {} (SG_IO: {}; cooked: {})",
|
||||||
|
device, sg_err, cooked_err
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify(device: &str, verbose: bool, force_cooked: bool) -> Result<Box<dyn Drive>, String> {
|
||||||
|
if force_cooked {
|
||||||
|
identify_cooked(device, verbose)
|
||||||
|
} else {
|
||||||
|
identify_device(device, verbose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_a_cdrom(verbose: bool) -> Option<Box<dyn Drive>> {
|
||||||
|
if let Some(img) = image_override() {
|
||||||
|
return Some(Box::new(img));
|
||||||
|
}
|
||||||
|
let mut candidates: Vec<String> = Vec::new();
|
||||||
|
candidates.push("/dev/cdrom".to_string());
|
||||||
|
for prefix in [
|
||||||
|
"/dev/cdroms/cdrom",
|
||||||
|
"/dev/sr",
|
||||||
|
"/dev/scd",
|
||||||
|
"/dev/hd",
|
||||||
|
"/dev/sg",
|
||||||
|
] {
|
||||||
|
for i in 0..4 {
|
||||||
|
candidates.push(format!("{}{}", prefix, i));
|
||||||
|
candidates.push(format!("{}{}", prefix, (b'a' + i) as char));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name in [
|
||||||
|
"/dev/cdu31a",
|
||||||
|
"/dev/cdu535",
|
||||||
|
"/dev/sbpcd",
|
||||||
|
"/dev/sonycd",
|
||||||
|
"/dev/mcd",
|
||||||
|
"/dev/sjcd",
|
||||||
|
"/dev/cm206cd",
|
||||||
|
"/dev/gscd",
|
||||||
|
"/dev/optcd",
|
||||||
|
] {
|
||||||
|
candidates.push(name.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
for dev in candidates {
|
||||||
|
if !seen.insert(dev.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !Path::new(&dev).exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match identify_device(&dev, verbose) {
|
||||||
|
Ok(d) => {
|
||||||
|
if verbose {
|
||||||
|
eprintln!("Using {}", dev);
|
||||||
|
}
|
||||||
|
return Some(d);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if verbose {
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
let user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
|
||||||
|
eprintln!("\n\nNo cdrom drives accessible to {} found.", user);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interface_name(d: &dyn Drive) -> &'static str {
|
||||||
|
match d.interface() {
|
||||||
|
InterfaceKind::SgIo => "SG_IO",
|
||||||
|
InterfaceKind::Cooked => "cooked ioctl",
|
||||||
|
InterfaceKind::Image => "image",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
//! Synthetic image backend used by tests and by the hidden
|
||||||
|
//! `CDRUSTANOIA_TEST_IMAGE` development interface.
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
bytes_to_samples, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead, TocEntry,
|
||||||
|
CD_FRAMESIZE_RAW, CD_FRAMEWORDS, MAXTRK,
|
||||||
|
};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ImageFaults {
|
||||||
|
/// Shift the beginning of each read by up to this many PCM words.
|
||||||
|
pub jitter_words: i64,
|
||||||
|
/// Only every Nth read is jittered (0 = every read when jitter_words > 0).
|
||||||
|
pub jitter_period: u32,
|
||||||
|
/// Leave the first N reads unjittered to establish an absolute anchor.
|
||||||
|
pub jitter_skip: u32,
|
||||||
|
/// Return at most this many sectors per read (0 = no limit).
|
||||||
|
pub max_read_sectors: i64,
|
||||||
|
/// Consistent sample discontinuity at a sector boundary.
|
||||||
|
pub discontinuities: Vec<(i64, i64)>, // (sector, delta words)
|
||||||
|
/// Consistently corrupt this source range.
|
||||||
|
pub scratch: Option<(i64, i64)>, // (word offset, word length)
|
||||||
|
/// Simulated duration of a read in milliseconds.
|
||||||
|
pub read_ms: i32,
|
||||||
|
/// Simulated cache-friendly timing when reading backwards.
|
||||||
|
pub backward_ms: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ImageConfig {
|
||||||
|
pub tracks: Vec<(i64, i64, u8)>, // start sector, end sector (inclusive), flags
|
||||||
|
pub faults: ImageFaults,
|
||||||
|
pub bigendianp: i32,
|
||||||
|
pub nsectors: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ImageConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tracks: Vec::new(),
|
||||||
|
faults: ImageFaults {
|
||||||
|
read_ms: 20,
|
||||||
|
backward_ms: 20,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
bigendianp: 0,
|
||||||
|
nsectors: 13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImageDrive {
|
||||||
|
samples: Vec<i16>,
|
||||||
|
toc: DiscToc,
|
||||||
|
faults: ImageFaults,
|
||||||
|
bigendianp: i32,
|
||||||
|
nsectors: i64,
|
||||||
|
opened: bool,
|
||||||
|
model: String,
|
||||||
|
read_count: u32,
|
||||||
|
last_begin: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageDrive {
|
||||||
|
pub fn from_bytes(data: Vec<u8>) -> Self {
|
||||||
|
let sectors = (data.len() / CD_FRAMESIZE_RAW) as i64;
|
||||||
|
let tracks = if sectors > 0 {
|
||||||
|
vec![(0, sectors - 1, 0u8)]
|
||||||
|
} else {
|
||||||
|
vec![(0, 0, 0u8)]
|
||||||
|
};
|
||||||
|
Self::with_config(
|
||||||
|
data,
|
||||||
|
ImageConfig {
|
||||||
|
tracks,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_config(data: Vec<u8>, config: ImageConfig) -> Self {
|
||||||
|
let samples = bytes_to_samples(&data);
|
||||||
|
let mut toc = DiscToc::default();
|
||||||
|
let tracks = config.tracks;
|
||||||
|
let n = tracks.len().min(MAXTRK - 1);
|
||||||
|
for (i, (start, _end, flags)) in tracks.iter().take(n).enumerate() {
|
||||||
|
toc.entries[i] = TocEntry {
|
||||||
|
flags: *flags,
|
||||||
|
track: (i + 1) as u8,
|
||||||
|
start_sector: *start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let leadout = if n > 0 {
|
||||||
|
tracks[n - 1].1 + 1
|
||||||
|
} else {
|
||||||
|
(data.len() / CD_FRAMESIZE_RAW) as i64
|
||||||
|
};
|
||||||
|
toc.entries[n] = TocEntry {
|
||||||
|
flags: 0x04,
|
||||||
|
track: 0xAA,
|
||||||
|
start_sector: leadout,
|
||||||
|
};
|
||||||
|
toc.tracks = n as i32;
|
||||||
|
let mut d = Self {
|
||||||
|
samples,
|
||||||
|
toc,
|
||||||
|
faults: config.faults,
|
||||||
|
bigendianp: config.bigendianp,
|
||||||
|
nsectors: config.nsectors,
|
||||||
|
opened: true,
|
||||||
|
model: "File based test interface".to_string(),
|
||||||
|
read_count: 0,
|
||||||
|
last_begin: -1,
|
||||||
|
};
|
||||||
|
d.fix_toc();
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_path(path: &Path) -> std::io::Result<Self> {
|
||||||
|
let data = fs::read(path)?;
|
||||||
|
let mut config = ImageConfig::default();
|
||||||
|
let mut sectors = (data.len() / CD_FRAMESIZE_RAW) as i64;
|
||||||
|
if sectors < 1 {
|
||||||
|
sectors = 1;
|
||||||
|
}
|
||||||
|
config.tracks = vec![(0, sectors - 1, 0)];
|
||||||
|
// Optional sidecar: one "start end flags" line per track.
|
||||||
|
let sidecar = path.with_extension(format!(
|
||||||
|
"{}.toc",
|
||||||
|
path.extension().and_then(|s| s.to_str()).unwrap_or("raw")
|
||||||
|
));
|
||||||
|
if let Ok(text) = fs::read_to_string(&sidecar) {
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut fields = line.split_whitespace();
|
||||||
|
let start: i64 = match fields.next().and_then(|s| s.parse().ok()) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let end: i64 = fields.next().and_then(|s| s.parse().ok()).unwrap_or(start);
|
||||||
|
let flags: u8 = fields
|
||||||
|
.next()
|
||||||
|
.and_then(|s| u8::from_str_radix(s, 16).ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
tracks.push((start, end, flags));
|
||||||
|
}
|
||||||
|
if !tracks.is_empty() {
|
||||||
|
config.tracks = tracks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut d = Self::with_config(data, config);
|
||||||
|
d.model = format!("Image {}", path.display());
|
||||||
|
d.opened = true;
|
||||||
|
Ok(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source_samples(&self) -> &[i16] {
|
||||||
|
&self.samples
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_toc(&mut self) {
|
||||||
|
let tracks = self.toc.tracks.max(0) as usize;
|
||||||
|
self.toc.fixup(tracks + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mapped_sample(&self, target_word: i64) -> Option<i16> {
|
||||||
|
if target_word < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut source = target_word;
|
||||||
|
for (sector, delta) in &self.faults.discontinuities {
|
||||||
|
if target_word / CD_FRAMEWORDS >= *sector {
|
||||||
|
source += *delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((start, len)) = self.faults.scratch {
|
||||||
|
if target_word >= start && target_word < start + len {
|
||||||
|
// A deterministic, repeated-ish garbage pattern.
|
||||||
|
let x = (target_word as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||||
|
return Some((x >> 48) as i16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.samples.get(source as usize).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 jitter = 0i64;
|
||||||
|
if self.faults.jitter_words > 0 {
|
||||||
|
self.read_count = self.read_count.wrapping_add(1);
|
||||||
|
let period = if self.faults.jitter_period == 0 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
self.faults.jitter_period
|
||||||
|
};
|
||||||
|
if self.read_count.is_multiple_of(period) {
|
||||||
|
let max = self.faults.jitter_words;
|
||||||
|
let x = (self.read_count as u64).wrapping_mul(6364136223846793005);
|
||||||
|
jitter = (x % (2 * max as u64 + 1)) as i64 - max;
|
||||||
|
}
|
||||||
|
if self.read_count <= self.faults.jitter_skip {
|
||||||
|
jitter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let start = begin_sector * CD_FRAMEWORDS + jitter;
|
||||||
|
for w in 0..words {
|
||||||
|
out.push(self.mapped_sample(start + w).unwrap_or(0));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drive for ImageDrive {
|
||||||
|
fn toc(&self) -> &DiscToc {
|
||||||
|
&self.toc
|
||||||
|
}
|
||||||
|
fn toc_mut(&mut self) -> &mut DiscToc {
|
||||||
|
&mut self.toc
|
||||||
|
}
|
||||||
|
fn opened(&self) -> bool {
|
||||||
|
self.opened
|
||||||
|
}
|
||||||
|
fn read_audio(
|
||||||
|
&mut self,
|
||||||
|
buffer: Option<&mut [u8]>,
|
||||||
|
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 words = self.read_words(begin, sectors);
|
||||||
|
if let Some(buf) = buffer {
|
||||||
|
let bytes = super::samples_to_bytes(&words);
|
||||||
|
let n = bytes.len().min(buf.len());
|
||||||
|
buf[..n].copy_from_slice(&bytes[..n]);
|
||||||
|
if super::host_big_endian() {
|
||||||
|
swap_raw16(&mut buf[..n]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_begin = begin;
|
||||||
|
Ok(sectors as usize)
|
||||||
|
}
|
||||||
|
fn read_audio_raw(
|
||||||
|
&mut self,
|
||||||
|
buffer: Option<&mut [u8]>,
|
||||||
|
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 words = self.read_words(begin, sectors);
|
||||||
|
if let Some(buf) = buffer {
|
||||||
|
let bytes = super::samples_to_bytes(&words);
|
||||||
|
let n = bytes.len().min(buf.len());
|
||||||
|
buf[..n].copy_from_slice(&bytes[..n]);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
let result = self.read_audio(buffer, begin, sectors);
|
||||||
|
let _elapsed = start.elapsed();
|
||||||
|
let ms = if begin < previous_begin {
|
||||||
|
self.faults.backward_ms
|
||||||
|
} else {
|
||||||
|
self.faults.read_ms
|
||||||
|
};
|
||||||
|
TimedRead {
|
||||||
|
result,
|
||||||
|
milliseconds: ms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn speed_set(&mut self, _speed: i32) -> i32 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.opened = false;
|
||||||
|
}
|
||||||
|
fn drive_model(&self) -> &str {
|
||||||
|
&self.model
|
||||||
|
}
|
||||||
|
fn bigendianp(&self) -> i32 {
|
||||||
|
self.bigendianp
|
||||||
|
}
|
||||||
|
fn set_bigendianp(&mut self, value: i32) {
|
||||||
|
self.bigendianp = value;
|
||||||
|
}
|
||||||
|
fn nsectors(&self) -> i64 {
|
||||||
|
self.nsectors
|
||||||
|
}
|
||||||
|
fn set_nsectors(&mut self, value: i64) {
|
||||||
|
self.nsectors = value;
|
||||||
|
}
|
||||||
|
fn interface(&self) -> InterfaceKind {
|
||||||
|
InterfaceKind::Image
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,422 @@
|
|||||||
|
//! Common CD-ROM device abstractions and TOC helpers.
|
||||||
|
//!
|
||||||
|
//! The layout and behaviour intentionally mirror cdparanoia 10.2's
|
||||||
|
//! `cdda_interface` API. Device specific backends live in sibling modules.
|
||||||
|
|
||||||
|
pub mod discover;
|
||||||
|
pub mod image;
|
||||||
|
pub mod linux;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub const CD_FRAMESIZE_RAW: usize = 2352;
|
||||||
|
pub const CD_FRAMESIZE: usize = 2048;
|
||||||
|
pub const CD_FRAMEWORDS: i64 = 1176;
|
||||||
|
pub const CD_FRAMESAMPLES: usize = CD_FRAMESIZE_RAW / 4;
|
||||||
|
pub const MAXTRK: usize = 100;
|
||||||
|
|
||||||
|
/// Error codes used by the original cdda_interface library.
|
||||||
|
pub mod error {
|
||||||
|
pub const DEVICE_NOT_OPEN: i64 = -400;
|
||||||
|
pub const INVALID_TRACK: i64 = -401;
|
||||||
|
pub const TRACK_NOT_AUDIO: i64 = -402;
|
||||||
|
pub const NO_AUDIO_TRACKS: i64 = -403;
|
||||||
|
pub const NO_MEDIUM: i64 = -404;
|
||||||
|
pub const OPTION_NOT_SUPPORTED: i64 = -405;
|
||||||
|
pub const INTERFACE_NOT_SUPPORTED: i64 = -100;
|
||||||
|
pub const PERMISSION_IOCTL: i64 = -102;
|
||||||
|
pub const PERMISSION_DATA: i64 = -103;
|
||||||
|
pub const KERNEL_MEMORY: i64 = -300;
|
||||||
|
pub const CANT_SET_CDDA: i64 = -1;
|
||||||
|
pub const CANT_READ_LEADOUT: i64 = -2;
|
||||||
|
pub const ILLEGAL_TRACKS: i64 = -3;
|
||||||
|
pub const CANT_READ_TOC_HEADER: i64 = -4;
|
||||||
|
pub const CANT_READ_TOC_ENTRY: i64 = -5;
|
||||||
|
pub const CANT_READ_ANY_DATA: i64 = -6;
|
||||||
|
pub const UNKNOWN_READ: i64 = -7;
|
||||||
|
pub const CANT_IDENTIFY: i64 = -8;
|
||||||
|
pub const ILLEGAL_TOC: i64 = -9;
|
||||||
|
pub const UNREADABLE_SECTOR: i64 = -10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TocEntry {
|
||||||
|
pub flags: u8,
|
||||||
|
pub track: u8,
|
||||||
|
pub start_sector: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TocEntry {
|
||||||
|
pub fn audio(&self) -> bool {
|
||||||
|
self.flags & 0x04 == 0
|
||||||
|
}
|
||||||
|
pub fn copyp(&self) -> bool {
|
||||||
|
self.flags & 0x02 != 0
|
||||||
|
}
|
||||||
|
pub fn preemp(&self) -> bool {
|
||||||
|
self.flags & 0x01 != 0
|
||||||
|
}
|
||||||
|
pub fn channels(&self) -> i32 {
|
||||||
|
if self.flags & 0x08 != 0 {
|
||||||
|
4
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DiscToc {
|
||||||
|
pub entries: [TocEntry; MAXTRK],
|
||||||
|
pub tracks: i32,
|
||||||
|
pub cd_extra: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DiscToc {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
entries: [TocEntry::default(); MAXTRK],
|
||||||
|
tracks: 0,
|
||||||
|
cd_extra: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscToc {
|
||||||
|
pub fn track_firstsector(&self, track: i32) -> i64 {
|
||||||
|
if self.tracks <= 0 {
|
||||||
|
return error::DEVICE_NOT_OPEN;
|
||||||
|
}
|
||||||
|
if track == 0 {
|
||||||
|
if self.entries[0].start_sector == 0 {
|
||||||
|
return error::INVALID_TRACK;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if track < 0 || track as usize > self.tracks as usize {
|
||||||
|
return error::INVALID_TRACK;
|
||||||
|
}
|
||||||
|
self.entries[track as usize - 1].start_sector
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_lastsector(&self, track: i32) -> i64 {
|
||||||
|
if self.tracks <= 0 {
|
||||||
|
return error::DEVICE_NOT_OPEN;
|
||||||
|
}
|
||||||
|
if track == 0 {
|
||||||
|
if self.entries[0].start_sector == 0 {
|
||||||
|
return error::INVALID_TRACK;
|
||||||
|
}
|
||||||
|
return self.entries[0].start_sector - 1;
|
||||||
|
}
|
||||||
|
if track < 1 || track as usize > self.tracks as usize {
|
||||||
|
return error::INVALID_TRACK;
|
||||||
|
}
|
||||||
|
self.entries[track as usize].start_sector - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disc_firstsector(&self) -> i64 {
|
||||||
|
if self.tracks <= 0 {
|
||||||
|
return error::DEVICE_NOT_OPEN;
|
||||||
|
}
|
||||||
|
for i in 0..self.tracks as usize {
|
||||||
|
if self.entries[i].audio() {
|
||||||
|
if i == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return self.entries[i].start_sector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error::NO_AUDIO_TRACKS
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disc_lastsector(&self) -> i64 {
|
||||||
|
if self.tracks <= 0 {
|
||||||
|
return error::DEVICE_NOT_OPEN;
|
||||||
|
}
|
||||||
|
for i in (0..self.tracks as usize).rev() {
|
||||||
|
if self.entries[i].audio() {
|
||||||
|
return self.entries[i + 1].start_sector - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error::NO_AUDIO_TRACKS
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sector_gettrack(&self, sector: i64) -> i32 {
|
||||||
|
if self.tracks <= 0 {
|
||||||
|
return error::DEVICE_NOT_OPEN as i32;
|
||||||
|
}
|
||||||
|
if sector < self.entries[0].start_sector {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for i in 0..self.tracks as usize {
|
||||||
|
if self.entries[i].start_sector <= sector && self.entries[i + 1].start_sector > sector {
|
||||||
|
return (i + 1) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error::INVALID_TRACK as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_bitmap(&self, track: i32, bit: u8, set: i32, clear: i32) -> i32 {
|
||||||
|
let track = if track == 0 { 1 } else { track };
|
||||||
|
if track < 1 || track as usize > self.tracks as usize {
|
||||||
|
return error::INVALID_TRACK as i32;
|
||||||
|
}
|
||||||
|
if self.entries[track as usize - 1].flags & bit != 0 {
|
||||||
|
set
|
||||||
|
} else {
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_channels(&self, track: i32) -> i32 {
|
||||||
|
self.track_bitmap(track, 8, 4, 2)
|
||||||
|
}
|
||||||
|
pub fn track_audiop(&self, track: i32) -> i32 {
|
||||||
|
self.track_bitmap(track, 4, 0, 1)
|
||||||
|
}
|
||||||
|
pub fn track_copyp(&self, track: i32) -> i32 {
|
||||||
|
self.track_bitmap(track, 2, 1, 0)
|
||||||
|
}
|
||||||
|
pub fn track_preemp(&self, track: i32) -> i32 {
|
||||||
|
self.track_bitmap(track, 1, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_audio_index(&self, idx: usize, tracks: usize) -> bool {
|
||||||
|
if idx >= tracks {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.entries[idx].audio()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reproduces `FixupTOC`'s non-ioctl repairs.
|
||||||
|
pub fn fixup(&mut self, tracks: usize) {
|
||||||
|
for j in 0..tracks {
|
||||||
|
if self.entries[j].start_sector < 0 {
|
||||||
|
self.entries[j].start_sector = 0;
|
||||||
|
}
|
||||||
|
if j + 1 < tracks && self.entries[j].start_sector > self.entries[j + 1].start_sector {
|
||||||
|
self.entries[j].start_sector = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tracks == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut last = self.entries[0].start_sector;
|
||||||
|
for j in 1..tracks {
|
||||||
|
if self.entries[j].start_sector < last {
|
||||||
|
self.entries[j].start_sector = last;
|
||||||
|
}
|
||||||
|
last = self.entries[j].start_sector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum InterfaceKind {
|
||||||
|
SgIo,
|
||||||
|
Cooked,
|
||||||
|
Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReadError {
|
||||||
|
pub errno: i32,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadError {
|
||||||
|
pub fn new(errno: i32, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
errno,
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn no_medium() -> Self {
|
||||||
|
Self::new(libc::ENOMEDIUM, "No medium present")
|
||||||
|
}
|
||||||
|
pub fn closed() -> Self {
|
||||||
|
Self::new(libc::EBADF, "Device not open")
|
||||||
|
}
|
||||||
|
pub fn io(message: impl Into<String>) -> Self {
|
||||||
|
Self::new(libc::EIO, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ReadError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{} (errno {})", self.message, self.errno)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TimedRead {
|
||||||
|
pub result: Result<usize, ReadError>,
|
||||||
|
pub milliseconds: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backend interface used by both the command line program and the C ABI.
|
||||||
|
///
|
||||||
|
/// Positions are absolute CD sectors (not sample positions). Audio data is
|
||||||
|
/// returned in host byte order, exactly like `cdda_read`.
|
||||||
|
pub trait Drive {
|
||||||
|
fn toc(&self) -> &DiscToc;
|
||||||
|
fn toc_mut(&mut self) -> &mut DiscToc;
|
||||||
|
fn opened(&self) -> bool;
|
||||||
|
/// Perform the media-dependent part of opening a drive (TOC read,
|
||||||
|
/// CDDA capability probe, endianness detection). Identify-only
|
||||||
|
/// operations leave this undone, just like cdda_identify/cdda_open.
|
||||||
|
fn open_disc(&mut self) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn read_audio(
|
||||||
|
&mut self,
|
||||||
|
buffer: Option<&mut [u8]>,
|
||||||
|
begin: i64,
|
||||||
|
sectors: i64,
|
||||||
|
) -> Result<usize, ReadError>;
|
||||||
|
/// Low-level read without host-endian conversion, matching the original
|
||||||
|
/// `cdrom_drive::read_audio` function pointer.
|
||||||
|
fn read_audio_raw(
|
||||||
|
&mut self,
|
||||||
|
buffer: Option<&mut [u8]>,
|
||||||
|
begin: i64,
|
||||||
|
sectors: i64,
|
||||||
|
) -> Result<usize, ReadError> {
|
||||||
|
self.read_audio(buffer, begin, sectors)
|
||||||
|
}
|
||||||
|
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead;
|
||||||
|
fn speed_set(&mut self, speed: i32) -> i32;
|
||||||
|
fn close(&mut self);
|
||||||
|
fn drive_model(&self) -> &str;
|
||||||
|
fn bigendianp(&self) -> i32;
|
||||||
|
fn set_bigendianp(&mut self, value: i32);
|
||||||
|
fn nsectors(&self) -> i64;
|
||||||
|
fn set_nsectors(&mut self, value: i64);
|
||||||
|
fn interface(&self) -> InterfaceKind;
|
||||||
|
fn cdda_version(&self) -> &'static str {
|
||||||
|
crate::VERSIONNUM
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience TOC wrappers.
|
||||||
|
fn tracks(&self) -> i32 {
|
||||||
|
self.toc().tracks
|
||||||
|
}
|
||||||
|
fn track_firstsector(&self, track: i32) -> i64 {
|
||||||
|
self.toc().track_firstsector(track)
|
||||||
|
}
|
||||||
|
fn track_lastsector(&self, track: i32) -> i64 {
|
||||||
|
self.toc().track_lastsector(track)
|
||||||
|
}
|
||||||
|
fn disc_firstsector(&self) -> i64 {
|
||||||
|
self.toc().disc_firstsector()
|
||||||
|
}
|
||||||
|
fn disc_lastsector(&self) -> i64 {
|
||||||
|
self.toc().disc_lastsector()
|
||||||
|
}
|
||||||
|
fn sector_gettrack(&self, sector: i64) -> i32 {
|
||||||
|
self.toc().sector_gettrack(sector)
|
||||||
|
}
|
||||||
|
fn track_audiop(&self, track: i32) -> bool {
|
||||||
|
self.toc().track_audiop(track) == 1
|
||||||
|
}
|
||||||
|
fn track_copyp(&self, track: i32) -> bool {
|
||||||
|
self.toc().track_copyp(track) == 1
|
||||||
|
}
|
||||||
|
fn track_preemp(&self, track: i32) -> bool {
|
||||||
|
self.toc().track_preemp(track) == 1
|
||||||
|
}
|
||||||
|
fn track_channels(&self, track: i32) -> i32 {
|
||||||
|
self.toc().track_channels(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap16(x: i16) -> i16 {
|
||||||
|
(x as u16).swap_bytes() as i16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_big_endian() -> bool {
|
||||||
|
cfg!(target_endian = "big")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap a buffer of host endian samples to the requested byte order.
|
||||||
|
pub fn swap_samples(buffer: &mut [i16], target_big_endian: bool) {
|
||||||
|
if host_big_endian() != target_big_endian {
|
||||||
|
for s in buffer {
|
||||||
|
*s = swap16(*s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert raw CDDA bytes into host endian i16 samples.
|
||||||
|
pub fn bytes_to_samples(bytes: &[u8]) -> Vec<i16> {
|
||||||
|
let mut out = Vec::with_capacity(bytes.len() / 2);
|
||||||
|
for chunk in bytes.chunks_exact(2) {
|
||||||
|
out.push(i16::from_ne_bytes([chunk[0], chunk[1]]));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn samples_to_bytes(samples: &[i16]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(samples.len() * 2);
|
||||||
|
for s in samples {
|
||||||
|
out.extend_from_slice(&s.to_ne_bytes());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap bytes in a raw buffer between little and big endian 16-bit words.
|
||||||
|
pub fn swap_raw16(buffer: &mut [u8]) {
|
||||||
|
for chunk in buffer.chunks_exact_mut(2) {
|
||||||
|
chunk.swap(0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn toc() -> DiscToc {
|
||||||
|
let mut t = DiscToc::default();
|
||||||
|
t.tracks = 2;
|
||||||
|
t.entries[0] = TocEntry {
|
||||||
|
flags: 0,
|
||||||
|
track: 1,
|
||||||
|
start_sector: 0,
|
||||||
|
};
|
||||||
|
t.entries[1] = TocEntry {
|
||||||
|
flags: 0,
|
||||||
|
track: 2,
|
||||||
|
start_sector: 100,
|
||||||
|
};
|
||||||
|
t.entries[2] = TocEntry {
|
||||||
|
flags: 4,
|
||||||
|
track: 0xAA,
|
||||||
|
start_sector: 200,
|
||||||
|
};
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toc_queries() {
|
||||||
|
let t = toc();
|
||||||
|
assert_eq!(t.track_firstsector(1), 0);
|
||||||
|
assert_eq!(t.track_lastsector(1), 99);
|
||||||
|
assert_eq!(t.track_firstsector(2), 100);
|
||||||
|
assert_eq!(t.track_lastsector(2), 199);
|
||||||
|
assert_eq!(t.disc_firstsector(), 0);
|
||||||
|
assert_eq!(t.disc_lastsector(), 199);
|
||||||
|
assert_eq!(t.sector_gettrack(0), 1);
|
||||||
|
assert_eq!(t.sector_gettrack(99), 1);
|
||||||
|
assert_eq!(t.sector_gettrack(100), 2);
|
||||||
|
assert_eq!(t.track_audiop(1), 1);
|
||||||
|
assert_eq!(t.track_audiop(2), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn byte_conversion() {
|
||||||
|
let s = [1i16, -2, 300];
|
||||||
|
let b = samples_to_bytes(&s);
|
||||||
|
assert_eq!(bytes_to_samples(&b), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#![allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::missing_safety_doc,
|
||||||
|
clippy::while_let_loop,
|
||||||
|
clippy::unnecessary_cast,
|
||||||
|
clippy::field_reassign_with_default
|
||||||
|
)]
|
||||||
|
|
||||||
|
//! cdrustanoia core library.
|
||||||
|
//!
|
||||||
|
//! This is a Rust port of the cdparanoia III 10.2 interface and paranoia
|
||||||
|
//! libraries, plus the command line helper code.
|
||||||
|
|
||||||
|
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 fn paranoia_version() -> &'static str {
|
||||||
|
VERSIONNUM
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cdda_version() -> &'static str {
|
||||||
|
VERSIONNUM
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
//! c_block/v_fragment/root data structures and vector operations.
|
||||||
|
|
||||||
|
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 flags: Option<Vec<u8>>,
|
||||||
|
pub begin: i64,
|
||||||
|
pub size: i64,
|
||||||
|
pub lastsector: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CBlock {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CBlock {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
vector: Vec::new(),
|
||||||
|
flags: None,
|
||||||
|
begin: 0,
|
||||||
|
size: 0,
|
||||||
|
lastsector: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_parts(vector: Vec<i16>, begin: i64, lastsector: bool) -> Self {
|
||||||
|
let size = vector.len() as i64;
|
||||||
|
Self {
|
||||||
|
vector,
|
||||||
|
flags: None,
|
||||||
|
begin,
|
||||||
|
size,
|
||||||
|
lastsector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn end(&self) -> i64 {
|
||||||
|
self.begin + self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn begin(&self) -> i64 {
|
||||||
|
self.begin
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn size(&self) -> i64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, pos: i64, data: &[i16]) {
|
||||||
|
let vs = self.size;
|
||||||
|
if pos < 0 || pos > vs {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pos = pos as usize;
|
||||||
|
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();
|
||||||
|
let pos = pos.min(flags.len());
|
||||||
|
flags.splice(pos..pos, std::iter::repeat_n(0, n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, cutpos: i64, mut cutsize: i64) {
|
||||||
|
let vs = self.size;
|
||||||
|
if cutpos < 0 || cutpos > vs {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if cutpos + cutsize > vs {
|
||||||
|
cutsize = vs - cutpos;
|
||||||
|
}
|
||||||
|
if cutsize < 0 {
|
||||||
|
cutsize = vs - cutpos;
|
||||||
|
}
|
||||||
|
if cutsize < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pos = cutpos as usize;
|
||||||
|
let n = cutsize as usize;
|
||||||
|
self.vector.drain(pos..pos + n);
|
||||||
|
self.size -= cutsize;
|
||||||
|
if let Some(flags) = self.flags.as_mut() {
|
||||||
|
let pos = pos.min(flags.len());
|
||||||
|
let end = (pos + n).min(flags.len());
|
||||||
|
flags.drain(pos..end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn overwrite(&mut self, pos: i64, data: &[i16]) {
|
||||||
|
if pos < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut size = data.len() as i64;
|
||||||
|
if pos + size > self.size {
|
||||||
|
size = self.size - pos;
|
||||||
|
}
|
||||||
|
if size <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let p = pos as usize;
|
||||||
|
let n = size as usize;
|
||||||
|
self.vector[p..p + n].copy_from_slice(&data[..n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, data: &[i16]) {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_front(&mut self, cut: i64) {
|
||||||
|
if cut <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = (cut as usize).min(self.vector.len());
|
||||||
|
self.vector.drain(..n);
|
||||||
|
self.size -= n as i64;
|
||||||
|
self.begin += n as i64;
|
||||||
|
if let Some(flags) = self.flags.as_mut() {
|
||||||
|
let n = n.min(flags.len());
|
||||||
|
flags.drain(..n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flag_at(&self, abs_word: i64) -> u8 {
|
||||||
|
let idx = abs_word - self.begin;
|
||||||
|
if idx < 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.flags
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|f| f.get(idx as usize).copied())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct VFragment {
|
||||||
|
pub vector: Vec<i16>,
|
||||||
|
pub begin: i64,
|
||||||
|
pub size: i64,
|
||||||
|
pub lastsector: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VFragment {
|
||||||
|
pub fn end(&self) -> i64 {
|
||||||
|
self.begin + self.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct RootBlock {
|
||||||
|
pub returned_limit: i64,
|
||||||
|
pub lastsector: bool,
|
||||||
|
pub vector: Option<CBlock>,
|
||||||
|
pub silence_flag: bool,
|
||||||
|
pub silence_begin: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootBlock {
|
||||||
|
#[inline]
|
||||||
|
pub fn begin(&self) -> i64 {
|
||||||
|
self.vector.as_ref().map(|v| v.begin).unwrap_or(-1)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn end(&self) -> i64 {
|
||||||
|
self.vector.as_ref().map(|v| v.end()).unwrap_or(-1)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.vector.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Offsets {
|
||||||
|
pub offpoints: i64,
|
||||||
|
pub newpoints: i64,
|
||||||
|
pub offaccum: i64,
|
||||||
|
pub offdiff: i64,
|
||||||
|
pub offmin: i64,
|
||||||
|
pub offmax: i64,
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
|||||||
|
//! Sample-value index, a safe Rust equivalent of isort.c.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct SortIndex {
|
||||||
|
buckets: HashMap<i16, Vec<u32>>,
|
||||||
|
pub begin: i64,
|
||||||
|
pub size: usize,
|
||||||
|
cursor: Option<(i16, usize, usize)>, // value, next bucket index, hi
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortIndex {
|
||||||
|
pub fn new(_maxsize: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
buckets: HashMap::new(),
|
||||||
|
begin: 0,
|
||||||
|
size: 0,
|
||||||
|
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());
|
||||||
|
if lo >= hi {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i, sample) in samples.iter().enumerate().take(hi).skip(lo) {
|
||||||
|
self.buckets.entry(*sample).or_default().push(i as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use cdrustanoia_core::device::image::ImageDrive;
|
||||||
|
use cdrustanoia_core::device::{bytes_to_samples, samples_to_bytes, Drive, CD_FRAMESIZE_RAW};
|
||||||
|
use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL};
|
||||||
|
|
||||||
|
fn make_source(sectors: usize) -> Vec<i16> {
|
||||||
|
let mut out = Vec::with_capacity(sectors * 1176);
|
||||||
|
let mut x: u64 = 0x1234_5678_9abc_def0;
|
||||||
|
for i in 0..sectors * 1176 {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
// Mix in some silence and a distinct waveform.
|
||||||
|
let v = if i % 5000 < 1000 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(x as i16).wrapping_add((i as i16).wrapping_mul(7))
|
||||||
|
};
|
||||||
|
out.push(v);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clean_image_roundtrip() {
|
||||||
|
let sectors = 80usize;
|
||||||
|
let source = make_source(sectors);
|
||||||
|
let bytes = samples_to_bytes(&source);
|
||||||
|
let drive = ImageDrive::from_bytes(bytes);
|
||||||
|
let mut p = Paranoia::new(Box::new(drive));
|
||||||
|
p.modeset(PARANOIA_MODE_FULL);
|
||||||
|
let mut got = Vec::with_capacity(sectors * 1176);
|
||||||
|
let mut cb = |_: i64, _: i32| {};
|
||||||
|
for _ in 0..sectors {
|
||||||
|
let sec = p.read(&mut cb).expect("read sector");
|
||||||
|
got.extend_from_slice(sec);
|
||||||
|
}
|
||||||
|
assert_eq!(got.len(), source.len());
|
||||||
|
assert_eq!(got, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drive_toc_helpers() {
|
||||||
|
let bytes = vec![0u8; CD_FRAMESIZE_RAW * 10];
|
||||||
|
let drive = ImageDrive::from_bytes(bytes);
|
||||||
|
assert_eq!(drive.tracks(), 1);
|
||||||
|
assert_eq!(drive.disc_firstsector(), 0);
|
||||||
|
assert_eq!(drive.disc_lastsector(), 9);
|
||||||
|
assert_eq!(drive.track_firstsector(1), 0);
|
||||||
|
assert_eq!(drive.track_lastsector(1), 9);
|
||||||
|
let _ = bytes_to_samples(&[0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_config(sectors: usize, config: cdrustanoia_core::device::image::ImageConfig) -> Vec<i16> {
|
||||||
|
let source = make_source(sectors);
|
||||||
|
let bytes = samples_to_bytes(&source);
|
||||||
|
let drive = ImageDrive::with_config(bytes, config);
|
||||||
|
let mut p = Paranoia::new(Box::new(drive));
|
||||||
|
p.modeset(PARANOIA_MODE_FULL);
|
||||||
|
let start = 5usize.min(sectors);
|
||||||
|
p.seek(start as i64, 0);
|
||||||
|
let mut got = Vec::with_capacity((sectors - start) * 1176);
|
||||||
|
let mut cb = |_: i64, _: i32| {};
|
||||||
|
for _ in start..sectors {
|
||||||
|
let sec = p.read(&mut cb).expect("read sector");
|
||||||
|
got.extend_from_slice(sec);
|
||||||
|
}
|
||||||
|
let source = &source[start * 1176..];
|
||||||
|
assert_eq!(got.len(), source.len());
|
||||||
|
if got != source {
|
||||||
|
let mut best: Option<(i64, usize)> = None;
|
||||||
|
for shift in -2000i64..=2000 {
|
||||||
|
let mut bad = 0usize;
|
||||||
|
for (i, value) in got.iter().enumerate().take(source.len()) {
|
||||||
|
let j = i as i64 + shift;
|
||||||
|
if j < 0 || j as usize >= source.len() || *value != source[j as usize] {
|
||||||
|
bad += 1;
|
||||||
|
if bad > 8 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bad <= 8 {
|
||||||
|
best = Some((shift, bad));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((shift, bad)) = best {
|
||||||
|
panic!(
|
||||||
|
"data shifted by {} words ({} mismatches); engine did not align absolute position",
|
||||||
|
shift, bad
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let idx = got
|
||||||
|
.iter()
|
||||||
|
.zip(source.iter())
|
||||||
|
.position(|(a, b)| a != b)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let where0 = source.iter().position(|x| *x == got[0]);
|
||||||
|
panic!(
|
||||||
|
"mismatch at word {} (sector {} offset {}): got {} expected {}; got0 found at {:?}; got[..8]={:?} src[..8]={:?}",
|
||||||
|
idx,
|
||||||
|
idx / 1176,
|
||||||
|
idx % 1176,
|
||||||
|
got[idx],
|
||||||
|
source[idx],
|
||||||
|
where0,
|
||||||
|
&got[..got.len().min(8)],
|
||||||
|
&source[..source.len().min(8)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
got
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jittered_image_roundtrip() {
|
||||||
|
use cdrustanoia_core::device::image::{ImageConfig, ImageFaults};
|
||||||
|
let sectors = 20usize;
|
||||||
|
let config = ImageConfig {
|
||||||
|
tracks: vec![(0, sectors as i64 - 1, 0)],
|
||||||
|
faults: ImageFaults {
|
||||||
|
jitter_words: 180,
|
||||||
|
jitter_period: 1,
|
||||||
|
jitter_skip: 10,
|
||||||
|
max_read_sectors: 0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
run_config(sectors, config);
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
/******************************************************************
|
||||||
|
* CopyPolicy: GNU Lesser General Public License 2.1 applies
|
||||||
|
* Copyright (C) 2001-2008 Xiph.org
|
||||||
|
* Original version by Heiko Eissfeldt heiko@escape.colossus.de
|
||||||
|
*
|
||||||
|
* Toplevel interface header; applications include this
|
||||||
|
*
|
||||||
|
******************************************************************/
|
||||||
|
|
||||||
|
#ifndef _cdda_interface_h_
|
||||||
|
#define _cdda_interface_h_
|
||||||
|
|
||||||
|
#ifndef CD_FRAMESIZE
|
||||||
|
#define CD_FRAMESIZE 2048
|
||||||
|
#endif
|
||||||
|
#ifndef CD_FRAMESIZE_RAW
|
||||||
|
#define CD_FRAMESIZE_RAW 2352
|
||||||
|
#endif
|
||||||
|
#define CD_FRAMESAMPLES (CD_FRAMESIZE_RAW / 4)
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
#define MAXTRK 100
|
||||||
|
|
||||||
|
typedef struct TOC { /* structure of table of contents */
|
||||||
|
unsigned char bFlags;
|
||||||
|
unsigned char bTrack;
|
||||||
|
int32_t dwStartSector;
|
||||||
|
} TOC;
|
||||||
|
|
||||||
|
/* interface types */
|
||||||
|
#define GENERIC_SCSI 0
|
||||||
|
#define COOKED_IOCTL 1
|
||||||
|
#define TEST_INTERFACE 2
|
||||||
|
#define SGIO_SCSI 3
|
||||||
|
#define SGIO_SCSI_BUGGY1 4
|
||||||
|
|
||||||
|
#define CDDA_MESSAGE_FORGETIT 0
|
||||||
|
#define CDDA_MESSAGE_PRINTIT 1
|
||||||
|
#define CDDA_MESSAGE_LOGIT 2
|
||||||
|
|
||||||
|
/* cdrom access function pointer */
|
||||||
|
|
||||||
|
typedef struct cdda_private_data cdda_private_data_t;
|
||||||
|
|
||||||
|
typedef struct cdrom_drive{
|
||||||
|
|
||||||
|
int opened; /* This struct may just represent a candidate for opening */
|
||||||
|
|
||||||
|
char *cdda_device_name;
|
||||||
|
char *ioctl_device_name;
|
||||||
|
|
||||||
|
int cdda_fd;
|
||||||
|
int ioctl_fd;
|
||||||
|
|
||||||
|
char *drive_model;
|
||||||
|
int drive_type;
|
||||||
|
int interface;
|
||||||
|
int bigendianp;
|
||||||
|
int nsectors;
|
||||||
|
|
||||||
|
int cd_extra;
|
||||||
|
int tracks;
|
||||||
|
TOC disc_toc[MAXTRK];
|
||||||
|
long audio_first_sector;
|
||||||
|
long audio_last_sector;
|
||||||
|
|
||||||
|
int errordest;
|
||||||
|
int messagedest;
|
||||||
|
char *errorbuf;
|
||||||
|
char *messagebuf;
|
||||||
|
|
||||||
|
/* functions specific to particular drives/interfaces */
|
||||||
|
|
||||||
|
int (*enable_cdda) (struct cdrom_drive *d, int onoff);
|
||||||
|
int (*read_toc) (struct cdrom_drive *d);
|
||||||
|
long (*read_audio) (struct cdrom_drive *d, void *p, long begin,
|
||||||
|
long sectors);
|
||||||
|
int (*set_speed) (struct cdrom_drive *d, int speed);
|
||||||
|
int error_retry;
|
||||||
|
int report_all;
|
||||||
|
|
||||||
|
int is_atapi;
|
||||||
|
int is_mmc;
|
||||||
|
|
||||||
|
cdda_private_data_t *private;
|
||||||
|
void *reserved;
|
||||||
|
unsigned char inqbytes[4];
|
||||||
|
|
||||||
|
/* Scsi parameters and state */
|
||||||
|
unsigned char density;
|
||||||
|
unsigned char orgdens;
|
||||||
|
unsigned int orgsize;
|
||||||
|
long bigbuff;
|
||||||
|
int adjust_ssize;
|
||||||
|
|
||||||
|
int fua;
|
||||||
|
int lun;
|
||||||
|
|
||||||
|
sigset_t sigset;
|
||||||
|
|
||||||
|
} cdrom_drive;
|
||||||
|
|
||||||
|
#define IS_AUDIO(d,i) (!(d->disc_toc[i].bFlags & 0x04))
|
||||||
|
|
||||||
|
/******** Identification/autosense functions */
|
||||||
|
|
||||||
|
extern cdrom_drive *cdda_find_a_cdrom(int messagedest, char **message);
|
||||||
|
extern cdrom_drive *cdda_identify(const char *device, int messagedest,
|
||||||
|
char **message);
|
||||||
|
extern cdrom_drive *cdda_identify_cooked(const char *device,int messagedest,
|
||||||
|
char **message);
|
||||||
|
extern cdrom_drive *cdda_identify_scsi(const char *generic_device,
|
||||||
|
const char *ioctl_device,
|
||||||
|
int messagedest, char **message);
|
||||||
|
#ifdef CDDA_TEST
|
||||||
|
extern cdrom_drive *cdda_identify_test(const char *filename,
|
||||||
|
int messagedest, char **message);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/******** Drive oriented functions */
|
||||||
|
|
||||||
|
extern char *cdda_version();
|
||||||
|
extern int cdda_speed_set(cdrom_drive *d, int speed);
|
||||||
|
extern void cdda_verbose_set(cdrom_drive *d,int err_action, int mes_action);
|
||||||
|
extern char *cdda_messages(cdrom_drive *d);
|
||||||
|
extern char *cdda_errors(cdrom_drive *d);
|
||||||
|
|
||||||
|
extern int cdda_close(cdrom_drive *d);
|
||||||
|
extern int cdda_open(cdrom_drive *d);
|
||||||
|
extern long cdda_read(cdrom_drive *d, void *buffer,
|
||||||
|
long beginsector, long sectors);
|
||||||
|
extern long cdda_read_timed(cdrom_drive *d, void *buffer,
|
||||||
|
long beginsector, long sectors, int *milliseconds);
|
||||||
|
|
||||||
|
extern long cdda_track_firstsector(cdrom_drive *d,int track);
|
||||||
|
extern long cdda_track_lastsector(cdrom_drive *d,int track);
|
||||||
|
extern long cdda_tracks(cdrom_drive *d);
|
||||||
|
extern int cdda_sector_gettrack(cdrom_drive *d,long sector);
|
||||||
|
extern int cdda_track_channels(cdrom_drive *d,int track);
|
||||||
|
extern int cdda_track_audiop(cdrom_drive *d,int track);
|
||||||
|
extern int cdda_track_copyp(cdrom_drive *d,int track);
|
||||||
|
extern int cdda_track_preemp(cdrom_drive *d,int track);
|
||||||
|
extern long cdda_disc_firstsector(cdrom_drive *d);
|
||||||
|
extern long cdda_disc_lastsector(cdrom_drive *d);
|
||||||
|
|
||||||
|
/* transport errors: */
|
||||||
|
|
||||||
|
#define TR_OK 0
|
||||||
|
#define TR_EWRITE 1 /* Error writing packet command (transport) */
|
||||||
|
#define TR_EREAD 2 /* Error reading packet data (transport) */
|
||||||
|
#define TR_UNDERRUN 3 /* Read underrun */
|
||||||
|
#define TR_OVERRUN 4 /* Read overrun */
|
||||||
|
#define TR_ILLEGAL 5 /* Illegal/rejected request */
|
||||||
|
#define TR_MEDIUM 6 /* Medium error */
|
||||||
|
#define TR_BUSY 7 /* Device busy */
|
||||||
|
#define TR_NOTREADY 8 /* Device not ready */
|
||||||
|
#define TR_FAULT 9 /* Devive failure */
|
||||||
|
#define TR_UNKNOWN 10 /* Unspecified error */
|
||||||
|
#define TR_STREAMING 11 /* loss of streaming */
|
||||||
|
|
||||||
|
static char *strerror_tr[]={
|
||||||
|
"Success",
|
||||||
|
"Error writing packet command to device",
|
||||||
|
"Error reading command from device",
|
||||||
|
"SCSI packet data underrun (too little data)",
|
||||||
|
"SCSI packet data overrun (too much data)",
|
||||||
|
"Illegal SCSI request (rejected by target)",
|
||||||
|
"Medium reading data from medium",
|
||||||
|
"Device busy",
|
||||||
|
"Device not ready",
|
||||||
|
"Target hardware fault",
|
||||||
|
"Unspecified error",
|
||||||
|
"Drive lost streaming"
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Errors returned by lib:
|
||||||
|
|
||||||
|
001: Unable to set CDROM to read audio mode
|
||||||
|
002: Unable to read table of contents lead-out
|
||||||
|
003: CDROM reporting illegal number of tracks
|
||||||
|
004: Unable to read table of contents header
|
||||||
|
005: Unable to read table of contents entry
|
||||||
|
006: Could not read any data from drive
|
||||||
|
007: Unknown, unrecoverable error reading data
|
||||||
|
008: Unable to identify CDROM model
|
||||||
|
009: CDROM reporting illegal table of contents
|
||||||
|
010: Unaddressable sector
|
||||||
|
|
||||||
|
100: Interface not supported
|
||||||
|
101: Drive is neither a CDROM nor a WORM device
|
||||||
|
102: Permision denied on cdrom (ioctl) device
|
||||||
|
103: Permision denied on cdrom (data) device
|
||||||
|
|
||||||
|
300: Kernel memory error
|
||||||
|
|
||||||
|
400: Device not open
|
||||||
|
401: Invalid track number
|
||||||
|
402: Track not audio data
|
||||||
|
403: No audio tracks on disc
|
||||||
|
404: No medium present
|
||||||
|
405: Option not supported by drive
|
||||||
|
|
||||||
|
*/
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/***
|
||||||
|
* CopyPolicy: GNU Lesser General Public License 2.1 applies
|
||||||
|
* Copyright (C) by Monty (xiphmont@mit.edu)
|
||||||
|
*
|
||||||
|
***/
|
||||||
|
|
||||||
|
#ifndef _CDROM_PARANOIA_
|
||||||
|
#define _CDROM_PARANOIA_
|
||||||
|
|
||||||
|
#define CD_FRAMEWORDS (CD_FRAMESIZE_RAW/2)
|
||||||
|
|
||||||
|
#define PARANOIA_CB_READ 0
|
||||||
|
#define PARANOIA_CB_VERIFY 1
|
||||||
|
#define PARANOIA_CB_FIXUP_EDGE 2
|
||||||
|
#define PARANOIA_CB_FIXUP_ATOM 3
|
||||||
|
#define PARANOIA_CB_SCRATCH 4
|
||||||
|
#define PARANOIA_CB_REPAIR 5
|
||||||
|
#define PARANOIA_CB_SKIP 6
|
||||||
|
#define PARANOIA_CB_DRIFT 7
|
||||||
|
#define PARANOIA_CB_BACKOFF 8
|
||||||
|
#define PARANOIA_CB_OVERLAP 9
|
||||||
|
#define PARANOIA_CB_FIXUP_DROPPED 10
|
||||||
|
#define PARANOIA_CB_FIXUP_DUPED 11
|
||||||
|
#define PARANOIA_CB_READERR 12
|
||||||
|
#define PARANOIA_CB_CACHEERR 13
|
||||||
|
|
||||||
|
#define PARANOIA_MODE_FULL 0xff
|
||||||
|
#define PARANOIA_MODE_DISABLE 0
|
||||||
|
|
||||||
|
#define PARANOIA_MODE_VERIFY 1
|
||||||
|
#define PARANOIA_MODE_FRAGMENT 2
|
||||||
|
#define PARANOIA_MODE_OVERLAP 4
|
||||||
|
#define PARANOIA_MODE_SCRATCH 8
|
||||||
|
#define PARANOIA_MODE_REPAIR 16
|
||||||
|
#define PARANOIA_MODE_NEVERSKIP 32
|
||||||
|
|
||||||
|
#ifndef CDP_COMPILE
|
||||||
|
typedef void cdrom_paranoia;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
extern char *paranoia_version();
|
||||||
|
extern cdrom_paranoia *paranoia_init(cdrom_drive *d);
|
||||||
|
extern void paranoia_modeset(cdrom_paranoia *p,int mode);
|
||||||
|
extern long paranoia_seek(cdrom_paranoia *p,long seek,int mode);
|
||||||
|
extern int16_t *paranoia_read(cdrom_paranoia *p,void(*callback)(long,int));
|
||||||
|
extern int16_t *paranoia_read_limited(cdrom_paranoia *p,void(*callback)(long,int),int maxretries);
|
||||||
|
extern void paranoia_free(cdrom_paranoia *p);
|
||||||
|
extern void paranoia_overlapset(cdrom_paranoia *p,long overlap);
|
||||||
|
extern int paranoia_cachemodel_size(cdrom_paranoia *p,int sectors);
|
||||||
|
#endif
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <endian.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
extern long buffering_write(int outf, char *buffer, long num);
|
||||||
|
extern int buffering_close(int fd);
|
||||||
|
|
||||||
|
/* I wonder how many alignment issues this is gonna trip in the
|
||||||
|
future... it shouldn't trip any... I guess we'll find out :) */
|
||||||
|
|
||||||
|
static inline int bigendianp(void){
|
||||||
|
int test=1;
|
||||||
|
char *hack=(char *)(&test);
|
||||||
|
if(hack[0])return(0);
|
||||||
|
return(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t swap32(int32_t x){
|
||||||
|
return((((u_int32_t)x & 0x000000ffU) << 24) |
|
||||||
|
(((u_int32_t)x & 0x0000ff00U) << 8) |
|
||||||
|
(((u_int32_t)x & 0x00ff0000U) >> 8) |
|
||||||
|
(((u_int32_t)x & 0xff000000U) >> 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t swap16(int16_t x){
|
||||||
|
return((((u_int16_t)x & 0x00ffU) << 8) |
|
||||||
|
(((u_int16_t)x & 0xff00U) >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#if BYTE_ORDER == LITTLE_ENDIAN
|
||||||
|
|
||||||
|
static inline int32_t be32_to_cpu(int32_t x){
|
||||||
|
return(swap32(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t be16_to_cpu(int16_t x){
|
||||||
|
return(swap16(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t le32_to_cpu(int32_t x){
|
||||||
|
return(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t le16_to_cpu(int16_t x){
|
||||||
|
return(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
static inline int32_t be32_to_cpu(int32_t x){
|
||||||
|
return(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t be16_to_cpu(int16_t x){
|
||||||
|
return(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t le32_to_cpu(int32_t x){
|
||||||
|
return(swap32(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t le16_to_cpu(int16_t x){
|
||||||
|
return(swap16(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static inline int32_t cpu_to_be32(int32_t x){
|
||||||
|
return(be32_to_cpu(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t cpu_to_le32(int32_t x){
|
||||||
|
return(le32_to_cpu(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t cpu_to_be16(int16_t x){
|
||||||
|
return(be16_to_cpu(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int16_t cpu_to_le16(int16_t x){
|
||||||
|
return(le16_to_cpu(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline char *copystring(const char *s){
|
||||||
|
if(s){
|
||||||
|
char *ret=malloc((strlen(s)+1)*sizeof(char));
|
||||||
|
strcpy(ret,s);
|
||||||
|
return(ret);
|
||||||
|
}
|
||||||
|
return(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline char *catstring(char *buff,const char *s){
|
||||||
|
if(s){
|
||||||
|
if(buff)
|
||||||
|
buff=realloc(buff,strlen(buff)+strlen(s)+1);
|
||||||
|
else
|
||||||
|
buff=calloc(strlen(s)+1,1);
|
||||||
|
strcat(buff,s);
|
||||||
|
}
|
||||||
|
return(buff);
|
||||||
|
}
|
||||||
|
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
PREFIX="${PREFIX:-/usr/local}"
|
||||||
|
DESTDIR="${DESTDIR:-}"
|
||||||
|
exec make -C "$(dirname "$0")" install PREFIX="$PREFIX" DESTDIR="$DESTDIR"
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
.TH CDPARANOIA 1 "11 Sep 2008"
|
||||||
|
.SH NAME
|
||||||
|
cdparanoia 10.2 (Paranoia release III) \- an audio CD reading utility which includes extra data verification features
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B cdparanoia
|
||||||
|
.RB [ options ]
|
||||||
|
.B span
|
||||||
|
.RB [ outfile ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B cdparanoia
|
||||||
|
retrieves audio tracks from CDDA capable CDROM drives. The data can
|
||||||
|
be saved to a file or directed to standard output in WAV, AIFF, AIFF-C
|
||||||
|
or raw format. Most ATAPI, SCSI and several proprietary CDROM drive
|
||||||
|
makes are supported;
|
||||||
|
.B cdparanoia
|
||||||
|
can determine if the target drive is CDDA capable.
|
||||||
|
.P
|
||||||
|
In addition to simple reading,
|
||||||
|
.B cdparanoia
|
||||||
|
adds extra-robust data verification, synchronization, error handling
|
||||||
|
and scratch reconstruction capability.
|
||||||
|
.SH OPTIONS
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-A --analyze-drive
|
||||||
|
Run and log a complete analysis of drive caching, timing and reading behavior;
|
||||||
|
verifies that cdparanoia is correctly modelling a sprcific drive's cache and
|
||||||
|
read behavior. Implies -vQL.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-v --verbose
|
||||||
|
Be absurdly verbose about the autosensing and reading process. Good
|
||||||
|
for setup and debugging.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-q --quiet
|
||||||
|
Do not print any progress or error information during the reading process.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-e --stderr-progress
|
||||||
|
Force output of progress information to stderr (for wrapper scripts).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-l --log-summary [file]
|
||||||
|
Save result summary to file, default filename cdparanoia.log.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-L --log-debug [file]
|
||||||
|
Save detailed device autosense and debugging output to a file, default filename cdparanoia.log.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-V --version
|
||||||
|
Print the program version and quit.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-Q --query
|
||||||
|
Perform CDROM drive autosense, query and print the CDROM table of
|
||||||
|
contents, then quit.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-s --search-for-drive
|
||||||
|
Forces a complete search for a cdrom drive, even if the /dev/cdrom link exists.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-h --help
|
||||||
|
Print a brief synopsis of
|
||||||
|
.B cdparanoia
|
||||||
|
usage and options.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-p --output-raw
|
||||||
|
Output headerless data as raw 16 bit PCM data with interleaved samples in host byte order. To force little or big endian byte order, use
|
||||||
|
.B \-r
|
||||||
|
or
|
||||||
|
.B \-R
|
||||||
|
as described below.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-r --output-raw-little-endian
|
||||||
|
Output headerless data as raw 16 bit PCM data with interleaved samples in LSB first byte order.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-R --output-raw-big-endian
|
||||||
|
Output headerless data as raw 16 bit PCM data with interleaved samples in MSB first byte order.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-w --output-wav
|
||||||
|
Output data in Micro$oft RIFF WAV format (note that WAV data is always
|
||||||
|
LSB first byte order).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-f --output-aiff
|
||||||
|
Output data in Apple AIFF format (note that AIFC data is
|
||||||
|
always in MSB first byte order).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-a --output-aifc
|
||||||
|
Output data in uncompressed Apple AIFF-C format (note that AIFF-C data is
|
||||||
|
always in MSB first byte order).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-B --batch "
|
||||||
|
|
||||||
|
Cdda2wav-style batch output flag; cdparanoia will split the output
|
||||||
|
into multiple files at track boundaries. Output file names are
|
||||||
|
prepended with 'track#.'
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-c --force-cdrom-little-endian
|
||||||
|
Some CDROM drives misreport their endianness (or do not report it at
|
||||||
|
all); it's possible that cdparanoia will guess wrong. Use
|
||||||
|
.B \-c
|
||||||
|
to force cdparanoia to treat the drive as a little endian device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-C --force-cdrom-big-endian
|
||||||
|
As above but force cdparanoia to treat the drive as a big endian device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-n --force-default-sectors " n
|
||||||
|
Force the interface backend to do atomic reads of
|
||||||
|
.B n
|
||||||
|
sectors per read. This number can be misleading; the kernel will often
|
||||||
|
split read requests into multiple atomic reads (the automated Paranoia
|
||||||
|
code is aware of this) or allow reads only wihin a restricted size
|
||||||
|
range.
|
||||||
|
.B This option should generally not be used.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-d --force-cdrom-device " device
|
||||||
|
Force the interface backend to read from
|
||||||
|
.B device
|
||||||
|
rather than the first readable CDROM drive it finds. This can be used
|
||||||
|
to specify devices of any valid interface type (ATAPI, SCSI or
|
||||||
|
proprietary).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-k --force-cooked-device " device
|
||||||
|
This option forces use of the old 'cooked ioctl' kernel
|
||||||
|
interface with the specified cdrom device. The cooked ioctl interface
|
||||||
|
is obsolete in Linux 2.6 if it is present at all.
|
||||||
|
.B \-k
|
||||||
|
cannot be used
|
||||||
|
with
|
||||||
|
.B \-d
|
||||||
|
or
|
||||||
|
.BR \-g .
|
||||||
|
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-g --force-generic-device " device
|
||||||
|
This option forces use of the old 'generic scsi' (sg) kernel
|
||||||
|
interface with the specified generic scsi device.
|
||||||
|
.B \-g
|
||||||
|
cannot be used with
|
||||||
|
.BR \-k .
|
||||||
|
.B \-g
|
||||||
|
may be used with
|
||||||
|
.B \-d
|
||||||
|
to explicitly set both the SCSI cdrom and
|
||||||
|
generic (sg) devices seperately. This option is only useful on
|
||||||
|
obsolete SCSI setups and when using the generic scsi (sg) driver.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-S --force-read-speed " number
|
||||||
|
Use this option explicitly to set the read rate of the CD drive (where
|
||||||
|
supported). This can reduce underruns on machines with slow disks, or
|
||||||
|
which are low on memory.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-t --toc-offset " number
|
||||||
|
Use this option to force the entire disc LBA addressing to shift by
|
||||||
|
the given amount; the value is added to the beginning offsets in the
|
||||||
|
TOC. This can be used to shift track boundaries for the whole disc
|
||||||
|
manually on sector granularity. The next option does something
|
||||||
|
similar...
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-T --toc-bias "
|
||||||
|
Some drives (usually random Toshibas) report the actual track
|
||||||
|
beginning offset values in the TOC, but then treat the beginning of
|
||||||
|
track 1 index 1 as sector 0 for all read operations. This results in
|
||||||
|
every track seeming to start too late (losing a bit of the beginning
|
||||||
|
and catching a bit of the next track).
|
||||||
|
.B \-T
|
||||||
|
accounts for this behavior. Note that this option will cause
|
||||||
|
cdparanoia to attempt to read sectors before or past the known user
|
||||||
|
data area of the disc, resulting in read errors at disc edges on most
|
||||||
|
drives and possibly even hard lockups on some buggy hardware.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-O --sample-offset " number
|
||||||
|
Use this option to force the entire disc to shift sample position
|
||||||
|
output by the given amount; This can be used to shift track boundaries
|
||||||
|
for the whole disc manually on sample granularity. Note that this will
|
||||||
|
cause cdparanoia to attempt to read partial sectors before or past the
|
||||||
|
known user data area of the disc, probably causing read errors on most
|
||||||
|
drives and possibly even hard lockups on some buggy hardware.
|
||||||
|
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-Z --disable-paranoia
|
||||||
|
Disable
|
||||||
|
.B all
|
||||||
|
data verification and correction features. When using -Z, cdparanoia
|
||||||
|
reads data exactly as would cdda2wav with an overlap setting of zero.
|
||||||
|
This option implies that
|
||||||
|
.B \-Y
|
||||||
|
is active.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-z --never-skip[=max_retries]
|
||||||
|
Do not accept any skips; retry forever if needed. An optional maximum
|
||||||
|
number of retries can be specified; for comparison, default without -z is
|
||||||
|
currently 20.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-Y --disable-extra-paranoia
|
||||||
|
Disables intra-read data verification; only overlap checking at read
|
||||||
|
boundaries is performed. It can wedge if errors occur in the attempted overlap area. Not recommended.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-X --abort-on-skip
|
||||||
|
If the read skips due to imperfect data, a scratch, whatever, abort reading this track. If output is to a file, delete the partially completed file.
|
||||||
|
|
||||||
|
.SH OUTPUT SMILIES
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-)
|
||||||
|
Normal operation, low/no jitter
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-|
|
||||||
|
Normal operation, considerable jitter
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-/
|
||||||
|
Read drift
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-P
|
||||||
|
Unreported loss of streaming in atomic read operation
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
8-|
|
||||||
|
Finding read problems at same point during reread; hard to correct
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-0
|
||||||
|
SCSI/ATAPI transport error
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-(
|
||||||
|
Scratch detected
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
;-(
|
||||||
|
Gave up trying to perform a correction
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
8-X
|
||||||
|
Aborted read due to known, uncorrectable error
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:^D
|
||||||
|
Finished extracting
|
||||||
|
|
||||||
|
.SH PROGRESS BAR SYMBOLS
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
<space>
|
||||||
|
No corrections needed
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
-
|
||||||
|
Jitter correction required
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
+
|
||||||
|
Unreported loss of streaming/other error in read
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
!
|
||||||
|
Errors found after stage 1 correction; the drive is making the
|
||||||
|
same error through multiple re-reads, and cdparanoia is having trouble
|
||||||
|
detecting them.
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
e
|
||||||
|
SCSI/ATAPI transport error (corrected)
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
V
|
||||||
|
Uncorrected error/skip
|
||||||
|
|
||||||
|
.SH SPAN ARGUMENT
|
||||||
|
|
||||||
|
The span argument specifies which track, tracks or subsections of
|
||||||
|
tracks to read. This argument is required.
|
||||||
|
.B NOTE:
|
||||||
|
Unless the span is a simple number, it's generally a good idea to
|
||||||
|
quote the span argument to protect it from the shell.
|
||||||
|
.P
|
||||||
|
The span argument may be a simple track number or an offset/span
|
||||||
|
specification. The syntax of an offset/span takes the rough form:
|
||||||
|
.P
|
||||||
|
1[ww:xx:yy.zz]-2[aa:bb:cc.dd]
|
||||||
|
.P
|
||||||
|
Here, 1 and 2 are track numbers; the numbers in brackets provide a
|
||||||
|
finer grained offset within a particular track. [aa:bb:cc.dd] is in
|
||||||
|
hours/minutes/seconds/sectors format. Zero fields need not be
|
||||||
|
specified: [::20], [:20], [20], [20.], etc, would be interpreted as
|
||||||
|
twenty seconds, [10:] would be ten minutes, [.30] would be thirty
|
||||||
|
sectors (75 sectors per second).
|
||||||
|
.P
|
||||||
|
When only a single offset is supplied, it is interpreted as a starting
|
||||||
|
offset and ripping will continue to the end of the track. If a single
|
||||||
|
offset is preceeded or followed by a hyphen, the implicit missing
|
||||||
|
offset is taken to be the start or end of the disc, respectively. Thus:
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B 1:[20.35]
|
||||||
|
Specifies ripping from track 1, second 20, sector 35 to the end of
|
||||||
|
track 1.
|
||||||
|
.TP
|
||||||
|
.B 1:[20.35]-
|
||||||
|
Specifies ripping from 1[20.35] to the end of the disc
|
||||||
|
.TP
|
||||||
|
.B \-2
|
||||||
|
Specifies ripping from the beginning of the disc up to (and including) track 2
|
||||||
|
.TP
|
||||||
|
.B \-2:[30.35]
|
||||||
|
Specifies ripping from the beginning of the disc up to 2:[30.35]
|
||||||
|
.TP
|
||||||
|
.B 2-4
|
||||||
|
Specifies ripping from the beginning of track 2 to the end of track 4.
|
||||||
|
.P
|
||||||
|
Again, don't forget to protect square brackets and preceeding hyphens from
|
||||||
|
the shell.
|
||||||
|
|
||||||
|
.SH EXAMPLES
|
||||||
|
|
||||||
|
A few examples, protected from the shell:
|
||||||
|
.TP
|
||||||
|
Query only with exhaustive search for a drive and full reporting of autosense:
|
||||||
|
.P
|
||||||
|
cdparanoia -vsQ
|
||||||
|
.TP
|
||||||
|
Extract an entire disc, putting each track in a seperate file:
|
||||||
|
.P
|
||||||
|
cdparanoia -B
|
||||||
|
.TP
|
||||||
|
Extract from track 1, time 0:30.12 to 1:10.00:
|
||||||
|
.P
|
||||||
|
cdparanoia "1[:30.12]-1[1:10]"
|
||||||
|
.TP
|
||||||
|
Extract from the beginning of the disc up to track 3:
|
||||||
|
.P
|
||||||
|
cdparanoia -- "-3"
|
||||||
|
.TP
|
||||||
|
The "--" above is to distinguish "-3" from an option flag.
|
||||||
|
.SH OUTPUT
|
||||||
|
|
||||||
|
The output file argument is optional; if it is not specified,
|
||||||
|
cdparanoia will output samples to one of
|
||||||
|
.BR cdda.wav ", " cdda.aifc ", or " cdda.raw
|
||||||
|
depending on whether
|
||||||
|
.BR \-w ", " \-a ", " \-r " or " \-R " is used (" \-w
|
||||||
|
is the implicit default). The output file argument of
|
||||||
|
.B \-
|
||||||
|
specifies standard output; all data formats may be piped.
|
||||||
|
|
||||||
|
.SH ACKNOWLEDGEMENTS
|
||||||
|
Cdparanoia sprang from and once drew heavily from the interface of
|
||||||
|
Heiko Eissfeldt's (heiko@colossus.escape.de) 'cdda2wav'
|
||||||
|
package. Cdparanoia would not have happened without it.
|
||||||
|
.P
|
||||||
|
Joerg Schilling has also contributed SCSI expertise through his
|
||||||
|
generic SCSI transport library.
|
||||||
|
.P
|
||||||
|
.SH AUTHOR
|
||||||
|
Monty <monty@xiph.org>
|
||||||
|
.P
|
||||||
|
Cdparanoia's homepage may be found at:
|
||||||
|
http://www.xiph.org/paranoia/
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
.TH CDRUSTANOIA 1 "11 Sep 2008"
|
||||||
|
.SH NAME
|
||||||
|
cdrustanoia 10.2 (Paranoia release III) \- an audio CD reading utility which includes extra data verification features
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B cdrustanoia
|
||||||
|
.RB [ options ]
|
||||||
|
.B span
|
||||||
|
.RB [ outfile ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B cdrustanoia
|
||||||
|
retrieves audio tracks from CDDA capable CDROM drives. The data can
|
||||||
|
be saved to a file or directed to standard output in WAV, AIFF, AIFF-C
|
||||||
|
or raw format. Most ATAPI, SCSI and several proprietary CDROM drive
|
||||||
|
makes are supported;
|
||||||
|
.B cdrustanoia
|
||||||
|
can determine if the target drive is CDDA capable.
|
||||||
|
.P
|
||||||
|
In addition to simple reading,
|
||||||
|
.B cdrustanoia
|
||||||
|
adds extra-robust data verification, synchronization, error handling
|
||||||
|
and scratch reconstruction capability.
|
||||||
|
.SH OPTIONS
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-A --analyze-drive
|
||||||
|
Run and log a complete analysis of drive caching, timing and reading behavior;
|
||||||
|
verifies that cdparanoia is correctly modelling a sprcific drive's cache and
|
||||||
|
read behavior. Implies -vQL.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-v --verbose
|
||||||
|
Be absurdly verbose about the autosensing and reading process. Good
|
||||||
|
for setup and debugging.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-q --quiet
|
||||||
|
Do not print any progress or error information during the reading process.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-e --stderr-progress
|
||||||
|
Force output of progress information to stderr (for wrapper scripts).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-l --log-summary [file]
|
||||||
|
Save result summary to file, default filename cdparanoia.log.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-L --log-debug [file]
|
||||||
|
Save detailed device autosense and debugging output to a file, default filename cdparanoia.log.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-V --version
|
||||||
|
Print the program version and quit.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-Q --query
|
||||||
|
Perform CDROM drive autosense, query and print the CDROM table of
|
||||||
|
contents, then quit.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-s --search-for-drive
|
||||||
|
Forces a complete search for a cdrom drive, even if the /dev/cdrom link exists.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-h --help
|
||||||
|
Print a brief synopsis of
|
||||||
|
.B cdparanoia
|
||||||
|
usage and options.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-p --output-raw
|
||||||
|
Output headerless data as raw 16 bit PCM data with interleaved samples in host byte order. To force little or big endian byte order, use
|
||||||
|
.B \-r
|
||||||
|
or
|
||||||
|
.B \-R
|
||||||
|
as described below.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-r --output-raw-little-endian
|
||||||
|
Output headerless data as raw 16 bit PCM data with interleaved samples in LSB first byte order.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-R --output-raw-big-endian
|
||||||
|
Output headerless data as raw 16 bit PCM data with interleaved samples in MSB first byte order.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-w --output-wav
|
||||||
|
Output data in Micro$oft RIFF WAV format (note that WAV data is always
|
||||||
|
LSB first byte order).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-f --output-aiff
|
||||||
|
Output data in Apple AIFF format (note that AIFC data is
|
||||||
|
always in MSB first byte order).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-a --output-aifc
|
||||||
|
Output data in uncompressed Apple AIFF-C format (note that AIFF-C data is
|
||||||
|
always in MSB first byte order).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-B --batch "
|
||||||
|
|
||||||
|
Cdda2wav-style batch output flag; cdparanoia will split the output
|
||||||
|
into multiple files at track boundaries. Output file names are
|
||||||
|
prepended with 'track#.'
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-c --force-cdrom-little-endian
|
||||||
|
Some CDROM drives misreport their endianness (or do not report it at
|
||||||
|
all); it's possible that cdparanoia will guess wrong. Use
|
||||||
|
.B \-c
|
||||||
|
to force cdparanoia to treat the drive as a little endian device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-C --force-cdrom-big-endian
|
||||||
|
As above but force cdparanoia to treat the drive as a big endian device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-n --force-default-sectors " n
|
||||||
|
Force the interface backend to do atomic reads of
|
||||||
|
.B n
|
||||||
|
sectors per read. This number can be misleading; the kernel will often
|
||||||
|
split read requests into multiple atomic reads (the automated Paranoia
|
||||||
|
code is aware of this) or allow reads only wihin a restricted size
|
||||||
|
range.
|
||||||
|
.B This option should generally not be used.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-d --force-cdrom-device " device
|
||||||
|
Force the interface backend to read from
|
||||||
|
.B device
|
||||||
|
rather than the first readable CDROM drive it finds. This can be used
|
||||||
|
to specify devices of any valid interface type (ATAPI, SCSI or
|
||||||
|
proprietary).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-k --force-cooked-device " device
|
||||||
|
This option forces use of the old 'cooked ioctl' kernel
|
||||||
|
interface with the specified cdrom device. The cooked ioctl interface
|
||||||
|
is obsolete in Linux 2.6 if it is present at all.
|
||||||
|
.B \-k
|
||||||
|
cannot be used
|
||||||
|
with
|
||||||
|
.B \-d
|
||||||
|
or
|
||||||
|
.BR \-g .
|
||||||
|
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-g --force-generic-device " device
|
||||||
|
This option forces use of the old 'generic scsi' (sg) kernel
|
||||||
|
interface with the specified generic scsi device.
|
||||||
|
.B \-g
|
||||||
|
cannot be used with
|
||||||
|
.BR \-k .
|
||||||
|
.B \-g
|
||||||
|
may be used with
|
||||||
|
.B \-d
|
||||||
|
to explicitly set both the SCSI cdrom and
|
||||||
|
generic (sg) devices seperately. This option is only useful on
|
||||||
|
obsolete SCSI setups and when using the generic scsi (sg) driver.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-S --force-read-speed " number
|
||||||
|
Use this option explicitly to set the read rate of the CD drive (where
|
||||||
|
supported). This can reduce underruns on machines with slow disks, or
|
||||||
|
which are low on memory.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-t --toc-offset " number
|
||||||
|
Use this option to force the entire disc LBA addressing to shift by
|
||||||
|
the given amount; the value is added to the beginning offsets in the
|
||||||
|
TOC. This can be used to shift track boundaries for the whole disc
|
||||||
|
manually on sector granularity. The next option does something
|
||||||
|
similar...
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-T --toc-bias "
|
||||||
|
Some drives (usually random Toshibas) report the actual track
|
||||||
|
beginning offset values in the TOC, but then treat the beginning of
|
||||||
|
track 1 index 1 as sector 0 for all read operations. This results in
|
||||||
|
every track seeming to start too late (losing a bit of the beginning
|
||||||
|
and catching a bit of the next track).
|
||||||
|
.B \-T
|
||||||
|
accounts for this behavior. Note that this option will cause
|
||||||
|
cdparanoia to attempt to read sectors before or past the known user
|
||||||
|
data area of the disc, resulting in read errors at disc edges on most
|
||||||
|
drives and possibly even hard lockups on some buggy hardware.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-O --sample-offset " number
|
||||||
|
Use this option to force the entire disc to shift sample position
|
||||||
|
output by the given amount; This can be used to shift track boundaries
|
||||||
|
for the whole disc manually on sample granularity. Note that this will
|
||||||
|
cause cdparanoia to attempt to read partial sectors before or past the
|
||||||
|
known user data area of the disc, probably causing read errors on most
|
||||||
|
drives and possibly even hard lockups on some buggy hardware.
|
||||||
|
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-Z --disable-paranoia
|
||||||
|
Disable
|
||||||
|
.B all
|
||||||
|
data verification and correction features. When using -Z, cdparanoia
|
||||||
|
reads data exactly as would cdda2wav with an overlap setting of zero.
|
||||||
|
This option implies that
|
||||||
|
.B \-Y
|
||||||
|
is active.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-z --never-skip[=max_retries]
|
||||||
|
Do not accept any skips; retry forever if needed. An optional maximum
|
||||||
|
number of retries can be specified; for comparison, default without -z is
|
||||||
|
currently 20.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-Y --disable-extra-paranoia
|
||||||
|
Disables intra-read data verification; only overlap checking at read
|
||||||
|
boundaries is performed. It can wedge if errors occur in the attempted overlap area. Not recommended.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-X --abort-on-skip
|
||||||
|
If the read skips due to imperfect data, a scratch, whatever, abort reading this track. If output is to a file, delete the partially completed file.
|
||||||
|
|
||||||
|
.SH OUTPUT SMILIES
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-)
|
||||||
|
Normal operation, low/no jitter
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-|
|
||||||
|
Normal operation, considerable jitter
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-/
|
||||||
|
Read drift
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-P
|
||||||
|
Unreported loss of streaming in atomic read operation
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
8-|
|
||||||
|
Finding read problems at same point during reread; hard to correct
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-0
|
||||||
|
SCSI/ATAPI transport error
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:-(
|
||||||
|
Scratch detected
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
;-(
|
||||||
|
Gave up trying to perform a correction
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
8-X
|
||||||
|
Aborted read due to known, uncorrectable error
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
:^D
|
||||||
|
Finished extracting
|
||||||
|
|
||||||
|
.SH PROGRESS BAR SYMBOLS
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
<space>
|
||||||
|
No corrections needed
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
-
|
||||||
|
Jitter correction required
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
+
|
||||||
|
Unreported loss of streaming/other error in read
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
!
|
||||||
|
Errors found after stage 1 correction; the drive is making the
|
||||||
|
same error through multiple re-reads, and cdparanoia is having trouble
|
||||||
|
detecting them.
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
e
|
||||||
|
SCSI/ATAPI transport error (corrected)
|
||||||
|
.TP
|
||||||
|
.B
|
||||||
|
V
|
||||||
|
Uncorrected error/skip
|
||||||
|
|
||||||
|
.SH SPAN ARGUMENT
|
||||||
|
|
||||||
|
The span argument specifies which track, tracks or subsections of
|
||||||
|
tracks to read. This argument is required.
|
||||||
|
.B NOTE:
|
||||||
|
Unless the span is a simple number, it's generally a good idea to
|
||||||
|
quote the span argument to protect it from the shell.
|
||||||
|
.P
|
||||||
|
The span argument may be a simple track number or an offset/span
|
||||||
|
specification. The syntax of an offset/span takes the rough form:
|
||||||
|
.P
|
||||||
|
1[ww:xx:yy.zz]-2[aa:bb:cc.dd]
|
||||||
|
.P
|
||||||
|
Here, 1 and 2 are track numbers; the numbers in brackets provide a
|
||||||
|
finer grained offset within a particular track. [aa:bb:cc.dd] is in
|
||||||
|
hours/minutes/seconds/sectors format. Zero fields need not be
|
||||||
|
specified: [::20], [:20], [20], [20.], etc, would be interpreted as
|
||||||
|
twenty seconds, [10:] would be ten minutes, [.30] would be thirty
|
||||||
|
sectors (75 sectors per second).
|
||||||
|
.P
|
||||||
|
When only a single offset is supplied, it is interpreted as a starting
|
||||||
|
offset and ripping will continue to the end of the track. If a single
|
||||||
|
offset is preceeded or followed by a hyphen, the implicit missing
|
||||||
|
offset is taken to be the start or end of the disc, respectively. Thus:
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B 1:[20.35]
|
||||||
|
Specifies ripping from track 1, second 20, sector 35 to the end of
|
||||||
|
track 1.
|
||||||
|
.TP
|
||||||
|
.B 1:[20.35]-
|
||||||
|
Specifies ripping from 1[20.35] to the end of the disc
|
||||||
|
.TP
|
||||||
|
.B \-2
|
||||||
|
Specifies ripping from the beginning of the disc up to (and including) track 2
|
||||||
|
.TP
|
||||||
|
.B \-2:[30.35]
|
||||||
|
Specifies ripping from the beginning of the disc up to 2:[30.35]
|
||||||
|
.TP
|
||||||
|
.B 2-4
|
||||||
|
Specifies ripping from the beginning of track 2 to the end of track 4.
|
||||||
|
.P
|
||||||
|
Again, don't forget to protect square brackets and preceeding hyphens from
|
||||||
|
the shell.
|
||||||
|
|
||||||
|
.SH EXAMPLES
|
||||||
|
|
||||||
|
A few examples, protected from the shell:
|
||||||
|
.TP
|
||||||
|
Query only with exhaustive search for a drive and full reporting of autosense:
|
||||||
|
.P
|
||||||
|
cdparanoia -vsQ
|
||||||
|
.TP
|
||||||
|
Extract an entire disc, putting each track in a seperate file:
|
||||||
|
.P
|
||||||
|
cdparanoia -B
|
||||||
|
.TP
|
||||||
|
Extract from track 1, time 0:30.12 to 1:10.00:
|
||||||
|
.P
|
||||||
|
cdparanoia "1[:30.12]-1[1:10]"
|
||||||
|
.TP
|
||||||
|
Extract from the beginning of the disc up to track 3:
|
||||||
|
.P
|
||||||
|
cdparanoia -- "-3"
|
||||||
|
.TP
|
||||||
|
The "--" above is to distinguish "-3" from an option flag.
|
||||||
|
.SH OUTPUT
|
||||||
|
|
||||||
|
The output file argument is optional; if it is not specified,
|
||||||
|
cdparanoia will output samples to one of
|
||||||
|
.BR cdda.wav ", " cdda.aifc ", or " cdda.raw
|
||||||
|
depending on whether
|
||||||
|
.BR \-w ", " \-a ", " \-r " or " \-R " is used (" \-w
|
||||||
|
is the implicit default). The output file argument of
|
||||||
|
.B \-
|
||||||
|
specifies standard output; all data formats may be piped.
|
||||||
|
|
||||||
|
.SH ACKNOWLEDGEMENTS
|
||||||
|
Cdparanoia sprang from and once drew heavily from the interface of
|
||||||
|
Heiko Eissfeldt's (heiko@colossus.escape.de) 'cdda2wav'
|
||||||
|
package. Cdparanoia would not have happened without it.
|
||||||
|
.P
|
||||||
|
Joerg Schilling has also contributed SCSI expertise through his
|
||||||
|
generic SCSI transport library.
|
||||||
|
.P
|
||||||
|
.SH AUTHOR
|
||||||
|
Monty <monty@xiph.org>
|
||||||
|
.P
|
||||||
|
Cdparanoia's homepage may be found at:
|
||||||
|
http://www.xiph.org/paranoia/
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/* Compile-time check that the installed headers expose the cdparanoia 10.2
|
||||||
|
* API and the expected 64-bit Linux ABI layout. */
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "cdda_interface.h"
|
||||||
|
#include "cdda_paranoia.h"
|
||||||
|
|
||||||
|
_Static_assert(sizeof(TOC) == 8, "TOC ABI");
|
||||||
|
_Static_assert(offsetof(TOC, bFlags) == 0, "TOC flags");
|
||||||
|
_Static_assert(offsetof(TOC, bTrack) == 1, "TOC track");
|
||||||
|
_Static_assert(offsetof(TOC, dwStartSector) == 4, "TOC sector");
|
||||||
|
_Static_assert(sizeof(cdrom_drive) == 1136, "cdrom_drive ABI");
|
||||||
|
_Static_assert(offsetof(cdrom_drive, disc_toc) == 64, "TOC array");
|
||||||
|
_Static_assert(offsetof(cdrom_drive, private) == 952, "private pointer");
|
||||||
|
_Static_assert(offsetof(cdrom_drive, sigset) == 1008, "sigset");
|
||||||
|
|
||||||
|
static void use_api(cdrom_drive *d, cdrom_paranoia *p)
|
||||||
|
{
|
||||||
|
(void)cdda_version();
|
||||||
|
(void)paranoia_version();
|
||||||
|
(void)cdda_open(d);
|
||||||
|
(void)cdda_read(d, 0, 0, 1);
|
||||||
|
(void)paranoia_init(d);
|
||||||
|
paranoia_modeset(p, PARANOIA_MODE_FULL);
|
||||||
|
(void)paranoia_read(p, 0);
|
||||||
|
paranoia_free(p);
|
||||||
|
(void)cdda_close(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
cdrom_drive *d = 0;
|
||||||
|
cdrom_paranoia *p = 0;
|
||||||
|
use_api(d, p);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user