├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Logic - Ballin [Bass Boosted].mp3 ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── kick.mp3 ├── kick.wav ├── pom.xml └── src ├── main └── java │ └── com │ └── goxr3plus │ └── streamplayer │ ├── application │ ├── AnotherDemoApplication.java │ ├── AnotherMain.java │ ├── AnotherStreamPlayerListener.java │ ├── DemoApplication.java │ └── Main.java │ ├── enums │ ├── AudioType.java │ └── Status.java │ ├── stream │ ├── DataSource.java │ ├── FileDataSource.java │ ├── Outlet.java │ ├── StreamDataSource.java │ ├── StreamPlayer.java │ ├── StreamPlayerEvent.java │ ├── StreamPlayerEventLauncher.java │ ├── StreamPlayerException.java │ ├── StreamPlayerInterface.java │ ├── StreamPlayerListener.java │ ├── ThreadFactoryWithNamePrefix.java │ └── UrlDataSource.java │ └── tools │ ├── IOInfo.java │ └── TimeTool.java └── test └── java └── com └── goxr3plus └── streamplayer ├── stream ├── SourceDataLineTest.java ├── StreamPlayerEventTest.java ├── StreamPlayerFutureImprovementTest.java ├── StreamPlayerMethodsTest.java └── StreamPlayerTest.java └── tools ├── IOInfoTest.java └── TimeToolTest.java /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: GOXR3PLUSTUDIO 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: goxr3plusstudio 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: https://www.paypal.me/GOXR3PLUSCOMPANY 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | /target/ 24 | 25 | *.iml 26 | *.DS_Store 27 | 28 | *.classpath 29 | *.project 30 | *.settings 31 | *.settings/* 32 | *.idea 33 | *.idea/* 34 | spartanizer.xml 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Logic - Ballin [Bass Boosted].mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goxr3plus/java-stream-player/09ddefe497a894dc9462d330792a9f09f69e2c77/Logic - Ballin [Bass Boosted].mp3 -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. 4 | List any dependencies that are required for this change. 5 | 6 | 10 | 11 | 12 | 13 | 14 | **What kind of change does this PR introduce?** (check at least one) 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 19 | - [ ] Code style update 20 | - [ ] Refactor 21 | - [ ] Build-related changes 22 | - [ ] This change requires a documentation update 23 | - [ ] Other, please describe: 24 | 25 | **Does this PR introduce a breaking change?** (check one) 26 | 27 | - [ ] Yes 28 | - [ ] No 29 | 30 | # Has This Been Tested? 31 | 32 | - [ ] Yes 33 | - [ ] No 34 | 35 | # Checklist: 36 | 37 | - [X] My code follows the style guidelines of this project 38 | - [X] I have performed a self-review of my own code 39 | - [X] I have commented my code, particularly in hard-to-understand areas 40 | - [X] I have made corresponding changes to the documentation 41 | - [X] My changes generate no new warnings 42 | - [X] I have added tests that prove my fix is effective or that my feature works 43 | - [X] New and existing unit tests pass locally with my changes 44 | - [X] Any dependent changes have been merged and published in downstream modules 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q3WBIC) 2 | 3 | --- 4 | 5 |

Java Stream Player ( Library )

6 |

7 | 🎶 8 |

9 |

10 | 11 | Java Audio Controller Library with (skip,skipTo,start,stop,pause,play,restart) 12 | This is the next version of JavaZoom BasicPlayer 13 | 14 |

15 | 16 | --- 17 | 18 | [![Latest Version](https://img.shields.io/github/release/goxr3plus/java-stream-player.svg?style=flat-square)](https://github.com/goxr3plus/java-stream-player/releases) 19 | [![HitCount](http://hits.dwyl.io/goxr3plus/java-stream-player.svg)](http://hits.dwyl.io/goxr3plus/java-stream-player) 20 | Patreon donate button 21 | PayPal donate button 22 | 23 | ### What audio formats it supports? 24 | - **Fully Supported ✔️** 25 | - WAV 26 | - MP3 27 | - **Partially not full tested 🚧** 28 | - OGG VORBIS 29 | - FLAC 30 | - MONKEY's AUDIO 31 | - SPEEX 32 | - **Not Supported Yet ❌** 33 | - AAC 34 | - THEORA 35 | - ... all the others 36 | 37 | 38 | ### Step 1. Add the JitPack repository to your build file 39 | https://jitpack.io/private#goxr3plus/java-stream-player 40 | ``` XML 41 | 42 | 43 | jitpack.io 44 | https://jitpack.io 45 | 46 | 47 | ``` 48 | 49 | ### Step 2. Add the dependency 50 | ``` XML 51 | 52 | com.github.goxr3plus 53 | java-stream-player 54 | 9.0.4 55 | 56 | ``` 57 | 58 | Example usage : 59 | 60 | ``` JAVA 61 | import java.io.File; 62 | import java.util.Map; 63 | 64 | import com.goxr3plus.streamplayer.enums.Status; 65 | import com.goxr3plus.streamplayer.stream.StreamPlayer; 66 | import com.goxr3plus.streamplayer.stream.StreamPlayerListener; 67 | import com.goxr3plus.streamplayer.stream.StreamPlayerEvent; 68 | import com.goxr3plus.streamplayer.stream.StreamPlayerException; 69 | 70 | /** 71 | * @author GOXR3PLUS 72 | * 73 | */ 74 | public class Main extends StreamPlayer implements StreamPlayerListener { 75 | 76 | private final String audioAbsolutePath = "Logic - Ballin [Bass Boosted].mp3"; 77 | 78 | /** 79 | * Constructor 80 | */ 81 | public Main() { 82 | 83 | try { 84 | 85 | // Register to the Listeners 86 | addStreamPlayerListener(this); 87 | 88 | // Open a File 89 | // open(new File("...")) //..Here must be the file absolute path 90 | // open(INPUTSTREAM) 91 | // open(AUDIOURL) 92 | 93 | // Example 94 | open(new File(audioAbsolutePath)); 95 | 96 | // Seek by bytes 97 | // seekBytes(500000L); 98 | 99 | // Seek +x seconds starting from the current position 100 | seekSeconds(15); 101 | seekSeconds(15); 102 | 103 | /* Seek starting from the begginning of the audio */ 104 | // seekTo(200); 105 | 106 | // Play it 107 | play(); 108 | // pause(); 109 | 110 | } catch (final Exception ex) { 111 | ex.printStackTrace(); 112 | } 113 | 114 | } 115 | 116 | @Override 117 | public void opened(final Object dataSource, final Map properties) { 118 | 119 | } 120 | 121 | @Override 122 | public void progress(final int nEncodedBytes, final long microsecondPosition, final byte[] pcmData, 123 | final Map properties) { 124 | 125 | // System.out.println("Encoded Bytes : " + nEncodedBytes); 126 | 127 | // Current time position in seconds:) by GOXR3PLUS STUDIO 128 | // This is not the more precise way ... 129 | // in XR3Player i am using different techniques . 130 | // https://github.com/goxr3plus/XR3Player 131 | // Just for demostration purposes :) 132 | // I will add more advanced techniques with milliseconds , microseconds , hours 133 | // and minutes soon 134 | 135 | // .MP3 OR .WAV 136 | final String extension = "mp3"; // THE SAMPLE Audio i am using is .MP3 SO ... :) 137 | 138 | long totalBytes = getTotalBytes(); 139 | if ("mp3".equals(extension) || "wav".equals(extension)) { 140 | 141 | // Calculate the progress until now 142 | double progress = (nEncodedBytes > 0 && totalBytes > 0) ? (nEncodedBytes * 1.0f / totalBytes * 1.0f) 143 | : -1.0f; 144 | // System.out.println(progress*100+"%"); 145 | 146 | System.out.println("Seconds : " + (int) (microsecondPosition / 1000000) + " s " + "Progress: [ " 147 | + progress * 100 + " ] %"); 148 | 149 | // .WHATEVER MUSIC FILE* 150 | } else { 151 | // System.out.println("Current time is : " + (int) (microsecondPosition / 152 | // 1000000) + " seconds"); 153 | } 154 | 155 | } 156 | 157 | @Override 158 | public void statusUpdated(final StreamPlayerEvent streamPlayerEvent) { 159 | 160 | // Player status 161 | final Status status = streamPlayerEvent.getPlayerStatus(); 162 | // System.out.println(streamPlayerEvent.getPlayerStatus()); 163 | 164 | // Examples 165 | 166 | if (status == Status.OPENED) { 167 | 168 | } else if (status == Status.OPENING) { 169 | 170 | } else if (status == Status.RESUMED) { 171 | 172 | } else if (status == Status.PLAYING) { 173 | 174 | } else if (status == Status.STOPPED) { 175 | 176 | } else if (status == Status.SEEKING) { 177 | 178 | } else if (status == Status.SEEKED) { 179 | 180 | } 181 | 182 | // etc... SEE XR3PLAYER https://github.com/goxr3plus/XR3Player for advanced 183 | // examples 184 | } 185 | 186 | public static void main(final String[] args) { 187 | new Main(); 188 | } 189 | 190 | } 191 | ``` 192 | 193 | 194 | ## Java Audio Tutorials and API's by GOXR3PLUS STUDIO 195 | - **Spectrum Analyzers** 196 | - [Java-Audio-Wave-Spectrum-API](https://github.com/goxr3plus/Java-Audio-Wave-Spectrum-API) 197 | ![image](https://github.com/goxr3plus/Java-Audio-Wave-Spectrum-API/raw/master/images/Screenshot_2.jpg?raw=true) 198 | - [Jave Spectrum Analyzers from Audio](https://github.com/goxr3plus/Java-Spectrum-Analyser-Tutorials) 199 | - [Capture Audio from Microphone and make complex spectrum analyzers](https://github.com/goxr3plus/Java-Microphone-Audio-Spectrum-Analyzers-Tutorial) 200 | 201 | - **Java multiple audio formats player** 202 | - [Java-stream-player](https://github.com/goxr3plus/java-stream-player) 203 | 204 | - **Speech Recognition/Translation/Synthenizers** 205 | - [Java Speech Recognition/Translation/Synthesizer based on Google Cloud Services](https://github.com/goxr3plus/java-google-speech-api) 206 | - [Java-Speech-Recognizer-Tutorial--Calculator](https://github.com/goxr3plus/Java-Speech-Recognizer-Tutorial--Calculator) 207 | - [Java+MaryTTS=Java Text To Speech](https://github.com/goxr3plus/Java-Text-To-Speech-Tutorial) 208 | - [Java Speech Recognition Program based on Google Cloud Services ](https://github.com/goxr3plus/Java-Google-Speech-Recognizer) 209 | - [Java Google Text To Speech](https://github.com/goxr3plus/Java-Google-Text-To-Speech) 210 | - [Full Google Translate Support using Java](https://github.com/goxr3plus/java-google-translator) 211 | - [Professional Java Google Desktop Translator](https://github.com/goxr3plus/Java-Google-Desktop-Translator) 212 | 213 | 214 | 215 | --- 216 | 217 | ### Looking for a ffmpeg wrapper in Java ? 218 | > Check this -> [jave2](https://github.com/a-schild/jave2) , currently updating it :) 219 | 220 | --- 221 | 222 | ### Originally being developed for [XR3Player Application](https://github.com/goxr3plus/XR3Player) 223 | 224 | Are you curious on how to make spectrum analysers in Java? Well the below tutorials plus the above code are the solution :) 225 | 226 | I hope you enjoy these tutorials and actually you can watch the youtube videos about them below: 227 | 228 | 229 | [![Java Spectrum Analyser Tutorial](http://img.youtube.com/vi/lwlioga8Row/0.jpg)](https://www.youtube.com/watch?v=lwlioga8Row) 230 | 231 | -------------------------------------------------------------------------------- /kick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goxr3plus/java-stream-player/09ddefe497a894dc9462d330792a9f09f69e2c77/kick.mp3 -------------------------------------------------------------------------------- /kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goxr3plus/java-stream-player/09ddefe497a894dc9462d330792a9f09f69e2c77/kick.wav -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.goxr3plus 8 | java-stream-player 9 | 10.0.0 10 | 11 | 12 | 13 | 1.8 14 | 1.8 15 | UTF-8 16 | 5.1.1 17 | 18 | 19 | 20 | ${project.basedir}/src/main/java 21 | 22 | 23 | ${project.basedir}/src/main/resources 24 | 25 | 26 | ${project.basedir}/src/test/java 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-source-plugin 31 | 3.0.1 32 | 33 | 34 | attach-sources 35 | 36 | jar 37 | 38 | 39 | 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-javadoc-plugin 44 | 3.0.0 45 | 46 | 47 | attach-javadocs 48 | 49 | jar 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.7.0 58 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-jar-plugin 67 | 3.1.1 68 | 69 | 70 | 71 | com.goxr3plus.streamplayer 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | jitpack.io 82 | https://jitpack.io 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | com.googlecode.soundlibs 93 | mp3spi 94 | 1.9.5.4 95 | 96 | 97 | 101 | com.googlecode.soundlibs 102 | tritonus-share 103 | 104 | 105 | 106 | 107 | 108 | org.jflac 109 | jflac-codec 110 | 1.5.2 111 | 112 | 113 | 114 | com.github.trilarion 115 | vorbis-support 116 | 1.1.0 117 | 118 | 119 | 120 | com.googlecode.soundlibs 121 | tritonus-all 122 | 0.3.7.2 123 | 124 | 125 | com.googlecode.soundlibs 126 | tritonus-share 127 | 128 | 129 | 130 | 131 | 132 | 133 | com.github.goxr3plus 134 | jaudiotagger 135 | 2.2.7 136 | 137 | 138 | 139 | commons-io 140 | commons-io 141 | 2.7 142 | 143 | 144 | 145 | 146 | org.junit.jupiter 147 | junit-jupiter-engine 148 | ${junit.version} 149 | test 150 | 151 | 152 | 153 | 154 | org.junit.jupiter 155 | junit-jupiter-api 156 | ${junit.version} 157 | test 158 | 159 | 160 | 161 | 162 | org.junit.jupiter 163 | junit-jupiter-params 164 | ${junit.version} 165 | test 166 | 167 | 168 | 169 | 170 | org.mockito 171 | mockito-junit-jupiter 172 | 3.0.0 173 | test 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/application/AnotherDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.application; 2 | 3 | import com.goxr3plus.streamplayer.stream.StreamPlayerInterface; 4 | import com.goxr3plus.streamplayer.stream.StreamPlayerListener; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * @author GOXR3PLUS 10 | * 11 | */ 12 | public class AnotherDemoApplication { 13 | 14 | private static final String AUDIO_FILE_NAME = "Logic - Ballin [Bass Boosted].mp3"; 15 | 16 | private final StreamPlayerInterface streamPlayer; 17 | private final StreamPlayerListener listener; 18 | 19 | public AnotherDemoApplication(StreamPlayerInterface streamPlayer) { 20 | this.streamPlayer = streamPlayer; 21 | this.listener = new AnotherStreamPlayerListener(AUDIO_FILE_NAME, streamPlayer); 22 | 23 | } 24 | 25 | 26 | void start() { 27 | try { 28 | 29 | // Register to the Listeners 30 | streamPlayer.addStreamPlayerListener(listener); 31 | 32 | // Open a File 33 | // open(new File("...")) //..Here must be the file absolute path 34 | // open(INPUTSTREAM) 35 | // open(AUDIOURL) 36 | 37 | // Example 38 | streamPlayer.open(new File(AUDIO_FILE_NAME)); 39 | 40 | //Seek by bytes 41 | //seekBytes(500000L); 42 | 43 | //Seek +x seconds starting from the current position 44 | streamPlayer.seekSeconds(15); // forward 15 seconds 45 | streamPlayer.seekSeconds(15); // forward 15 seconds again 46 | 47 | /* Seek starting from the begginning of the audio */ 48 | //seekTo(200); 49 | 50 | // Play it 51 | streamPlayer.play(); 52 | //pause(); 53 | 54 | } catch (final Exception ex) { 55 | ex.printStackTrace(); 56 | } 57 | } 58 | 59 | 60 | 61 | 62 | private String getExtension(String audioFileName) { 63 | return audioFileName.split("\\.(?=[^.]+$)")[1]; 64 | } 65 | 66 | 67 | // public static void main(final String[] args) { 68 | // new AnotherDemoApplication(); 69 | // } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/application/AnotherMain.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.application; 2 | 3 | import com.goxr3plus.streamplayer.stream.StreamPlayer; 4 | import com.goxr3plus.streamplayer.stream.StreamPlayerInterface; 5 | 6 | public class AnotherMain { 7 | public static void main(String[] args) { 8 | 9 | final StreamPlayerInterface streamPlayer = new StreamPlayer(); 10 | final AnotherDemoApplication application = new AnotherDemoApplication(streamPlayer); 11 | application.start(); 12 | 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/application/AnotherStreamPlayerListener.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.application; 2 | 3 | import com.goxr3plus.streamplayer.enums.Status; 4 | import com.goxr3plus.streamplayer.stream.StreamPlayer; 5 | import com.goxr3plus.streamplayer.stream.StreamPlayerEvent; 6 | import com.goxr3plus.streamplayer.stream.StreamPlayerInterface; 7 | import com.goxr3plus.streamplayer.stream.StreamPlayerListener; 8 | 9 | import java.util.Map; 10 | 11 | class AnotherStreamPlayerListener implements StreamPlayerListener { 12 | 13 | private final String audioFileName; 14 | private final StreamPlayerInterface streamPlayer; 15 | 16 | 17 | public AnotherStreamPlayerListener(String audioFileName, StreamPlayerInterface streamPlayer) { 18 | this.audioFileName = audioFileName; 19 | this.streamPlayer = streamPlayer; 20 | } 21 | 22 | /** 23 | * It is called when the StreamPlayer open(Object object) method is called. 24 | * 25 | * @param dataSource the data source 26 | * @param properties the properties 27 | */ 28 | @Override 29 | public void opened(Object dataSource, Map properties) { 30 | System.out.println("The StreamPlayer was opened."); 31 | } 32 | 33 | /** 34 | * Is called several times per second when StreamPlayer run method is 35 | * running. 36 | * 37 | * @param nEncodedBytes the n encoded bytes 38 | * @param microsecondPosition the microsecond position 39 | * @param pcmData the pcm data 40 | * @param properties the properties 41 | */ 42 | @Override 43 | public void progress(int nEncodedBytes, long microsecondPosition, byte[] pcmData, Map properties) { 44 | 45 | String extension = getExtension(audioFileName); 46 | 47 | 48 | long totalBytes = streamPlayer.getTotalBytes(); 49 | if ("mp3".equals(extension) || "wav".equals(extension)) { 50 | 51 | // Calculate the progress until now 52 | double progress = (nEncodedBytes > 0 && totalBytes > 0) 53 | ? ((double) nEncodedBytes / (double)totalBytes ) 54 | : -1.0d; 55 | 56 | // TODO: Understand why the nEncodedBytes doesn't update each call of progress. 57 | 58 | System.out.println("Seconds : " + (int) (microsecondPosition / 1000000) + " s " + "Progress: [ " + progress * 100 + " ] %"); 59 | final String message = String.format("Time: %.1f s, Progress: %.2f %%, encoded %d of %d bytes.", 60 | microsecondPosition / 1000000d, 61 | progress * 100d, 62 | nEncodedBytes, 63 | totalBytes); 64 | System.out.println(message); 65 | } 66 | 67 | 68 | } 69 | 70 | /** 71 | * Is called every time the status of the StreamPlayer changes. 72 | * 73 | * @param event the event 74 | */ 75 | @Override 76 | public void statusUpdated(StreamPlayerEvent event) { 77 | // Player status 78 | final Status status = event.getPlayerStatus(); 79 | 80 | // Do different things depending on the status. 81 | // See XR3PLAYER https://github.com/goxr3plus/XR3Player for advanced examples 82 | 83 | } 84 | 85 | private String getExtension(String audioFileName) { 86 | return audioFileName.split("\\.(?=[^.]+$)")[1]; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/application/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.application; 2 | 3 | import com.goxr3plus.streamplayer.enums.Status; 4 | import com.goxr3plus.streamplayer.stream.StreamPlayer; 5 | import com.goxr3plus.streamplayer.stream.StreamPlayerEvent; 6 | import com.goxr3plus.streamplayer.stream.StreamPlayerListener; 7 | 8 | import java.io.File; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author GOXR3PLUS 13 | * 14 | */ 15 | public class DemoApplication extends StreamPlayer implements StreamPlayerListener { 16 | 17 | private static final String AUDIO_FILE_NAME = "Logic - Ballin [Bass Boosted].mp3"; 18 | 19 | 20 | void start() { 21 | try { 22 | 23 | // Register to the Listeners 24 | addStreamPlayerListener(this); 25 | 26 | // Open a File 27 | // open(new File("...")) //..Here must be the file absolute path 28 | // open(INPUTSTREAM) 29 | // open(AUDIOURL) 30 | 31 | // Example 32 | open(new File(AUDIO_FILE_NAME)); 33 | 34 | //Seek by bytes 35 | //seekBytes(500000L); 36 | 37 | //Seek +x seconds starting from the current position 38 | seekSeconds(15); // forward 15 seconds 39 | seekSeconds(15); // forward 15 seconds again 40 | 41 | /* Seek starting from the begginning of the audio */ 42 | //seekTo(200); 43 | 44 | // Play it 45 | play(); 46 | //pause(); 47 | 48 | } catch (final Exception ex) { 49 | ex.printStackTrace(); 50 | } 51 | } 52 | 53 | @Override 54 | public void opened(final Object dataSource, final Map properties) { 55 | 56 | } 57 | 58 | @Override 59 | public void progress(final int nEncodedBytes, final long microsecondPosition, final byte[] pcmData,final Map properties) { 60 | 61 | // System.out.println("Encoded Bytes : " + nEncodedBytes); 62 | 63 | // Current time position in seconds:) by GOXR3PLUS STUDIO 64 | // This is not the more precise way ... 65 | // in XR3Player i am using different techniques . 66 | //https://github.com/goxr3plus/XR3Player 67 | // Just for demostration purposes :) 68 | // I will add more advanced techniques with milliseconds , microseconds , hours 69 | // and minutes soon 70 | 71 | // .MP3 OR .WAV 72 | //THE SAMPLE Audio i am using is .MP3 SO ... :) 73 | String extension = getExtension(AUDIO_FILE_NAME); 74 | 75 | 76 | long totalBytes = getTotalBytes(); 77 | if ("mp3".equals(extension) || "wav".equals(extension)) { 78 | 79 | // Calculate the progress until now 80 | double progress = (nEncodedBytes > 0 && totalBytes > 0) 81 | ? (nEncodedBytes * 1.0f / totalBytes * 1.0f) 82 | : -1.0f; 83 | 84 | 85 | System.out.println("Seconds : " + (int) (microsecondPosition / 1000000) + " s " + "Progress: [ " + progress * 100 + " ] %"); 86 | 87 | 88 | // .WHATEVER MUSIC FILE* 89 | } 90 | 91 | 92 | } 93 | 94 | private String getExtension(String audioFileName) { 95 | return audioFileName.split("\\.(?=[^.]+$)")[1]; 96 | } 97 | 98 | @Override 99 | public void statusUpdated(final StreamPlayerEvent streamPlayerEvent) { 100 | 101 | // Player status 102 | final Status status = streamPlayerEvent.getPlayerStatus(); 103 | 104 | // Do different things depending on the status. 105 | // See XR3PLAYER https://github.com/goxr3plus/XR3Player for advanced examples 106 | } 107 | 108 | public static void main(final String[] args) { 109 | new DemoApplication(); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/application/Main.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.application; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | 6 | final DemoApplication application = new DemoApplication(); 7 | application.start(); 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/enums/AudioType.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.enums; 2 | 3 | /** 4 | * Indicates the type of data. 5 | * 6 | * @author GOXR3PLUS 7 | */ 8 | public enum AudioType { 9 | 10 | /** 11 | * Audio is URL 12 | */ 13 | URL, 14 | /** 15 | * Audio is File 16 | */ 17 | FILE, 18 | /** 19 | * Audio is InputStream 20 | */ 21 | INPUTSTREAM, 22 | /** 23 | * Audio is UNKOWN 24 | */ 25 | UNKNOWN 26 | } -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/enums/Status.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | 15 | Also(warning!): 16 | 17 | 1)You are not allowed to sell this product to third party. 18 | 2)You can't change license and made it like you are the owner,author etc. 19 | 3)All redistributions of source code files must contain all copyright 20 | notices that are currently in this file, and this list of conditions without 21 | modification. 22 | */ 23 | 24 | 25 | package com.goxr3plus.streamplayer.enums; 26 | 27 | 28 | /** 29 | * Status of Stream Player. 30 | * 31 | * @author GOXR3PLUS 32 | */ 33 | public enum Status { 34 | 35 | /** 36 | * INITIALIZING 37 | */ 38 | INIT, 39 | 40 | /** UNKOWN STATUS. */ 41 | NOT_SPECIFIED, 42 | 43 | /** In the process of opening the AudioInputStream. */ 44 | OPENING, 45 | 46 | /** AudioInputStream is opened. */ 47 | OPENED, 48 | 49 | /** play event has been fired. */ 50 | PLAYING, 51 | 52 | /** player is stopped. */ 53 | STOPPED, 54 | 55 | /** player is paused. */ 56 | PAUSED, 57 | 58 | /** resume event is fired. */ 59 | RESUMED, 60 | 61 | /** player is in the process of seeking. */ 62 | SEEKING, 63 | 64 | /** 65 | * The player is buffering 66 | */ 67 | BUFFERING, 68 | 69 | /** seek work has been done. */ 70 | SEEKED, 71 | 72 | /** EOM stands for "END OF MEDIA". */ 73 | EOM, 74 | 75 | /** player pan has changed. */ 76 | PAN, 77 | 78 | /** player gain has changed. */ 79 | GAIN 80 | 81 | } -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/DataSource.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import javax.naming.OperationNotSupportedException; 4 | import javax.sound.sampled.AudioFileFormat; 5 | import javax.sound.sampled.AudioInputStream; 6 | import javax.sound.sampled.UnsupportedAudioFileException; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.net.URL; 11 | import java.time.Duration; 12 | 13 | public interface DataSource { 14 | static DataSource newDataSource(Object source) throws OperationNotSupportedException { 15 | if (source instanceof File) { 16 | return new FileDataSource((File) source); 17 | } 18 | if (source instanceof URL) { 19 | return new UrlDataSource((URL) source); 20 | } 21 | if (source instanceof InputStream) { 22 | return new StreamDataSource((InputStream) source); 23 | } 24 | throw new OperationNotSupportedException(); 25 | } 26 | 27 | Object getSource(); // TODO: Try to make this method not needed. 28 | 29 | /** 30 | * Returns a string representation of the object. In general, the 31 | * {@code toString} method returns a string that 32 | * "textually represents" this object. The result should 33 | * be a concise but informative representation that is easy for a 34 | * person to read. 35 | * It is recommended that all subclasses override this method. 36 | *

37 | * The {@code toString} method for class {@code Object} 38 | * returns a string consisting of the name of the class of which the 39 | * object is an instance, the at-sign character `{@code @}', and 40 | * the unsigned hexadecimal representation of the hash code of the 41 | * object. In other words, this method returns a string equal to the 42 | * value of: 43 | *

44 | *
45 |      * getClass().getName() + '@' + Integer.toHexString(hashCode())
46 |      * 
47 | * 48 | * @return a string representation of the object. 49 | */ 50 | @Override 51 | String toString(); 52 | 53 | /** 54 | * @return the format of the source data 55 | * @throws UnsupportedAudioFileException if the file type is unsupported 56 | * @throws IOException if there is a runtime problem with IO. 57 | */ 58 | AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException; 59 | 60 | /** 61 | * @return a stream representing the input data, regardless of source. 62 | * @throws UnsupportedAudioFileException if the file type is unsupported 63 | * @throws IOException if there is a runtime problem with IO. 64 | */ 65 | AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException; 66 | 67 | /** 68 | * @return The duration of the source data in seconds, or -1 if duration is unavailable. 69 | */ 70 | int getDurationInSeconds(); 71 | 72 | /** 73 | * @return The duration of the source data in milliseconds, or -1 if duration is unavailable. 74 | */ 75 | long getDurationInMilliseconds(); 76 | 77 | /** 78 | * @return The duration of the source data in a {@code java.time.Duration} instance, or null if unavailable 79 | */ 80 | Duration getDuration(); 81 | 82 | /** 83 | * @return true if the DataSource is a FileDataSource, 84 | * which happens if the source used to create it is a File 85 | */ 86 | boolean isFile(); 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/FileDataSource.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import com.goxr3plus.streamplayer.enums.AudioType; 4 | import com.goxr3plus.streamplayer.tools.TimeTool; 5 | 6 | import javax.sound.sampled.AudioFileFormat; 7 | import javax.sound.sampled.AudioInputStream; 8 | import javax.sound.sampled.AudioSystem; 9 | import javax.sound.sampled.UnsupportedAudioFileException; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.time.Duration; 13 | 14 | public class FileDataSource implements DataSource { 15 | 16 | private final File source; 17 | 18 | FileDataSource(File source) { 19 | this.source = source; 20 | } 21 | 22 | @Override 23 | public AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException { 24 | return AudioSystem.getAudioFileFormat(this.source); 25 | } 26 | 27 | @Override 28 | public AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException { 29 | return AudioSystem.getAudioInputStream(source); 30 | } 31 | 32 | @Override 33 | public int getDurationInSeconds() { 34 | return TimeTool.durationInSeconds(source.getAbsolutePath(), AudioType.FILE); 35 | } 36 | 37 | @Override 38 | public long getDurationInMilliseconds() { 39 | return TimeTool.durationInMilliseconds(source.getAbsolutePath(), AudioType.FILE); 40 | } 41 | 42 | @Override 43 | public Duration getDuration() { 44 | return Duration.ofMillis(getDurationInMilliseconds()); 45 | } 46 | 47 | @Override 48 | public Object getSource() { 49 | return source; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "FileDataSource with " + source.toString(); 55 | } 56 | 57 | @Override 58 | public boolean isFile() { 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/Outlet.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import javax.sound.sampled.*; 4 | import java.util.logging.Logger; 5 | 6 | /** 7 | * Owner of the SourceDataLine which is the output line of the player. 8 | * Also owns controls for the SourceDataLine. 9 | * Future goal is to move all handling of the SourceDataLine to here, 10 | * so that the StreamPlayer doesn't have to call {@link #getSourceDataLine()}. 11 | * Another goal is to remove some of the setter and getter methods of this class, 12 | * by moving all code that needs them to this class. 13 | */ 14 | public class Outlet { 15 | 16 | private final Logger logger; 17 | private FloatControl balanceControl; 18 | private FloatControl gainControl; 19 | private BooleanControl muteControl; 20 | private FloatControl panControl; 21 | private SourceDataLine sourceDataLine; 22 | 23 | /** 24 | * @param logger used to log messages 25 | */ 26 | public Outlet(Logger logger) { 27 | this.logger = logger; 28 | } 29 | 30 | 31 | /** 32 | * @return the balance control of the {@link #sourceDataLine} 33 | */ 34 | public FloatControl getBalanceControl() { 35 | return balanceControl; 36 | } 37 | 38 | /** 39 | * @return the gain control of the {@link #sourceDataLine} 40 | */ 41 | public FloatControl getGainControl() { 42 | return gainControl; 43 | } 44 | 45 | /** 46 | * @return the mute control of the {@link #sourceDataLine} 47 | */ 48 | public BooleanControl getMuteControl() { 49 | return muteControl; 50 | } 51 | 52 | /** 53 | * @return the pan control of the {@link #sourceDataLine} 54 | */ 55 | public FloatControl getPanControl() { 56 | return panControl; 57 | } 58 | 59 | /** 60 | * @return the {@link #sourceDataLine}, which is the output audio signal of the player 61 | */ 62 | public SourceDataLine getSourceDataLine() { 63 | return sourceDataLine; 64 | } 65 | 66 | 67 | /** 68 | * @param balanceControl to be set on the {@link #sourceDataLine} 69 | */ 70 | public void setBalanceControl(FloatControl balanceControl) { 71 | this.balanceControl = balanceControl; 72 | } 73 | 74 | /** 75 | * @param gainControl to be set on the {@link #sourceDataLine} 76 | */ 77 | public void setGainControl(FloatControl gainControl) { 78 | this.gainControl = gainControl; 79 | } 80 | 81 | /** 82 | * @param muteControl to be set on the {@link #sourceDataLine} 83 | */ 84 | public void setMuteControl(BooleanControl muteControl) { 85 | this.muteControl = muteControl; 86 | } 87 | 88 | /** 89 | * @param panControl to be set on the {@link #sourceDataLine} 90 | */ 91 | public void setPanControl(FloatControl panControl) { 92 | this.panControl = panControl; 93 | } 94 | 95 | /** 96 | * @param sourceDataLine representing the audio output of the player. 97 | * Usually taken from {@link AudioSystem#getLine(Line.Info)}. 98 | */ 99 | public void setSourceDataLine(SourceDataLine sourceDataLine) { 100 | this.sourceDataLine = sourceDataLine; 101 | } 102 | 103 | 104 | /** 105 | * Check if the Control is Supported by m_line. 106 | * 107 | * @param control the control 108 | * @param component the component 109 | * 110 | * @return true, if successful 111 | */ 112 | public boolean hasControl(final Control.Type control, final Control component) { 113 | return component != null && (sourceDataLine != null) && (sourceDataLine.isControlSupported(control)); 114 | } 115 | 116 | /** 117 | * Returns Gain value. 118 | * 119 | * @return The Gain Value 120 | */ 121 | public float getGainValue() { 122 | 123 | if (hasControl(FloatControl.Type.MASTER_GAIN, getGainControl())) { 124 | return getGainControl().getValue(); 125 | } else { 126 | return 0.0F; 127 | } 128 | } 129 | 130 | /** 131 | * Stop the {@link #sourceDataLine} in a nice way. 132 | * Also nullify it. (Is that necessary?) 133 | */ 134 | void drainStopAndFreeDataLine() { 135 | // Free audio resources. 136 | if (sourceDataLine != null) { 137 | sourceDataLine.drain(); 138 | sourceDataLine.stop(); 139 | sourceDataLine.close(); 140 | this.sourceDataLine = null; // TODO: Is this necessary? Will it not be garbage collected? 141 | } 142 | } 143 | 144 | /** 145 | * Flush and close the {@link #sourceDataLine} in a nice way. 146 | * Also nullify it. (Is that necessary?) 147 | */ 148 | void flushAndFreeDataLine() { 149 | if (sourceDataLine != null) { 150 | sourceDataLine.flush(); 151 | sourceDataLine.close(); 152 | this.sourceDataLine = null; // TODO: Is this necessary? Will it not be garbage collected? 153 | } 154 | } 155 | 156 | /** 157 | * Flush and stop the {@link #sourceDataLine}, if it's running. 158 | */ 159 | void flushAndStop() { 160 | // Flush and stop the source data line 161 | if (sourceDataLine != null && sourceDataLine.isRunning()) { // TODO: Risk for NullPointerException? 162 | sourceDataLine.flush(); 163 | sourceDataLine.stop(); 164 | } 165 | } 166 | 167 | /** 168 | * @return true if the {@link #sourceDataLine} is startable. 169 | */ 170 | boolean isStartable() { 171 | return sourceDataLine != null && !sourceDataLine.isRunning(); 172 | } 173 | 174 | 175 | /** 176 | * Start the {@link #sourceDataLine} 177 | */ 178 | void start() { 179 | sourceDataLine.start(); 180 | } 181 | 182 | /** 183 | * Open the {@link #sourceDataLine}. 184 | * Also create controls for it. 185 | * @param format The wanted audio format. 186 | * @param bufferSize the desired buffer size for the {@link #sourceDataLine} 187 | * @throws LineUnavailableException 188 | */ 189 | void open(AudioFormat format, int bufferSize) throws LineUnavailableException { 190 | logger.info("Entered OpenLine()!:\n"); 191 | 192 | if (sourceDataLine != null) { 193 | sourceDataLine.open(format, bufferSize); 194 | 195 | // opened? 196 | if (sourceDataLine.isOpen()) { 197 | 198 | // Master_Gain Control? 199 | if (sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) 200 | setGainControl((FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN)); 201 | else setGainControl(null); 202 | 203 | // PanControl? 204 | if (sourceDataLine.isControlSupported(FloatControl.Type.PAN)) 205 | setPanControl((FloatControl) sourceDataLine.getControl(FloatControl.Type.PAN)); 206 | else setPanControl(null); 207 | 208 | // Mute? 209 | BooleanControl muteControl1 = sourceDataLine.isControlSupported(BooleanControl.Type.MUTE) 210 | ? (BooleanControl) sourceDataLine.getControl(BooleanControl.Type.MUTE) 211 | : null; 212 | setMuteControl(muteControl1); 213 | 214 | // Speakers Balance? 215 | FloatControl balanceControl = sourceDataLine.isControlSupported(FloatControl.Type.BALANCE) 216 | ? (FloatControl) sourceDataLine.getControl(FloatControl.Type.BALANCE) 217 | : null; 218 | setBalanceControl(balanceControl); 219 | } 220 | } 221 | logger.info("Exited OpenLine()!:\n"); 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamDataSource.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.time.Duration; 6 | 7 | import javax.sound.sampled.AudioFileFormat; 8 | import javax.sound.sampled.AudioInputStream; 9 | import javax.sound.sampled.AudioSystem; 10 | import javax.sound.sampled.UnsupportedAudioFileException; 11 | 12 | public class StreamDataSource implements DataSource { 13 | 14 | private final InputStream source; 15 | 16 | StreamDataSource(InputStream source) { 17 | this.source = source; 18 | } 19 | 20 | @Override 21 | public AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException { 22 | return AudioSystem.getAudioFileFormat(source); 23 | } 24 | 25 | @Override 26 | public AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException { 27 | return AudioSystem.getAudioInputStream(source); 28 | } 29 | 30 | @Override 31 | public int getDurationInSeconds() { 32 | return -1; 33 | } 34 | 35 | @Override 36 | public long getDurationInMilliseconds() { 37 | return -1; 38 | } 39 | 40 | @Override 41 | public Duration getDuration() { 42 | return null; 43 | } 44 | 45 | @Override 46 | public Object getSource() { 47 | return source; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "StreamDataSource with " + source.toString(); 53 | } 54 | 55 | @Override 56 | public boolean isFile() { 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * 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 3 | * Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will 4 | * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 5 | * Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see 6 | * . Also(warning!): 1)You are not allowed to sell this product to third party. 2)You can't change license and made it 7 | * like you are the owner,author etc. 3)All redistributions of source code files must contain all copyright notices that are currently in this file, 8 | * and this list of conditions without modification. 9 | */ 10 | 11 | package com.goxr3plus.streamplayer.stream; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.net.URL; 17 | import java.nio.ByteBuffer; 18 | import java.nio.ByteOrder; 19 | import java.time.Duration; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Objects; 26 | import java.util.concurrent.Callable; 27 | import java.util.concurrent.ExecutionException; 28 | import java.util.concurrent.ExecutorService; 29 | import java.util.concurrent.Executors; 30 | import java.util.concurrent.Future; 31 | import java.util.logging.Level; 32 | import java.util.logging.Logger; 33 | 34 | import javax.naming.OperationNotSupportedException; 35 | import javax.sound.sampled.AudioFileFormat; 36 | import javax.sound.sampled.AudioFormat; 37 | import javax.sound.sampled.AudioInputStream; 38 | import javax.sound.sampled.AudioSystem; 39 | import javax.sound.sampled.BooleanControl; 40 | import javax.sound.sampled.DataLine; 41 | import javax.sound.sampled.FloatControl; 42 | import javax.sound.sampled.Line; 43 | import javax.sound.sampled.LineUnavailableException; 44 | import javax.sound.sampled.Mixer; 45 | import javax.sound.sampled.SourceDataLine; 46 | import javax.sound.sampled.UnsupportedAudioFileException; 47 | 48 | import org.tritonus.share.sampled.TAudioFormat; 49 | import org.tritonus.share.sampled.file.TAudioFileFormat; 50 | 51 | import com.goxr3plus.streamplayer.enums.Status; 52 | import com.goxr3plus.streamplayer.stream.StreamPlayerException.PlayerException; 53 | 54 | import javazoom.spi.PropertiesContainer; 55 | 56 | /** 57 | * StreamPlayer is a class based on JavaSound API. It has been successfully tested under Java 10 58 | * 59 | * @author GOXR3PLUS (www.goxr3plus.co.nf) 60 | * @author JavaZOOM (www.javazoom.net) 61 | */ 62 | public class StreamPlayer implements StreamPlayerInterface, Callable { 63 | 64 | /** 65 | * Class logger 66 | */ 67 | private final Logger logger; 68 | 69 | // -------------------AUDIO----------------,----- 70 | 71 | private volatile Status status = Status.NOT_SPECIFIED; 72 | 73 | /** 74 | * The data source 75 | */ 76 | private DataSource source; 77 | 78 | /** The audio input stream. */ 79 | private volatile AudioInputStream audioInputStream; 80 | 81 | /** The encoded audio input stream. */ 82 | private AudioInputStream encodedAudioInputStream; 83 | 84 | /** The audio file format. */ 85 | private AudioFileFormat audioFileFormat; 86 | 87 | // -------------------LOCKS--------------------- 88 | 89 | /** 90 | * It is used for synchronization in place of audioInputStream 91 | */ 92 | private final Object audioLock = new Object(); 93 | 94 | // -------------------VARIABLES--------------------- 95 | /** Name of the mixer to use */ 96 | private String mixerName; 97 | 98 | /** The current mixer */ 99 | private Mixer mixer = null; 100 | 101 | /** The current line buffer size. */ 102 | private int currentLineBufferSize = -1; 103 | 104 | /** The line buffer size. */ 105 | private int lineBufferSize = -1; 106 | 107 | /** The encoded audio length. */ 108 | private int encodedAudioLength = -1; 109 | 110 | /** 111 | * Speed Factor of the Audio 112 | */ 113 | private double speedFactor = 1; 114 | 115 | /** The Constant EXTERNAL_BUFFER_SIZE. */ 116 | private static final int EXTERNAL_BUFFER_SIZE = 4096; 117 | 118 | /** The Constant SKIP_INACCURACY_SIZE. */ 119 | // private static final int SKIP_INACCURACY_SIZE = 1200 120 | 121 | byte[] trimBuffer; 122 | 123 | // -------------------CLASSES--------------------- 124 | 125 | /** 126 | * This is starting a Thread for StreamPlayer to Run 127 | */ 128 | private final ExecutorService streamPlayerExecutorService; 129 | private Future future; 130 | 131 | /** 132 | * This executor service is used in order the playerState events to be executed 133 | * in an order 134 | */ 135 | private final ExecutorService eventsExecutorService; 136 | 137 | /** Holds a list of Linteners to be notified about Stream PlayerEvents */ 138 | private final ArrayList listeners; 139 | 140 | /** The empty map. */ 141 | private final Map emptyMap = new HashMap<>(); 142 | 143 | // Properties when the File/URL/InputStream is opened. 144 | Map audioProperties; 145 | 146 | /** 147 | * Responsible for the output SourceDataLine and the controls that depend on it. 148 | */ 149 | private final Outlet outlet; 150 | 151 | /** 152 | * Default parameter less Constructor. A default logger will be used. 153 | */ 154 | public StreamPlayer() { 155 | this(Logger.getLogger(StreamPlayer.class.getName())); 156 | 157 | } 158 | 159 | /** 160 | * Constructor with a logger. 161 | * @param logger The logger that will be used by the player 162 | */ 163 | public StreamPlayer(Logger logger) { 164 | this(logger, 165 | Executors.newSingleThreadExecutor(new ThreadFactoryWithNamePrefix("StreamPlayer")), 166 | Executors.newSingleThreadExecutor(new ThreadFactoryWithNamePrefix("StreamPlayerEvent"))); 167 | } 168 | 169 | /** 170 | * Constructor with settable logger and executor services. 171 | * @param logger The logger that will be used by the player 172 | * @param streamPlayerExecutorService Executor service for the stream player 173 | * @param eventsExecutorService Executor service for events. 174 | */ 175 | public StreamPlayer(Logger logger, ExecutorService streamPlayerExecutorService, ExecutorService eventsExecutorService) { 176 | this.logger = logger; 177 | this.streamPlayerExecutorService = streamPlayerExecutorService; 178 | this.eventsExecutorService = eventsExecutorService; 179 | listeners = new ArrayList<>(); 180 | outlet = new Outlet(logger); 181 | reset(); 182 | } 183 | 184 | /** 185 | * Freeing the resources. 186 | */ 187 | @Override 188 | public void reset() { 189 | 190 | // Close the stream 191 | synchronized (audioLock) { 192 | closeStream(); 193 | } 194 | 195 | outlet.flushAndFreeDataLine(); 196 | 197 | // AudioFile 198 | audioInputStream = null; 199 | audioFileFormat = null; 200 | encodedAudioInputStream = null; 201 | encodedAudioLength = -1; 202 | 203 | // Controls 204 | outlet.setGainControl(null); 205 | outlet.setPanControl(null); 206 | outlet.setBalanceControl(null); 207 | 208 | // Notify the Status 209 | status = Status.NOT_SPECIFIED; 210 | generateEvent(Status.NOT_SPECIFIED, AudioSystem.NOT_SPECIFIED, null); 211 | 212 | } 213 | 214 | /** 215 | * Notify listeners about a BasicPlayerEvent. 216 | * 217 | * @param status event code. 218 | * @param encodedStreamPosition in the stream when the event occurs. 219 | * @param description the description 220 | * 221 | * @return A String Describing if any problem occurred 222 | */ 223 | private String generateEvent(final Status status, final int encodedStreamPosition, final Object description) { 224 | try { 225 | return eventsExecutorService 226 | .submit(new StreamPlayerEventLauncher(this, status, encodedStreamPosition, description, listeners)) 227 | .get(); 228 | } catch (InterruptedException | ExecutionException ex) { 229 | logger.log(Level.WARNING, "Problem in StreamPlayer generateEvent() method", ex); 230 | } 231 | return "Problem in StreamPlayer generateEvent() method"; 232 | } 233 | 234 | /** 235 | * Add a listener to be notified. 236 | * 237 | * @param streamPlayerListener the listener 238 | */ 239 | @Override 240 | public void addStreamPlayerListener(final StreamPlayerListener streamPlayerListener) { 241 | 242 | Objects.requireNonNull(streamPlayerListener, 243 | "null is not allowed as StreamPlayerListener value."); 244 | 245 | listeners.add(streamPlayerListener); 246 | } 247 | 248 | /** 249 | * Remove registered listener. 250 | * 251 | * @param streamPlayerListener the listener 252 | */ 253 | @Override 254 | public void removeStreamPlayerListener(final StreamPlayerListener streamPlayerListener) { 255 | if (listeners != null) 256 | listeners.remove(streamPlayerListener); 257 | 258 | } 259 | 260 | /** 261 | * Open the specific object which can be File,URL or InputStream. 262 | * 263 | * @param object the object [File or URL or InputStream ] 264 | * 265 | * @throws StreamPlayerException the stream player exception 266 | * @deprecated Use one of {@link #open(File)}, {@link #open(URL)} or {@link #open(InputStream)} instead. 267 | * 268 | */ 269 | @Override 270 | @Deprecated 271 | public void open(final Object object) throws StreamPlayerException { 272 | 273 | logger.info(() -> "open(" + object + ")\n"); 274 | if (object == null) 275 | return; 276 | 277 | try { 278 | source = DataSource.newDataSource(object); 279 | } catch (OperationNotSupportedException e) { 280 | e.printStackTrace(); 281 | } 282 | initAudioInputStream(); 283 | } 284 | 285 | /** 286 | * Open the specified file for playback. 287 | * 288 | * @param file the file to be played 289 | * @throws StreamPlayerException the stream player exception 290 | */ 291 | @Override 292 | public void open(File file) throws StreamPlayerException { 293 | 294 | logger.info(() -> "open(" + file + ")\n"); 295 | source = new FileDataSource(file); 296 | initAudioInputStream(); 297 | } 298 | 299 | /** 300 | * Open the specified location for playback. 301 | * 302 | * @param url the location to be played 303 | * @throws StreamPlayerException the stream player exception 304 | */ 305 | @Override 306 | public void open(URL url) throws StreamPlayerException { 307 | logger.info(() -> "open(" + url + ")\n"); 308 | source = new UrlDataSource(url); 309 | initAudioInputStream(); 310 | } 311 | 312 | /** 313 | * Open the specified stream for playback. 314 | * 315 | * @param stream the stream to be played 316 | * @throws StreamPlayerException the stream player exception 317 | */ 318 | @Override 319 | public void open(InputStream stream) throws StreamPlayerException { 320 | logger.info(() -> "open(" + stream + ")\n"); 321 | source = new StreamDataSource(stream); 322 | initAudioInputStream(); 323 | } 324 | 325 | /** 326 | * Create AudioInputStream and AudioFileFormat from the data source. 327 | * 328 | * @throws StreamPlayerException the stream player exception 329 | */ 330 | private void initAudioInputStream() throws StreamPlayerException { 331 | try { 332 | 333 | logger.info("Entered initAudioInputStream\n"); 334 | 335 | // Reset 336 | reset(); 337 | 338 | // Notify Status 339 | status = Status.OPENING; 340 | generateEvent(Status.OPENING, getEncodedStreamPosition(), source); 341 | 342 | // Audio resources from file||URL||inputStream. 343 | audioInputStream = source.getAudioInputStream(); 344 | 345 | // Audio resources from file||URL||inputStream. 346 | audioFileFormat = source.getAudioFileFormat(); 347 | 348 | // Create the Line 349 | createLine(); 350 | 351 | // Determine Properties 352 | determineProperties(); 353 | 354 | // Generate Open Event 355 | status = Status.OPENED; 356 | generateEvent(Status.OPENED, getEncodedStreamPosition(), null); 357 | 358 | } catch (LineUnavailableException | UnsupportedAudioFileException | IOException e) { 359 | logger.log(Level.INFO, e.getMessage(), e); 360 | throw new StreamPlayerException(e); 361 | } 362 | 363 | logger.info("Exited initAudioInputStream\n"); 364 | } 365 | 366 | 367 | /** 368 | * Determines Properties when the File/URL/InputStream is opened. 369 | */ 370 | private void determineProperties() { 371 | logger.info("Entered determineProperties()!\n"); 372 | 373 | // Add AudioFileFormat properties. 374 | // Expect if it is null(something bad happened). 375 | if (audioFileFormat == null) 376 | return; 377 | 378 | if (!(audioFileFormat instanceof TAudioFileFormat)) 379 | audioProperties = new HashMap<>(); 380 | else { 381 | // Tritonus SPI compliant audio file format. 382 | audioProperties = audioFileFormat.properties(); 383 | 384 | // Clone the Map because it is not mutable. 385 | audioProperties = deepCopy(audioProperties); 386 | 387 | } 388 | 389 | // Add JavaSound properties. 390 | if (audioFileFormat.getByteLength() > 0) 391 | audioProperties.put("audio.length.bytes", audioFileFormat.getByteLength()); 392 | if (audioFileFormat.getFrameLength() > 0) 393 | audioProperties.put("audio.length.frames", audioFileFormat.getFrameLength()); 394 | if (audioFileFormat.getType() != null) 395 | audioProperties.put("audio.type", audioFileFormat.getType()); 396 | 397 | // AudioFormat properties. 398 | final AudioFormat audioFormat = audioFileFormat.getFormat(); 399 | if (audioFormat.getFrameRate() > 0) 400 | audioProperties.put("audio.framerate.fps", audioFormat.getFrameRate()); 401 | if (audioFormat.getFrameSize() > 0) 402 | audioProperties.put("audio.framesize.bytes", audioFormat.getFrameSize()); 403 | if (audioFormat.getSampleRate() > 0) 404 | audioProperties.put("audio.samplerate.hz", audioFormat.getSampleRate()); 405 | if (audioFormat.getSampleSizeInBits() > 0) 406 | audioProperties.put("audio.samplesize.bits", audioFormat.getSampleSizeInBits()); 407 | if (audioFormat.getChannels() > 0) 408 | audioProperties.put("audio.channels", audioFormat.getChannels()); 409 | // Tritonus SPI compliant audio format. 410 | if (audioFormat instanceof TAudioFormat) 411 | audioProperties.putAll(audioFormat.properties()); 412 | 413 | // Add SourceDataLine 414 | audioProperties.put("basicplayer.sourcedataline", outlet.getSourceDataLine()); 415 | 416 | // Keep this final reference for the lambda expression 417 | final Map audioPropertiesCopy = audioProperties; // TODO: Remove, it's meaningless. 418 | 419 | // Notify all registered StreamPlayerListeners 420 | listeners.forEach(listener -> listener.opened(source.getSource(), audioPropertiesCopy)); 421 | 422 | logger.info("Exited determineProperties()!\n"); 423 | 424 | } 425 | 426 | /** 427 | * Initiating Audio resources from AudioSystem.
428 | * 429 | * @throws LineUnavailableException the line unavailable exception 430 | * @throws StreamPlayerException 431 | */ 432 | private void initLine() throws LineUnavailableException, StreamPlayerException { 433 | 434 | logger.info("Initiating the line..."); 435 | 436 | if (outlet.getSourceDataLine() == null) 437 | createLine(); 438 | if (!outlet.getSourceDataLine().isOpen()) { 439 | currentLineBufferSize = lineBufferSize >= 0 ? lineBufferSize : outlet.getSourceDataLine().getBufferSize(); 440 | openLine(audioInputStream.getFormat(), currentLineBufferSize); 441 | } else { 442 | AudioFormat format = audioInputStream == null ? null : audioInputStream.getFormat(); 443 | if (!outlet.getSourceDataLine().getFormat().equals(format)) { // TODO: Check if bug, does equals work as intended? 444 | outlet.getSourceDataLine().close(); 445 | currentLineBufferSize = lineBufferSize >= 0 ? lineBufferSize : outlet.getSourceDataLine().getBufferSize(); 446 | openLine(audioInputStream.getFormat(), currentLineBufferSize); 447 | } 448 | } 449 | } 450 | 451 | /** The frame size. */ 452 | // private int frameSize 453 | 454 | /** 455 | * Change the Speed Rate of the Audio , this variable affects the Sample Rate , 456 | * for example 1.0 is normal , 0.5 is half the speed and 2.0 is double the speed 457 | * Note that you have to restart the audio for this to take effect 458 | * 459 | * @param speedFactor speedFactor 460 | */ 461 | @Override 462 | public void setSpeedFactor(final double speedFactor) { 463 | this.speedFactor = speedFactor; 464 | 465 | } 466 | 467 | /** 468 | * Inits a DateLine.
469 | *

470 | * From the AudioInputStream, i.e. from the sound file, we fetch information 471 | * about the format of the audio data. These information include the sampling 472 | * frequency, the number of channels and the size of the samples. There 473 | * information are needed to ask JavaSound for a suitable output line for this 474 | * audio file. Furthermore, we have to give JavaSound a hint about how big the 475 | * internal buffer for the line should be. Here, we say 476 | * AudioSystem.NOT_SPECIFIED, signaling that we don't care about the exact size. 477 | * JavaSound will use some default value for the buffer size. 478 | * 479 | * @throws LineUnavailableException the line unavailable exception 480 | * @throws StreamPlayerException 481 | */ 482 | private void createLine() throws LineUnavailableException, StreamPlayerException { 483 | 484 | logger.info("Entered CreateLine()!:\n"); 485 | 486 | if (outlet.getSourceDataLine() != null) 487 | logger.warning("Warning Source DataLine is not null!\n"); 488 | else { 489 | final AudioFormat sourceFormat = audioInputStream.getFormat(); 490 | 491 | logger.info(() -> "Create Line : Source format : " + sourceFormat + "\n"); 492 | 493 | // Calculate the Sample Size in bits 494 | int nSampleSizeInBits = sourceFormat.getSampleSizeInBits(); 495 | if (sourceFormat.getEncoding() == AudioFormat.Encoding.ULAW || sourceFormat.getEncoding() == AudioFormat.Encoding.ALAW 496 | || nSampleSizeInBits != 8) 497 | nSampleSizeInBits = 16; 498 | 499 | final AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 500 | (float) (sourceFormat.getSampleRate() * speedFactor), nSampleSizeInBits, sourceFormat.getChannels(), 501 | nSampleSizeInBits / 8 * sourceFormat.getChannels(), sourceFormat.getSampleRate(), false); 502 | 503 | // int frameSize = sourceFormat.getChannels() * (nSampleSizeInBits / 8) 504 | 505 | logger.info(() -> "Sample Rate =" + targetFormat.getSampleRate() + ",Frame Rate=" 506 | + targetFormat.getFrameRate() + ",Bit Rate=" + targetFormat.getSampleSizeInBits() 507 | + "Target format: " + targetFormat + "\n"); 508 | 509 | // Keep a reference on encoded stream to progress notification. 510 | encodedAudioInputStream = audioInputStream; 511 | try { 512 | // Get total length in bytes of the encoded stream. 513 | encodedAudioLength = encodedAudioInputStream.available(); 514 | } catch (final IOException e) { 515 | logger.warning("Cannot get m_encodedaudioInputStream.available()\n" + e); 516 | } 517 | 518 | // Create decoded Stream 519 | audioInputStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream); 520 | final DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, audioInputStream.getFormat(), 521 | AudioSystem.NOT_SPECIFIED); 522 | if (!AudioSystem.isLineSupported(lineInfo)) 523 | throw new StreamPlayerException(PlayerException.LINE_NOT_SUPPORTED); 524 | 525 | // ----------About the mixer 526 | if (mixerName == null) 527 | // Primary Sound Driver 528 | mixerName = getMixers().get(0); 529 | 530 | // Continue 531 | mixer = getMixer(mixerName); 532 | if (mixer == null) { 533 | outlet.setSourceDataLine((SourceDataLine) AudioSystem.getLine(lineInfo)); 534 | mixerName = null; 535 | } else { 536 | logger.info("Mixer: " + mixer.getMixerInfo()); 537 | outlet.setSourceDataLine((SourceDataLine) mixer.getLine(lineInfo)); 538 | } 539 | 540 | // -------------------------------------------------------------------------------- 541 | logger.info(() -> "Line : " + outlet.getSourceDataLine()); 542 | logger.info(() -> "Line Info : " + outlet.getSourceDataLine().getLineInfo()); 543 | logger.info(() -> "Line AudioFormat: " + outlet.getSourceDataLine().getFormat() + "\n"); 544 | logger.info("Exited CREATELINE()!:\n"); 545 | } 546 | } 547 | 548 | /** 549 | * Open the line. 550 | * 551 | * @throws LineUnavailableException the line unavailable exception 552 | * @param audioFormat 553 | * @param currentLineBufferSize 554 | */ 555 | private void openLine(AudioFormat audioFormat, int currentLineBufferSize) throws LineUnavailableException { 556 | outlet.open(audioFormat, currentLineBufferSize); 557 | } 558 | 559 | /** 560 | * Starts the play back. 561 | * 562 | * @throws StreamPlayerException the stream player exception 563 | */ 564 | @Override 565 | public void play() throws StreamPlayerException { 566 | if (status == Status.STOPPED) 567 | initAudioInputStream(); 568 | if (status != Status.OPENED) 569 | return; 570 | 571 | // Shutdown previous Thread Running 572 | awaitTermination(); 573 | 574 | // Open SourceDataLine. 575 | try { 576 | initLine(); 577 | } catch (final LineUnavailableException ex) { 578 | throw new StreamPlayerException(PlayerException.CAN_NOT_INIT_LINE, ex); 579 | } 580 | 581 | // Open the sourceDataLine 582 | if (outlet.isStartable()) { 583 | outlet.start(); 584 | 585 | // Proceed only if we have not problems 586 | logger.info("Submitting new StreamPlayer Thread"); 587 | streamPlayerExecutorService.submit(this); 588 | 589 | // Update the status 590 | status = Status.PLAYING; 591 | generateEvent(Status.PLAYING, getEncodedStreamPosition(), null); 592 | } 593 | } 594 | 595 | /** 596 | * Pauses the play back.
597 | *

598 | * Player Status = PAUSED. * @return False if failed(so simple...) 599 | * 600 | * @return true, if successful 601 | */ 602 | @Override 603 | public boolean pause() { 604 | if (outlet.getSourceDataLine() == null || status != Status.PLAYING) 605 | return false; 606 | status = Status.PAUSED; 607 | logger.info("pausePlayback() completed"); 608 | generateEvent(Status.PAUSED, getEncodedStreamPosition(), null); 609 | return true; 610 | } 611 | 612 | /** 613 | * Stops the play back.
614 | *

615 | * Player Status = STOPPED.
616 | * Thread should free Audio resources. 617 | */ 618 | @Override 619 | public void stop() { 620 | if (status == Status.STOPPED) 621 | return; 622 | if (isPlaying()) 623 | pause(); 624 | status = Status.STOPPED; 625 | // generateEvent(Status.STOPPED, getEncodedStreamPosition(), null); 626 | logger.info("StreamPlayer stopPlayback() completed"); 627 | } 628 | 629 | /** 630 | * Resumes the play back.
631 | *

632 | * Player Status = PLAYING* 633 | * 634 | * @return False if failed(so simple...) 635 | */ 636 | @Override 637 | public boolean resume() { 638 | if (outlet.getSourceDataLine() == null || status != Status.PAUSED) 639 | return false; 640 | outlet.start(); 641 | status = Status.PLAYING; 642 | generateEvent(Status.RESUMED, getEncodedStreamPosition(), null); 643 | logger.info("resumePlayback() completed"); 644 | return true; 645 | 646 | } 647 | 648 | /** 649 | * Await for the termination of StreamPlayerExecutorService Thread 650 | */ 651 | private void awaitTermination() { 652 | if (future != null && !future.isDone()) { 653 | try { 654 | // future.get() [Don't use this cause it may hang forever and ever...] 655 | 656 | // Wait ~1 second and then cancel the future 657 | final Thread delay = new Thread(() -> { 658 | try { 659 | for (int i = 0; i < 50; i++) { 660 | if (!future.isDone()) 661 | Thread.sleep(20); 662 | else 663 | break; 664 | logger.log(Level.INFO, "StreamPlayer Future is not yet done..."); 665 | } 666 | 667 | } catch (final InterruptedException ex) { 668 | Thread.currentThread().interrupt(); 669 | logger.log(Level.INFO, ex.getMessage(), ex); 670 | } 671 | }); 672 | 673 | // Start the delay Thread 674 | delay.start(); 675 | // Join until delay Thread is finished 676 | delay.join(); 677 | 678 | } catch (final InterruptedException ex) { 679 | Thread.currentThread().interrupt(); 680 | logger.log(Level.WARNING, ex.getMessage(), ex); 681 | } finally { 682 | // Harmless if task already completed 683 | future.cancel(true); // interrupt if running 684 | } 685 | } 686 | } 687 | 688 | /** 689 | * Skip bytes in the File input stream. It will skip N frames matching to bytes, 690 | * so it will never skip given bytes len 691 | * 692 | * @param bytes the bytes 693 | * 694 | * @return value bigger than 0 for File and value = 0 for URL and InputStream 695 | * 696 | * @throws StreamPlayerException the stream player exception 697 | */ 698 | @Override 699 | public long seekBytes(final long bytes) throws StreamPlayerException { 700 | long totalSkipped = 0; 701 | 702 | // If it is File 703 | if (source.isFile()) { 704 | 705 | // Check if the requested bytes are more than totalBytes of Audio 706 | final long bytesLength = getTotalBytes(); 707 | logger.log(Level.INFO, "Bytes: " + bytes + " BytesLength: " + bytesLength); 708 | if ((bytesLength <= 0) || (bytes >= bytesLength)) { 709 | generateEvent(Status.EOM, getEncodedStreamPosition(), null); 710 | return totalSkipped; 711 | } 712 | 713 | logger.info(() -> "Bytes to skip : " + bytes); 714 | final Status previousStatus = status; 715 | status = Status.SEEKING; 716 | 717 | try { 718 | synchronized (audioLock) { 719 | generateEvent(Status.SEEKING, AudioSystem.NOT_SPECIFIED, null); 720 | initAudioInputStream(); 721 | if (audioInputStream != null) { 722 | 723 | long skipped; 724 | // Loop until bytes are really skipped. 725 | while (totalSkipped < bytes) { // totalSkipped < (bytes-SKIP_INACCURACY_SIZE))) 726 | skipped = audioInputStream.skip(bytes - totalSkipped); 727 | if (skipped == 0) 728 | break; 729 | totalSkipped += skipped; 730 | logger.info("Skipped : " + totalSkipped + "/" + bytes); 731 | if (totalSkipped == -1) 732 | throw new StreamPlayerException( 733 | PlayerException.SKIP_NOT_SUPPORTED); 734 | 735 | logger.info("Skeeping:" + totalSkipped); 736 | } 737 | } 738 | } 739 | generateEvent(Status.SEEKED, getEncodedStreamPosition(), null); 740 | status = Status.OPENED; 741 | if (previousStatus == Status.PLAYING) 742 | play(); 743 | else if (previousStatus == Status.PAUSED) { 744 | play(); 745 | pause(); 746 | } 747 | 748 | } catch (final IOException ex) { 749 | logger.log(Level.WARNING, ex.getMessage(), ex); 750 | } 751 | } 752 | return totalSkipped; 753 | } 754 | 755 | /** 756 | * Skip x seconds of audio 757 | * See {@link #seekBytes(long)} 758 | * 759 | * @param seconds Seconds to Skip 760 | */ 761 | @Override 762 | //todo not finished needs more validations 763 | public long seekSeconds(int seconds) throws StreamPlayerException { 764 | int durationInSeconds = this.getDurationInSeconds(); 765 | 766 | //Validate 767 | validateSeconds(seconds, durationInSeconds); 768 | 769 | //Calculate Bytes 770 | long totalBytes = getTotalBytes(); 771 | double percentage = (seconds * 100) / durationInSeconds; 772 | long bytes = (long) (totalBytes * (percentage / 100)); 773 | 774 | return seekBytes(this.getEncodedStreamPosition() + bytes); 775 | } 776 | 777 | // /** 778 | // * Skip seconds of audio based on the pattern 779 | // * See {@link #seek(long)} 780 | // * 781 | // * @param pattern A string in the format (HH:MM:SS) WHERE h = HOURS , M = minutes , S = seconds 782 | // */ 783 | // public void seek(String pattern) throws StreamPlayerException { 784 | // long bytes = 0; 785 | // 786 | // seek(bytes); 787 | // } 788 | 789 | /** 790 | * Go to X time of the Audio 791 | * See {@link #seekBytes(long)} 792 | * 793 | * @param seconds Seconds to Skip 794 | */ 795 | @Override 796 | public long seekTo(int seconds) throws StreamPlayerException { 797 | int durationInSeconds = this.getDurationInSeconds(); 798 | 799 | //Validate 800 | validateSeconds(seconds, durationInSeconds); 801 | 802 | //Calculate Bytes 803 | long totalBytes = getTotalBytes(); 804 | double percentage = (seconds * 100) / durationInSeconds; 805 | long bytes = (long) (totalBytes * (percentage / 100)); 806 | 807 | return seekBytes(bytes); 808 | } 809 | 810 | 811 | private void validateSeconds(int seconds, int durationInSeconds) { 812 | if (seconds < 0) { 813 | throw new UnsupportedOperationException("Trying to skip negative seconds "); 814 | } else if (seconds >= durationInSeconds) { 815 | throw new UnsupportedOperationException("Trying to skip with seconds {" + seconds + "} > maximum {" + durationInSeconds + "}"); 816 | } 817 | } 818 | 819 | 820 | 821 | /** 822 | * @return The duration of the source data in seconds, or -1 if duration is unavailable. 823 | */ 824 | @Override 825 | public int getDurationInSeconds() { 826 | return source.getDurationInSeconds(); 827 | } 828 | 829 | /** 830 | * @return The duration of the source data in milliseconds, or -1 if duration is unavailable. 831 | */ 832 | @Override 833 | public long getDurationInMilliseconds() { 834 | return source.getDurationInMilliseconds(); 835 | } 836 | 837 | /** 838 | * @return The duration of the source data in a {@code java.time.Duration} instance, or null if unavailable 839 | */ 840 | @Override 841 | public Duration getDuration() { 842 | return source.getDuration(); 843 | } 844 | 845 | /** 846 | * Main loop. 847 | *

848 | * Player Status == STOPPED || SEEKING = End of Thread + Freeing Audio 849 | * Resources.
850 | * Player Status == PLAYING = Audio stream data sent to Audio line.
851 | * Player Status == PAUSED = Waiting for another status. 852 | */ 853 | @Override 854 | public Void call() { 855 | int nBytesRead = 0; 856 | final int audioDataLength = EXTERNAL_BUFFER_SIZE; 857 | final ByteBuffer audioDataBuffer = ByteBuffer.allocate(audioDataLength); 858 | audioDataBuffer.order(ByteOrder.LITTLE_ENDIAN); 859 | 860 | // Lock stream while playing. 861 | synchronized (audioLock) { 862 | // Main play/pause loop. 863 | while ((nBytesRead != -1) && status != Status.STOPPED && status != Status.NOT_SPECIFIED 864 | && status != Status.SEEKING) { 865 | 866 | try { 867 | // Playing? 868 | if (status == Status.PLAYING) { 869 | 870 | // System.out.println("Inside Stream Player Run method") 871 | int toRead = audioDataLength; 872 | int totalRead = 0; 873 | 874 | // Reads up a specified maximum number of bytes from audio stream 875 | // wtf i have written here omg //to fix! cause it is complicated 876 | for (; toRead > 0 && (nBytesRead = audioInputStream.read(audioDataBuffer.array(), totalRead, 877 | toRead)) != -1; toRead -= nBytesRead, totalRead += nBytesRead) 878 | 879 | // Check for under run 880 | if (outlet.getSourceDataLine().available() >= outlet.getSourceDataLine().getBufferSize()) 881 | logger.info(() -> "Underrun> Available=" + outlet.getSourceDataLine().available() 882 | + " , SourceDataLineBuffer=" + outlet.getSourceDataLine().getBufferSize()); 883 | 884 | // Check if anything has been read 885 | if (totalRead > 0) { 886 | trimBuffer = audioDataBuffer.array(); 887 | if (totalRead < trimBuffer.length) { 888 | trimBuffer = new byte[totalRead]; 889 | // Copies an array from the specified source array, beginning at the specified 890 | // position, to the specified position of the destination array 891 | // The number of components copied is equal to the length argument. 892 | System.arraycopy(audioDataBuffer.array(), 0, trimBuffer, 0, totalRead); 893 | } 894 | 895 | // Writes audio data to the mixer via this source data line 896 | outlet.getSourceDataLine().write(trimBuffer, 0, totalRead); 897 | 898 | // Compute position in bytes in encoded stream. 899 | final int nEncodedBytes = getEncodedStreamPosition(); 900 | 901 | // Notify all registered Listeners 902 | listeners.forEach(listener -> { 903 | if (audioInputStream instanceof PropertiesContainer) { 904 | // Pass audio parameters such as instant 905 | // bit rate, ... 906 | listener.progress(nEncodedBytes, outlet.getSourceDataLine().getMicrosecondPosition(), 907 | trimBuffer, ((PropertiesContainer) audioInputStream).properties()); 908 | } else 909 | // Pass audio parameters 910 | listener.progress(nEncodedBytes, outlet.getSourceDataLine().getMicrosecondPosition(), 911 | trimBuffer, emptyMap); 912 | }); 913 | 914 | } 915 | 916 | } else if (status == Status.PAUSED) { 917 | // Flush and stop the source data line 918 | outlet.flushAndStop(); 919 | goOutOfPause(); 920 | 921 | } 922 | } catch (final IOException ex) { 923 | logger.log(Level.WARNING, "\"Decoder Exception: \" ", ex); 924 | status = Status.STOPPED; 925 | generateEvent(Status.STOPPED, getEncodedStreamPosition(), null); 926 | } 927 | } 928 | // Free audio resources. 929 | outlet.drainStopAndFreeDataLine(); 930 | 931 | // Close stream. 932 | closeStream(); 933 | 934 | // Notification of "End Of Media" 935 | if (nBytesRead == -1) 936 | generateEvent(Status.EOM, AudioSystem.NOT_SPECIFIED, null); 937 | 938 | } 939 | // Generate Event 940 | status = Status.STOPPED; 941 | generateEvent(Status.STOPPED, AudioSystem.NOT_SPECIFIED, null); 942 | 943 | // Log 944 | logger.info("Decoding thread completed"); 945 | 946 | return null; 947 | } 948 | 949 | private void goOutOfPause() { 950 | try { 951 | while (status == Status.PAUSED) { 952 | Thread.sleep(50); 953 | } 954 | } catch (final InterruptedException ex) { 955 | Thread.currentThread().interrupt(); 956 | logger.warning("Thread cannot sleep.\n" + ex); 957 | } 958 | } 959 | 960 | /** 961 | * Calculates the current position of the encoded audio based on
962 | * nEncodedBytes = encodedAudioLength - 963 | * encodedAudioInputStream.available(); 964 | * 965 | * @return The Position of the encoded stream in term of bytes 966 | */ 967 | @Override 968 | public int getEncodedStreamPosition() { 969 | int position = -1; 970 | if (source.isFile() && encodedAudioInputStream != null) 971 | try { 972 | position = encodedAudioLength - encodedAudioInputStream.available(); 973 | } catch (final IOException ex) { 974 | logger.log(Level.WARNING, "Cannot get m_encodedaudioInputStream.available()", ex); 975 | stop(); 976 | } 977 | return position; 978 | } 979 | 980 | /** 981 | * Close stream. 982 | */ 983 | private void closeStream() { 984 | try { 985 | if (audioInputStream != null) { 986 | audioInputStream.close(); 987 | logger.info("Stream closed"); 988 | } 989 | } catch (final IOException ex) { 990 | logger.warning("Cannot close stream\n" + ex); 991 | } 992 | } 993 | 994 | /** 995 | * Return SourceDataLine buffer size. 996 | * 997 | * @return -1 maximum buffer size. 998 | */ 999 | @Override 1000 | public int getLineBufferSize() { 1001 | return lineBufferSize; 1002 | } 1003 | 1004 | /** 1005 | * Return SourceDataLine current buffer size. 1006 | * 1007 | * @return The current line buffer size 1008 | */ 1009 | @Override 1010 | public int getLineCurrentBufferSize() { 1011 | return currentLineBufferSize; 1012 | } 1013 | 1014 | /** 1015 | * Returns all available mixers. 1016 | * 1017 | * @return A List of available Mixers 1018 | */ 1019 | @Override 1020 | public List getMixers() { 1021 | final List mixers = new ArrayList<>(); 1022 | 1023 | // Obtains an array of mixer info objects that represents the set of 1024 | // audio mixers that are currently installed on the system. 1025 | final Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo(); 1026 | 1027 | Arrays.stream(mixerInfos).forEach(mInfo -> { 1028 | // line info 1029 | final Line.Info lineInfo = new Line.Info(SourceDataLine.class); 1030 | final Mixer mixer = AudioSystem.getMixer(mInfo); 1031 | 1032 | // if line supported 1033 | if (mixer.isLineSupported(lineInfo)) 1034 | mixers.add(mInfo.getName()); 1035 | 1036 | }); 1037 | 1038 | return mixers; 1039 | } 1040 | 1041 | /** 1042 | * Returns the mixer with this name. 1043 | * 1044 | * @param name the name 1045 | * 1046 | * @return The Mixer with that name 1047 | */ 1048 | private Mixer getMixer(final String name) { 1049 | Mixer mixer = null; 1050 | 1051 | // Obtains an array of mixer info objects that represents the set of 1052 | // audio mixers that are currently installed on the system. 1053 | final Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo(); 1054 | 1055 | if (name != null) 1056 | for (Mixer.Info mixerInfo : mixerInfos) 1057 | if (mixerInfo.getName().equals(name)) { 1058 | mixer = AudioSystem.getMixer(mixerInfo); 1059 | break; 1060 | } 1061 | return mixer; 1062 | } 1063 | 1064 | /** 1065 | * Set the name of the mixer to use. This should be called before opening a Line. 1066 | * 1067 | * @param mixerName the name 1068 | */ 1069 | public void setMixerName(String mixerName) { 1070 | this.mixerName = mixerName; 1071 | } 1072 | 1073 | /** 1074 | * Returns the name of the mixer 1075 | * 1076 | * @return the name of the mixer 1077 | */ 1078 | public String getMixerName(){ 1079 | return mixerName; 1080 | } 1081 | 1082 | /** 1083 | * Returns the mixer that is currently being used, if there is no line created this will return null 1084 | * 1085 | * @return The Mixer being used 1086 | */ 1087 | public Mixer getCurrentMixer(){ 1088 | return mixer; 1089 | } 1090 | 1091 | /** 1092 | * Returns Gain value. 1093 | * 1094 | * @return The Gain Value 1095 | */ 1096 | @Override 1097 | public float getGainValue() { 1098 | return outlet.getGainValue(); 1099 | } 1100 | 1101 | /** 1102 | * Returns maximum Gain value. 1103 | * 1104 | * @return The Maximum Gain Value 1105 | */ 1106 | @Override 1107 | public float getMaximumGain() { 1108 | return !outlet.hasControl(FloatControl.Type.MASTER_GAIN, outlet.getGainControl()) ? 0.0F : outlet.getGainControl().getMaximum(); 1109 | 1110 | } 1111 | 1112 | /** 1113 | * Returns minimum Gain value. 1114 | * 1115 | * @return The Minimum Gain Value 1116 | */ 1117 | @Override 1118 | public float getMinimumGain() { 1119 | 1120 | return !outlet.hasControl(FloatControl.Type.MASTER_GAIN, outlet.getGainControl()) ? 0.0F : outlet.getGainControl().getMinimum(); 1121 | 1122 | } 1123 | 1124 | /** 1125 | * Returns Pan precision. 1126 | *

1127 | * Obtains the resolution or granularity of the control, in the units that the control measures. 1128 | * The precision is the size of the increment between discrete valid values for this control, 1129 | * over the set of supported floating-point values. 1130 | * 1131 | * @return The Precision Value for the pan control, if it exists, otherwise 0.0. 1132 | */ 1133 | @Override 1134 | public float getPrecision() { 1135 | return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) 1136 | ? 0 1137 | : outlet.getPanControl().getPrecision(); 1138 | 1139 | } 1140 | 1141 | /** 1142 | * Returns Pan value. 1143 | * 1144 | * @return The Pan Value 1145 | */ 1146 | @Override 1147 | public float getPan() { 1148 | return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) ? 0.0F : outlet.getPanControl().getValue(); 1149 | 1150 | } 1151 | 1152 | /** 1153 | * Return the mute Value(true || false). 1154 | * 1155 | * @return True if muted , False if not 1156 | */ 1157 | @Override 1158 | public boolean getMute() { 1159 | return outlet.hasControl(BooleanControl.Type.MUTE, outlet.getMuteControl()) && outlet.getMuteControl().getValue(); 1160 | } 1161 | 1162 | /** 1163 | * Return the balance Value. 1164 | * 1165 | * @return The Balance Value 1166 | */ 1167 | @Override 1168 | public float getBalance() { 1169 | return !outlet.hasControl(FloatControl.Type.BALANCE, outlet.getBalanceControl()) ? 0f : outlet.getBalanceControl().getValue(); 1170 | } 1171 | 1172 | /**** 1173 | * Return the total size of this file in bytes. 1174 | * 1175 | * @return encodedAudioLength 1176 | */ 1177 | @Override 1178 | public long getTotalBytes() { 1179 | return encodedAudioLength; 1180 | } 1181 | 1182 | /** 1183 | * @return BytePosition 1184 | */ 1185 | @Override 1186 | public int getPositionByte() { 1187 | final int positionByte = AudioSystem.NOT_SPECIFIED; 1188 | if (audioProperties != null) { 1189 | if (audioProperties.containsKey("mp3.position.byte")) 1190 | return (Integer) audioProperties.get("mp3.position.byte"); 1191 | if (audioProperties.containsKey("ogg.position.byte")) 1192 | return (Integer) audioProperties.get("ogg.position.byte"); 1193 | } 1194 | return positionByte; 1195 | } 1196 | 1197 | /** The source data line. */ 1198 | public Outlet getOutlet() { 1199 | return outlet; 1200 | } 1201 | 1202 | /** 1203 | * This method will return the status of the player 1204 | * 1205 | * @return The Player Status 1206 | */ 1207 | @Override 1208 | public Status getStatus() { 1209 | return status; 1210 | } 1211 | 1212 | /** 1213 | * Deep copy of a Map. 1214 | * 1215 | * @param map The Map to be Copied 1216 | * 1217 | * @return the map that is an exact copy of the given map 1218 | */ 1219 | private Map deepCopy(final Map map) { 1220 | final HashMap copier = new HashMap<>(); 1221 | if (map != null) 1222 | map.keySet().forEach(key -> copier.put(key, map.get(key))); 1223 | return copier; 1224 | } 1225 | 1226 | /** 1227 | * Set SourceDataLine buffer size. It affects audio latency. (the delay between 1228 | * line.write(data) and real sound). Minimum value should be over 10000 bytes. 1229 | * 1230 | * @param size -1 means maximum buffer size available. 1231 | */ 1232 | @Override 1233 | public void setLineBufferSize(final int size) { 1234 | lineBufferSize = size; 1235 | } 1236 | 1237 | /** 1238 | * Sets Pan value. Line should be opened before calling this method. Linear 1239 | * scale : -1.0 ... +1.0 1240 | * 1241 | * @param fPan the new pan 1242 | */ 1243 | @Override 1244 | public void setPan(final double fPan) { 1245 | 1246 | if (!outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) || fPan < -1.0 || fPan > 1.0) 1247 | return; 1248 | logger.info(() -> "Pan : " + fPan); 1249 | outlet.getPanControl().setValue((float) fPan); 1250 | generateEvent(Status.PAN, getEncodedStreamPosition(), null); 1251 | 1252 | } 1253 | 1254 | /** 1255 | * Sets Gain value. Line should be opened before calling this method. Linear 1256 | * scale 0.0 ... 1.0 Threshold Coef. : 1/2 to avoid saturation. 1257 | * 1258 | * @param fGain The new gain value 1259 | */ 1260 | @Override 1261 | public void setGain(final double fGain) { 1262 | if (isPlaying() || isPaused() && outlet.hasControl(FloatControl.Type.MASTER_GAIN, outlet.getGainControl())) { 1263 | final double logScaleGain = 20 * Math.log10(fGain); 1264 | outlet.getGainControl().setValue((float) logScaleGain); 1265 | } 1266 | } 1267 | 1268 | @Override 1269 | public void setLogScaleGain(final double logScaleGain) { 1270 | if (isPlaying() || isPaused() && outlet.hasControl(FloatControl.Type.MASTER_GAIN, outlet.getGainControl())) { 1271 | outlet.getGainControl().setValue((float) logScaleGain); 1272 | } 1273 | } 1274 | 1275 | /** 1276 | * Set the mute of the Line. Note that mute status does not affect gain. 1277 | * 1278 | * @param mute True to mute the audio of False to unmute it 1279 | */ 1280 | @Override 1281 | public void setMute(final boolean mute) { 1282 | if (outlet.hasControl(BooleanControl.Type.MUTE, outlet.getMuteControl()) && outlet.getMuteControl().getValue() != mute) 1283 | outlet.getMuteControl().setValue(mute); 1284 | } 1285 | 1286 | /** 1287 | * Represents a control for the relative balance of a stereo signal between two 1288 | * stereo speakers. The valid range of values is -1.0 (left channel only) to 1.0 1289 | * (right channel only). The default is 0.0 (centered). 1290 | * 1291 | * @param fBalance the new balance 1292 | */ 1293 | @Override 1294 | public void setBalance(final float fBalance) { 1295 | if (outlet.hasControl(FloatControl.Type.BALANCE, outlet.getBalanceControl()) && fBalance >= -1.0 && fBalance <= 1.0) 1296 | outlet.getBalanceControl().setValue(fBalance); 1297 | else 1298 | try { 1299 | throw new StreamPlayerException(PlayerException.BALANCE_CONTROL_NOT_SUPPORTED); 1300 | } catch (final StreamPlayerException ex) { 1301 | logger.log(Level.WARNING, ex.getMessage(), ex); 1302 | } 1303 | } 1304 | 1305 | /** 1306 | * Changes specific values from equalizer. 1307 | * 1308 | * @param array the array 1309 | * @param stop the stop 1310 | */ 1311 | @Override 1312 | public void setEqualizer(final float[] array, final int stop) { 1313 | if (!isPausedOrPlaying() || !(audioInputStream instanceof PropertiesContainer)) 1314 | return; 1315 | // Map map = ((PropertiesContainer) audioInputStream).properties() 1316 | final float[] equalizer = (float[]) ((PropertiesContainer) audioInputStream).properties().get("mp3.equalizer"); 1317 | if (stop >= 0) System.arraycopy(array, 0, equalizer, 0, stop); 1318 | 1319 | } 1320 | 1321 | /** 1322 | * Changes a value from equalizer. 1323 | * 1324 | * @param value the value 1325 | * @param key the key 1326 | */ 1327 | @Override 1328 | public void setEqualizerKey(final float value, final int key) { 1329 | if (!isPausedOrPlaying() || !(audioInputStream instanceof PropertiesContainer)) 1330 | return; 1331 | // Map map = ((PropertiesContainer) audioInputStream).properties() 1332 | final float[] equalizer = (float[]) ((PropertiesContainer) audioInputStream).properties().get("mp3.equalizer"); 1333 | equalizer[key] = value; 1334 | 1335 | } 1336 | 1337 | /** 1338 | * @return The Speech Factor of the Audio 1339 | */ 1340 | @Override 1341 | public double getSpeedFactor() { 1342 | return this.speedFactor; 1343 | } 1344 | 1345 | /** 1346 | * Checks if is unknown. 1347 | * 1348 | * @return If Status==STATUS.UNKNOWN. 1349 | */ 1350 | @Override 1351 | public boolean isUnknown() { 1352 | return status == Status.NOT_SPECIFIED; 1353 | } 1354 | 1355 | /** 1356 | * Checks if is playing. 1357 | * 1358 | * @return true if player is playing ,false if not. 1359 | */ 1360 | @Override 1361 | public boolean isPlaying() { 1362 | return status == Status.PLAYING; 1363 | } 1364 | 1365 | /** 1366 | * Checks if is paused. 1367 | * 1368 | * @return true if player is paused ,false if not. 1369 | */ 1370 | @Override 1371 | public boolean isPaused() { 1372 | return status == Status.PAUSED; 1373 | } 1374 | 1375 | /** 1376 | * Checks if is paused or playing. 1377 | * 1378 | * @return true if player is paused/playing,false if not 1379 | */ 1380 | @Override 1381 | public boolean isPausedOrPlaying() { 1382 | return isPlaying() || isPaused(); 1383 | } 1384 | 1385 | /** 1386 | * Checks if is stopped. 1387 | * 1388 | * @return true if player is stopped ,false if not 1389 | */ 1390 | @Override 1391 | public boolean isStopped() { 1392 | return status == Status.STOPPED; 1393 | } 1394 | 1395 | /** 1396 | * Checks if is opened. 1397 | * 1398 | * @return true if player is opened ,false if not 1399 | */ 1400 | @Override 1401 | public boolean isOpened() { 1402 | return status == Status.OPENED; 1403 | } 1404 | 1405 | /** 1406 | * Checks if is seeking. 1407 | * 1408 | * @return true if player is seeking ,false if not 1409 | */ 1410 | @Override 1411 | public boolean isSeeking() { 1412 | return status == Status.SEEKING; 1413 | } 1414 | 1415 | Logger getLogger() { 1416 | return logger; 1417 | } 1418 | 1419 | @Override 1420 | public SourceDataLine getSourceDataLine() { 1421 | return outlet.getSourceDataLine(); 1422 | } 1423 | } 1424 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayerEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | 15 | Also(warning!): 16 | 17 | 1)You are not allowed to sell this product to third party. 18 | 2)You can't change license and made it like you are the owner,author etc. 19 | 3)All redistributions of source code files must contain all copyright 20 | notices that are currently in this file, and this list of conditions without 21 | modification. 22 | */ 23 | package com.goxr3plus.streamplayer.stream; 24 | 25 | import com.goxr3plus.streamplayer.enums.Status; 26 | 27 | /** 28 | * The Class StreamPlayerEvent. 29 | * 30 | * @author GOXR3PLUS (www.goxr3plus.co.nf) 31 | */ 32 | public class StreamPlayerEvent { 33 | 34 | /** The status. */ 35 | private Status playerStatus = Status.NOT_SPECIFIED; 36 | 37 | /** The stream position. */ 38 | private int encodedStreamPosition = -1; 39 | 40 | /** The source. */ 41 | private StreamPlayer source = null; 42 | 43 | /** The description. */ 44 | private Object description = null; 45 | 46 | /** 47 | * Constructor. 48 | * 49 | * @param source 50 | * the source 51 | * @param status 52 | * the status 53 | * @param encodededStreamPosition 54 | * the stream position 55 | * @param description 56 | * the description 57 | */ 58 | public StreamPlayerEvent(StreamPlayer source, Status status, int encodededStreamPosition, Object description) { 59 | this.source = source; 60 | this.playerStatus = status; 61 | this.encodedStreamPosition = encodededStreamPosition; 62 | this.description = description; 63 | } 64 | 65 | /** 66 | * Returns the Player Status 67 | * 68 | * @return The player Status (paused,playing,...) 69 | * @see Status 70 | */ 71 | public Status getPlayerStatus() { 72 | return playerStatus; 73 | } 74 | 75 | /** 76 | * Returns the encoded stream position 77 | * 78 | * @return EncodedStreamPosition = the position of the encoded audio stream 79 | * right now.. 80 | */ 81 | public int getEncodedStreamPosition() { 82 | return encodedStreamPosition; 83 | } 84 | 85 | /** 86 | * Gets the description. 87 | * 88 | * @return the description 89 | */ 90 | public Object getDescription() { 91 | return description; 92 | } 93 | 94 | /** 95 | * Gets the source. 96 | * 97 | * @return the source 98 | */ 99 | public Object getSource() { 100 | return source; 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | return "Source :=" + source + " , Player Status := " + playerStatus + " , EncodedStreamPosition :=" 106 | + encodedStreamPosition + " , Description :=" + description; 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayerEventLauncher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | 15 | Also(warning!): 16 | 17 | 1)You are not allowed to sell this product to third party. 18 | 2)You can't change license and made it like you are the owner,author etc. 19 | 3)All redistributions of source code files must contain all copyright 20 | notices that are currently in this file, and this list of conditions without 21 | modification. 22 | */ 23 | package com.goxr3plus.streamplayer.stream; 24 | 25 | import java.util.List; 26 | import java.util.concurrent.Callable; 27 | import java.util.logging.Level; 28 | import java.util.logging.Logger; 29 | 30 | import com.goxr3plus.streamplayer.enums.Status; 31 | 32 | /** 33 | * The Class StreamPlayerEventLauncher. 34 | * 35 | * @author GOXR3PLUS (www.goxr3plus.co.nf) 36 | */ 37 | public class StreamPlayerEventLauncher implements Callable { 38 | 39 | private final Logger logger; 40 | /** The player state. */ 41 | private Status playerState = Status.NOT_SPECIFIED; 42 | 43 | /** The stream position. */ 44 | private int encodedStreamPosition = -1; 45 | 46 | /** The description. */ 47 | private Object description = null; 48 | 49 | /** The listeners. */ 50 | private List listeners = null; 51 | 52 | /** The source. */ 53 | private StreamPlayer source = null; 54 | 55 | /** 56 | * Instantiates a new stream player event launcher. 57 | * 58 | * @param source 59 | * the source 60 | * @param playerStatus 61 | * the play state 62 | * @param encodedStreamPosition 63 | * the stream position 64 | * @param description 65 | * the description 66 | * @param listeners 67 | */ 68 | public StreamPlayerEventLauncher(StreamPlayer source, Status playerStatus, int encodedStreamPosition, Object description, 69 | List listeners) { 70 | this.source = source; 71 | this.playerState = playerStatus; 72 | this.encodedStreamPosition = encodedStreamPosition; 73 | this.description = description; 74 | this.listeners = listeners; 75 | this.logger = source.getLogger(); 76 | } 77 | 78 | @Override 79 | public String call() { 80 | // Notify all the listeners that the state has been updated 81 | if (listeners != null) { 82 | listeners.forEach(listener -> listener 83 | .statusUpdated(new StreamPlayerEvent(source, playerState, encodedStreamPosition, description))); 84 | } 85 | logger.log(Level.INFO, "Stream player Status -> " + playerState); 86 | return "OK"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayerException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | 15 | Also(warning!): 16 | 17 | 1)You are not allowed to sell this product to third party. 18 | 2)You can't change license and made it like you are the owner,author etc. 19 | 3)All redistributions of source code files must contain all copyright 20 | notices that are currently in this file, and this list of conditions without 21 | modification. 22 | */ 23 | 24 | package com.goxr3plus.streamplayer.stream; 25 | 26 | import java.io.PrintStream; 27 | import java.io.PrintWriter; 28 | 29 | /** 30 | * Special exceptions of StreamPlayer. 31 | * 32 | * @author GOXR3PLUS (www.goxr3plus.co.nf) 33 | * @author http://www.javazoom.net 34 | */ 35 | @SuppressWarnings("serial") 36 | public class StreamPlayerException extends Exception { 37 | 38 | /** 39 | * Type of exception. 40 | * 41 | * @author GOXR3PLUS 42 | */ 43 | public enum PlayerException { 44 | 45 | /** The gain control not supported. */ 46 | GAIN_CONTROL_NOT_SUPPORTED, 47 | /** The pan control not supported. */ 48 | PAN_CONTROL_NOT_SUPPORTED, 49 | /** The mute control not supported. */ 50 | MUTE_CONTROL_NOT_SUPPORTED, 51 | /** The balance control not supported. */ 52 | BALANCE_CONTROL_NOT_SUPPORTED, 53 | /** The wait error. */ 54 | WAIT_ERROR, 55 | /** The can not init line. */ 56 | CAN_NOT_INIT_LINE, 57 | /** 58 | * LINE IS NOT SUPPORTED 59 | */ 60 | LINE_NOT_SUPPORTED, 61 | /** The skip not supported. */ 62 | SKIP_NOT_SUPPORTED, 63 | } 64 | 65 | /** The cause. */ 66 | private final Throwable cause; 67 | 68 | /** 69 | * Constructor. 70 | * 71 | * @param paramString String Parameter 72 | */ 73 | public StreamPlayerException(PlayerException paramString) { 74 | super(paramString.toString()); 75 | cause = null; 76 | } 77 | 78 | /** 79 | * Constructor. 80 | * 81 | * @param paramThrowable the param throwable 82 | */ 83 | public StreamPlayerException(Throwable paramThrowable) { 84 | cause = paramThrowable; 85 | } 86 | 87 | /** 88 | * Constructor. 89 | * 90 | * @param paramString the param string 91 | * @param paramThrowable the param throwable 92 | */ 93 | public StreamPlayerException(PlayerException paramString, Throwable paramThrowable) { 94 | super(paramString.toString()); 95 | cause = paramThrowable; 96 | } 97 | 98 | @Override 99 | public Throwable getCause() { 100 | return cause; 101 | } 102 | 103 | @Override 104 | public String getMessage() { 105 | 106 | if (super.getMessage() != null) 107 | return super.getMessage(); 108 | else if (cause != null) 109 | return cause.toString(); 110 | 111 | return null; 112 | } 113 | 114 | @Override 115 | public void printStackTrace() { 116 | printStackTrace(System.err); 117 | } 118 | 119 | @Override 120 | public void printStackTrace(PrintStream printStream) { 121 | synchronized (printStream) { 122 | PrintWriter localPrintWriter = new PrintWriter(printStream, false); 123 | printStackTrace(localPrintWriter); 124 | localPrintWriter.flush(); 125 | } 126 | } 127 | 128 | @Override 129 | public void printStackTrace(PrintWriter printWriter) { 130 | if (cause != null) 131 | cause.printStackTrace(printWriter); 132 | 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayerInterface.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import com.goxr3plus.streamplayer.enums.Status; 4 | 5 | import javax.sound.sampled.SourceDataLine; 6 | import java.io.File; 7 | import java.io.InputStream; 8 | import java.net.URL; 9 | import java.time.Duration; 10 | import java.util.List; 11 | 12 | public interface StreamPlayerInterface { 13 | /** 14 | * Freeing the resources. 15 | */ 16 | void reset(); 17 | 18 | /** 19 | * Add a listener to be notified. 20 | * 21 | * @param streamPlayerListener the listener 22 | */ 23 | void addStreamPlayerListener(StreamPlayerListener streamPlayerListener); 24 | 25 | /** 26 | * Remove registered listener. 27 | * 28 | * @param streamPlayerListener the listener 29 | */ 30 | void removeStreamPlayerListener(StreamPlayerListener streamPlayerListener); 31 | 32 | /** 33 | * Open the specified object which can be File,URL or InputStream. 34 | * 35 | * @param object the object [File or URL or InputStream ] 36 | * 37 | * @throws StreamPlayerException the stream player exception 38 | * @deprecated Use one of {@link #open(File)}, {@link #open(URL)} or {@link #open(InputStream)} instead. 39 | */ 40 | @Deprecated 41 | void open(Object object) throws StreamPlayerException; 42 | 43 | /** 44 | * Open the specified file for playback. 45 | * 46 | * @param file the file to be played 47 | * 48 | * @throws StreamPlayerException the stream player exception 49 | */ 50 | void open(File file) throws StreamPlayerException; 51 | 52 | /** 53 | * Open the specified location for playback. 54 | * 55 | * @param url the location to be played 56 | * 57 | * @throws StreamPlayerException the stream player exception 58 | */ 59 | void open(URL url) throws StreamPlayerException; 60 | 61 | /** 62 | * Open the specified stream for playback. 63 | * 64 | * @param stream the stream to be played 65 | * 66 | * @throws StreamPlayerException the stream player exception 67 | */ 68 | void open(InputStream stream) throws StreamPlayerException; 69 | 70 | /** 71 | * Change the Speed Rate of the Audio , this variable affects the Sample Rate , 72 | * for example 1.0 is normal , 0.5 is half the speed and 2.0 is double the speed 73 | * Note that you have to restart the audio for this to take effect 74 | * 75 | * @param speedFactor speedFactor 76 | */ 77 | void setSpeedFactor(double speedFactor); 78 | 79 | /** 80 | * Starts the play back. 81 | * 82 | * @throws StreamPlayerException the stream player exception 83 | */ 84 | void play() throws StreamPlayerException; 85 | 86 | /** 87 | * Pauses the play back.
88 | *

89 | * Player Status = PAUSED. * @return False if failed(so simple...) 90 | * 91 | * @return true, if successful 92 | */ 93 | boolean pause(); 94 | 95 | /** 96 | * Stops the play back.
97 | *

98 | * Player Status = STOPPED.
99 | * Thread should free Audio resources. 100 | */ 101 | void stop(); 102 | 103 | /** 104 | * Resumes the play back.
105 | *

106 | * Player Status = PLAYING* 107 | * 108 | * @return False if failed(so simple...) 109 | */ 110 | boolean resume(); 111 | 112 | /** 113 | * Skip bytes in the File input stream. It will skip N frames matching to bytes, 114 | * so it will never skip given bytes len 115 | * 116 | * @param bytes the bytes 117 | * 118 | * @return value bigger than 0 for File and value = 0 for URL and InputStream 119 | * 120 | * @throws StreamPlayerException the stream player exception 121 | */ 122 | long seekBytes(long bytes) throws StreamPlayerException; 123 | 124 | /** 125 | * Skip x seconds of audio 126 | * See {@link #seekBytes(long)} 127 | * 128 | * @param seconds Seconds to Skip 129 | */ 130 | //todo not finished needs more validations 131 | long seekSeconds(int seconds) throws StreamPlayerException; 132 | 133 | /** 134 | * Go to X time of the Audio 135 | * See {@link #seekBytes(long)} 136 | * 137 | * @param seconds Seconds to Skip 138 | */ 139 | long seekTo(int seconds) throws StreamPlayerException; 140 | 141 | int getDurationInSeconds(); 142 | 143 | long getDurationInMilliseconds(); 144 | 145 | Duration getDuration(); 146 | 147 | /** 148 | * Calculates the current position of the encoded audio based on
149 | * nEncodedBytes = encodedAudioLength - 150 | * encodedAudioInputStream.available(); 151 | * 152 | * @return The Position of the encoded stream in term of bytes 153 | */ 154 | int getEncodedStreamPosition(); 155 | 156 | /** 157 | * Return SourceDataLine buffer size. 158 | * 159 | * @return -1 maximum buffer size. 160 | */ 161 | int getLineBufferSize(); 162 | 163 | /** 164 | * Return SourceDataLine current buffer size. 165 | * 166 | * @return The current line buffer size 167 | */ 168 | int getLineCurrentBufferSize(); 169 | 170 | /** 171 | * Returns all available mixers. 172 | * 173 | * @return A List of available Mixers 174 | */ 175 | List getMixers(); 176 | 177 | /** 178 | * Returns Gain value. 179 | * 180 | * @return The Gain Value 181 | */ 182 | float getGainValue(); 183 | 184 | /** 185 | * Returns maximum Gain value. 186 | * 187 | * @return The Maximum Gain Value 188 | */ 189 | float getMaximumGain(); 190 | 191 | /** 192 | * Returns minimum Gain value. 193 | * 194 | * @return The Minimum Gain Value 195 | */ 196 | float getMinimumGain(); 197 | 198 | /** 199 | * Returns Pan precision. 200 | * 201 | * @return The Precision Value 202 | */ 203 | float getPrecision(); 204 | 205 | /** 206 | * Returns Pan value. 207 | * 208 | * @return The Pan Value 209 | */ 210 | float getPan(); 211 | 212 | /** 213 | * Return the mute Value(true || false). 214 | * 215 | * @return True if muted , False if not 216 | */ 217 | boolean getMute(); 218 | 219 | /** 220 | * Return the balance Value. 221 | * 222 | * @return The Balance Value 223 | */ 224 | float getBalance(); 225 | 226 | /** 227 | * Return the total size of this file in bytes. 228 | * 229 | * @return encodedAudioLength 230 | */ 231 | long getTotalBytes(); 232 | 233 | /** 234 | * @return BytePosition 235 | */ 236 | int getPositionByte(); 237 | 238 | /** 239 | * Gets the source data line. 240 | * 241 | * @return The SourceDataLine 242 | */ 243 | SourceDataLine getSourceDataLine(); 244 | 245 | /** 246 | * This method will return the status of the player 247 | * 248 | * @return The Player Status 249 | */ 250 | Status getStatus(); 251 | 252 | /** 253 | * Set SourceDataLine buffer size. It affects audio latency. (the delay between 254 | * line.write(data) and real sound). Minimum value should be over 10000 bytes. 255 | * 256 | * @param size -1 means maximum buffer size available. 257 | */ 258 | void setLineBufferSize(int size); 259 | 260 | /** 261 | * Set the name of the mixer. This should be called before opening a Line. 262 | * 263 | * @param mixerName the name 264 | */ 265 | void setMixerName(String mixerName); 266 | 267 | /** 268 | * Sets Pan value. Line should be opened before calling this method. Linear 269 | * scale : -1.0 ... +1.0 270 | * 271 | * @param fPan the new pan 272 | */ 273 | void setPan(double fPan); 274 | 275 | /** 276 | * Sets Gain value. Line should be opened before calling this method. Linear 277 | * scale 0.0 ... 1.0 Threshold Coef. : 1/2 to avoid saturation. 278 | * 279 | * @param fGain The new gain value 280 | */ 281 | void setGain(double fGain); 282 | 283 | void setLogScaleGain(double logScaleGain); 284 | 285 | /** 286 | * Set the mute of the Line. Note that mute status does not affect gain. 287 | * 288 | * @param mute True to mute the audio of False to unmute it 289 | */ 290 | void setMute(boolean mute); 291 | 292 | /** 293 | * Represents a control for the relative balance of a stereo signal between two 294 | * stereo speakers. The valid range of values is -1.0 (left channel only) to 1.0 295 | * (right channel only). The default is 0.0 (centered). 296 | * 297 | * @param fBalance the new balance 298 | */ 299 | void setBalance(float fBalance); 300 | 301 | /** 302 | * Changes specific values from equalizer. 303 | * 304 | * @param array the array 305 | * @param stop the stop 306 | */ 307 | void setEqualizer(float[] array, int stop); 308 | 309 | /** 310 | * Changes a value from equalizer. 311 | * 312 | * @param value the value 313 | * @param key the key 314 | */ 315 | void setEqualizerKey(float value, int key); 316 | 317 | /** 318 | * @return The Speech Factor of the Audio 319 | */ 320 | double getSpeedFactor(); 321 | 322 | /** 323 | * Checks if is unknown. 324 | * 325 | * @return If Status==STATUS.UNKNOWN. 326 | */ 327 | boolean isUnknown(); 328 | 329 | /** 330 | * Checks if is playing. 331 | * 332 | * @return true if player is playing ,false if not. 333 | */ 334 | boolean isPlaying(); 335 | 336 | /** 337 | * Checks if is paused. 338 | * 339 | * @return true if player is paused ,false if not. 340 | */ 341 | boolean isPaused(); 342 | 343 | /** 344 | * Checks if is paused or playing. 345 | * 346 | * @return true if player is paused/playing,false if not 347 | */ 348 | boolean isPausedOrPlaying(); 349 | 350 | /** 351 | * Checks if is stopped. 352 | * 353 | * @return true if player is stopped ,false if not 354 | */ 355 | boolean isStopped(); 356 | 357 | /** 358 | * Checks if is opened. 359 | * 360 | * @return true if player is opened ,false if not 361 | */ 362 | boolean isOpened(); 363 | 364 | /** 365 | * Checks if is seeking. 366 | * 367 | * @return true if player is seeking ,false if not 368 | */ 369 | boolean isSeeking(); 370 | } 371 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayerListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | 15 | Also(warning!): 16 | 17 | 1)You are not allowed to sell this product to third party. 18 | 2)You can't change license and made it like you are the owner,author etc. 19 | 3)All redistributions of source code files must contain all copyright 20 | notices that are currently in this file, and this list of conditions without 21 | modification. 22 | */ 23 | package com.goxr3plus.streamplayer.stream; 24 | 25 | import java.util.Map; 26 | 27 | /** 28 | * Used to notify for events that are happening on StreamPlayer. 29 | * 30 | * @author GOXR3PLUS (www.goxr3plus.co.nf) 31 | */ 32 | public interface StreamPlayerListener { 33 | 34 | /** 35 | * It is called when the StreamPlayer open(Object object) method is called. 36 | * 37 | * @param dataSource the data source 38 | * @param properties the properties 39 | */ 40 | void opened(Object dataSource , Map properties); 41 | 42 | /** 43 | * Is called several times per second when StreamPlayer run method is 44 | * running. 45 | * 46 | * @param nEncodedBytes the n encoded bytes 47 | * @param microsecondPosition the microsecond position 48 | * @param pcmData the pcm data 49 | * @param properties the properties 50 | */ 51 | void progress(int nEncodedBytes , long microsecondPosition , byte[] pcmData , Map properties); 52 | 53 | /** 54 | * Is called every time the status of the StreamPlayer changes. 55 | * 56 | * @param event the event 57 | */ 58 | void statusUpdated(StreamPlayerEvent event); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/ThreadFactoryWithNamePrefix.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | /** 7 | * ThreadFactory with the ability to set the thread name prefix. This class is exactly similar to 8 | * {@link java.util.concurrent.Executors#defaultThreadFactory()} from JDK8, except for the thread naming feature. 9 | * 10 | *

11 | * The factory creates threads that have names on the form prefix-N-thread-M, where prefix is a string provided in the constructor, 12 | * N is the sequence number of this factory, and M is the sequence number of the thread created by this factory. 13 | */ 14 | public class ThreadFactoryWithNamePrefix implements ThreadFactory { 15 | 16 | // Note: The source code for this class was based entirely on 17 | // Executors.DefaultThreadFactory class from the JDK8 source. 18 | // The only change made is the ability to configure the thread 19 | // name prefix. 20 | 21 | private static final AtomicInteger poolNumber = new AtomicInteger(1); 22 | private final ThreadGroup group; 23 | private final AtomicInteger threadNumber = new AtomicInteger(1); 24 | private final String namePrefix; 25 | 26 | /** 27 | * Creates a new ThreadFactory where threads are created with a name prefix of prefix. 28 | * 29 | * @param prefix 30 | * Thread name prefix. Never use a value of "pool" as in that case you might as well have used 31 | * {@link java.util.concurrent.Executors#defaultThreadFactory()}. 32 | */ 33 | public ThreadFactoryWithNamePrefix(String prefix) { 34 | SecurityManager s = System.getSecurityManager(); 35 | group = ( s != null ) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); 36 | namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-"; 37 | } 38 | 39 | @Override 40 | public Thread newThread(Runnable r) { 41 | Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); 42 | if (t.isDaemon()) { 43 | t.setDaemon(false); 44 | } 45 | if (t.getPriority() != Thread.NORM_PRIORITY) { 46 | t.setPriority(Thread.NORM_PRIORITY); 47 | } 48 | return t; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/UrlDataSource.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import javax.sound.sampled.AudioFileFormat; 4 | import javax.sound.sampled.AudioInputStream; 5 | import javax.sound.sampled.AudioSystem; 6 | import javax.sound.sampled.UnsupportedAudioFileException; 7 | import java.io.IOException; 8 | import java.net.URL; 9 | import java.time.Duration; 10 | 11 | public class UrlDataSource implements DataSource { 12 | 13 | private final URL source; 14 | 15 | UrlDataSource(URL source) { 16 | this.source = source; 17 | } 18 | 19 | @Override 20 | public AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException { 21 | return AudioSystem.getAudioFileFormat(source); 22 | } 23 | 24 | @Override 25 | public AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException { 26 | return AudioSystem.getAudioInputStream(source); 27 | } 28 | 29 | @Override 30 | public int getDurationInSeconds() { 31 | return -1; 32 | } 33 | 34 | @Override 35 | public long getDurationInMilliseconds() { 36 | return -1; 37 | } 38 | 39 | @Override 40 | public Duration getDuration() { 41 | return null; 42 | } 43 | 44 | @Override 45 | public Object getSource() { 46 | return source; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "UrlDataSource with " + source.toString(); 52 | } 53 | 54 | @Override 55 | public boolean isFile() { 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/tools/IOInfo.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.tools; 2 | 3 | import org.apache.commons.io.FilenameUtils; 4 | 5 | public class IOInfo { 6 | 7 | /** 8 | * Returns the extension of file(without (.)) for example (ai.mp3)->(mp3) 9 | * and to lowercase (Mp3 -> mp3) 10 | * 11 | * @param absolutePath The File absolute path 12 | * 13 | * @return the File extension 14 | */ 15 | public static String getFileExtension(final String absolutePath) { 16 | return FilenameUtils.getExtension(absolutePath).toLowerCase(); 17 | 18 | // int i = path.lastIndexOf('.'); // characters contained before (.) 19 | // 20 | // if the name is not empty 21 | // if (i > 0 && i < path.length() - 1) 22 | // return path.substring(i + 1).toLowerCase() 23 | // 24 | // return null 25 | } 26 | 27 | /** 28 | * Returns the name of the file for example if file path is (C:/Give me 29 | * more/no no/media.ogg) it returns (media.ogg) 30 | * 31 | * @param absolutePath the path 32 | * 33 | * @return the File title+extension 34 | */ 35 | public static String getFileName(final String absolutePath) { 36 | return FilenameUtils.getName(absolutePath); 37 | 38 | } 39 | 40 | /** 41 | * Returns the title of the file for example if file name is (club.mp3) 42 | * it returns (club) 43 | * 44 | * @param absolutePath The File absolute path 45 | * 46 | * @return the File title 47 | */ 48 | public static String getFileTitle(final String absolutePath) { 49 | return FilenameUtils.getBaseName(absolutePath); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/tools/TimeTool.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.tools; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import javax.sound.sampled.AudioFormat; 7 | import javax.sound.sampled.AudioInputStream; 8 | import javax.sound.sampled.AudioSystem; 9 | import javax.sound.sampled.UnsupportedAudioFileException; 10 | 11 | import org.jaudiotagger.audio.mp3.MP3AudioHeader; 12 | import org.jaudiotagger.audio.mp3.MP3File; 13 | 14 | import com.goxr3plus.streamplayer.enums.AudioType; 15 | 16 | public final class TimeTool { 17 | 18 | private TimeTool() { 19 | } 20 | 21 | /** 22 | * Returns the time in format %02d:%02d. 23 | * 24 | * @param seconds the seconds 25 | * @return the time edited on hours 26 | */ 27 | public static String getTimeEditedOnHours(final int seconds) { 28 | 29 | return String.format("%02d:%02d", seconds / 60, seconds % 60); 30 | 31 | } 32 | 33 | /** 34 | * Returns the time in format %02d:%02d:%02d if( minutes >60 ) or 35 | * %02dsec if (seconds<60) %02d:%02d. 36 | * 37 | * @param seconds the seconds 38 | * @return the time edited in format %02d:%02d:%02d if( minutes >60 ) or 39 | * %02d:%02d. [[SuppressWarningsSpartan]] 40 | */ 41 | public static String getTimeEdited(final int seconds) { 42 | if (seconds < 60) // duration < 1 minute 43 | return String.format("%02ds", seconds % 60); 44 | else if ((seconds / 60) / 60 <= 0) // duration < 1 hour 45 | return String.format("%02dm:%02d", (seconds / 60) % 60, seconds % 60); 46 | else 47 | return String.format("%02dh:%02dm:%02d", (seconds / 60) / 60, (seconds / 60) % 60, seconds % 60); 48 | } 49 | 50 | /** 51 | * /** Returns the time in format %02d:%02d:%02d if( minutes >60 ) or 52 | * %02d:%02d. 53 | * 54 | * @param ms The milliseconds 55 | * @return The Time edited in format %02d:%02d:%02d if( minutes >60 ) or 56 | * %02d:%02d. 57 | * 58 | */ 59 | public static String millisecondsToTime(final long ms) { 60 | final int millis = (int) ((ms % 1000) / 100); 61 | // int seconds = (int) ((ms / 1000) % 60); 62 | // int minutes = (int) ((ms / (1000 * 60)) % 60); 63 | // int hours = (int) ((ms / (1000 * 60 * 60)) % 24); 64 | 65 | // if (minutes > 60) 66 | // return String.format("%02d:%02d:%02d.%d", hours, minutes, seconds, millis); 67 | // else 68 | // return String.format("%02d:%02d.%d", minutes, seconds, millis); 69 | 70 | return String.format(".%d", millis); 71 | 72 | } 73 | 74 | /** 75 | * Returns the time of Audio to seconds 76 | * 77 | * @param name the name 78 | * @param type
79 | * 1->URL
80 | * 2->FILE
81 | * 3->INPUTSTREAM 82 | * @return time in milliseconds 83 | */ 84 | public static int durationInSeconds(final String name, final AudioType type) { 85 | 86 | final long time = TimeTool.durationInMilliseconds(name, type); 87 | 88 | return (int) ((time == 0 || time == -1) ? time : time / 1000); 89 | 90 | // Long microseconds = (Long)AudioSystem.getAudioFileFormat(new 91 | // File(audio)).properties().get("duration") int mili = (int)(microseconds / 92 | // 1000L); 93 | // int sec = milli / 1000 % 60; 94 | // int min = milli / 1000 / 60; 95 | 96 | } 97 | 98 | /** 99 | * This method determines the duration of given data. 100 | * 101 | * @param input The name of the input 102 | * @param audioType URL, FILE, INPUTSTREAM, UNKOWN; 103 | * @return Returns the duration of URL/FILE/INPUTSTREAM in milliseconds 104 | */ 105 | public static long durationInMilliseconds(final String input, final AudioType audioType) { 106 | return audioType == AudioType.FILE ? durationInMilliseconds_Part2(new File(input)) 107 | : (audioType == AudioType.URL || audioType == AudioType.INPUTSTREAM || audioType == AudioType.UNKNOWN) 108 | ? -1 109 | : -1; 110 | } 111 | 112 | /** 113 | * Used by method durationInMilliseconds() to get file duration. 114 | * 115 | * @param file the file 116 | * @return the int 117 | */ 118 | private static long durationInMilliseconds_Part2(final File file) { 119 | long milliseconds = -1; 120 | 121 | // exists? 122 | if (file.exists() && file.length() != 0) { 123 | 124 | // extension? 125 | final String extension = IOInfo.getFileExtension(file.getName()); 126 | 127 | // MP3? 128 | if ("mp3".equals(extension)) { 129 | try { 130 | milliseconds = new MP3File(file).getMP3AudioHeader().getTrackLength() * 1000; 131 | if (milliseconds == 0) { 132 | MP3AudioHeader header = new MP3File(file).getMP3AudioHeader(); 133 | int samplesPerFrame; 134 | switch(header.getMpegLayer()) { 135 | case("Layer 1"): 136 | samplesPerFrame = 384; 137 | break; 138 | case("Layer 2"): 139 | samplesPerFrame = 576; 140 | break; 141 | case("Layer 3"): 142 | samplesPerFrame = 1152; 143 | break; 144 | default: 145 | samplesPerFrame = 1152; 146 | break; 147 | } 148 | 149 | double frameLengthInMilliseconds = (((double) samplesPerFrame / header.getSampleRateAsNumber()) * 1000); 150 | milliseconds = (long) (header.getNumberOfFrames() * frameLengthInMilliseconds); 151 | } 152 | 153 | // milliseconds = (int) ( (Long) 154 | // AudioSystem.getAudioFileFormat(file).properties().get("duration") / 1000 ); 155 | 156 | // Get the result of mp3agic if the duration is bigger than 6 minutes 157 | // if (milliseconds / 1000 > 60 * 9) { 158 | // System.out.println("Entered.."); 159 | // milliseconds = tryWithMp3Agic(file); 160 | // } 161 | 162 | } catch (final Exception ex) { 163 | System.err.println("Problem getting the time of-> " + file.getAbsolutePath()); 164 | } 165 | // } 166 | } 167 | // WAVE || OGG? 168 | else if ("ogg".equals(extension) || "wav".equals(extension)) { 169 | try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file)) { 170 | final AudioFormat format = audioInputStream.getFormat(); 171 | milliseconds = (long) (((double) file.length() / ( format.getFrameSize() * (double) format.getFrameRate())) * 1000); 172 | } catch (IOException | UnsupportedAudioFileException ex) { 173 | System.err.println("Problem getting the time of-> " + file.getAbsolutePath()); 174 | } 175 | } 176 | } 177 | 178 | // System.out.println("Passed with error") 179 | return milliseconds < 0 ? -1 : milliseconds; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/stream/SourceDataLineTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.BDDMockito; 7 | 8 | import javax.sound.sampled.SourceDataLine; 9 | import java.io.File; 10 | import java.util.logging.Logger; 11 | 12 | import static java.lang.Math.log10; 13 | import static org.junit.jupiter.api.Assertions.*; 14 | import static org.mockito.ArgumentMatchers.booleanThat; 15 | import static org.mockito.Mockito.mock; 16 | 17 | public class SourceDataLineTest { 18 | 19 | StreamPlayer player; 20 | private File audioFile; 21 | 22 | @BeforeEach 23 | void setup() { 24 | final Logger logger = mock(Logger.class); 25 | player = new StreamPlayer(logger); 26 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); 27 | } 28 | 29 | @AfterEach 30 | void tearDown() { 31 | player.stop(); 32 | } 33 | 34 | @Test 35 | void gain() throws StreamPlayerException, InterruptedException { 36 | // Setup 37 | final double gain1 = 0.83; 38 | final double gain2 = 0.2; 39 | final double delta = 0.05; 40 | final boolean listen = false; 41 | 42 | // Exercise 43 | final float initialGain = player.getGainValue(); 44 | player.open(audioFile); 45 | player.seekTo(30); 46 | player.play(); 47 | player.setGain(gain1); 48 | final float actualGain1First = player.getGainValue(); 49 | if (listen) Thread.sleep(2000); 50 | final float actualGain1 = player.getGainValue(); 51 | 52 | player.setGain(gain2); 53 | if (listen) Thread.sleep(2000); 54 | final float actualGain2 = player.getGainValue(); 55 | 56 | player.setGain(gain1); 57 | if (listen) Thread.sleep(2000); 58 | 59 | player.stop(); 60 | 61 | // Verify 62 | assertEquals(0, initialGain); 63 | assertEquals(actualGain1First, actualGain1); 64 | assertEquals(20*log10(gain1), actualGain1, delta); // TODO: Investigate probable bug. 65 | // fail("Test not done"); 66 | } 67 | 68 | /** 69 | * Plays music if "listen" is true. 70 | * Varies the gain, and checks that it can be read back. 71 | * If listen is true, it plays for 2 seconds per gain level. 72 | * 73 | * @throws StreamPlayerException 74 | * @throws InterruptedException 75 | */ 76 | @Test 77 | void logScaleGain() throws StreamPlayerException, InterruptedException { 78 | // Setup 79 | final boolean listen = false; 80 | 81 | // Exercise 82 | 83 | player.open(audioFile); 84 | player.seekTo(30); 85 | player.play(); 86 | 87 | assertGainCanBeSetTo(-10, listen); 88 | assertGainCanBeSetTo(-75, listen); 89 | assertGainCanBeSetTo(0, listen); 90 | assertGainCanBeSetTo(6, listen); 91 | 92 | player.stop(); 93 | } 94 | 95 | private void assertGainCanBeSetTo(double gain, boolean listen) throws InterruptedException { 96 | final float atGain = playAtGain(listen, gain); 97 | assertEquals(gain, atGain, 0.01); 98 | } 99 | 100 | private float playAtGain(boolean listen, double gain) throws InterruptedException { 101 | player.setLogScaleGain(gain); 102 | if (listen) { 103 | Thread.sleep(2000); 104 | } 105 | return player.getGainValue(); 106 | } 107 | 108 | @Test 109 | void balance() throws StreamPlayerException { 110 | // Setup 111 | final float wantedBalance = 0.5f; 112 | 113 | //Exercise 114 | player.open(audioFile); 115 | player.play(); // Necessary to be able to set the balance 116 | 117 | final float initialBalance = player.getBalance(); 118 | player.setBalance(wantedBalance); 119 | player.stop(); // Probably not needed, but cleanup is good. 120 | final float actualBalance = player.getBalance(); // Can be made before or after stop() 121 | 122 | // Verify 123 | assertEquals(0, initialBalance); 124 | assertEquals(wantedBalance, actualBalance); 125 | } 126 | 127 | @Test 128 | void pan() throws StreamPlayerException { 129 | double delta = 1e-6; 130 | final float initialPan = player.getPan(); 131 | assertEquals(0, initialPan); 132 | 133 | player.open(audioFile); 134 | player.play(); 135 | 136 | double pan = -0.9; 137 | player.setPan(pan); 138 | assertEquals(pan, player.getPan(), delta); 139 | 140 | double outsideRange = 1.1; 141 | player.setPan(outsideRange); 142 | assertEquals(pan, player.getPan(), delta); 143 | } 144 | 145 | @Test 146 | void mute() throws StreamPlayerException { 147 | assertFalse(player.getMute()); 148 | player.setMute(true); 149 | assertFalse(player.getMute()); 150 | player.open(audioFile); 151 | player.setMute(true); 152 | assertFalse(player.getMute()); 153 | 154 | player.play(); 155 | player.setMute(true); 156 | assertTrue(player.getMute()); // setMute works only after play() has been called. 157 | 158 | 159 | player.setMute(false); 160 | assertFalse(player.getMute()); 161 | } 162 | 163 | @Test 164 | void sourceDataLine() throws StreamPlayerException { 165 | assertNull(player.getSourceDataLine()); 166 | 167 | player.open(audioFile); 168 | assertNotNull(player.getSourceDataLine()); 169 | 170 | player.play(); 171 | 172 | assertNotNull(player.getSourceDataLine()); 173 | } 174 | 175 | @Test 176 | void playAndPause() throws StreamPlayerException, InterruptedException { 177 | boolean listen = true; 178 | player.open(audioFile); 179 | player.play(); 180 | player.seekTo(30); 181 | if (listen) Thread.sleep(200); 182 | 183 | player.pause(); 184 | if (listen) Thread.sleep(100); 185 | 186 | player.resume(); // TODO: Examine what happens if play() is called instead. 187 | if (listen) Thread.sleep(200); 188 | //player.stop(); 189 | 190 | // TODO: asserts and listen=false 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerEventTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import com.goxr3plus.streamplayer.enums.Status; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.configuration.IMockitoConfiguration; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | import static org.mockito.Mockito.mock; 10 | 11 | class StreamPlayerEventTest { 12 | 13 | private StreamPlayer source; 14 | private Object description; 15 | private Status status; 16 | private int encodededStreamPosition; 17 | private StreamPlayerEvent event; 18 | 19 | @BeforeEach 20 | void setUp() { 21 | description = new Object(); 22 | source = mock(StreamPlayer.class); 23 | status = Status.RESUMED; 24 | encodededStreamPosition = 12345; 25 | event = new StreamPlayerEvent(source, status, encodededStreamPosition, description); 26 | } 27 | 28 | @Test 29 | void itReturnsTheStatus() { 30 | assertEquals(status, event.getPlayerStatus()); 31 | } 32 | 33 | @Test 34 | void itReturnsTheEncodedStreamPosition() { 35 | assertEquals(encodededStreamPosition, event.getEncodedStreamPosition()); 36 | } 37 | 38 | @Test 39 | void itReturnsTheSource() { 40 | assertSame(source, event.getSource()); 41 | } 42 | 43 | @Test 44 | void itReturnsTheDescription() { 45 | assertSame(description, event.getDescription()); 46 | } 47 | 48 | @Test 49 | void itReturnsAString() { 50 | final String actual = event.toString(); 51 | final String expected = "Source :=" 52 | + source.toString() 53 | + " , Player Status := RESUMED , EncodedStreamPosition :=12345 , Description :=" 54 | + description.toString(); 55 | assertEquals(expected, actual); 56 | } 57 | 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import com.goxr3plus.streamplayer.enums.Status; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Disabled; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.sound.sampled.AudioFileFormat; 10 | import javax.sound.sampled.AudioSystem; 11 | import javax.sound.sampled.SourceDataLine; 12 | import javax.sound.sampled.UnsupportedAudioFileException; 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.List; 16 | import java.util.logging.Logger; 17 | 18 | import static org.junit.jupiter.api.Assertions.*; 19 | import static org.mockito.Mockito.mock; 20 | 21 | /** 22 | * Tests of all or most of the public methods of StreamPlayer. 23 | * These unit tests are written primarily as documentation of the behavior and as example use case, 24 | * not as a part of test driven development. 25 | */ 26 | public class StreamPlayerFutureImprovementTest { 27 | StreamPlayer player; 28 | private File audioFile; 29 | 30 | @BeforeEach 31 | void setup() { 32 | final Logger logger = mock(Logger.class); 33 | player = new StreamPlayer(logger); 34 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); 35 | } 36 | 37 | /** 38 | * This test fails if it's permitted to add a null to the StreamPlayer listener list. 39 | */ 40 | @Test 41 | @Disabled("This test fails with the current implementation. The test exists to illustrate a future improvement.") 42 | void addStreamPlayerListener_dontAcceptNull() { 43 | // We can't allow nulls in the list of listeners, because they will cause NullPointerExceptions. 44 | // One way to handle it is to require that an exception is thrown immediately when we 45 | // try to add the null. 46 | assertThrows(NullPointerException.class, () -> player.addStreamPlayerListener(null)); 47 | 48 | // An alternative way would be to use some kind of null annotation, to disallow 49 | // nulls being passed at compile time. 50 | } 51 | 52 | 53 | @Test 54 | @DisplayName("When play() is called without first calling open(), an exception is thrown") 55 | @Disabled("This test fails with the current implementation. The test exists to illustrate a future improvement.") 56 | void playingUnopenedSourceThrowsException() { 57 | 58 | assertThrows(Exception.class, () -> player.play()); 59 | } 60 | 61 | @Test 62 | @Disabled("This test fails with the current implementation. The test exists to illustrate a future improvement.") 63 | void seekBytes() throws StreamPlayerException { 64 | player.open(audioFile); 65 | player.play(); 66 | int positionByte1 = player.getPositionByte(); 67 | 68 | player.seekBytes(100); 69 | int positionByte2 = player.getPositionByte(); 70 | 71 | assertTrue( positionByte2 > positionByte1); 72 | 73 | // TODO: It seems that getPositionByte doesn't work. 74 | // It isn't called from within this project, except for in this test. 75 | // It is however called by XR3Player. If XR3Player needs this method, it must be tested 76 | // within this project. The method relies on a map, which doesn't seem to be updated by play() 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNotNull; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | import static org.junit.jupiter.api.Assertions.fail; 10 | import static org.mockito.ArgumentMatchers.any; 11 | import static org.mockito.ArgumentMatchers.anyInt; 12 | import static org.mockito.ArgumentMatchers.anyLong; 13 | import static org.mockito.Mockito.atLeast; 14 | import static org.mockito.Mockito.atMost; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.spy; 17 | import static org.mockito.Mockito.times; 18 | import static org.mockito.Mockito.verify; 19 | 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.logging.Logger; 25 | 26 | import javax.sound.sampled.*; 27 | 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Disabled; 30 | import org.junit.jupiter.api.Test; 31 | import org.mockito.ArgumentCaptor; 32 | 33 | import com.goxr3plus.streamplayer.enums.Status; 34 | 35 | /** 36 | * Tests of all or most of the public methods of StreamPlayer. 37 | * These unit tests are written primarily as documentation of the behavior and as example use case, 38 | * not as a part of test driven development. 39 | */ 40 | public class StreamPlayerMethodsTest { 41 | StreamPlayer player; 42 | private File audioFile; 43 | 44 | @BeforeEach 45 | void setup() { 46 | final Logger logger = mock(Logger.class); 47 | player = new StreamPlayer(logger); 48 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); 49 | } 50 | 51 | @Test 52 | void duration() throws StreamPlayerException { 53 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); 54 | player.open(audioFile); 55 | assertEquals(245, player.getDurationInSeconds()); 56 | assertEquals(245000, player.getDurationInMilliseconds()); 57 | assertNotNull(player.getDuration()); 58 | assertEquals(245, player.getDuration().getSeconds()); 59 | assertEquals(player.getDuration().toMillis(), player.getDurationInMilliseconds()); 60 | 61 | audioFile = new File("kick.wav"); 62 | player.open(audioFile); 63 | assertEquals(0, player.getDurationInSeconds()); 64 | assertEquals(111, player.getDurationInMilliseconds()); 65 | 66 | audioFile = new File("kick.mp3"); 67 | player.open(audioFile); 68 | assertEquals(0, player.getDurationInSeconds()); 69 | // Note: the result of calculating a .mp3's duration is different than that of a .wav file 70 | assertEquals(156, player.getDurationInMilliseconds()); 71 | } 72 | 73 | @Test 74 | void balance() throws StreamPlayerException { 75 | // Setup 76 | final float wantedBalance = 0.5f; 77 | 78 | //Exercise 79 | player.open(audioFile); 80 | player.play(); // Necessary to be able to set the balance 81 | 82 | final float initialBalance = player.getBalance(); 83 | player.setBalance(wantedBalance); 84 | player.stop(); // Probably not needed, but cleanup is good. 85 | final float actualBalance = player.getBalance(); // Can be made before or after stop() 86 | 87 | // Verify 88 | assertEquals(0, initialBalance); 89 | assertEquals(wantedBalance, actualBalance); 90 | } 91 | 92 | @Test 93 | void status() throws StreamPlayerException { 94 | // Setup 95 | final File audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); 96 | 97 | // Exercise 98 | final Status initialStatus = player.getStatus(); 99 | 100 | player.open(audioFile); 101 | final Status statusAfterOpen = player.getStatus(); 102 | 103 | player.stop(); 104 | final Status statusAfterFirstStop = player.getStatus(); 105 | 106 | player.play(); 107 | final Status statusAfterPlay = player.getStatus(); 108 | 109 | player.pause(); 110 | final Status statusAfterPause = player.getStatus(); 111 | 112 | player.seekTo(40); 113 | final Status statusAfterSeeking = player.getStatus(); 114 | 115 | player.stop(); 116 | final Status statusAfterSecondStop = player.getStatus(); 117 | 118 | // Verify 119 | assertEquals(Status.NOT_SPECIFIED, initialStatus); 120 | assertEquals(Status.OPENED, statusAfterOpen); 121 | assertEquals(Status.STOPPED, statusAfterFirstStop); 122 | assertEquals(Status.PLAYING, statusAfterPlay); 123 | assertEquals(Status.PAUSED, statusAfterPause); 124 | assertEquals(Status.PAUSED, statusAfterSeeking); // Still paused (or paused again) 125 | assertEquals(Status.STOPPED, statusAfterSecondStop); 126 | } 127 | 128 | @Test 129 | void gain() throws StreamPlayerException, InterruptedException { 130 | // Setup 131 | final double gain1_dB = 0.5; 132 | final double gain2 = 0.2; 133 | final double delta = 0.05; 134 | 135 | // By setting listen to true, you an listen to the musig being played, 136 | // and hear that the gain changes. 137 | // This is totally against the rules for unit testing, but can be useful. 138 | final boolean listen = false; 139 | 140 | // Exercise 141 | final float initialGain = player.getGainValue(); 142 | player.open(audioFile); 143 | player.seekTo(30); 144 | player.play(); 145 | player.setGain(gain1_dB); 146 | final float actualGain0 = player.getGainValue(); 147 | if (listen) Thread.sleep(2000); 148 | final float actualGain1 = player.getGainValue(); 149 | 150 | player.setGain(gain2); 151 | if (listen) Thread.sleep(2000); 152 | final float actualGain2 = player.getGainValue(); 153 | 154 | player.setGain(gain1_dB); 155 | if (listen) Thread.sleep(2000); 156 | 157 | player.stop(); 158 | 159 | // Verify 160 | assertEquals(0, initialGain); 161 | assertEquals(actualGain0, actualGain1); 162 | assertEquals(20.0 * Math.log10(gain1_dB), actualGain1, delta); 163 | 164 | // TODO: Consider changing the API. setGain() and getGainValue() have different scales. 165 | // setGain(linear scale), 166 | // whereas getGainValue() returns a logarithmic dB scale value. This is inconsistent. 167 | } 168 | 169 | /** 170 | * Plays music if "listen" is true. 171 | * Varies the gain, and checks that it can be read back. 172 | * If listen is true, it plays for 2 seconds per gain level. 173 | * 174 | * @throws StreamPlayerException 175 | * @throws InterruptedException 176 | */ 177 | @Test 178 | void logScaleGain() throws StreamPlayerException, InterruptedException { 179 | // Setup 180 | final boolean listen = false; // Set to true to listen to the test. 181 | 182 | // Exercise 183 | 184 | player.open(audioFile); 185 | player.seekTo(30); 186 | player.play(); 187 | 188 | assertGainCanBeSetTo(-10, listen); 189 | assertGainCanBeSetTo(-75, listen); 190 | assertGainCanBeSetTo(0, listen); 191 | assertGainCanBeSetTo(6, listen); 192 | 193 | player.stop(); 194 | } 195 | 196 | private void assertGainCanBeSetTo(double gain, boolean listen) throws InterruptedException { 197 | final float atGain = playAtGain(listen, gain); 198 | assertEquals(gain, atGain, 0.01); 199 | } 200 | 201 | private float playAtGain(boolean listen, double gain) throws InterruptedException { 202 | player.setLogScaleGain(gain); 203 | if (listen) { 204 | Thread.sleep(2000); 205 | } 206 | return player.getGainValue(); 207 | } 208 | 209 | /** 210 | * Test that the maximum gain is greater than the minimum gain. That is about all we can expect. 211 | * The actual values depend on the available {@link SourceDataLine}. 212 | * We don't know anything about its scale beforehand. 213 | *

214 | * The player must be started before maximum and minimum gains can be queried. 215 | *

216 | * // TODO: Is it really acceptable that we cannot check gain before the player is started? 217 | * 218 | * @throws StreamPlayerException 219 | */ 220 | @Test 221 | void maximumGain() throws StreamPlayerException { 222 | 223 | player.open(audioFile); 224 | player.play(); 225 | final float maximumGain = player.getMaximumGain(); 226 | final float minimumGain = player.getMinimumGain(); 227 | player.stop(); 228 | 229 | assertTrue(minimumGain < maximumGain, 230 | String.format("Maximum gain (%.2f) should be greater than minimum gain (%.2f).", 231 | maximumGain, minimumGain) 232 | ); 233 | } 234 | 235 | @Test 236 | void totalBytes() throws StreamPlayerException, InterruptedException { 237 | int expectedLengthOfExampleAudioFile = 5877062; 238 | 239 | 240 | assertEquals(-1, player.getTotalBytes()); 241 | 242 | player.open(audioFile); 243 | assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes()); 244 | 245 | player.play(); 246 | assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes()); 247 | } 248 | 249 | @Test 250 | void stopped() { 251 | 252 | assertFalse(player.isStopped()); 253 | 254 | player.stop(); 255 | assertTrue(player.isStopped()); 256 | } 257 | 258 | @Test 259 | void sourceDataLine() throws StreamPlayerException { 260 | assertNull(player.getSourceDataLine()); 261 | 262 | player.open(audioFile); 263 | assertNotNull(player.getSourceDataLine()); 264 | 265 | player.play(); 266 | 267 | assertNotNull(player.getSourceDataLine()); 268 | } 269 | 270 | @Test 271 | void playing() throws StreamPlayerException { 272 | 273 | assertFalse(player.isPlaying()); 274 | 275 | player.open(audioFile); 276 | assertFalse(player.isPlaying()); 277 | 278 | player.play(); 279 | assertTrue(player.isPlaying()); 280 | 281 | player.pause(); 282 | assertFalse(player.isPlaying()); 283 | } 284 | 285 | @Test 286 | void pausedOrPlaying() throws StreamPlayerException { 287 | 288 | assertFalse(player.isPausedOrPlaying()); 289 | 290 | player.open(audioFile); 291 | assertFalse(player.isPausedOrPlaying()); 292 | 293 | player.play(); 294 | assertTrue(player.isPausedOrPlaying()); 295 | 296 | player.pause(); 297 | assertTrue(player.isPausedOrPlaying()); 298 | 299 | player.stop(); 300 | assertFalse(player.isPausedOrPlaying()); 301 | } 302 | 303 | @Test 304 | void paused() throws StreamPlayerException { 305 | assertFalse(player.isPaused()); 306 | 307 | player.open(audioFile); 308 | assertFalse(player.isPaused()); 309 | 310 | player.play(); 311 | assertFalse(player.isPaused()); 312 | 313 | player.pause(); 314 | assertTrue(player.isPaused()); 315 | } 316 | 317 | @Test 318 | void addStreamPlayerListener() throws StreamPlayerException, InterruptedException { 319 | // Setup 320 | final StreamPlayerListener listener = mock(StreamPlayerListener.class); 321 | 322 | ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(Object.class); 323 | ArgumentCaptor propertiesCaptor1 = ArgumentCaptor.forClass(Map.class); 324 | 325 | // Execute 326 | player.addStreamPlayerListener(listener); 327 | player.open(audioFile); 328 | player.play(); 329 | Thread.sleep(30); 330 | 331 | // Verify 332 | verify(listener).opened(dataSourceCaptor.capture(), propertiesCaptor1.capture()); 333 | Object value = dataSourceCaptor.getValue(); 334 | assertTrue(value instanceof File); 335 | 336 | Map value11 = propertiesCaptor1.getValue(); 337 | 338 | assertTrue(value11.containsKey("basicplayer.sourcedataline")); 339 | 340 | verify(listener, times(4)).statusUpdated(any()); 341 | 342 | verify(listener, times(1)).opened(any(), any()); 343 | 344 | verify(listener, atLeast(4)).progress(anyInt(), anyLong(), any(), any()); 345 | verify(listener, atMost(30)).progress(anyInt(), anyLong(), any(), any()); 346 | 347 | // TODO: Make separate tests for the different calls made to the listener 348 | // TODO: Do we need to test the values passed to these methods? 349 | 350 | } 351 | 352 | @Test 353 | void mute() throws StreamPlayerException { 354 | // TODO: How can mute be tested, without too much assumptions about the actual implementation? 355 | // A manual test would involve listening. 356 | 357 | 358 | assertFalse(player.getMute()); 359 | player.open(audioFile); 360 | player.play(); 361 | player.setMute(true); 362 | assertTrue(player.getMute()); 363 | player.setMute(false); 364 | assertFalse(player.getMute()); 365 | 366 | } 367 | 368 | @Test 369 | void speedFactor() throws StreamPlayerException, InterruptedException { 370 | assertEquals(player.getSpeedFactor(), 1); 371 | 372 | double fast = 1; 373 | player.setSpeedFactor(fast); 374 | assertEquals(fast, player.getSpeedFactor()); 375 | 376 | double slow = 0.5; 377 | player.open(audioFile); 378 | player.play(); 379 | player.setSpeedFactor(slow); 380 | Thread.sleep(50); 381 | assertEquals(slow, player.getSpeedFactor()); 382 | 383 | // TODO: Find a way to verify that the speed factor actually works. That it can be read back is no proof. 384 | // I might be possible to play a short sequence of known length, and measure the time it takes. 385 | // But things that take time are generally not advisable in unit tests. 386 | 387 | 388 | } 389 | 390 | @Test 391 | void equalizer() { 392 | player.setEqualizer(null, 0); 393 | // TODO: Find out what the intention of setEqualizer() is, and make a test for that assumption. 394 | } 395 | 396 | @Test 397 | void play() throws StreamPlayerException, InterruptedException { 398 | // Setup 399 | player.open(audioFile); 400 | 401 | // Pre-validate 402 | assertFalse(player.isPlaying()); 403 | 404 | // Execute 405 | player.play(); 406 | 407 | // Verify 408 | assertTrue(player.isPlaying()); 409 | 410 | // TODO: Find way to verify that the player is actually playing, that doesn't need listening. 411 | // The method might look at the playing position, but it must be fairly quick. 412 | } 413 | 414 | @Test 415 | void resume() throws StreamPlayerException { 416 | assertFalse(player.isPlaying()); 417 | 418 | player.open(audioFile); 419 | assertFalse(player.isPlaying()); 420 | 421 | player.play(); 422 | assertTrue(player.isPlaying()); 423 | 424 | player.pause(); 425 | assertFalse(player.isPlaying()); 426 | 427 | 428 | player.resume(); 429 | assertTrue(player.isPlaying()); 430 | } 431 | 432 | @Test 433 | void pause() throws StreamPlayerException { 434 | 435 | // Setup 436 | player.open(audioFile); 437 | player.play(); 438 | // Pre-validate 439 | assertFalse(player.isPaused()); 440 | 441 | // Execute 442 | player.pause(); 443 | 444 | // Verify 445 | assertTrue(player.isPaused()); 446 | 447 | } 448 | 449 | @Test 450 | void stop() { 451 | 452 | assertFalse(player.isStopped()); 453 | 454 | player.stop(); 455 | 456 | assertTrue(player.isStopped()); 457 | 458 | // TODO: Find a way to verify that playback is stopped by running the stop method. 459 | // The isStopped() method is not enough. 460 | } 461 | 462 | @Test 463 | void pan() throws StreamPlayerException { 464 | double delta = 1e-6; 465 | final float initialPan = player.getPan(); 466 | assertEquals(0, initialPan); 467 | 468 | player.open(audioFile); 469 | player.play(); 470 | 471 | double pan = -0.9; 472 | player.setPan(pan); 473 | assertEquals(pan, player.getPan(), delta); 474 | 475 | // If we set the pan outside the permitted range, it will not change 476 | // The permitted range is undefined. 477 | double outsideRange = 1.1; 478 | player.setPan(outsideRange); 479 | assertEquals(pan, player.getPan(), delta); 480 | 481 | float precision = player.getPrecision(); 482 | assertNotEquals(0, precision); 483 | double expected = 128.0; // Possibly platform dependent. Tested on a Mac with Intellij. 484 | assertEquals(expected, 1.0/precision, 2.0); 485 | } 486 | 487 | @Test 488 | void unknown() { 489 | player.isUnknown(); 490 | // This is a useless test of a useless method. 491 | // TODO: Remove player.isUnknown(). It's not used, and it's useless. 492 | // There is already getStatus(). 493 | } 494 | 495 | @Test 496 | void open() throws StreamPlayerException { 497 | File file = spy(audioFile); 498 | player.open(file); 499 | verify(file, atLeast(1)).getPath(); 500 | 501 | // It's unclear what the contract of open() is; what we need it to do. 502 | // It's a pre-requisite for play(), but play() doesn't throw an 503 | // exception if open() is missing. 504 | } 505 | 506 | @Test 507 | void mixers() { 508 | List mixers = player.getMixers(); 509 | // TODO: Make this method player.getMixers() private, remove it from the interface. 510 | // There is nothing that can be done with the information outside the private scope. 511 | } 512 | 513 | 514 | 515 | 516 | // The methods tested below aren't used elsewhere in this project, nor in XR3Player 517 | // TODO: Consider each of the tested methods below, to see if they can be removed from StreamPlayer. 518 | 519 | @Test 520 | @Disabled("This test is a placeholder for a suggested future test to be written.") 521 | void lineBufferSize() { 522 | player.getLineBufferSize(); 523 | player.setLineBufferSize(0); 524 | fail("Test not done"); 525 | } 526 | 527 | @Test 528 | void lineCurrentBufferSize() throws StreamPlayerException { 529 | // TODO: Document the purpose of getLineCurrentBufferSize(). What is it good for? 530 | // Can it be removed? The method doesn't really return the current line buffer size, 531 | // but a cached value, which might be the same thing. Hard to say. 532 | 533 | assertEquals(-1, player.getLineCurrentBufferSize(), "Initially, the buffer size is undefined, coded as -1."); 534 | 535 | player.open(audioFile); 536 | assertEquals(-1, player.getLineCurrentBufferSize(), "After the player is opened, the buffer size is undefined"); 537 | 538 | player.play(); 539 | assertEquals(2 * 44100, player.getLineCurrentBufferSize(), "After the play starts, the buffer size 1 second at CD sampling rate"); 540 | } 541 | 542 | @Test 543 | @Disabled("This test is a placeholder for a suggested future test to be written.") 544 | void minimumGain() { 545 | player.getMinimumGain(); 546 | 547 | fail("Test not done"); 548 | } 549 | 550 | @Test 551 | @Disabled("This test is a placeholder for a suggested future test to be written.") 552 | void positionByte() { 553 | player.getPositionByte(); 554 | 555 | fail("Test not done"); 556 | } 557 | 558 | @Test 559 | void precision() throws StreamPlayerException { 560 | assertEquals(0f, player.getPrecision()); 561 | 562 | player.open(audioFile); 563 | player.play(); 564 | 565 | assertNotEquals(0f, player.getPrecision()); 566 | // On one computer the precision = 1/128. There are no guarantees. 567 | } 568 | 569 | @Test 570 | void opened() throws StreamPlayerException { 571 | assertFalse(player.isOpened()); 572 | 573 | player.open(audioFile); 574 | assertTrue(player.isOpened()); 575 | } 576 | 577 | @Test 578 | @Disabled("This test is a placeholder for a suggested future test to be written.") 579 | void seeking() { 580 | player.isSeeking(); 581 | 582 | fail("Test not done"); 583 | } 584 | 585 | @Test 586 | @Disabled("This test is a placeholder for a suggested future test to be written.") 587 | void removeStreamPlayerListener() { 588 | player.removeStreamPlayerListener(null); 589 | 590 | fail("Test not done"); 591 | } 592 | 593 | @Test 594 | void seekTo() throws StreamPlayerException, IOException, UnsupportedAudioFileException { 595 | 596 | // Some tests before we do the real tests 597 | AudioFileFormat audioFileFormat = AudioSystem.getAudioFileFormat(audioFile); 598 | 599 | 600 | // Setup 601 | player.open(audioFile); 602 | player.play(); 603 | player.pause(); 604 | int encodedStreamPosition1 = player.getEncodedStreamPosition(); 605 | 606 | // Execute 607 | player.seekTo(10); 608 | 609 | // Verify 610 | int encodedStreamPosition2 = player.getEncodedStreamPosition(); 611 | assertTrue(encodedStreamPosition2 > encodedStreamPosition1); 612 | 613 | // Execute: go backwards 614 | player.seekTo(5); 615 | 616 | // Verify: position goes backwards 617 | int encodedStreamPosition3 = player.getEncodedStreamPosition(); 618 | assertTrue(encodedStreamPosition3 < encodedStreamPosition2); 619 | } 620 | 621 | @Test 622 | @Disabled("This test is a placeholder for a suggested future test to be written.") 623 | void equalizerKey() { 624 | player.setEqualizerKey(0, 0); 625 | 626 | fail("Test not done"); 627 | } 628 | 629 | @Test 630 | void setMixer() throws StreamPlayerException { 631 | //Get all available mixers 632 | List mixers = player.getMixers(); 633 | 634 | //Use the last mixer (this is never the default) 635 | String mixerName = mixers.get(mixers.size()-1); 636 | 637 | //Set the mixer 638 | player.setMixerName(mixerName); 639 | 640 | //Create a line, this will either use the set mixer or set the name to null 641 | player.open(audioFile); 642 | 643 | //The name of the mixers should correspond 644 | assertEquals(mixerName, player.getMixerName()); 645 | 646 | //Get the mixer of the used mixerName 647 | Mixer mixer = null; 648 | final Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo(); 649 | for (Mixer.Info mixerInfo : mixerInfos) { 650 | if (mixerInfo.getName().equals(mixerName)) { 651 | mixer = AudioSystem.getMixer(mixerInfo); 652 | break; 653 | } 654 | } 655 | 656 | //The mixer that is being used should be the same as the one we found 657 | assertEquals(player.getCurrentMixer(), mixer); 658 | } 659 | 660 | } 661 | -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.File; 8 | import java.util.logging.Logger; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | import static org.mockito.Mockito.*; 12 | 13 | class StreamPlayerTest { 14 | 15 | /** 16 | * This test was written to demonstrate some testing techniques. 17 | * As it is now, it's not deterministic. It passes sometimes and fails sometimes. Such test 18 | * is not acceptable in a test suite that must always pass. Therefore it's disabled 19 | * but kept, so that the test (and the production code) can be improved and then enabled. 20 | * 21 | * When the test is improved such that it is worthy of production code, it should be renamed 22 | * with it's new purpose. 23 | */ 24 | @Test 25 | @DisplayName("Demonstration of spying") 26 | @Disabled("This test is unreliable. It fails sometimes, for a reason that is hard to understand.") 27 | void demonstrationOfSpying() throws StreamPlayerException { 28 | 29 | // By using a mocked logger instead of a real one, we get rid of annoying logging messages in the unit test. 30 | final Logger logger = mock(Logger.class); 31 | 32 | final File audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); 33 | 34 | // Setup the spy 35 | final StreamPlayer streamPlayer = new StreamPlayer(logger); 36 | final StreamPlayer spy = spy(streamPlayer); 37 | 38 | // Execute & verify 39 | 40 | // Call open, via the spy 41 | spy.open(audioFile); 42 | 43 | // verify that getEncodedStreamPosition is called exactly two times 44 | verify(spy, times(2)).getEncodedStreamPosition(); 45 | 46 | // Call play, via the spy 47 | spy.play(); 48 | 49 | // Verify that getEncodedStreamPosition is now called 3 times (the 2 previous times + one more time) 50 | verify(spy, times(3)).getEncodedStreamPosition(); 51 | 52 | spy.stop(); 53 | // Verify that there are in total 4 calls of getEncodedStreamPosition after the player is stopped. 54 | verify(spy, times(4)).getEncodedStreamPosition(); 55 | 56 | // We can only spy on public methods. 57 | // TODO: Look into initAudioInputStream, and check if we really need to call getEncodedStreamPosition() twice. 58 | } 59 | } -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/tools/IOInfoTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.tools; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.CsvSource; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | class IOInfoTest { 10 | 11 | @ParameterizedTest 12 | @CsvSource({ 13 | "/home/myself/player/files/someFile.JpEg, jpeg", 14 | "c:\\Users\\myself\\git\\someFile.JaVa, java" 15 | }) 16 | @DisplayName("It returns the extension of the file, in lowercase") 17 | void itReturnsExtensionAsLowercase(String fullFilePath, String expectedExtension) { 18 | assertEquals(expectedExtension, IOInfo.getFileExtension(fullFilePath)); 19 | } 20 | 21 | @ParameterizedTest 22 | @CsvSource({ 23 | "/home/myself/player/files/someFile.JpEg, someFile.JpEg", 24 | "c:\\Users\\myself\\git\\someFile.JaVa, someFile.JaVa" 25 | }) 26 | @DisplayName("It returns the filename from an absolute path") 27 | void itReturnsTheFilenameFromAbsolutePath(String absolutePath, String expectedName) { 28 | assertEquals(expectedName, IOInfo.getFileName(absolutePath)); 29 | } 30 | 31 | @ParameterizedTest 32 | @CsvSource({ 33 | "/home/myself/player/files/someFile.JpEg, someFile", 34 | "c:\\Users\\myself\\git\\someFile.JaVa, someFile" 35 | }) 36 | @DisplayName("It returns the filename from an absolute path") 37 | void itReturnsTheBaseNameFromAbsolutePath(String absolutePath, String expectedName) { 38 | assertEquals(expectedName, IOInfo.getFileTitle(absolutePath)); 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/com/goxr3plus/streamplayer/tools/TimeToolTest.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.tools; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.Arguments; 6 | import org.junit.jupiter.params.provider.CsvSource; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | 9 | import java.util.stream.Stream; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class TimeToolTest { 14 | 15 | /** 16 | * @return a stream of arguments for the test. Each argument have the same form. 17 | * They can contain any type of objects. 18 | */ 19 | private static Stream timesForTest() { 20 | return Stream.of( 21 | Arguments.of(0, "00:00", "00s"), 22 | Arguments.of(3661, "61:01", "01h:01m:01") 23 | ); 24 | } 25 | 26 | @ParameterizedTest 27 | @MethodSource("timesForTest") 28 | void getTimeEditedOnHours(int seconds, String expectedOnHours, String ignored) { 29 | final String actual = TimeTool.getTimeEditedOnHours(seconds); 30 | assertEquals(expectedOnHours, actual); 31 | } 32 | 33 | @ParameterizedTest 34 | @MethodSource("timesForTest") 35 | void getTimeEdited(int seconds, String ignored, String timeEdited) { 36 | final String actual = TimeTool.getTimeEdited(seconds); 37 | assertEquals(timeEdited, actual); 38 | } 39 | 40 | @ParameterizedTest 41 | @CsvSource({ 42 | "0, '.0' ", 43 | "1, '.0' ", 44 | "999, '.9' ", 45 | "1001, '.0' ", // Slightly over 1 second TODO: check if this is a bug, 46 | "600001, '.0' " // Slightly over 10 minutes TODO: check if this is a bug 47 | }) 48 | void millisecondsToTime(long millis, String expected) { 49 | final String actual = TimeTool.millisecondsToTime(millis); 50 | assertEquals(expected, actual); 51 | 52 | } 53 | 54 | @Test 55 | void durationInSeconds() { 56 | final int duration = TimeTool.durationInSeconds("aName", null); 57 | assertEquals(-1, duration); 58 | // TODO: Wouldn't it be better if an exception was thrown? 59 | } 60 | 61 | @Test 62 | void durationInMilliseconds() { 63 | final long duration = TimeTool.durationInMilliseconds("aName", null); 64 | assertEquals(-1L, duration); 65 | // TODO: Wouldn't it be better if an exception was thrown? 66 | } 67 | } --------------------------------------------------------------------------------