commit 36246afe8caf04fdfacea0a320fe69b70417040d Author: Mark Qvist Date: Tue Mar 11 16:41:15 2025 +0100 Public repo init diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..5b80e79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.DS_Store +*.pyc +testutils +build +dist +docs/build +lxst*.egg-info +examples/LXST +LXST/Utilities/LXST +RNS diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cfe676c --- /dev/null +++ b/LICENSE @@ -0,0 +1,403 @@ +Attribution-NonCommercial-NoDerivatives 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 +International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-NoDerivatives 4.0 International Public +License ("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + c. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + d. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + e. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + f. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + g. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + h. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce and reproduce, but not Share, Adapted Material + for NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material, You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + For the avoidance of doubt, You do not have permission under + this Public License to Share Adapted Material. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only and provided You do not Share Adapted Material; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/LXST/Call.py b/LXST/Call.py new file mode 100644 index 0000000..c67b237 --- /dev/null +++ b/LXST/Call.py @@ -0,0 +1,55 @@ +import RNS +from .Pipeline import Pipeline +from .Codecs import * +from .Sources import * +from .Sinks import * +from . import APP_NAME + +class CallEndpoint(): + def __init__(self, identity): + self.identity = identity + self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "call", "endpoint") + self.destination.set_link_established_callback(self._incoming_call) + self.active_call = None + self.auto_answer = True + self.receive_pipeline = None + self.transmit_pipeline = None + self._incoming_call_callback = None + + def announce(self): + if self.destination: + self.destination.announce() + + @property + def incoming_call_callback(self): + return self._incoming_call_callback + + @incoming_call_callback.setter + def incoming_call_callback(self, callback): + if callable(callback): + self._incoming_call_callback = callback + else: + raise TypeError(f"Invalid callback for {self}: Not callable") + + def _incoming_call(self, link): + RNS.log(f"Incoming call on {self}", RNS.LOG_DEBUG) + if callable(self._incoming_call_callback): + self._incoming_call_callback(link) + + def answer(self, call_link): + RNS.log(f"Answering call on {call_link}", RNS.LOG_DEBUG) + self.active_call = call_link + + self.receive_pipeline = Pipeline(source=PacketSource(self), + codec=Opus(), + sink=LineSink()) + + self.transmit_pipeline = Pipeline(source=LineSource(target_frame_ms=target_frame_ms), + codec=Opus(), + sink=PacketSink(self)) + + def terminate(self): + self.receive_pipeline.stop() + self.transmit_pipeline.stop() + if self.active_call: + self.active_call.teardown() \ No newline at end of file diff --git a/LXST/Codecs/Codec.py b/LXST/Codecs/Codec.py new file mode 100644 index 0000000..911d7f5 --- /dev/null +++ b/LXST/Codecs/Codec.py @@ -0,0 +1,62 @@ +import numpy as np +from .libs.pydub import AudioSegment + +TYPE_MAP_FACTOR = np.iinfo("int16").max + +class Codec(): + preferred_samplerate = None + frame_quanta_ms = None + frame_max_ms = None + valid_frame_ms = None + source = None + sink = None + +class CodecError(Exception): + pass + +class Null(Codec): + def __init__(self): + pass + + def encode(self, frame): + return frame + + def decode(self, frame): + return frame + +def resample_bytes(sample_bytes, bitdepth, channels, input_rate, output_rate, normalize=False): + sample_width = bitdepth//8 + audio = AudioSegment( + sample_bytes, + frame_rate=input_rate, + sample_width=sample_width, + channels=channels) + + if normalize: + audio = audio.apply_gain(-audio.max_dBFS) + + resampled_audio = audio.set_frame_rate(output_rate) + resampled_bytes = resampled_audio.get_array_of_samples().tobytes() + + # rate_factor = input_rate/output_rate + # input_samples = int(len(sample_bytes)/channels/sample_width) + # output_samples = int(len(resampled_bytes)/channels/sample_width) + # target_samples = int(input_samples/rate_factor) + # if output_samples < target_samples: + # print("Mismatch") + # add_samples = int(target_samples-output_samples) + # fill = resampled_bytes[-sample_width:]*add_samples + # resampled_bytes += fill + + return resampled_bytes + +def resample(input_samples, bitdepth, channels, input_rate, output_rate, normalize=False): + sample_width = bitdepth//8 + input_samples = input_samples*TYPE_MAP_FACTOR + input_samples = input_samples.astype(np.int16) + resampled_bytes = resample_bytes(input_samples.tobytes(), bitdepth, channels, input_rate, output_rate, normalize) + output_samples = np.frombuffer(resampled_bytes, dtype=np.int16)/TYPE_MAP_FACTOR + output_samples = output_samples.reshape(output_samples.shape[0]//channels, channels) + output_samples = output_samples.astype(np.float32) + + return output_samples \ No newline at end of file diff --git a/LXST/Codecs/Codec2.py b/LXST/Codecs/Codec2.py new file mode 100644 index 0000000..b371343 --- /dev/null +++ b/LXST/Codecs/Codec2.py @@ -0,0 +1,120 @@ +import time +import math +import struct +import pycodec2 +import numpy as np +from .Codec import Codec, CodecError, resample_bytes + +# TODO: Remove debug +import RNS + +class Codec2(Codec): + CODEC2_700C = 700 + CODEC2_1200 = 1200 + CODEC2_1300 = 1300 + CODEC2_1400 = 1400 + CODEC2_1600 = 1600 + CODEC2_2400 = 2400 + CODEC2_3200 = 3200 + + INPUT_RATE = 8000 + OUTPUT_RATE = 8000 + FRAME_QUANTA_MS = 40 + TYPE_MAP_FACTOR = np.iinfo("int16").max + + MODE_HEADERS = {CODEC2_700C: 0x00, + CODEC2_1200: 0x01, + CODEC2_1300: 0x02, + CODEC2_1400: 0x03, + CODEC2_1600: 0x04, + CODEC2_2400: 0x05, + CODEC2_3200: 0x06} + + HEADER_MODES = {0x00: CODEC2_700C, + 0x01: CODEC2_1200, + 0x02: CODEC2_1300, + 0x03: CODEC2_1400, + 0x04: CODEC2_1600, + 0x05: CODEC2_2400, + 0x06: CODEC2_3200} + + def __init__(self, mode=CODEC2_2400): + self.frame_quanta_ms = self.FRAME_QUANTA_MS + self.channels = 1 + self.bitdepth = 16 + self.c2 = None + self.output_samplerate = self.OUTPUT_RATE + self.set_mode(mode) + + def set_mode(self, mode): + self.mode = mode + self.mode_header = self.MODE_HEADERS[self.mode].to_bytes() + self.c2 = pycodec2.Codec2(self.mode) + + def encode(self, frame): + if frame.shape[1] == 0: + raise CodecError("Cannot encode frame with 0 channels") + elif frame.shape[1] > self.channels: + frame = frame[:, 1] + + + input_samples = frame*self.TYPE_MAP_FACTOR + input_samples = input_samples.astype(np.int16) + + if self.source: + if self.source.samplerate != self.INPUT_RATE: + frame_bytes = input_samples.tobytes() + resampled_bytes = resample_bytes(frame_bytes, self.bitdepth, self.channels, self.source.samplerate, self.INPUT_RATE) + input_samples = np.frombuffer(resampled_bytes, dtype=np.int16) + + SPF = self.c2.samples_per_frame() + N_FRAMES = math.floor(len(input_samples)/SPF) + input_frames = np.array(input_samples[0:N_FRAMES*SPF], dtype=np.int16) + + encoded = b"" + for pi in range(0, N_FRAMES): + pstart = pi*SPF + pend = (pi+1)*SPF + input_frame = input_frames[pstart:pend] + encoded_frame = self.c2.encode(input_frame) + encoded += encoded_frame + + # TODO: Remove debug + # print(f"SPF : {SPF}") + # print(f"N_FRAMES : {N_FRAMES}") + + return self.mode_header+encoded + + def decode(self, frame_bytes): + frame_header = frame_bytes[0] + frame_bytes = frame_bytes[1:] + frame_mode = self.HEADER_MODES[frame_header] + if self.mode != frame_mode: + self.set_mode(frame_mode) + + SPF = self.c2.samples_per_frame() + BPF = self.c2.bytes_per_frame() + STRUCT_FORMAT = f"{SPF}h" + N_FRAMES = math.floor(len(frame_bytes)/BPF) + + # TODO: Remove debug + # print(f"BPF : {BPF}") + # print(f"N_FRAMES : {N_FRAMES}") + + decoded = b"" + for pi in range(0, N_FRAMES): + pstart = pi*BPF + pend = (pi+1)*BPF + encoded_frame = frame_bytes[pstart:pend] + decoded_frame = self.c2.decode(encoded_frame) + decoded += struct.pack(STRUCT_FORMAT, *decoded_frame) + + if self.sink: + if self.sink.samplerate != self.OUTPUT_RATE: + decoded = resample_bytes(decoded, self.bitdepth, self.channels, self.OUTPUT_RATE, self.sink.samplerate) + + decoded_samples = np.frombuffer(decoded, dtype="int16")/self.TYPE_MAP_FACTOR + frame_samples = np.zeros((len(decoded_samples), 1), dtype="float32") + frame_samples[:, 0] = decoded_samples + + return frame_samples \ No newline at end of file diff --git a/LXST/Codecs/Opus.py b/LXST/Codecs/Opus.py new file mode 100644 index 0000000..f85defc --- /dev/null +++ b/LXST/Codecs/Opus.py @@ -0,0 +1,179 @@ +import io +import RNS +import time +import math +import numpy as np +from .Codec import Codec, CodecError, resample_bytes +from .libs.pyogg import OpusEncoder, OpusDecoder + +class Opus(Codec): + FRAME_QUANTA_MS = 2.5 + FRAME_MAX_MS = 60 + VALID_FRAME_MS = [2.5, 5, 10, 20, 40, 60] + TYPE_MAP_FACTOR = np.iinfo("int16").max + + PROFILE_VOICE_LOW = 0x00 + PROFILE_VOICE_MEDIUM = 0x01 + PROFILE_VOICE_HIGH = 0x02 + PROFILE_VOICE_MAX = 0x03 + PROFILE_AUDIO_MIN = 0x04 + PROFILE_AUDIO_LOW = 0x05 + PROFILE_AUDIO_MEDIUM = 0x06 + PROFILE_AUDIO_HIGH = 0x07 + PROFILE_AUDIO_MAX = 0x08 + + def __init__(self, profile=PROFILE_VOICE_LOW): + self.frame_quanta_ms = self.FRAME_QUANTA_MS + self.frame_max_ms = self.FRAME_MAX_MS + self.valid_frame_ms = self.VALID_FRAME_MS + self.channels = 1 + self.input_channels = 1 + self.output_channels = 2 + self.bitdepth = 16 + self.opus_encoder = OpusEncoder() + self.opus_decoder = OpusDecoder() + self.encoder_configured = False + self.decoder_configured = False + self.bitrate_ceiling = 6000 + self.output_bytes = 0 + self.output_ms = 0 + self.output_bitrate = 0 + self.set_profile(profile) + + def set_profile(self, profile): + if profile == self.PROFILE_VOICE_LOW: + self.profile = profile + self.channels = 1 + self.input_channels = self.channels + self.output_samplerate = 8000 + self.opus_encoder.set_application("voip") + elif profile == self.PROFILE_VOICE_MEDIUM: + self.profile = profile + self.channels = 1 + self.input_channels = self.channels + self.output_samplerate = 24000 + self.opus_encoder.set_application("voip") + elif profile == self.PROFILE_VOICE_HIGH: + self.profile = profile + self.channels = 1 + self.input_channels = self.channels + self.output_samplerate = 48000 + self.opus_encoder.set_application("voip") + elif profile == self.PROFILE_VOICE_MAX: + self.profile = profile + self.channels = 2 + self.input_channels = self.channels + self.output_samplerate = 48000 + self.opus_encoder.set_application("voip") + elif profile == self.PROFILE_AUDIO_MIN: + self.profile = profile + self.channels = 1 + self.input_channels = self.channels + self.output_samplerate = 8000 + self.opus_encoder.set_application("audio") + elif profile == self.PROFILE_AUDIO_LOW: + self.profile = profile + self.channels = 1 + self.input_channels = self.channels + self.output_samplerate = 12000 + self.opus_encoder.set_application("audio") + elif profile == self.PROFILE_AUDIO_MEDIUM: + self.profile = profile + self.channels = 2 + self.input_channels = self.channels + self.output_samplerate = 24000 + self.opus_encoder.set_application("audio") + elif profile == self.PROFILE_AUDIO_HIGH: + self.profile = profile + self.channels = 2 + self.input_channels = self.channels + self.output_samplerate = 48000 + self.opus_encoder.set_application("audio") + elif profile == self.PROFILE_AUDIO_MAX: + self.profile = profile + self.channels = 2 + self.input_channels = self.channels + self.output_samplerate = 48000 + self.opus_encoder.set_application("audio") + else: + raise CodecError(f"Unsupported profile configured for {self}") + + def update_bitrate(self, frame_duration_ms): + if self.profile == self.PROFILE_VOICE_LOW: + self.bitrate_ceiling = 6000 + elif self.profile == self.PROFILE_VOICE_MEDIUM: + self.bitrate_ceiling = 8000 + elif self.profile == self.PROFILE_VOICE_HIGH: + self.bitrate_ceiling = 16000 + elif self.profile == self.PROFILE_VOICE_MAX: + self.bitrate_ceiling = 32000 + elif self.profile == self.PROFILE_AUDIO_MIN: + self.bitrate_ceiling = 8000 + elif self.profile == self.PROFILE_AUDIO_LOW: + self.bitrate_ceiling = 14000 + elif self.profile == self.PROFILE_AUDIO_MEDIUM: + self.bitrate_ceiling = 28000 + elif self.profile == self.PROFILE_AUDIO_HIGH: + self.bitrate_ceiling = 56000 + elif self.profile == self.PROFILE_AUDIO_MAX: + self.bitrate_ceiling = 128000 + + max_bytes_per_frame = math.ceil((self.bitrate_ceiling/8)*(frame_duration_ms/1000)) + configured_bitrate = (max_bytes_per_frame*8)/(frame_duration_ms/1000) + self.opus_encoder.set_max_bytes_per_frame(max_bytes_per_frame) + + def encode(self, frame): + if frame.shape[1] == 0: + raise CodecError("Cannot encode frame with 0 channels") + elif frame.shape[1] > self.input_channels: + frame = frame[:, 0:self.input_channels] + elif frame.shape[1] < self.input_channels: + new_frame = np.zeros(shape=(frame.shape[0], self.input_channels)) + for n in range(0, frame.shape[1]): new_frame[:, n] = frame[:, n] + for n in range(frame.shape[1], new_frame.shape[1]): new_frame[:, n] = frame[:, frame.shape[1]-1] + frame = new_frame + + input_samples = frame*self.TYPE_MAP_FACTOR + input_samples = input_samples.astype(np.int16) + + if self.source.samplerate != self.output_samplerate: + frame_bytes = input_samples.tobytes() + resampled_bytes = resample_bytes(frame_bytes, self.bitdepth, self.input_channels, self.source.samplerate, self.output_samplerate) + input_samples = np.frombuffer(resampled_bytes, dtype=np.int16) + input_samples = input_samples.reshape(len(input_samples)//self.input_channels, self.input_channels) + + frame_duration_ms = (input_samples.shape[0]/self.output_samplerate)*1000 + self.update_bitrate(frame_duration_ms) + + if not self.encoder_configured: + self.input_channels = self.channels + self.opus_encoder.set_sampling_frequency(self.output_samplerate) + self.opus_encoder.set_channels(self.input_channels) + RNS.log(f"{self} encoder set to {self.input_channels} channels, {RNS.prettyfrequency(self.output_samplerate)}", RNS.LOG_DEBUG) + self.encoder_configured = True + + input_bytes = input_samples.tobytes() + # TODO: Pad input bytes on partial frame + encoded_frame = self.opus_encoder.encode(input_bytes).tobytes() + + self.output_bytes += len(encoded_frame) + self.output_ms += frame_duration_ms + self.output_bitrate = (self.output_bytes*8)/(self.output_ms/1000) + + return encoded_frame + + def decode(self, frame_bytes): + if not self.decoder_configured: + if self.sink and self.sink.channels: output_channels = self.sink.channels + else: output_channels = self.output_channels if self.output_channels > self.channels else self.channels + self.channels = output_channels + self.opus_decoder.set_channels(output_channels) + self.opus_decoder.set_sampling_frequency(self.sink.samplerate) + self.decoder_configured = True + RNS.log(f"{self} decoder set to {self.channels} channels, {RNS.prettyfrequency(self.sink.samplerate)}", RNS.LOG_DEBUG) + + decoded_frame_bytes = self.opus_decoder.decode(memoryview(bytearray(frame_bytes))) + decoded_samples = np.frombuffer(decoded_frame_bytes, dtype="int16")/self.TYPE_MAP_FACTOR + frame_samples = decoded_samples.reshape(len(decoded_samples)//self.channels, self.channels) + + return frame_samples \ No newline at end of file diff --git a/LXST/Codecs/Raw.py b/LXST/Codecs/Raw.py new file mode 100644 index 0000000..70652d8 --- /dev/null +++ b/LXST/Codecs/Raw.py @@ -0,0 +1,58 @@ +import RNS +import numpy as np +from .Codec import Codec + +class Raw(Codec): + BITDEPTH_16 = 0x00 + BITDEPTH_32 = 0x01 + BITDEPTH_64 = 0x02 + BITDEPTH_128 = 0x03 + BITDEPTHS = ["float16", "float32", "float64", "float128"] + + def __init__(self, channels=None, bitdepth=16): + if channels: + channels = min(max(channels, 1), 32) + + self.bitdepth = bitdepth + self.channels = channels + + if self.bitdepth >= 128: + self.dtype = self.BITDEPTHS[self.BITDEPTH_128] + self.header_bitdpeth = self.BITDEPTH_128 + elif self.bitdepth >= 64: + self.dtype = self.BITDEPTHS[self.BITDEPTH_64] + self.header_bitdpeth = self.BITDEPTH_64 + elif self.bitdepth >= 32: + self.dtype = self.BITDEPTHS[self.BITDEPTH_32] + self.header_bitdpeth = self.BITDEPTH_32 + else: + self.dtype = self.BITDEPTHS[self.BITDEPTH_16] + self.header_bitdpeth = self.BITDEPTH_16 + + def encode(self, frame): + if self.channels == None: + self.channels = frame.shape[1] + RNS.log(f"{self} encoder set to {self.channels} channels", RNS.LOG_DEBUG) + + if frame.shape[1] > self.channels: + frame = frame[:, range(0, self.channels)] + elif frame.shape[1] < self.channels: + new_frame = np.zeros(shape=(frame.shape[0], self.channels)) + for n in range(0, frame.shape[1]): new_frame[:, n] = frame[:, n] + for n in range(frame.shape[1], new_frame.shape[1]): new_frame[:, n] = frame[:, frame.shape[1]-1] + frame = new_frame + + frame_header = (self.header_bitdpeth << 6) | self.channels-1 + frame_bytes = frame_header.to_bytes()+frame.astype(self.dtype).tobytes() + return frame_bytes + + def decode(self, frame_bytes): + frame_header = frame_bytes[0] + frame_channels = (frame_header & 0b00111111)+1 + frame_bitdepth = frame_header >> 6 + frame_dtype = self.BITDEPTHS[frame_bitdepth] + frame_samples = np.frombuffer(frame_bytes[1:], dtype=frame_dtype) + frame_samples = frame_samples.reshape(len(frame_samples)//frame_channels, frame_channels) + if not self.channels: self.channels = frame_channels + + return frame_samples \ No newline at end of file diff --git a/LXST/Codecs/__init__.py b/LXST/Codecs/__init__.py new file mode 100644 index 0000000..b081b02 --- /dev/null +++ b/LXST/Codecs/__init__.py @@ -0,0 +1,29 @@ +from .Codec import CodecError as CodecError +from .Codec import Codec as Codec +from .Codec import Null as Null +from .Raw import Raw as Raw +from .Codec2 import Codec2 as Codec2 +from .Opus import Opus as Opus + +NULL = 0xFF +RAW = 0x00 +OPUS = 0x01 +CODEC2 = 0x02 + +def codec_header_byte(codec): + if codec == Raw: + return RAW.to_bytes() + elif codec == Opus: + return OPUS.to_bytes() + elif codec == Codec2: + return CODEC2.to_bytes() + + raise TypeError(f"No header mapping for codec type {codec}") + +def codec_type(header_byte): + if header_byte == RAW: + return Raw + elif header_byte == OPUS: + return Opus + elif header_byte == CODEC2: + return Codec2 \ No newline at end of file diff --git a/LXST/Codecs/libs/pydub/__init__.py b/LXST/Codecs/libs/pydub/__init__.py new file mode 100644 index 0000000..65e30b4 --- /dev/null +++ b/LXST/Codecs/libs/pydub/__init__.py @@ -0,0 +1 @@ +from .audio_segment import AudioSegment \ No newline at end of file diff --git a/LXST/Codecs/libs/pydub/audio_segment.py b/LXST/Codecs/libs/pydub/audio_segment.py new file mode 100644 index 0000000..14ea46e --- /dev/null +++ b/LXST/Codecs/libs/pydub/audio_segment.py @@ -0,0 +1,1399 @@ +from __future__ import division + +import array +import os +import subprocess +from tempfile import TemporaryFile, NamedTemporaryFile +import wave +import sys +import struct +from .logging_utils import log_conversion, log_subprocess_output +from .utils import mediainfo_json, fsdecode +import base64 +from collections import namedtuple + +try: + from StringIO import StringIO +except: + from io import StringIO + +from io import BytesIO + +try: + from itertools import izip +except: + izip = zip + +from .utils import ( + _fd_or_path_or_tempfile, + db_to_float, + ratio_to_db, + get_encoder_name, + get_array_type, + audioop, +) +from .exceptions import ( + TooManyMissingFrames, + InvalidDuration, + InvalidID3TagVersion, + InvalidTag, + CouldntDecodeError, + CouldntEncodeError, + MissingAudioParameter, +) + +if sys.version_info >= (3, 0): + basestring = str + xrange = range + StringIO = BytesIO + + +class ClassPropertyDescriptor(object): + + def __init__(self, fget, fset=None): + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + if klass is None: + klass = type(obj) + return self.fget.__get__(obj, klass)() + + def __set__(self, obj, value): + if not self.fset: + raise AttributeError("can't set attribute") + type_ = type(obj) + return self.fset.__get__(obj, type_)(value) + + def setter(self, func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + self.fset = func + return self + + +def classproperty(func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + + return ClassPropertyDescriptor(func) + + +AUDIO_FILE_EXT_ALIASES = { + "m4a": "mp4", + "wave": "wav", +} + +WavSubChunk = namedtuple('WavSubChunk', ['id', 'position', 'size']) +WavData = namedtuple('WavData', ['audio_format', 'channels', 'sample_rate', + 'bits_per_sample', 'raw_data']) + + +def extract_wav_headers(data): + # def search_subchunk(data, subchunk_id): + pos = 12 # The size of the RIFF chunk descriptor + subchunks = [] + while pos + 8 <= len(data) and len(subchunks) < 10: + subchunk_id = data[pos:pos + 4] + subchunk_size = struct.unpack_from(' 2**32: + raise CouldntDecodeError("Unable to process >4GB files") + + # Set the file size in the RIFF chunk descriptor + data[4:8] = struct.pack(' b'\x7f'[0]]) + old_bytes = struct.pack(pack_fmt, b0, b1, b2) + byte_buffer.write(old_bytes) + + self._data = byte_buffer.getvalue() + self.sample_width = 4 + self.frame_width = self.channels * self.sample_width + + super(AudioSegment, self).__init__(*args, **kwargs) + + @property + def raw_data(self): + """ + public access to the raw audio data as a bytestring + """ + return self._data + + def get_array_of_samples(self, array_type_override=None): + """ + returns the raw_data as an array of samples + """ + if array_type_override is None: + array_type_override = self.array_type + return array.array(array_type_override, self._data) + + @property + def array_type(self): + return get_array_type(self.sample_width * 8) + + def __len__(self): + """ + returns the length of this audio segment in milliseconds + """ + return round(1000 * (self.frame_count() / self.frame_rate)) + + def __eq__(self, other): + try: + return self._data == other._data + except: + return False + + def __hash__(self): + return hash(AudioSegment) ^ hash((self.channels, self.frame_rate, self.sample_width, self._data)) + + def __ne__(self, other): + return not (self == other) + + def __iter__(self): + return (self[i] for i in xrange(len(self))) + + def __getitem__(self, millisecond): + if isinstance(millisecond, slice): + if millisecond.step: + return ( + self[i:i + millisecond.step] + for i in xrange(*millisecond.indices(len(self))) + ) + + start = millisecond.start if millisecond.start is not None else 0 + end = millisecond.stop if millisecond.stop is not None \ + else len(self) + + start = min(start, len(self)) + end = min(end, len(self)) + else: + start = millisecond + end = millisecond + 1 + + start = self._parse_position(start) * self.frame_width + end = self._parse_position(end) * self.frame_width + data = self._data[start:end] + + # ensure the output is as long as the requester is expecting + expected_length = end - start + missing_frames = (expected_length - len(data)) // self.frame_width + if missing_frames: + if missing_frames > self.frame_count(ms=2): + raise TooManyMissingFrames( + "You should never be filling in " + " more than 2 ms with silence here, " + "missing frames: %s" % missing_frames) + silence = audioop.mul(data[:self.frame_width], + self.sample_width, 0) + data += (silence * missing_frames) + + return self._spawn(data) + + def get_sample_slice(self, start_sample=None, end_sample=None): + """ + Get a section of the audio segment by sample index. + + NOTE: Negative indices do *not* address samples backword + from the end of the audio segment like a python list. + This is intentional. + """ + max_val = int(self.frame_count()) + + def bounded(val, default): + if val is None: + return default + if val < 0: + return 0 + if val > max_val: + return max_val + return val + + start_i = bounded(start_sample, 0) * self.frame_width + end_i = bounded(end_sample, max_val) * self.frame_width + + data = self._data[start_i:end_i] + return self._spawn(data) + + def __add__(self, arg): + if isinstance(arg, AudioSegment): + return self.append(arg, crossfade=0) + else: + return self.apply_gain(arg) + + def __radd__(self, rarg): + """ + Permit use of sum() builtin with an iterable of AudioSegments + """ + if rarg == 0: + return self + raise TypeError("Gains must be the second addend after the " + "AudioSegment") + + def __sub__(self, arg): + if isinstance(arg, AudioSegment): + raise TypeError("AudioSegment objects can't be subtracted from " + "each other") + else: + return self.apply_gain(-arg) + + def __mul__(self, arg): + """ + If the argument is an AudioSegment, overlay the multiplied audio + segment. + + If it's a number, just use the string multiply operation to repeat the + audio. + + The following would return an AudioSegment that contains the + audio of audio_seg eight times + + `audio_seg * 8` + """ + if isinstance(arg, AudioSegment): + return self.overlay(arg, position=0, loop=True) + else: + return self._spawn(data=self._data * arg) + + def _spawn(self, data, overrides={}): + """ + Creates a new audio segment using the metadata from the current one + and the data passed in. Should be used whenever an AudioSegment is + being returned by an operation that would alters the current one, + since AudioSegment objects are immutable. + """ + # accept lists of data chunks + if isinstance(data, list): + data = b''.join(data) + + if isinstance(data, array.array): + try: + data = data.tobytes() + except: + data = data.tostring() + + # accept file-like objects + if hasattr(data, 'read'): + if hasattr(data, 'seek'): + data.seek(0) + data = data.read() + + metadata = { + 'sample_width': self.sample_width, + 'frame_rate': self.frame_rate, + 'frame_width': self.frame_width, + 'channels': self.channels + } + metadata.update(overrides) + return self.__class__(data=data, metadata=metadata) + + @classmethod + def _sync(cls, *segs): + channels = max(seg.channels for seg in segs) + frame_rate = max(seg.frame_rate for seg in segs) + sample_width = max(seg.sample_width for seg in segs) + + return tuple( + seg.set_channels(channels).set_frame_rate(frame_rate).set_sample_width(sample_width) + for seg in segs + ) + + def _parse_position(self, val): + if val < 0: + val = len(self) - abs(val) + val = self.frame_count(ms=len(self)) if val == float("inf") else \ + self.frame_count(ms=val) + return int(val) + + @classmethod + def empty(cls): + return cls(b'', metadata={ + "channels": 1, + "sample_width": 1, + "frame_rate": 1, + "frame_width": 1 + }) + + @classmethod + def silent(cls, duration=1000, frame_rate=11025): + """ + Generate a silent audio segment. + duration specified in milliseconds (default duration: 1000ms, default frame_rate: 11025). + """ + frames = int(frame_rate * (duration / 1000.0)) + data = b"\0\0" * frames + return cls(data, metadata={"channels": 1, + "sample_width": 2, + "frame_rate": frame_rate, + "frame_width": 2}) + + @classmethod + def from_mono_audiosegments(cls, *mono_segments): + if not len(mono_segments): + raise ValueError("At least one AudioSegment instance is required") + + segs = cls._sync(*mono_segments) + + if segs[0].channels != 1: + raise ValueError( + "AudioSegment.from_mono_audiosegments requires all arguments are mono AudioSegment instances") + + channels = len(segs) + sample_width = segs[0].sample_width + frame_rate = segs[0].frame_rate + + frame_count = max(int(seg.frame_count()) for seg in segs) + data = array.array( + segs[0].array_type, + b'\0' * (frame_count * sample_width * channels) + ) + + for i, seg in enumerate(segs): + data[i::channels] = seg.get_array_of_samples() + + return cls( + data, + channels=channels, + sample_width=sample_width, + frame_rate=frame_rate, + ) + + @classmethod + def from_file_using_temporary_files(cls, file, format=None, codec=None, parameters=None, start_second=None, duration=None, **kwargs): + orig_file = file + file, close_file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False) + + if format: + format = format.lower() + format = AUDIO_FILE_EXT_ALIASES.get(format, format) + + def is_format(f): + f = f.lower() + if format == f: + return True + if isinstance(orig_file, basestring): + return orig_file.lower().endswith(".{0}".format(f)) + if isinstance(orig_file, bytes): + return orig_file.lower().endswith((".{0}".format(f)).encode('utf8')) + return False + + if is_format("wav"): + try: + obj = cls._from_safe_wav(file) + if close_file: + file.close() + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[start_second*1000:] + elif start_second is None and duration is not None: + return obj[:duration*1000] + else: + return obj[start_second*1000:(start_second+duration)*1000] + except: + file.seek(0) + elif is_format("raw") or is_format("pcm"): + sample_width = kwargs['sample_width'] + frame_rate = kwargs['frame_rate'] + channels = kwargs['channels'] + metadata = { + 'sample_width': sample_width, + 'frame_rate': frame_rate, + 'channels': channels, + 'frame_width': channels * sample_width + } + obj = cls(data=file.read(), metadata=metadata) + if close_file: + file.close() + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[start_second * 1000:] + elif start_second is None and duration is not None: + return obj[:duration * 1000] + else: + return obj[start_second * 1000:(start_second + duration) * 1000] + + input_file = NamedTemporaryFile(mode='wb', delete=False) + try: + input_file.write(file.read()) + except(OSError): + input_file.flush() + input_file.close() + input_file = NamedTemporaryFile(mode='wb', delete=False, buffering=2 ** 31 - 1) + if close_file: + file.close() + close_file = True + file = open(orig_file, buffering=2 ** 13 - 1, mode='rb') + reader = file.read(2 ** 31 - 1) + while reader: + input_file.write(reader) + reader = file.read(2 ** 31 - 1) + input_file.flush() + if close_file: + file.close() + + output = NamedTemporaryFile(mode="rb", delete=False) + + conversion_command = [cls.converter, + '-y', # always overwrite existing files + ] + + # If format is not defined + # ffmpeg/avconv will detect it automatically + if format: + conversion_command += ["-f", format] + + if codec: + # force audio decoder + conversion_command += ["-acodec", codec] + + conversion_command += [ + "-i", input_file.name, # input_file options (filename last) + "-vn", # Drop any video streams if there are any + "-f", "wav" # output options (filename last) + ] + + if start_second is not None: + conversion_command += ["-ss", str(start_second)] + + if duration is not None: + conversion_command += ["-t", str(duration)] + + conversion_command += [output.name] + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + log_conversion(conversion_command) + + with open(os.devnull, 'rb') as devnull: + p = subprocess.Popen(conversion_command, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p_out, p_err = p.communicate() + + log_subprocess_output(p_out) + log_subprocess_output(p_err) + + try: + if p.returncode != 0: + raise CouldntDecodeError( + "Decoding failed. ffmpeg returned error code: {0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format( + p.returncode, p_err.decode(errors='ignore') )) + obj = cls._from_safe_wav(output) + finally: + input_file.close() + output.close() + os.unlink(input_file.name) + os.unlink(output.name) + + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[0:] + elif start_second is None and duration is not None: + return obj[:duration * 1000] + else: + return obj[0:duration * 1000] + + + @classmethod + def from_file(cls, file, format=None, codec=None, parameters=None, start_second=None, duration=None, **kwargs): + orig_file = file + try: + filename = fsdecode(file) + except TypeError: + filename = None + file, close_file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False) + + if format: + format = format.lower() + format = AUDIO_FILE_EXT_ALIASES.get(format, format) + + def is_format(f): + f = f.lower() + if format == f: + return True + + if filename: + return filename.lower().endswith(".{0}".format(f)) + + return False + + if is_format("wav"): + try: + if start_second is None and duration is None: + return cls._from_safe_wav(file) + elif start_second is not None and duration is None: + return cls._from_safe_wav(file)[start_second*1000:] + elif start_second is None and duration is not None: + return cls._from_safe_wav(file)[:duration*1000] + else: + return cls._from_safe_wav(file)[start_second*1000:(start_second+duration)*1000] + except: + file.seek(0) + elif is_format("raw") or is_format("pcm"): + sample_width = kwargs['sample_width'] + frame_rate = kwargs['frame_rate'] + channels = kwargs['channels'] + metadata = { + 'sample_width': sample_width, + 'frame_rate': frame_rate, + 'channels': channels, + 'frame_width': channels * sample_width + } + if start_second is None and duration is None: + return cls(data=file.read(), metadata=metadata) + elif start_second is not None and duration is None: + return cls(data=file.read(), metadata=metadata)[start_second*1000:] + elif start_second is None and duration is not None: + return cls(data=file.read(), metadata=metadata)[:duration*1000] + else: + return cls(data=file.read(), metadata=metadata)[start_second*1000:(start_second+duration)*1000] + + conversion_command = [cls.converter, + '-y', # always overwrite existing files + ] + + # If format is not defined + # ffmpeg/avconv will detect it automatically + if format: + conversion_command += ["-f", format] + + if codec: + # force audio decoder + conversion_command += ["-acodec", codec] + + read_ahead_limit = kwargs.get('read_ahead_limit', -1) + if filename: + conversion_command += ["-i", filename] + stdin_parameter = None + stdin_data = None + else: + if cls.converter == 'ffmpeg': + conversion_command += ["-read_ahead_limit", str(read_ahead_limit), + "-i", "cache:pipe:0"] + else: + conversion_command += ["-i", "-"] + stdin_parameter = subprocess.PIPE + stdin_data = file.read() + + if codec: + info = None + else: + info = mediainfo_json(orig_file, read_ahead_limit=read_ahead_limit) + if info: + audio_streams = [x for x in info['streams'] + if x['codec_type'] == 'audio'] + # This is a workaround for some ffprobe versions that always say + # that mp3/mp4/aac/webm/ogg files contain fltp samples + audio_codec = audio_streams[0].get('codec_name') + if (audio_streams[0].get('sample_fmt') == 'fltp' and + audio_codec in ['mp3', 'mp4', 'aac', 'webm', 'ogg']): + bits_per_sample = 16 + else: + bits_per_sample = audio_streams[0]['bits_per_sample'] + if bits_per_sample == 8: + acodec = 'pcm_u8' + else: + acodec = 'pcm_s%dle' % bits_per_sample + + conversion_command += ["-acodec", acodec] + + conversion_command += [ + "-vn", # Drop any video streams if there are any + "-f", "wav" # output options (filename last) + ] + + if start_second is not None: + conversion_command += ["-ss", str(start_second)] + + if duration is not None: + conversion_command += ["-t", str(duration)] + + conversion_command += ["-"] + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + log_conversion(conversion_command) + + p = subprocess.Popen(conversion_command, stdin=stdin_parameter, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p_out, p_err = p.communicate(input=stdin_data) + + if p.returncode != 0 or len(p_out) == 0: + if close_file: + file.close() + raise CouldntDecodeError( + "Decoding failed. ffmpeg returned error code: {0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format( + p.returncode, p_err.decode(errors='ignore') )) + + p_out = bytearray(p_out) + fix_wav_headers(p_out) + p_out = bytes(p_out) + obj = cls(p_out) + + if close_file: + file.close() + + if start_second is None and duration is None: + return obj + elif start_second is not None and duration is None: + return obj[0:] + elif start_second is None and duration is not None: + return obj[:duration * 1000] + else: + return obj[0:duration * 1000] + + @classmethod + def from_mp3(cls, file, parameters=None): + return cls.from_file(file, 'mp3', parameters=parameters) + + @classmethod + def from_flv(cls, file, parameters=None): + return cls.from_file(file, 'flv', parameters=parameters) + + @classmethod + def from_ogg(cls, file, parameters=None): + return cls.from_file(file, 'ogg', parameters=parameters) + + @classmethod + def from_wav(cls, file, parameters=None): + return cls.from_file(file, 'wav', parameters=parameters) + + @classmethod + def from_raw(cls, file, **kwargs): + return cls.from_file(file, 'raw', sample_width=kwargs['sample_width'], frame_rate=kwargs['frame_rate'], + channels=kwargs['channels']) + + @classmethod + def _from_safe_wav(cls, file): + file, close_file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False) + file.seek(0) + obj = cls(data=file) + if close_file: + file.close() + return obj + + def export(self, out_f=None, format='mp3', codec=None, bitrate=None, parameters=None, tags=None, id3v2_version='4', + cover=None): + """ + Export an AudioSegment to a file with given options + + out_f (string): + Path to destination audio file. Also accepts os.PathLike objects on + python >= 3.6 + + format (string) + Format for destination audio file. + ('mp3', 'wav', 'raw', 'ogg' or other ffmpeg/avconv supported files) + + codec (string) + Codec used to encode the destination file. + + bitrate (string) + Bitrate used when encoding destination file. (64, 92, 128, 256, 312k...) + Each codec accepts different bitrate arguments so take a look at the + ffmpeg documentation for details (bitrate usually shown as -b, -ba or + -a:b). + + parameters (list of strings) + Aditional ffmpeg/avconv parameters + + tags (dict) + Set metadata information to destination files + usually used as tags. ({title='Song Title', artist='Song Artist'}) + + id3v2_version (string) + Set ID3v2 version for tags. (default: '4') + + cover (file) + Set cover for audio file from image file. (png or jpg) + """ + id3v2_allowed_versions = ['3', '4'] + + if format == "raw" and (codec is not None or parameters is not None): + raise AttributeError( + 'Can not invoke ffmpeg when export format is "raw"; ' + 'specify an ffmpeg raw format like format="s16le" instead ' + 'or call export(format="raw") with no codec or parameters') + + out_f, _ = _fd_or_path_or_tempfile(out_f, 'wb+') + out_f.seek(0) + + if format == "raw": + out_f.write(self._data) + out_f.seek(0) + return out_f + + # wav with no ffmpeg parameters can just be written directly to out_f + easy_wav = format == "wav" and codec is None and parameters is None + + if easy_wav: + data = out_f + else: + data = NamedTemporaryFile(mode="wb", delete=False) + + pcm_for_wav = self._data + if self.sample_width == 1: + # convert to unsigned integers for wav + pcm_for_wav = audioop.bias(self._data, 1, 128) + + wave_data = wave.open(data, 'wb') + wave_data.setnchannels(self.channels) + wave_data.setsampwidth(self.sample_width) + wave_data.setframerate(self.frame_rate) + # For some reason packing the wave header struct with + # a float in python 2 doesn't throw an exception + wave_data.setnframes(int(self.frame_count())) + wave_data.writeframesraw(pcm_for_wav) + wave_data.close() + + # for easy wav files, we're done (wav data is written directly to out_f) + if easy_wav: + out_f.seek(0) + return out_f + + output = NamedTemporaryFile(mode="w+b", delete=False) + + # build converter command to export + conversion_command = [ + self.converter, + '-y', # always overwrite existing files + "-f", "wav", "-i", data.name, # input options (filename last) + ] + + if codec is None: + codec = self.DEFAULT_CODECS.get(format, None) + + if cover is not None: + if cover.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff')) and format == "mp3": + conversion_command.extend(["-i", cover, "-map", "0", "-map", "1", "-c:v", "mjpeg"]) + else: + raise AttributeError( + "Currently cover images are only supported by MP3 files. The allowed image formats are: .tif, .jpg, .bmp, .jpeg and .png.") + + if codec is not None: + # force audio encoder + conversion_command.extend(["-acodec", codec]) + + if bitrate is not None: + conversion_command.extend(["-b:a", bitrate]) + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + if tags is not None: + if not isinstance(tags, dict): + raise InvalidTag("Tags must be a dictionary.") + else: + # Extend converter command with tags + # print(tags) + for key, value in tags.items(): + conversion_command.extend( + ['-metadata', '{0}={1}'.format(key, value)]) + + if format == 'mp3': + # set id3v2 tag version + if id3v2_version not in id3v2_allowed_versions: + raise InvalidID3TagVersion( + "id3v2_version not allowed, allowed versions: %s" % id3v2_allowed_versions) + conversion_command.extend([ + "-id3v2_version", id3v2_version + ]) + + if sys.platform == 'darwin' and codec == 'mp3': + conversion_command.extend(["-write_xing", "0"]) + + conversion_command.extend([ + "-f", format, output.name, # output options (filename last) + ]) + + log_conversion(conversion_command) + + # read stdin / write stdout + with open(os.devnull, 'rb') as devnull: + p = subprocess.Popen(conversion_command, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p_out, p_err = p.communicate() + + log_subprocess_output(p_out) + log_subprocess_output(p_err) + + if p.returncode != 0: + raise CouldntEncodeError( + "Encoding failed. ffmpeg/avlib returned error code: {0}\n\nCommand:{1}\n\nOutput from ffmpeg/avlib:\n\n{2}".format( + p.returncode, conversion_command, p_err.decode(errors='ignore') )) + + output.seek(0) + out_f.write(output.read()) + + data.close() + output.close() + + os.unlink(data.name) + os.unlink(output.name) + + out_f.seek(0) + return out_f + + def get_frame(self, index): + frame_start = index * self.frame_width + frame_end = frame_start + self.frame_width + return self._data[frame_start:frame_end] + + def frame_count(self, ms=None): + """ + returns the number of frames for the given number of milliseconds, or + if not specified, the number of frames in the whole AudioSegment + """ + if ms is not None: + return ms * (self.frame_rate / 1000.0) + else: + return float(len(self._data) // self.frame_width) + + def set_sample_width(self, sample_width): + if sample_width == self.sample_width: + return self + + frame_width = self.channels * sample_width + + return self._spawn( + audioop.lin2lin(self._data, self.sample_width, sample_width), + overrides={'sample_width': sample_width, 'frame_width': frame_width} + ) + + def set_frame_rate(self, frame_rate): + if frame_rate == self.frame_rate: + return self + + if self._data: + converted, _ = audioop.ratecv(self._data, self.sample_width, + self.channels, self.frame_rate, + frame_rate, None) + else: + converted = self._data + + return self._spawn(data=converted, + overrides={'frame_rate': frame_rate}) + + def set_channels(self, channels): + if channels == self.channels: + return self + + if channels == 2 and self.channels == 1: + fn = audioop.tostereo + frame_width = self.frame_width * 2 + fac = 1 + converted = fn(self._data, self.sample_width, fac, fac) + elif channels == 1 and self.channels == 2: + fn = audioop.tomono + frame_width = self.frame_width // 2 + fac = 0.5 + converted = fn(self._data, self.sample_width, fac, fac) + elif channels == 1: + channels_data = [seg.get_array_of_samples() for seg in self.split_to_mono()] + frame_count = int(self.frame_count()) + converted = array.array( + channels_data[0].typecode, + b'\0' * (frame_count * self.sample_width) + ) + for raw_channel_data in channels_data: + for i in range(frame_count): + converted[i] += raw_channel_data[i] // self.channels + frame_width = self.frame_width // self.channels + elif self.channels == 1: + dup_channels = [self for iChannel in range(channels)] + return AudioSegment.from_mono_audiosegments(*dup_channels) + else: + raise ValueError( + "AudioSegment.set_channels only supports mono-to-multi channel and multi-to-mono channel conversion") + + return self._spawn(data=converted, + overrides={ + 'channels': channels, + 'frame_width': frame_width}) + + def split_to_mono(self): + if self.channels == 1: + return [self] + + samples = self.get_array_of_samples() + + mono_channels = [] + for i in range(self.channels): + samples_for_current_channel = samples[i::self.channels] + + try: + mono_data = samples_for_current_channel.tobytes() + except AttributeError: + mono_data = samples_for_current_channel.tostring() + + mono_channels.append( + self._spawn(mono_data, overrides={"channels": 1, "frame_width": self.sample_width}) + ) + + return mono_channels + + @property + def rms(self): + return audioop.rms(self._data, self.sample_width) + + @property + def dBFS(self): + rms = self.rms + if not rms: + return -float("infinity") + return ratio_to_db(self.rms / self.max_possible_amplitude) + + @property + def max(self): + return audioop.max(self._data, self.sample_width) + + @property + def max_possible_amplitude(self): + bits = self.sample_width * 8 + max_possible_val = (2 ** bits) + + # since half is above 0 and half is below the max amplitude is divided + return max_possible_val / 2 + + @property + def max_dBFS(self): + return ratio_to_db(self.max, self.max_possible_amplitude) + + @property + def duration_seconds(self): + return self.frame_rate and self.frame_count() / self.frame_rate or 0.0 + + def get_dc_offset(self, channel=1): + """ + Returns a value between -1.0 and 1.0 representing the DC offset of a + channel (1 for left, 2 for right). + """ + if not 1 <= channel <= 2: + raise ValueError("channel value must be 1 (left) or 2 (right)") + + if self.channels == 1: + data = self._data + elif channel == 1: + data = audioop.tomono(self._data, self.sample_width, 1, 0) + else: + data = audioop.tomono(self._data, self.sample_width, 0, 1) + + return float(audioop.avg(data, self.sample_width)) / self.max_possible_amplitude + + def remove_dc_offset(self, channel=None, offset=None): + """ + Removes DC offset of given channel. Calculates offset if it's not given. + Offset values must be in range -1.0 to 1.0. If channel is None, removes + DC offset from all available channels. + """ + if channel and not 1 <= channel <= 2: + raise ValueError("channel value must be None, 1 (left) or 2 (right)") + + if offset and not -1.0 <= offset <= 1.0: + raise ValueError("offset value must be in range -1.0 to 1.0") + + if offset: + offset = int(round(offset * self.max_possible_amplitude)) + + def remove_data_dc(data, off): + if not off: + off = audioop.avg(data, self.sample_width) + return audioop.bias(data, self.sample_width, -off) + + if self.channels == 1: + return self._spawn(data=remove_data_dc(self._data, offset)) + + left_channel = audioop.tomono(self._data, self.sample_width, 1, 0) + right_channel = audioop.tomono(self._data, self.sample_width, 0, 1) + + if not channel or channel == 1: + left_channel = remove_data_dc(left_channel, offset) + + if not channel or channel == 2: + right_channel = remove_data_dc(right_channel, offset) + + left_channel = audioop.tostereo(left_channel, self.sample_width, 1, 0) + right_channel = audioop.tostereo(right_channel, self.sample_width, 0, 1) + + return self._spawn(data=audioop.add(left_channel, right_channel, + self.sample_width)) + + def apply_gain(self, volume_change): + return self._spawn(data=audioop.mul(self._data, self.sample_width, + db_to_float(float(volume_change)))) + + def overlay(self, seg, position=0, loop=False, times=None, gain_during_overlay=None): + """ + Overlay the provided segment on to this segment starting at the + specificed position and using the specfied looping beahvior. + + seg (AudioSegment): + The audio segment to overlay on to this one. + + position (optional int): + The position to start overlaying the provided segment in to this + one. + + loop (optional bool): + Loop seg as many times as necessary to match this segment's length. + Overrides loops param. + + times (optional int): + Loop seg the specified number of times or until it matches this + segment's length. 1 means once, 2 means twice, ... 0 would make the + call a no-op + gain_during_overlay (optional int): + Changes this segment's volume by the specified amount during the + duration of time that seg is overlaid on top of it. When negative, + this has the effect of 'ducking' the audio under the overlay. + """ + + if loop: + # match loop=True's behavior with new times (count) mechinism. + times = -1 + elif times is None: + # no times specified, just once through + times = 1 + elif times == 0: + # it's a no-op, make a copy since we never mutate + return self._spawn(self._data) + + output = StringIO() + + seg1, seg2 = AudioSegment._sync(self, seg) + sample_width = seg1.sample_width + spawn = seg1._spawn + + output.write(seg1[:position]._data) + + # drop down to the raw data + seg1 = seg1[position:]._data + seg2 = seg2._data + pos = 0 + seg1_len = len(seg1) + seg2_len = len(seg2) + while times: + remaining = max(0, seg1_len - pos) + if seg2_len >= remaining: + seg2 = seg2[:remaining] + seg2_len = remaining + # we've hit the end, we're done looping (if we were) and this + # is our last go-around + times = 1 + + if gain_during_overlay: + seg1_overlaid = seg1[pos:pos + seg2_len] + seg1_adjusted_gain = audioop.mul(seg1_overlaid, self.sample_width, + db_to_float(float(gain_during_overlay))) + output.write(audioop.add(seg1_adjusted_gain, seg2, sample_width)) + else: + output.write(audioop.add(seg1[pos:pos + seg2_len], seg2, + sample_width)) + pos += seg2_len + + # dec times to break our while loop (eventually) + times -= 1 + + output.write(seg1[pos:]) + + return spawn(data=output) + + def append(self, seg, crossfade=100): + seg1, seg2 = AudioSegment._sync(self, seg) + + if not crossfade: + return seg1._spawn(seg1._data + seg2._data) + elif crossfade > len(self): + raise ValueError("Crossfade is longer than the original AudioSegment ({}ms > {}ms)".format( + crossfade, len(self) + )) + elif crossfade > len(seg): + raise ValueError("Crossfade is longer than the appended AudioSegment ({}ms > {}ms)".format( + crossfade, len(seg) + )) + + xf = seg1[-crossfade:].fade(to_gain=-120, start=0, end=float('inf')) + xf *= seg2[:crossfade].fade(from_gain=-120, start=0, end=float('inf')) + + output = TemporaryFile() + + output.write(seg1[:-crossfade]._data) + output.write(xf._data) + output.write(seg2[crossfade:]._data) + + output.seek(0) + obj = seg1._spawn(data=output) + output.close() + return obj + + def fade(self, to_gain=0, from_gain=0, start=None, end=None, + duration=None): + """ + Fade the volume of this audio segment. + + to_gain (float): + resulting volume_change in db + + start (int): + default = beginning of the segment + when in this segment to start fading in milliseconds + + end (int): + default = end of the segment + when in this segment to start fading in milliseconds + + duration (int): + default = until the end of the audio segment + the duration of the fade + """ + if None not in [duration, end, start]: + raise TypeError('Only two of the three arguments, "start", ' + '"end", and "duration" may be specified') + + # no fade == the same audio + if to_gain == 0 and from_gain == 0: + return self + + start = min(len(self), start) if start is not None else None + end = min(len(self), end) if end is not None else None + + if start is not None and start < 0: + start += len(self) + if end is not None and end < 0: + end += len(self) + + if duration is not None and duration < 0: + raise InvalidDuration("duration must be a positive integer") + + if duration: + if start is not None: + end = start + duration + elif end is not None: + start = end - duration + else: + duration = end - start + + from_power = db_to_float(from_gain) + + output = [] + + # original data - up until the crossfade portion, as is + before_fade = self[:start]._data + if from_gain != 0: + before_fade = audioop.mul(before_fade, + self.sample_width, + from_power) + output.append(before_fade) + + gain_delta = db_to_float(to_gain) - from_power + + # fades longer than 100ms can use coarse fading (one gain step per ms), + # shorter fades will have audible clicks so they use precise fading + # (one gain step per sample) + if duration > 100: + scale_step = gain_delta / duration + + for i in range(duration): + volume_change = from_power + (scale_step * i) + chunk = self[start + i] + chunk = audioop.mul(chunk._data, + self.sample_width, + volume_change) + + output.append(chunk) + else: + start_frame = self.frame_count(ms=start) + end_frame = self.frame_count(ms=end) + fade_frames = end_frame - start_frame + scale_step = gain_delta / fade_frames + + for i in range(int(fade_frames)): + volume_change = from_power + (scale_step * i) + sample = self.get_frame(int(start_frame + i)) + sample = audioop.mul(sample, self.sample_width, volume_change) + + output.append(sample) + + # original data after the crossfade portion, at the new volume + after_fade = self[end:]._data + if to_gain != 0: + after_fade = audioop.mul(after_fade, + self.sample_width, + db_to_float(to_gain)) + output.append(after_fade) + + return self._spawn(data=output) + + def fade_out(self, duration): + return self.fade(to_gain=-120, duration=duration, end=float('inf')) + + def fade_in(self, duration): + return self.fade(from_gain=-120, duration=duration, start=0) + + def reverse(self): + return self._spawn( + data=audioop.reverse(self._data, self.sample_width) + ) + + def _repr_html_(self): + src = """ + + """ + fh = self.export() + data = base64.b64encode(fh.read()).decode('ascii') + return src.format(base64=data) + + +from . import effects diff --git a/LXST/Codecs/libs/pydub/effects.py b/LXST/Codecs/libs/pydub/effects.py new file mode 100644 index 0000000..0210521 --- /dev/null +++ b/LXST/Codecs/libs/pydub/effects.py @@ -0,0 +1,341 @@ +import sys +import math +import array +from .utils import ( + db_to_float, + ratio_to_db, + register_pydub_effect, + make_chunks, + audioop, + get_min_max_value +) +from .silence import split_on_silence +from .exceptions import TooManyMissingFrames, InvalidDuration + +if sys.version_info >= (3, 0): + xrange = range + + +@register_pydub_effect +def apply_mono_filter_to_each_channel(seg, filter_fn): + n_channels = seg.channels + + channel_segs = seg.split_to_mono() + channel_segs = [filter_fn(channel_seg) for channel_seg in channel_segs] + + out_data = seg.get_array_of_samples() + for channel_i, channel_seg in enumerate(channel_segs): + for sample_i, sample in enumerate(channel_seg.get_array_of_samples()): + index = (sample_i * n_channels) + channel_i + out_data[index] = sample + + return seg._spawn(out_data) + + +@register_pydub_effect +def normalize(seg, headroom=0.1): + """ + headroom is how close to the maximum volume to boost the signal up to (specified in dB) + """ + peak_sample_val = seg.max + + # if the max is 0, this audio segment is silent, and can't be normalized + if peak_sample_val == 0: + return seg + + target_peak = seg.max_possible_amplitude * db_to_float(-headroom) + + needed_boost = ratio_to_db(target_peak / peak_sample_val) + return seg.apply_gain(needed_boost) + + +@register_pydub_effect +def speedup(seg, playback_speed=1.5, chunk_size=150, crossfade=25): + # we will keep audio in 150ms chunks since one waveform at 20Hz is 50ms long + # (20 Hz is the lowest frequency audible to humans) + + # portion of AUDIO TO KEEP. if playback speed is 1.25 we keep 80% (0.8) and + # discard 20% (0.2) + atk = 1.0 / playback_speed + + if playback_speed < 2.0: + # throwing out more than half the audio - keep 50ms chunks + ms_to_remove_per_chunk = int(chunk_size * (1 - atk) / atk) + else: + # throwing out less than half the audio - throw out 50ms chunks + ms_to_remove_per_chunk = int(chunk_size) + chunk_size = int(atk * chunk_size / (1 - atk)) + + # the crossfade cannot be longer than the amount of audio we're removing + crossfade = min(crossfade, ms_to_remove_per_chunk - 1) + + # DEBUG + #print("chunk: {0}, rm: {1}".format(chunk_size, ms_to_remove_per_chunk)) + + chunks = make_chunks(seg, chunk_size + ms_to_remove_per_chunk) + if len(chunks) < 2: + raise Exception("Could not speed up AudioSegment, it was too short {2:0.2f}s for the current settings:\n{0}ms chunks at {1:0.1f}x speedup".format( + chunk_size, playback_speed, seg.duration_seconds)) + + # we'll actually truncate a bit less than we calculated to make up for the + # crossfade between chunks + ms_to_remove_per_chunk -= crossfade + + # we don't want to truncate the last chunk since it is not guaranteed to be + # the full chunk length + last_chunk = chunks[-1] + chunks = [chunk[:-ms_to_remove_per_chunk] for chunk in chunks[:-1]] + + out = chunks[0] + for chunk in chunks[1:]: + out = out.append(chunk, crossfade=crossfade) + + out += last_chunk + return out + + +@register_pydub_effect +def strip_silence(seg, silence_len=1000, silence_thresh=-16, padding=100): + if padding > silence_len: + raise InvalidDuration("padding cannot be longer than silence_len") + + chunks = split_on_silence(seg, silence_len, silence_thresh, padding) + crossfade = padding / 2 + + if not len(chunks): + return seg[0:0] + + seg = chunks[0] + for chunk in chunks[1:]: + seg = seg.append(chunk, crossfade=crossfade) + + return seg + + +@register_pydub_effect +def compress_dynamic_range(seg, threshold=-20.0, ratio=4.0, attack=5.0, release=50.0): + """ + Keyword Arguments: + + threshold - default: -20.0 + Threshold in dBFS. default of -20.0 means -20dB relative to the + maximum possible volume. 0dBFS is the maximum possible value so + all values for this argument sould be negative. + + ratio - default: 4.0 + Compression ratio. Audio louder than the threshold will be + reduced to 1/ratio the volume. A ratio of 4.0 is equivalent to + a setting of 4:1 in a pro-audio compressor like the Waves C1. + + attack - default: 5.0 + Attack in milliseconds. How long it should take for the compressor + to kick in once the audio has exceeded the threshold. + + release - default: 50.0 + Release in milliseconds. How long it should take for the compressor + to stop compressing after the audio has falled below the threshold. + + + For an overview of Dynamic Range Compression, and more detailed explanation + of the related terminology, see: + + http://en.wikipedia.org/wiki/Dynamic_range_compression + """ + + thresh_rms = seg.max_possible_amplitude * db_to_float(threshold) + + look_frames = int(seg.frame_count(ms=attack)) + def rms_at(frame_i): + return seg.get_sample_slice(frame_i - look_frames, frame_i).rms + def db_over_threshold(rms): + if rms == 0: return 0.0 + db = ratio_to_db(rms / thresh_rms) + return max(db, 0) + + output = [] + + # amount to reduce the volume of the audio by (in dB) + attenuation = 0.0 + + attack_frames = seg.frame_count(ms=attack) + release_frames = seg.frame_count(ms=release) + for i in xrange(int(seg.frame_count())): + rms_now = rms_at(i) + + # with a ratio of 4.0 this means the volume will exceed the threshold by + # 1/4 the amount (of dB) that it would otherwise + max_attenuation = (1 - (1.0 / ratio)) * db_over_threshold(rms_now) + + attenuation_inc = max_attenuation / attack_frames + attenuation_dec = max_attenuation / release_frames + + if rms_now > thresh_rms and attenuation <= max_attenuation: + attenuation += attenuation_inc + attenuation = min(attenuation, max_attenuation) + else: + attenuation -= attenuation_dec + attenuation = max(attenuation, 0) + + frame = seg.get_frame(i) + if attenuation != 0.0: + frame = audioop.mul(frame, + seg.sample_width, + db_to_float(-attenuation)) + + output.append(frame) + + return seg._spawn(data=b''.join(output)) + + +# Invert the phase of the signal. + +@register_pydub_effect + +def invert_phase(seg, channels=(1, 1)): + """ + channels- specifies which channel (left or right) to reverse the phase of. + Note that mono AudioSegments will become stereo. + """ + if channels == (1, 1): + inverted = audioop.mul(seg._data, seg.sample_width, -1.0) + return seg._spawn(data=inverted) + + else: + if seg.channels == 2: + left, right = seg.split_to_mono() + else: + raise Exception("Can't implicitly convert an AudioSegment with " + str(seg.channels) + " channels to stereo.") + + if channels == (1, 0): + left = left.invert_phase() + else: + right = right.invert_phase() + + return seg.from_mono_audiosegments(left, right) + + + +# High and low pass filters based on implementation found on Stack Overflow: +# http://stackoverflow.com/questions/13882038/implementing-simple-high-and-low-pass-filters-in-c + +@register_pydub_effect +def low_pass_filter(seg, cutoff): + """ + cutoff - Frequency (in Hz) where higher frequency signal will begin to + be reduced by 6dB per octave (doubling in frequency) above this point + """ + RC = 1.0 / (cutoff * 2 * math.pi) + dt = 1.0 / seg.frame_rate + + alpha = dt / (RC + dt) + + original = seg.get_array_of_samples() + filteredArray = array.array(seg.array_type, original) + + frame_count = int(seg.frame_count()) + + last_val = [0] * seg.channels + for i in range(seg.channels): + last_val[i] = filteredArray[i] = original[i] + + for i in range(1, frame_count): + for j in range(seg.channels): + offset = (i * seg.channels) + j + last_val[j] = last_val[j] + (alpha * (original[offset] - last_val[j])) + filteredArray[offset] = int(last_val[j]) + + return seg._spawn(data=filteredArray) + + +@register_pydub_effect +def high_pass_filter(seg, cutoff): + """ + cutoff - Frequency (in Hz) where lower frequency signal will begin to + be reduced by 6dB per octave (doubling in frequency) below this point + """ + RC = 1.0 / (cutoff * 2 * math.pi) + dt = 1.0 / seg.frame_rate + + alpha = RC / (RC + dt) + + minval, maxval = get_min_max_value(seg.sample_width * 8) + + original = seg.get_array_of_samples() + filteredArray = array.array(seg.array_type, original) + + frame_count = int(seg.frame_count()) + + last_val = [0] * seg.channels + for i in range(seg.channels): + last_val[i] = filteredArray[i] = original[i] + + for i in range(1, frame_count): + for j in range(seg.channels): + offset = (i * seg.channels) + j + offset_minus_1 = ((i-1) * seg.channels) + j + + last_val[j] = alpha * (last_val[j] + original[offset] - original[offset_minus_1]) + filteredArray[offset] = int(min(max(last_val[j], minval), maxval)) + + return seg._spawn(data=filteredArray) + + +@register_pydub_effect +def pan(seg, pan_amount): + """ + pan_amount should be between -1.0 (100% left) and +1.0 (100% right) + + When pan_amount == 0.0 the left/right balance is not changed. + + Panning does not alter the *perceived* loundness, but since loudness + is decreasing on one side, the other side needs to get louder to + compensate. When panned hard left, the left channel will be 3dB louder. + """ + if not -1.0 <= pan_amount <= 1.0: + raise ValueError("pan_amount should be between -1.0 (100% left) and +1.0 (100% right)") + + max_boost_db = ratio_to_db(2.0) + boost_db = abs(pan_amount) * max_boost_db + + boost_factor = db_to_float(boost_db) + reduce_factor = db_to_float(max_boost_db) - boost_factor + + reduce_db = ratio_to_db(reduce_factor) + + # Cut boost in half (max boost== 3dB) - in reality 2 speakers + # do not sum to a full 6 dB. + boost_db = boost_db / 2.0 + + if pan_amount < 0: + return seg.apply_gain_stereo(boost_db, reduce_db) + else: + return seg.apply_gain_stereo(reduce_db, boost_db) + + +@register_pydub_effect +def apply_gain_stereo(seg, left_gain=0.0, right_gain=0.0): + """ + left_gain - amount of gain to apply to the left channel (in dB) + right_gain - amount of gain to apply to the right channel (in dB) + + note: mono audio segments will be converted to stereo + """ + if seg.channels == 1: + left = right = seg + elif seg.channels == 2: + left, right = seg.split_to_mono() + + l_mult_factor = db_to_float(left_gain) + r_mult_factor = db_to_float(right_gain) + + left_data = audioop.mul(left._data, left.sample_width, l_mult_factor) + left_data = audioop.tostereo(left_data, left.sample_width, 1, 0) + + right_data = audioop.mul(right._data, right.sample_width, r_mult_factor) + right_data = audioop.tostereo(right_data, right.sample_width, 0, 1) + + output = audioop.add(left_data, right_data, seg.sample_width) + + return seg._spawn(data=output, + overrides={'channels': 2, + 'frame_width': 2 * seg.sample_width}) diff --git a/LXST/Codecs/libs/pydub/exceptions.py b/LXST/Codecs/libs/pydub/exceptions.py new file mode 100644 index 0000000..79d0743 --- /dev/null +++ b/LXST/Codecs/libs/pydub/exceptions.py @@ -0,0 +1,32 @@ +class PydubException(Exception): + """ + Base class for any Pydub exception + """ + + +class TooManyMissingFrames(PydubException): + pass + + +class InvalidDuration(PydubException): + pass + + +class InvalidTag(PydubException): + pass + + +class InvalidID3TagVersion(PydubException): + pass + + +class CouldntDecodeError(PydubException): + pass + + +class CouldntEncodeError(PydubException): + pass + + +class MissingAudioParameter(PydubException): + pass diff --git a/LXST/Codecs/libs/pydub/generators.py b/LXST/Codecs/libs/pydub/generators.py new file mode 100644 index 0000000..b04cb4c --- /dev/null +++ b/LXST/Codecs/libs/pydub/generators.py @@ -0,0 +1,142 @@ +""" +Each generator will return float samples from -1.0 to 1.0, which can be +converted to actual audio with 8, 16, 24, or 32 bit depth using the +SiganlGenerator.to_audio_segment() method (on any of it's subclasses). + +See Wikipedia's "waveform" page for info on some of the generators included +here: http://en.wikipedia.org/wiki/Waveform +""" + +import math +import array +import itertools +import random +from .audio_segment import AudioSegment +from .utils import ( + db_to_float, + get_frame_width, + get_array_type, + get_min_max_value +) + + + +class SignalGenerator(object): + def __init__(self, sample_rate=44100, bit_depth=16): + self.sample_rate = sample_rate + self.bit_depth = bit_depth + + def to_audio_segment(self, duration=1000.0, volume=0.0): + """ + Duration in milliseconds + (default: 1 second) + Volume in DB relative to maximum amplitude + (default 0.0 dBFS, which is the maximum value) + """ + minval, maxval = get_min_max_value(self.bit_depth) + sample_width = get_frame_width(self.bit_depth) + array_type = get_array_type(self.bit_depth) + + gain = db_to_float(volume) + sample_count = int(self.sample_rate * (duration / 1000.0)) + + sample_data = (int(val * maxval * gain) for val in self.generate()) + sample_data = itertools.islice(sample_data, 0, sample_count) + + data = array.array(array_type, sample_data) + + try: + data = data.tobytes() + except: + data = data.tostring() + + return AudioSegment(data=data, metadata={ + "channels": 1, + "sample_width": sample_width, + "frame_rate": self.sample_rate, + "frame_width": sample_width, + }) + + def generate(self): + raise NotImplementedError("SignalGenerator subclasses must implement the generate() method, and *should not* call the superclass implementation.") + + + +class Sine(SignalGenerator): + def __init__(self, freq, **kwargs): + super(Sine, self).__init__(**kwargs) + self.freq = freq + + def generate(self): + sine_of = (self.freq * 2 * math.pi) / self.sample_rate + sample_n = 0 + while True: + yield math.sin(sine_of * sample_n) + sample_n += 1 + + + +class Pulse(SignalGenerator): + def __init__(self, freq, duty_cycle=0.5, **kwargs): + super(Pulse, self).__init__(**kwargs) + self.freq = freq + self.duty_cycle = duty_cycle + + def generate(self): + sample_n = 0 + + # in samples + cycle_length = self.sample_rate / float(self.freq) + pulse_length = cycle_length * self.duty_cycle + + while True: + if (sample_n % cycle_length) < pulse_length: + yield 1.0 + else: + yield -1.0 + sample_n += 1 + + + +class Square(Pulse): + def __init__(self, freq, **kwargs): + kwargs['duty_cycle'] = 0.5 + super(Square, self).__init__(freq, **kwargs) + + + +class Sawtooth(SignalGenerator): + def __init__(self, freq, duty_cycle=1.0, **kwargs): + super(Sawtooth, self).__init__(**kwargs) + self.freq = freq + self.duty_cycle = duty_cycle + + def generate(self): + sample_n = 0 + + # in samples + cycle_length = self.sample_rate / float(self.freq) + midpoint = cycle_length * self.duty_cycle + ascend_length = midpoint + descend_length = cycle_length - ascend_length + + while True: + cycle_position = sample_n % cycle_length + if cycle_position < midpoint: + yield (2 * cycle_position / ascend_length) - 1.0 + else: + yield 1.0 - (2 * (cycle_position - midpoint) / descend_length) + sample_n += 1 + + + +class Triangle(Sawtooth): + def __init__(self, freq, **kwargs): + kwargs['duty_cycle'] = 0.5 + super(Triangle, self).__init__(freq, **kwargs) + + +class WhiteNoise(SignalGenerator): + def generate(self): + while True: + yield (random.random() * 2) - 1.0 diff --git a/LXST/Codecs/libs/pydub/logging_utils.py b/LXST/Codecs/libs/pydub/logging_utils.py new file mode 100644 index 0000000..a312bd2 --- /dev/null +++ b/LXST/Codecs/libs/pydub/logging_utils.py @@ -0,0 +1,14 @@ +""" + +""" +import logging + +converter_logger = logging.getLogger("pydub.converter") + +def log_conversion(conversion_command): + converter_logger.debug("subprocess.call(%s)", repr(conversion_command)) + +def log_subprocess_output(output): + if output: + for line in output.rstrip().splitlines(): + converter_logger.debug('subprocess output: %s', line.rstrip()) diff --git a/LXST/Codecs/libs/pydub/playback.py b/LXST/Codecs/libs/pydub/playback.py new file mode 100644 index 0000000..72ce4a5 --- /dev/null +++ b/LXST/Codecs/libs/pydub/playback.py @@ -0,0 +1,71 @@ +""" +Support for playing AudioSegments. Pyaudio will be used if it's installed, +otherwise will fallback to ffplay. Pyaudio is a *much* nicer solution, but +is tricky to install. See my notes on installing pyaudio in a virtualenv (on +OSX 10.10): https://gist.github.com/jiaaro/9767512210a1d80a8a0d +""" + +import subprocess +from tempfile import NamedTemporaryFile +from .utils import get_player_name, make_chunks + +def _play_with_ffplay(seg): + PLAYER = get_player_name() + with NamedTemporaryFile("w+b", suffix=".wav") as f: + seg.export(f.name, "wav") + subprocess.call([PLAYER, "-nodisp", "-autoexit", "-hide_banner", f.name]) + + +def _play_with_pyaudio(seg): + import pyaudio + + p = pyaudio.PyAudio() + stream = p.open(format=p.get_format_from_width(seg.sample_width), + channels=seg.channels, + rate=seg.frame_rate, + output=True) + + # Just in case there were any exceptions/interrupts, we release the resource + # So as not to raise OSError: Device Unavailable should play() be used again + try: + # break audio into half-second chunks (to allows keyboard interrupts) + for chunk in make_chunks(seg, 500): + stream.write(chunk._data) + finally: + stream.stop_stream() + stream.close() + + p.terminate() + + +def _play_with_simpleaudio(seg): + import simpleaudio + return simpleaudio.play_buffer( + seg.raw_data, + num_channels=seg.channels, + bytes_per_sample=seg.sample_width, + sample_rate=seg.frame_rate + ) + + +def play(audio_segment): + try: + playback = _play_with_simpleaudio(audio_segment) + try: + playback.wait_done() + except KeyboardInterrupt: + playback.stop() + except ImportError: + pass + else: + return + + try: + _play_with_pyaudio(audio_segment) + return + except ImportError: + pass + else: + return + + _play_with_ffplay(audio_segment) diff --git a/LXST/Codecs/libs/pydub/pyaudioop.py b/LXST/Codecs/libs/pydub/pyaudioop.py new file mode 100644 index 0000000..c636a4a --- /dev/null +++ b/LXST/Codecs/libs/pydub/pyaudioop.py @@ -0,0 +1,591 @@ +try: + from __builtin__ import max as builtin_max + from __builtin__ import min as builtin_min +except ImportError: + from builtins import max as builtin_max + from builtins import min as builtin_min +import math +import struct +try: + from fractions import gcd +except ImportError: # Python 3.9+ + from math import gcd +from ctypes import create_string_buffer + + +class error(Exception): + pass + + +def _check_size(size): + if size != 1 and size != 2 and size != 4: + raise error("Size should be 1, 2 or 4") + + +def _check_params(length, size): + _check_size(size) + if length % size != 0: + raise error("not a whole number of frames") + + +def _sample_count(cp, size): + return len(cp) / size + + +def _get_samples(cp, size, signed=True): + for i in range(_sample_count(cp, size)): + yield _get_sample(cp, size, i, signed) + + +def _struct_format(size, signed): + if size == 1: + return "b" if signed else "B" + elif size == 2: + return "h" if signed else "H" + elif size == 4: + return "i" if signed else "I" + + +def _get_sample(cp, size, i, signed=True): + fmt = _struct_format(size, signed) + start = i * size + end = start + size + return struct.unpack_from(fmt, buffer(cp)[start:end])[0] + + +def _put_sample(cp, size, i, val, signed=True): + fmt = _struct_format(size, signed) + struct.pack_into(fmt, cp, i * size, val) + + +def _get_maxval(size, signed=True): + if signed and size == 1: + return 0x7f + elif size == 1: + return 0xff + elif signed and size == 2: + return 0x7fff + elif size == 2: + return 0xffff + elif signed and size == 4: + return 0x7fffffff + elif size == 4: + return 0xffffffff + + +def _get_minval(size, signed=True): + if not signed: + return 0 + elif size == 1: + return -0x80 + elif size == 2: + return -0x8000 + elif size == 4: + return -0x80000000 + + +def _get_clipfn(size, signed=True): + maxval = _get_maxval(size, signed) + minval = _get_minval(size, signed) + return lambda val: builtin_max(min(val, maxval), minval) + + +def _overflow(val, size, signed=True): + minval = _get_minval(size, signed) + maxval = _get_maxval(size, signed) + if minval <= val <= maxval: + return val + + bits = size * 8 + if signed: + offset = 2**(bits-1) + return ((val + offset) % (2**bits)) - offset + else: + return val % (2**bits) + + +def getsample(cp, size, i): + _check_params(len(cp), size) + if not (0 <= i < len(cp) / size): + raise error("Index out of range") + return _get_sample(cp, size, i) + + +def max(cp, size): + _check_params(len(cp), size) + + if len(cp) == 0: + return 0 + + return builtin_max(abs(sample) for sample in _get_samples(cp, size)) + + +def minmax(cp, size): + _check_params(len(cp), size) + + max_sample, min_sample = 0, 0 + for sample in _get_samples(cp, size): + max_sample = builtin_max(sample, max_sample) + min_sample = builtin_min(sample, min_sample) + + return min_sample, max_sample + + +def avg(cp, size): + _check_params(len(cp), size) + sample_count = _sample_count(cp, size) + if sample_count == 0: + return 0 + return sum(_get_samples(cp, size)) / sample_count + + +def rms(cp, size): + _check_params(len(cp), size) + + sample_count = _sample_count(cp, size) + if sample_count == 0: + return 0 + + sum_squares = sum(sample**2 for sample in _get_samples(cp, size)) + return int(math.sqrt(sum_squares / sample_count)) + + +def _sum2(cp1, cp2, length): + size = 2 + total = 0 + for i in range(length): + total += getsample(cp1, size, i) * getsample(cp2, size, i) + return total + + +def findfit(cp1, cp2): + size = 2 + + if len(cp1) % 2 != 0 or len(cp2) % 2 != 0: + raise error("Strings should be even-sized") + + if len(cp1) < len(cp2): + raise error("First sample should be longer") + + len1 = _sample_count(cp1, size) + len2 = _sample_count(cp2, size) + + sum_ri_2 = _sum2(cp2, cp2, len2) + sum_aij_2 = _sum2(cp1, cp1, len2) + sum_aij_ri = _sum2(cp1, cp2, len2) + + result = (sum_ri_2 * sum_aij_2 - sum_aij_ri * sum_aij_ri) / sum_aij_2 + + best_result = result + best_i = 0 + + for i in range(1, len1 - len2 + 1): + aj_m1 = _get_sample(cp1, size, i - 1) + aj_lm1 = _get_sample(cp1, size, i + len2 - 1) + + sum_aij_2 += aj_lm1**2 - aj_m1**2 + sum_aij_ri = _sum2(buffer(cp1)[i*size:], cp2, len2) + + result = (sum_ri_2 * sum_aij_2 - sum_aij_ri * sum_aij_ri) / sum_aij_2 + + if result < best_result: + best_result = result + best_i = i + + factor = _sum2(buffer(cp1)[best_i*size:], cp2, len2) / sum_ri_2 + + return best_i, factor + + +def findfactor(cp1, cp2): + size = 2 + + if len(cp1) % 2 != 0: + raise error("Strings should be even-sized") + + if len(cp1) != len(cp2): + raise error("Samples should be same size") + + sample_count = _sample_count(cp1, size) + + sum_ri_2 = _sum2(cp2, cp2, sample_count) + sum_aij_ri = _sum2(cp1, cp2, sample_count) + + return sum_aij_ri / sum_ri_2 + + +def findmax(cp, len2): + size = 2 + sample_count = _sample_count(cp, size) + + if len(cp) % 2 != 0: + raise error("Strings should be even-sized") + + if len2 < 0 or sample_count < len2: + raise error("Input sample should be longer") + + if sample_count == 0: + return 0 + + result = _sum2(cp, cp, len2) + best_result = result + best_i = 0 + + for i in range(1, sample_count - len2 + 1): + sample_leaving_window = getsample(cp, size, i - 1) + sample_entering_window = getsample(cp, size, i + len2 - 1) + + result -= sample_leaving_window**2 + result += sample_entering_window**2 + + if result > best_result: + best_result = result + best_i = i + + return best_i + + +def avgpp(cp, size): + _check_params(len(cp), size) + sample_count = _sample_count(cp, size) + + prevextremevalid = False + prevextreme = None + avg = 0 + nextreme = 0 + + prevval = getsample(cp, size, 0) + val = getsample(cp, size, 1) + + prevdiff = val - prevval + + for i in range(1, sample_count): + val = getsample(cp, size, i) + diff = val - prevval + + if diff * prevdiff < 0: + if prevextremevalid: + avg += abs(prevval - prevextreme) + nextreme += 1 + + prevextremevalid = True + prevextreme = prevval + + prevval = val + if diff != 0: + prevdiff = diff + + if nextreme == 0: + return 0 + + return avg / nextreme + + +def maxpp(cp, size): + _check_params(len(cp), size) + sample_count = _sample_count(cp, size) + + prevextremevalid = False + prevextreme = None + max = 0 + + prevval = getsample(cp, size, 0) + val = getsample(cp, size, 1) + + prevdiff = val - prevval + + for i in range(1, sample_count): + val = getsample(cp, size, i) + diff = val - prevval + + if diff * prevdiff < 0: + if prevextremevalid: + extremediff = abs(prevval - prevextreme) + if extremediff > max: + max = extremediff + prevextremevalid = True + prevextreme = prevval + + prevval = val + if diff != 0: + prevdiff = diff + + return max + + +def cross(cp, size): + _check_params(len(cp), size) + + crossings = 0 + last_sample = 0 + for sample in _get_samples(cp, size): + if sample <= 0 < last_sample or sample >= 0 > last_sample: + crossings += 1 + last_sample = sample + + return crossings + + +def mul(cp, size, factor): + _check_params(len(cp), size) + clip = _get_clipfn(size) + + result = create_string_buffer(len(cp)) + + for i, sample in enumerate(_get_samples(cp, size)): + sample = clip(int(sample * factor)) + _put_sample(result, size, i, sample) + + return result.raw + + +def tomono(cp, size, fac1, fac2): + _check_params(len(cp), size) + clip = _get_clipfn(size) + + sample_count = _sample_count(cp, size) + + result = create_string_buffer(len(cp) / 2) + + for i in range(0, sample_count, 2): + l_sample = getsample(cp, size, i) + r_sample = getsample(cp, size, i + 1) + + sample = (l_sample * fac1) + (r_sample * fac2) + sample = clip(sample) + + _put_sample(result, size, i / 2, sample) + + return result.raw + + +def tostereo(cp, size, fac1, fac2): + _check_params(len(cp), size) + + sample_count = _sample_count(cp, size) + + result = create_string_buffer(len(cp) * 2) + clip = _get_clipfn(size) + + for i in range(sample_count): + sample = _get_sample(cp, size, i) + + l_sample = clip(sample * fac1) + r_sample = clip(sample * fac2) + + _put_sample(result, size, i * 2, l_sample) + _put_sample(result, size, i * 2 + 1, r_sample) + + return result.raw + + +def add(cp1, cp2, size): + _check_params(len(cp1), size) + + if len(cp1) != len(cp2): + raise error("Lengths should be the same") + + clip = _get_clipfn(size) + sample_count = _sample_count(cp1, size) + result = create_string_buffer(len(cp1)) + + for i in range(sample_count): + sample1 = getsample(cp1, size, i) + sample2 = getsample(cp2, size, i) + + sample = clip(sample1 + sample2) + + _put_sample(result, size, i, sample) + + return result.raw + + +def bias(cp, size, bias): + _check_params(len(cp), size) + + result = create_string_buffer(len(cp)) + + for i, sample in enumerate(_get_samples(cp, size)): + sample = _overflow(sample + bias, size) + _put_sample(result, size, i, sample) + + return result.raw + + +def reverse(cp, size): + _check_params(len(cp), size) + sample_count = _sample_count(cp, size) + + result = create_string_buffer(len(cp)) + for i, sample in enumerate(_get_samples(cp, size)): + _put_sample(result, size, sample_count - i - 1, sample) + + return result.raw + + +def lin2lin(cp, size, size2): + _check_params(len(cp), size) + _check_size(size2) + + if size == size2: + return cp + + new_len = (len(cp) / size) * size2 + + result = create_string_buffer(new_len) + + for i in range(_sample_count(cp, size)): + sample = _get_sample(cp, size, i) + if size < size2: + sample = sample << (4 * size2 / size) + elif size > size2: + sample = sample >> (4 * size / size2) + + sample = _overflow(sample, size2) + + _put_sample(result, size2, i, sample) + + return result.raw + + +def ratecv(cp, size, nchannels, inrate, outrate, state, weightA=1, weightB=0): + _check_params(len(cp), size) + if nchannels < 1: + raise error("# of channels should be >= 1") + + bytes_per_frame = size * nchannels + frame_count = len(cp) / bytes_per_frame + + if bytes_per_frame / nchannels != size: + raise OverflowError("width * nchannels too big for a C int") + + if weightA < 1 or weightB < 0: + raise error("weightA should be >= 1, weightB should be >= 0") + + if len(cp) % bytes_per_frame != 0: + raise error("not a whole number of frames") + + if inrate <= 0 or outrate <= 0: + raise error("sampling rate not > 0") + + d = gcd(inrate, outrate) + inrate /= d + outrate /= d + + prev_i = [0] * nchannels + cur_i = [0] * nchannels + + if state is None: + d = -outrate + else: + d, samps = state + + if len(samps) != nchannels: + raise error("illegal state argument") + + prev_i, cur_i = zip(*samps) + prev_i, cur_i = list(prev_i), list(cur_i) + + q = frame_count / inrate + ceiling = (q + 1) * outrate + nbytes = ceiling * bytes_per_frame + + result = create_string_buffer(nbytes) + + samples = _get_samples(cp, size) + out_i = 0 + while True: + while d < 0: + if frame_count == 0: + samps = zip(prev_i, cur_i) + retval = result.raw + + # slice off extra bytes + trim_index = (out_i * bytes_per_frame) - len(retval) + retval = buffer(retval)[:trim_index] + + return (retval, (d, tuple(samps))) + + for chan in range(nchannels): + prev_i[chan] = cur_i[chan] + cur_i[chan] = samples.next() + + cur_i[chan] = ( + (weightA * cur_i[chan] + weightB * prev_i[chan]) + / (weightA + weightB) + ) + + frame_count -= 1 + d += outrate + + while d >= 0: + for chan in range(nchannels): + cur_o = ( + (prev_i[chan] * d + cur_i[chan] * (outrate - d)) + / outrate + ) + _put_sample(result, size, out_i, _overflow(cur_o, size)) + out_i += 1 + d -= inrate + +def _sign(num): + return -1 if num < 0 else 0 if num == 0 else 1 + +def lin2ulaw(cp, size): + maxval = _get_maxval(size) + result = create_string_buffer(len(cp)) + + for i in range(_sample_count(cp, size)): + sample = _get_sample(cp, size, i) + val = _sign(sample/maxval)*math.log(1+255*math.abs(sample/maxval))/math.log(1+255) + _put_sample(result, size, i, val) + + return result + +def ulaw2lin(cp, size): + maxval = _get_maxval(size) + result = create_string_buffer(len(cp)) + + for i in range(_sample_count(cp, size)): + sample = _get_sample(cp, size, i) + val = (_sign(sample)*((1+255)**math.abs(sample)-1)/255) * maxval + _put_sample(result, size, i, val) + + return result + +def lin2alaw(cp, size): + maxval = _get_maxval(size) + result = create_string_buffer(len(cp)) + + for i in range(_sample_count(cp, size)): + sample = _get_sample(cp, size, i) + val = None + if math.abs(sample/maxval) < 1/87.6: + val = _sign(sample/maxval)*87.6*math.abs(sample/maxval)/(1+math.log(87.6)) + else: + val = _sign(sample/maxval)*(1+math.log(87.6*math.abs(sample/maxval)))/(1+math.log(87.6)) + _put_sample(result, size, i, val) + + return result + +def alaw2lin(cp, size): + maxval = _get_maxval(size) + result = create_string_buffer(len(cp)) + + for i in range(_sample_count(cp, size)): + sample = _get_sample(cp, size, i) + val = None + if math.abs(sample) < 1/(1+math.log(87.6)): + val = _sign(sample) * math.abs(sample)*(1+math.log(87.6))/87.6 * maxval + else: + val = _sign(sample) * (math.e**(-1+math.abs(sample)*(1+math.log(87.6)))) / 87.6 * maxval + _put_sample(result, size, i, val) + + return result + +def lin2adpcm(cp, size, state): + raise NotImplementedError() + + +def adpcm2lin(cp, size, state): + raise NotImplementedError() diff --git a/LXST/Codecs/libs/pydub/scipy_effects.py b/LXST/Codecs/libs/pydub/scipy_effects.py new file mode 100644 index 0000000..abab2b4 --- /dev/null +++ b/LXST/Codecs/libs/pydub/scipy_effects.py @@ -0,0 +1,175 @@ +""" +This module provides scipy versions of high_pass_filter, and low_pass_filter +as well as an additional band_pass_filter. + +Of course, you will need to install scipy for these to work. + +When this module is imported the high and low pass filters from this module +will be used when calling audio_segment.high_pass_filter() and +audio_segment.high_pass_filter() instead of the slower, less powerful versions +provided by pydub.effects. +""" +from scipy.signal import butter, sosfilt +from .utils import (register_pydub_effect,stereo_to_ms,ms_to_stereo) + + +def _mk_butter_filter(freq, type, order): + """ + Args: + freq: The cutoff frequency for highpass and lowpass filters. For + band filters, a list of [low_cutoff, high_cutoff] + type: "lowpass", "highpass", or "band" + order: nth order butterworth filter (default: 5th order). The + attenuation is -6dB/octave beyond the cutoff frequency (for 1st + order). A Higher order filter will have more attenuation, each level + adding an additional -6dB (so a 3rd order butterworth filter would + be -18dB/octave). + + Returns: + function which can filter a mono audio segment + + """ + def filter_fn(seg): + assert seg.channels == 1 + + nyq = 0.5 * seg.frame_rate + try: + freqs = [f / nyq for f in freq] + except TypeError: + freqs = freq / nyq + + sos = butter(order, freqs, btype=type, output='sos') + y = sosfilt(sos, seg.get_array_of_samples()) + + return seg._spawn(y.astype(seg.array_type)) + + return filter_fn + + +@register_pydub_effect +def band_pass_filter(seg, low_cutoff_freq, high_cutoff_freq, order=5): + filter_fn = _mk_butter_filter([low_cutoff_freq, high_cutoff_freq], 'band', order=order) + return seg.apply_mono_filter_to_each_channel(filter_fn) + + +@register_pydub_effect +def high_pass_filter(seg, cutoff_freq, order=5): + filter_fn = _mk_butter_filter(cutoff_freq, 'highpass', order=order) + return seg.apply_mono_filter_to_each_channel(filter_fn) + + +@register_pydub_effect +def low_pass_filter(seg, cutoff_freq, order=5): + filter_fn = _mk_butter_filter(cutoff_freq, 'lowpass', order=order) + return seg.apply_mono_filter_to_each_channel(filter_fn) + + +@register_pydub_effect +def _eq(seg, focus_freq, bandwidth=100, mode="peak", gain_dB=0, order=2): + """ + Args: + focus_freq - middle frequency or known frequency of band (in Hz) + bandwidth - range of the equalizer band + mode - Mode of Equalization(Peak/Notch(Bell Curve),High Shelf, Low Shelf) + order - Rolloff factor(1 - 6dB/Octave 2 - 12dB/Octave) + + Returns: + Equalized/Filtered AudioSegment + """ + filt_mode = ["peak", "low_shelf", "high_shelf"] + if mode not in filt_mode: + raise ValueError("Incorrect Mode Selection") + + if gain_dB >= 0: + if mode == "peak": + sec = band_pass_filter(seg, focus_freq - bandwidth/2, focus_freq + bandwidth/2, order = order) + seg = seg.overlay(sec - (3 - gain_dB)) + return seg + + if mode == "low_shelf": + sec = low_pass_filter(seg, focus_freq, order=order) + seg = seg.overlay(sec - (3 - gain_dB)) + return seg + + if mode == "high_shelf": + sec = high_pass_filter(seg, focus_freq, order=order) + seg = seg.overlay(sec - (3 - gain_dB)) + return seg + + if gain_dB < 0: + if mode == "peak": + sec = high_pass_filter(seg, focus_freq - bandwidth/2, order=order) + seg = seg.overlay(sec - (3 + gain_dB)) + gain_dB + sec = low_pass_filter(seg, focus_freq + bandwidth/2, order=order) + seg = seg.overlay(sec - (3 + gain_dB)) + gain_dB + return seg + + if mode == "low_shelf": + sec = high_pass_filter(seg, focus_freq, order=order) + seg = seg.overlay(sec - (3 + gain_dB)) + gain_dB + return seg + + if mode=="high_shelf": + sec=low_pass_filter(seg, focus_freq, order=order) + seg=seg.overlay(sec - (3 + gain_dB)) +gain_dB + return seg + + +@register_pydub_effect +def eq(seg, focus_freq, bandwidth=100, channel_mode="L+R", filter_mode="peak", gain_dB=0, order=2): + """ + Args: + focus_freq - middle frequency or known frequency of band (in Hz) + bandwidth - range of the equalizer band + channel_mode - Select Channels to be affected by the filter. + L+R - Standard Stereo Filter + L - Only Left Channel is Filtered + R - Only Right Channel is Filtered + M+S - Blumlien Stereo Filter(Mid-Side) + M - Only Mid Channel is Filtered + S - Only Side Channel is Filtered + Mono Audio Segments are completely filtered. + filter_mode - Mode of Equalization(Peak/Notch(Bell Curve),High Shelf, Low Shelf) + order - Rolloff factor(1 - 6dB/Octave 2 - 12dB/Octave) + + Returns: + Equalized/Filtered AudioSegment + """ + channel_modes = ["L+R", "M+S", "L", "R", "M", "S"] + if channel_mode not in channel_modes: + raise ValueError("Incorrect Channel Mode Selection") + + if seg.channels == 1: + return _eq(seg, focus_freq, bandwidth, filter_mode, gain_dB, order) + + if channel_mode == "L+R": + return _eq(seg, focus_freq, bandwidth, filter_mode, gain_dB, order) + + if channel_mode == "L": + seg = seg.split_to_mono() + seg = [_eq(seg[0], focus_freq, bandwidth, filter_mode, gain_dB, order), seg[1]] + return AudioSegment.from_mono_audio_segements(seg[0], seg[1]) + + if channel_mode == "R": + seg = seg.split_to_mono() + seg = [seg[0], _eq(seg[1], focus_freq, bandwidth, filter_mode, gain_dB, order)] + return AudioSegment.from_mono_audio_segements(seg[0], seg[1]) + + if channel_mode == "M+S": + seg = stereo_to_ms(seg) + seg = _eq(seg, focus_freq, bandwidth, filter_mode, gain_dB, order) + return ms_to_stereo(seg) + + if channel_mode == "M": + seg = stereo_to_ms(seg).split_to_mono() + seg = [_eq(seg[0], focus_freq, bandwidth, filter_mode, gain_dB, order), seg[1]] + seg = AudioSegment.from_mono_audio_segements(seg[0], seg[1]) + return ms_to_stereo(seg) + + if channel_mode == "S": + seg = stereo_to_ms(seg).split_to_mono() + seg = [seg[0], _eq(seg[1], focus_freq, bandwidth, filter_mode, gain_dB, order)] + seg = AudioSegment.from_mono_audio_segements(seg[0], seg[1]) + return ms_to_stereo(seg) + + diff --git a/LXST/Codecs/libs/pydub/silence.py b/LXST/Codecs/libs/pydub/silence.py new file mode 100644 index 0000000..0ad1499 --- /dev/null +++ b/LXST/Codecs/libs/pydub/silence.py @@ -0,0 +1,182 @@ +""" +Various functions for finding/manipulating silence in AudioSegments +""" +import itertools + +from .utils import db_to_float + + +def detect_silence(audio_segment, min_silence_len=1000, silence_thresh=-16, seek_step=1): + """ + Returns a list of all silent sections [start, end] in milliseconds of audio_segment. + Inverse of detect_nonsilent() + + audio_segment - the segment to find silence in + min_silence_len - the minimum length for any silent section + silence_thresh - the upper bound for how quiet is silent in dFBS + seek_step - step size for interating over the segment in ms + """ + seg_len = len(audio_segment) + + # you can't have a silent portion of a sound that is longer than the sound + if seg_len < min_silence_len: + return [] + + # convert silence threshold to a float value (so we can compare it to rms) + silence_thresh = db_to_float(silence_thresh) * audio_segment.max_possible_amplitude + + # find silence and add start and end indicies to the to_cut list + silence_starts = [] + + # check successive (1 sec by default) chunk of sound for silence + # try a chunk at every "seek step" (or every chunk for a seek step == 1) + last_slice_start = seg_len - min_silence_len + slice_starts = range(0, last_slice_start + 1, seek_step) + + # guarantee last_slice_start is included in the range + # to make sure the last portion of the audio is searched + if last_slice_start % seek_step: + slice_starts = itertools.chain(slice_starts, [last_slice_start]) + + for i in slice_starts: + audio_slice = audio_segment[i:i + min_silence_len] + if audio_slice.rms <= silence_thresh: + silence_starts.append(i) + + # short circuit when there is no silence + if not silence_starts: + return [] + + # combine the silence we detected into ranges (start ms - end ms) + silent_ranges = [] + + prev_i = silence_starts.pop(0) + current_range_start = prev_i + + for silence_start_i in silence_starts: + continuous = (silence_start_i == prev_i + seek_step) + + # sometimes two small blips are enough for one particular slice to be + # non-silent, despite the silence all running together. Just combine + # the two overlapping silent ranges. + silence_has_gap = silence_start_i > (prev_i + min_silence_len) + + if not continuous and silence_has_gap: + silent_ranges.append([current_range_start, + prev_i + min_silence_len]) + current_range_start = silence_start_i + prev_i = silence_start_i + + silent_ranges.append([current_range_start, + prev_i + min_silence_len]) + + return silent_ranges + + +def detect_nonsilent(audio_segment, min_silence_len=1000, silence_thresh=-16, seek_step=1): + """ + Returns a list of all nonsilent sections [start, end] in milliseconds of audio_segment. + Inverse of detect_silent() + + audio_segment - the segment to find silence in + min_silence_len - the minimum length for any silent section + silence_thresh - the upper bound for how quiet is silent in dFBS + seek_step - step size for interating over the segment in ms + """ + silent_ranges = detect_silence(audio_segment, min_silence_len, silence_thresh, seek_step) + len_seg = len(audio_segment) + + # if there is no silence, the whole thing is nonsilent + if not silent_ranges: + return [[0, len_seg]] + + # short circuit when the whole audio segment is silent + if silent_ranges[0][0] == 0 and silent_ranges[0][1] == len_seg: + return [] + + prev_end_i = 0 + nonsilent_ranges = [] + for start_i, end_i in silent_ranges: + nonsilent_ranges.append([prev_end_i, start_i]) + prev_end_i = end_i + + if end_i != len_seg: + nonsilent_ranges.append([prev_end_i, len_seg]) + + if nonsilent_ranges[0] == [0, 0]: + nonsilent_ranges.pop(0) + + return nonsilent_ranges + + +def split_on_silence(audio_segment, min_silence_len=1000, silence_thresh=-16, keep_silence=100, + seek_step=1): + """ + Returns list of audio segments from splitting audio_segment on silent sections + + audio_segment - original pydub.AudioSegment() object + + min_silence_len - (in ms) minimum length of a silence to be used for + a split. default: 1000ms + + silence_thresh - (in dBFS) anything quieter than this will be + considered silence. default: -16dBFS + + keep_silence - (in ms or True/False) leave some silence at the beginning + and end of the chunks. Keeps the sound from sounding like it + is abruptly cut off. + When the length of the silence is less than the keep_silence duration + it is split evenly between the preceding and following non-silent + segments. + If True is specified, all the silence is kept, if False none is kept. + default: 100ms + + seek_step - step size for interating over the segment in ms + """ + + # from the itertools documentation + def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + + if isinstance(keep_silence, bool): + keep_silence = len(audio_segment) if keep_silence else 0 + + output_ranges = [ + [ start - keep_silence, end + keep_silence ] + for (start,end) + in detect_nonsilent(audio_segment, min_silence_len, silence_thresh, seek_step) + ] + + for range_i, range_ii in pairwise(output_ranges): + last_end = range_i[1] + next_start = range_ii[0] + if next_start < last_end: + range_i[1] = (last_end+next_start)//2 + range_ii[0] = range_i[1] + + return [ + audio_segment[ max(start,0) : min(end,len(audio_segment)) ] + for start,end in output_ranges + ] + + +def detect_leading_silence(sound, silence_threshold=-50.0, chunk_size=10): + """ + Returns the millisecond/index that the leading silence ends. + + audio_segment - the segment to find silence in + silence_threshold - the upper bound for how quiet is silent in dFBS + chunk_size - chunk size for interating over the segment in ms + """ + trim_ms = 0 # ms + assert chunk_size > 0 # to avoid infinite loop + while sound[trim_ms:trim_ms+chunk_size].dBFS < silence_threshold and trim_ms < len(sound): + trim_ms += chunk_size + + # if there is no end it should return the length of the segment + return min(trim_ms, len(sound)) + + diff --git a/LXST/Codecs/libs/pydub/utils.py b/LXST/Codecs/libs/pydub/utils.py new file mode 100644 index 0000000..cb60ec9 --- /dev/null +++ b/LXST/Codecs/libs/pydub/utils.py @@ -0,0 +1,434 @@ +from __future__ import division + +import json +import os +import re +import sys +from subprocess import Popen, PIPE +from math import log, ceil +from tempfile import TemporaryFile +from warnings import warn +from functools import wraps + +try: + import audioop +except ImportError: + import pyaudioop as audioop + +if sys.version_info >= (3, 0): + basestring = str + +FRAME_WIDTHS = { + 8: 1, + 16: 2, + 32: 4, +} +ARRAY_TYPES = { + 8: "b", + 16: "h", + 32: "i", +} +ARRAY_RANGES = { + 8: (-0x80, 0x7f), + 16: (-0x8000, 0x7fff), + 32: (-0x80000000, 0x7fffffff), +} + + +def get_frame_width(bit_depth): + return FRAME_WIDTHS[bit_depth] + + +def get_array_type(bit_depth, signed=True): + t = ARRAY_TYPES[bit_depth] + if not signed: + t = t.upper() + return t + + +def get_min_max_value(bit_depth): + return ARRAY_RANGES[bit_depth] + + +def _fd_or_path_or_tempfile(fd, mode='w+b', tempfile=True): + close_fd = False + if fd is None and tempfile: + fd = TemporaryFile(mode=mode) + close_fd = True + + if isinstance(fd, basestring): + fd = open(fd, mode=mode) + close_fd = True + + try: + if isinstance(fd, os.PathLike): + fd = open(fd, mode=mode) + close_fd = True + except AttributeError: + # module os has no attribute PathLike, so we're on python < 3.6. + # The protocol we're trying to support doesn't exist, so just pass. + pass + + return fd, close_fd + + +def db_to_float(db, using_amplitude=True): + """ + Converts the input db to a float, which represents the equivalent + ratio in power. + """ + db = float(db) + if using_amplitude: + return 10 ** (db / 20) + else: # using power + return 10 ** (db / 10) + + +def ratio_to_db(ratio, val2=None, using_amplitude=True): + """ + Converts the input float to db, which represents the equivalent + to the ratio in power represented by the multiplier passed in. + """ + ratio = float(ratio) + + # accept 2 values and use the ratio of val1 to val2 + if val2 is not None: + ratio = ratio / val2 + + # special case for multiply-by-zero (convert to silence) + if ratio == 0: + return -float('inf') + + if using_amplitude: + return 20 * log(ratio, 10) + else: # using power + return 10 * log(ratio, 10) + + +def register_pydub_effect(fn, name=None): + """ + decorator for adding pydub effects to the AudioSegment objects. + example use: + @register_pydub_effect + def normalize(audio_segment): + ... + or you can specify a name: + @register_pydub_effect("normalize") + def normalize_audio_segment(audio_segment): + ... + """ + if isinstance(fn, basestring): + name = fn + return lambda fn: register_pydub_effect(fn, name) + + if name is None: + name = fn.__name__ + + from .audio_segment import AudioSegment + setattr(AudioSegment, name, fn) + return fn + + +def make_chunks(audio_segment, chunk_length): + """ + Breaks an AudioSegment into chunks that are milliseconds + long. + if chunk_length is 50 then you'll get a list of 50 millisecond long audio + segments back (except the last one, which can be shorter) + """ + number_of_chunks = ceil(len(audio_segment) / float(chunk_length)) + return [audio_segment[i * chunk_length:(i + 1) * chunk_length] + for i in range(int(number_of_chunks))] + + +def which(program): + """ + Mimics behavior of UNIX which command. + """ + # Add .exe program extension for windows support + if os.name == "nt" and not program.endswith(".exe"): + program += ".exe" + + envdir_list = [os.curdir] + os.environ["PATH"].split(os.pathsep) + + for envdir in envdir_list: + program_path = os.path.join(envdir, program) + if os.path.isfile(program_path) and os.access(program_path, os.X_OK): + return program_path + + +def get_encoder_name(): + """ + Return enconder default application for system, either avconv or ffmpeg + """ + if which("avconv"): + return "avconv" + elif which("ffmpeg"): + return "ffmpeg" + else: + # should raise exception + warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning) + return "ffmpeg" + + +def get_player_name(): + """ + Return enconder default application for system, either avconv or ffmpeg + """ + if which("avplay"): + return "avplay" + elif which("ffplay"): + return "ffplay" + else: + # should raise exception + warn("Couldn't find ffplay or avplay - defaulting to ffplay, but may not work", RuntimeWarning) + return "ffplay" + + +def get_prober_name(): + """ + Return probe application, either avconv or ffmpeg + """ + if which("avprobe"): + return "avprobe" + elif which("ffprobe"): + return "ffprobe" + else: + # should raise exception + warn("Couldn't find ffprobe or avprobe - defaulting to ffprobe, but may not work", RuntimeWarning) + return "ffprobe" + + +def fsdecode(filename): + """Wrapper for os.fsdecode which was introduced in python 3.2 .""" + + if sys.version_info >= (3, 2): + PathLikeTypes = (basestring, bytes) + if sys.version_info >= (3, 6): + PathLikeTypes += (os.PathLike,) + if isinstance(filename, PathLikeTypes): + return os.fsdecode(filename) + else: + if isinstance(filename, bytes): + return filename.decode(sys.getfilesystemencoding()) + if isinstance(filename, basestring): + return filename + + raise TypeError("type {0} not accepted by fsdecode".format(type(filename))) + + +def get_extra_info(stderr): + """ + avprobe sometimes gives more information on stderr than + on the json output. The information has to be extracted + from stderr of the format of: + ' Stream #0:0: Audio: flac, 88200 Hz, stereo, s32 (24 bit)' + or (macOS version): + ' Stream #0:0: Audio: vorbis' + ' 44100 Hz, stereo, fltp, 320 kb/s' + + :type stderr: str + :rtype: list of dict + """ + extra_info = {} + + re_stream = r'(?P +)Stream #0[:\.](?P([0-9]+))(?P.+)\n?(?! *Stream)((?P +)(?P.+))?' + for i in re.finditer(re_stream, stderr): + if i.group('space_end') is not None and len(i.group('space_start')) <= len( + i.group('space_end')): + content_line = ','.join([i.group('content_0'), i.group('content_1')]) + else: + content_line = i.group('content_0') + tokens = [x.strip() for x in re.split('[:,]', content_line) if x] + extra_info[int(i.group('stream_id'))] = tokens + return extra_info + + +def mediainfo_json(filepath, read_ahead_limit=-1): + """Return json dictionary with media info(codec, duration, size, bitrate...) from filepath + """ + prober = get_prober_name() + command_args = [ + "-v", "info", + "-show_format", + "-show_streams", + ] + try: + command_args += [fsdecode(filepath)] + stdin_parameter = None + stdin_data = None + except TypeError: + if prober == 'ffprobe': + command_args += ["-read_ahead_limit", str(read_ahead_limit), + "cache:pipe:0"] + else: + command_args += ["-"] + stdin_parameter = PIPE + file, close_file = _fd_or_path_or_tempfile(filepath, 'rb', tempfile=False) + file.seek(0) + stdin_data = file.read() + if close_file: + file.close() + + command = [prober, '-of', 'json'] + command_args + res = Popen(command, stdin=stdin_parameter, stdout=PIPE, stderr=PIPE) + output, stderr = res.communicate(input=stdin_data) + output = output.decode("utf-8", 'ignore') + stderr = stderr.decode("utf-8", 'ignore') + + info = json.loads(output) + + if not info: + # If ffprobe didn't give any information, just return it + # (for example, because the file doesn't exist) + return info + + extra_info = get_extra_info(stderr) + + audio_streams = [x for x in info['streams'] if x['codec_type'] == 'audio'] + if len(audio_streams) == 0: + return info + + # We just operate on the first audio stream in case there are more + stream = audio_streams[0] + + def set_property(stream, prop, value): + if prop not in stream or stream[prop] == 0: + stream[prop] = value + + for token in extra_info[stream['index']]: + m = re.match(r'([su]([0-9]{1,2})p?) \(([0-9]{1,2}) bit\)$', token) + m2 = re.match(r'([su]([0-9]{1,2})p?)( \(default\))?$', token) + if m: + set_property(stream, 'sample_fmt', m.group(1)) + set_property(stream, 'bits_per_sample', int(m.group(2))) + set_property(stream, 'bits_per_raw_sample', int(m.group(3))) + elif m2: + set_property(stream, 'sample_fmt', m2.group(1)) + set_property(stream, 'bits_per_sample', int(m2.group(2))) + set_property(stream, 'bits_per_raw_sample', int(m2.group(2))) + elif re.match(r'(flt)p?( \(default\))?$', token): + set_property(stream, 'sample_fmt', token) + set_property(stream, 'bits_per_sample', 32) + set_property(stream, 'bits_per_raw_sample', 32) + elif re.match(r'(dbl)p?( \(default\))?$', token): + set_property(stream, 'sample_fmt', token) + set_property(stream, 'bits_per_sample', 64) + set_property(stream, 'bits_per_raw_sample', 64) + return info + + +def mediainfo(filepath): + """Return dictionary with media info(codec, duration, size, bitrate...) from filepath + """ + + prober = get_prober_name() + command_args = [ + "-v", "quiet", + "-show_format", + "-show_streams", + filepath + ] + + command = [prober, '-of', 'old'] + command_args + res = Popen(command, stdout=PIPE) + output = res.communicate()[0].decode("utf-8") + + if res.returncode != 0: + command = [prober] + command_args + output = Popen(command, stdout=PIPE).communicate()[0].decode("utf-8") + + rgx = re.compile(r"(?:(?P.*?):)?(?P.*?)\=(?P.*?)$") + info = {} + + if sys.platform == 'win32': + output = output.replace("\r", "") + + for line in output.split("\n"): + # print(line) + mobj = rgx.match(line) + + if mobj: + # print(mobj.groups()) + inner_dict, key, value = mobj.groups() + + if inner_dict: + try: + info[inner_dict] + except KeyError: + info[inner_dict] = {} + info[inner_dict][key] = value + else: + info[key] = value + + return info + + +def cache_codecs(function): + cache = {} + + @wraps(function) + def wrapper(): + try: + return cache[0] + except: + cache[0] = function() + return cache[0] + + return wrapper + + +@cache_codecs +def get_supported_codecs(): + encoder = get_encoder_name() + command = [encoder, "-codecs"] + res = Popen(command, stdout=PIPE, stderr=PIPE) + output = res.communicate()[0].decode("utf-8") + if res.returncode != 0: + return [] + + if sys.platform == 'win32': + output = output.replace("\r", "") + + + rgx = re.compile(r"^([D.][E.][AVS.][I.][L.][S.]) (\w*) +(.*)") + decoders = set() + encoders = set() + for line in output.split('\n'): + match = rgx.match(line.strip()) + if not match: + continue + flags, codec, name = match.groups() + + if flags[0] == 'D': + decoders.add(codec) + + if flags[1] == 'E': + encoders.add(codec) + + return (decoders, encoders) + + +def get_supported_decoders(): + return get_supported_codecs()[0] + + +def get_supported_encoders(): + return get_supported_codecs()[1] + +def stereo_to_ms(audio_segment): + ''' + Left-Right -> Mid-Side + ''' + channel = audio_segment.split_to_mono() + channel = [channel[0].overlay(channel[1]), channel[0].overlay(channel[1].invert_phase())] + return AudioSegment.from_mono_audiosegments(channel[0], channel[1]) + +def ms_to_stereo(audio_segment): + ''' + Mid-Side -> Left-Right + ''' + channel = audio_segment.split_to_mono() + channel = [channel[0].overlay(channel[1]) - 3, channel[0].overlay(channel[1].invert_phase()) - 3] + return AudioSegment.from_mono_audiosegments(channel[0], channel[1]) + diff --git a/LXST/Codecs/libs/pyogg/__init__.py b/LXST/Codecs/libs/pyogg/__init__.py new file mode 100644 index 0000000..a97b0d2 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/__init__.py @@ -0,0 +1,108 @@ +import ctypes + +from .pyogg_error import PyOggError +from .ogg import PYOGG_OGG_AVAIL +from .vorbis import PYOGG_VORBIS_AVAIL, PYOGG_VORBIS_FILE_AVAIL, PYOGG_VORBIS_ENC_AVAIL +from .opus import PYOGG_OPUS_AVAIL, PYOGG_OPUS_FILE_AVAIL, PYOGG_OPUS_ENC_AVAIL +from .flac import PYOGG_FLAC_AVAIL + + +#: PyOgg version number. Versions should comply with PEP440. +__version__ = '0.7' + + +if (PYOGG_OGG_AVAIL and PYOGG_VORBIS_AVAIL and PYOGG_VORBIS_FILE_AVAIL): + # VorbisFile + from .vorbis_file import VorbisFile + # VorbisFileStream + from .vorbis_file_stream import VorbisFileStream + +else: + class VorbisFile: # type: ignore + def __init__(*args, **kw): + if not PYOGG_OGG_AVAIL: + raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + raise PyOggError("The Vorbis libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + + class VorbisFileStream: # type: ignore + def __init__(*args, **kw): + if not PYOGG_OGG_AVAIL: + raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + raise PyOggError("The Vorbis libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + + + +if (PYOGG_OGG_AVAIL and PYOGG_OPUS_AVAIL and PYOGG_OPUS_FILE_AVAIL): + # OpusFile + from .opus_file import OpusFile + # OpusFileStream + from .opus_file_stream import OpusFileStream + +else: + class OpusFile: # type: ignore + def __init__(*args, **kw): + if not PYOGG_OGG_AVAIL: + raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + if not PYOGG_OPUS_AVAIL: + raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + if not PYOGG_OPUS_FILE_AVAIL: + raise PyOggError("The OpusFile library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + raise PyOggError("Unknown initialisation error") + + class OpusFileStream: # type: ignore + def __init__(*args, **kw): + if not PYOGG_OGG_AVAIL: + raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + if not PYOGG_OPUS_AVAIL: + raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + if not PYOGG_OPUS_FILE_AVAIL: + raise PyOggError("The OpusFile library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + raise PyOggError("Unknown initialisation error") + + +if PYOGG_OPUS_AVAIL: + # OpusEncoder + from .opus_encoder import OpusEncoder + # OpusBufferedEncoder + from .opus_buffered_encoder import OpusBufferedEncoder + # OpusDecoder + from .opus_decoder import OpusDecoder + +else: + class OpusEncoder: # type: ignore + def __init__(*args, **kw): + raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + + class OpusBufferedEncoder: # type: ignore + def __init__(*args, **kw): + raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + + class OpusDecoder: # type: ignore + def __init__(*args, **kw): + raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + +if (PYOGG_OGG_AVAIL and PYOGG_OPUS_AVAIL): + # OggOpusWriter + from .ogg_opus_writer import OggOpusWriter + +else: + class OggOpusWriter: # type: ignore + def __init__(*args, **kw): + if not PYOGG_OGG_AVAIL: + raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + raise PyOggError("The Opus library was't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + + +if PYOGG_FLAC_AVAIL: + # FlacFile + from .flac_file import FlacFile + # FlacFileStream + from .flac_file_stream import FlacFileStream +else: + class FlacFile: # type: ignore + def __init__(*args, **kw): + raise PyOggError("The FLAC libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") + + class FlacFileStream: # type: ignore + def __init__(*args, **kw): + raise PyOggError("The FLAC libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)") diff --git a/LXST/Codecs/libs/pyogg/audio_file.py b/LXST/Codecs/libs/pyogg/audio_file.py new file mode 100644 index 0000000..4fb77a2 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/audio_file.py @@ -0,0 +1,59 @@ +from .pyogg_error import PyOggError + +class AudioFile: + """Abstract base class for audio files. + + This class is a base class for audio files (such as Vorbis, Opus, + and FLAC). It should not be instatiated directly. + """ + + def __init__(self): + raise PyOggError("AudioFile is an Abstract Base Class "+ + "and should not be instantiated") + + def as_array(self): + """Returns the buffer as a NumPy array. + + The shape of the returned array is in units of (number of + samples per channel, number of channels). + + The data type is either 8-bit or 16-bit signed integers, + depending on bytes_per_sample. + + The buffer is not copied, but rather the NumPy array + shares the memory with the buffer. + + """ + # Assumes that self.buffer is a one-dimensional array of + # bytes and that channels are interleaved. + + import numpy # type: ignore + + assert self.buffer is not None + assert self.channels is not None + + # The following code assumes that the bytes in the buffer + # represent 8-bit or 16-bit signed ints. Ensure the number of + # bytes per sample matches that assumption. + assert self.bytes_per_sample == 1 or self.bytes_per_sample == 2 + + # Create a dictionary mapping bytes per sample to numpy data + # types + dtype = { + 1: numpy.int8, + 2: numpy.int16 + } + + # Convert the ctypes buffer to a NumPy array + array = numpy.frombuffer( + self.buffer, + dtype=dtype[self.bytes_per_sample] + ) + + # Reshape the array + return array.reshape( + (len(self.buffer) + // self.bytes_per_sample + // self.channels, + self.channels) + ) diff --git a/LXST/Codecs/libs/pyogg/flac.py b/LXST/Codecs/libs/pyogg/flac.py new file mode 100644 index 0000000..d44509e --- /dev/null +++ b/LXST/Codecs/libs/pyogg/flac.py @@ -0,0 +1,2061 @@ +############################################################ +# Flac license: # +############################################################ +""" +Copyright (C) 2000-2009 Josh Coalson +Copyright (C) 2011-2016 Xiph.Org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import ctypes +from ctypes import c_int, c_int8, c_int16, c_int32, c_int64, c_uint, c_uint8, c_uint16, c_uint32, c_uint64, c_float, c_long, c_ulong, c_char, c_bool, c_char_p, c_ubyte, c_longlong, c_ulonglong, c_size_t, c_void_p, c_double, POINTER, pointer, cast, CFUNCTYPE, Structure, Union +import ctypes.util +import sys +from traceback import print_exc as _print_exc +import os + +from .ogg import * + +from .library_loader import ExternalLibrary, ExternalLibraryError + +__here = os.getcwd() + +libflac = None + +try: + names = { + "Windows": "libFLAC.dll", + "Darwin": "libFLAC.8.dylib", + "external": "FLAC" + } + libflac = Library.load(names, tests = [lambda lib: hasattr(lib, "FLAC__EntropyCodingMethodTypeString")]) +except ExternalLibraryError: + pass +except: + _print_exc() + +if libflac: + PYOGG_FLAC_AVAIL = True +else: + PYOGG_FLAC_AVAIL = False + +# ctypes +c_ubyte_p = POINTER(c_ubyte) +c_uchar_p = c_ubyte_p +c_uint_p = POINTER(c_uint) +c_size_t_p = POINTER(c_size_t) +c_off_t = c_int32 +# /ctypes + +if PYOGG_FLAC_AVAIL: + # Sanity check also satisfies mypy type checking + assert libflac is not None + + # ordinals + + FLAC__int8 = c_int8 + FLAC__uint8 = c_uint8 + + FLAC__int16 = c_int16 + + FLAC__int32 = c_int32 + FLAC__int32_p = POINTER(FLAC__int32) + + FLAC__int64 = c_int64 + FLAC__uint16 = c_uint16 + FLAC__uint32 = c_uint32 + FLAC__uint64 = c_uint64 + + FLAC__uint64_p = POINTER(FLAC__uint64) + + FLAC__bool = c_bool + + FLAC__byte = c_uint8 + + FLAC__byte_p = POINTER(FLAC__byte) + + c_char_p_p = POINTER(c_char_p) + + # /ordinals + + # callback + + FLAC__IOHandle = CFUNCTYPE(c_void_p) + + FLAC__IOCallback_Read = CFUNCTYPE(c_size_t, + c_void_p, + c_size_t, + c_size_t, + FLAC__IOHandle) + + FLAC__IOCallback_Write = CFUNCTYPE(c_size_t, c_void_p, c_size_t, c_size_t, FLAC__IOHandle) + + FLAC__IOCallback_Seek = CFUNCTYPE(c_int, FLAC__IOHandle, FLAC__int64, c_int) + + FLAC__IOCallback_Tell = CFUNCTYPE(FLAC__int64, FLAC__IOHandle) + + FLAC__IOCallback_Eof = CFUNCTYPE(c_int, FLAC__IOHandle) + + FLAC__IOCallback_Close = CFUNCTYPE(c_int, FLAC__IOHandle) + + class FLAC__IOCallbacks(Structure): + _fields_ = [("read", FLAC__IOCallback_Read), + ("write", FLAC__IOCallback_Write), + ("seek", FLAC__IOCallback_Seek), + ("tell", FLAC__IOCallback_Tell), + ("eof", FLAC__IOCallback_Eof), + ("close", FLAC__IOCallback_Close)] + + # /callback + + # format + + FLAC__MAX_METADATA_TYPE_CODE =(126) + FLAC__MIN_BLOCK_SIZE =(16) + FLAC__MAX_BLOCK_SIZE =(65535) + FLAC__SUBSET_MAX_BLOCK_SIZE_48000HZ =(4608) + FLAC__MAX_CHANNELS =(8) + FLAC__MIN_BITS_PER_SAMPLE =(4) + FLAC__MAX_BITS_PER_SAMPLE =(32) + FLAC__REFERENCE_CODEC_MAX_BITS_PER_SAMPLE =(24) + FLAC__MAX_SAMPLE_RATE =(655350) + FLAC__MAX_LPC_ORDER =(32) + FLAC__SUBSET_MAX_LPC_ORDER_48000HZ =(12) + FLAC__MIN_QLP_COEFF_PRECISION =(5) + FLAC__MAX_QLP_COEFF_PRECISION =(15) + FLAC__MAX_FIXED_ORDER =(4) + FLAC__MAX_RICE_PARTITION_ORDER =(15) + FLAC__SUBSET_MAX_RICE_PARTITION_ORDER =(8) + + FLAC__VERSION_STRING = c_char_p.in_dll(libflac, "FLAC__VERSION_STRING") + + FLAC__VENDOR_STRING = c_char_p.in_dll(libflac, "FLAC__VENDOR_STRING") + + FLAC__STREAM_SYNC_STRING = (FLAC__byte * 4).in_dll(libflac, "FLAC__STREAM_SYNC_STRING") + + FLAC__STREAM_SYNC = c_uint.in_dll(libflac, "FLAC__STREAM_SYNC") + + FLAC__STREAM_SYNC_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_SYNC_LEN") + + FLAC__STREAM_SYNC_LENGTH =(4) + + + + FLAC__EntropyCodingMethodType = c_int + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE = 0 + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2 = 1 + + + + libflac.FLAC__EntropyCodingMethodTypeString.restype = c_char_p + libflac.FLAC__EntropyCodingMethodTypeString.argtypes = [] + + def FLAC__EntropyCodingMethodTypeString(): + return libflac.FLAC__EntropyCodingMethodTypeString() + + + + class FLAC__EntropyCodingMethod_PartitionedRiceContents(Structure): + _fields_ = [("parameters", c_uint_p), + ("raw_bits", c_uint_p), + ("capacity_by_order", c_uint)] + + class FLAC__EntropyCodingMethod_PartitionedRice(Structure): + _fields_ = [("order", c_uint), + ("contents", POINTER(FLAC__EntropyCodingMethod_PartitionedRiceContents))] + + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_ORDER_LEN = c_uint.in_dll(libflac, "FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_ORDER_LEN") + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_PARAMETER_LEN = c_uint.in_dll(libflac, "FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_PARAMETER_LEN") + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2_PARAMETER_LEN = c_uint.in_dll(libflac, "FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE2_PARAMETER_LEN") + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_RAW_LEN = c_uint.in_dll(libflac, "FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_RAW_LEN") + + FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_ESCAPE_PARAMETER = c_uint.in_dll(libflac, "FLAC__ENTROPY_CODING_METHOD_PARTITIONED_RICE_ESCAPE_PARAMETER") + + + class FLAC__EntropyCodingMethod_data(Union): + _fields_ = [("partitioned_rice", FLAC__EntropyCodingMethod_PartitionedRice)] + + class FLAC__EntropyCodingMethod(Structure): + _fields_ = [("type", POINTER(FLAC__EntropyCodingMethodType)), + ("data", FLAC__EntropyCodingMethod_data)] + + FLAC__ENTROPY_CODING_METHOD_TYPE_LEN = c_uint.in_dll(libflac, "FLAC__ENTROPY_CODING_METHOD_TYPE_LEN") + + + + FLAC__SubframeType = c_int + FLAC__SUBFRAME_TYPE_CONSTANT = 0 + FLAC__SUBFRAME_TYPE_VERBATIM = 1 + FLAC__SUBFRAME_TYPE_FIXED = 2 + FLAC__SUBFRAME_TYPE_LPC = 3 + + + + libflac.FLAC__SubframeTypeString.restype = c_char_p + libflac.FLAC__SubframeTypeString.argtypes = [] + + def FLAC__SubframeTypeString(): + return libflac.FLAC__SubframeTypeString() + + + + class FLAC__Subframe_Constant(Structure): + _fields_ = [("value", FLAC__int32)] + + + class FLAC__Subframe_Verbatim(Structure): + _fields_ = [("data", FLAC__int32_p)] + + + class FLAC__Subframe_Fixed(Structure): + _fields_ = [("entropy_coding_method", FLAC__EntropyCodingMethod), + ("order", c_uint), + ("warmup", FLAC__int32 * FLAC__MAX_FIXED_ORDER), + ("residual", FLAC__int32_p)] + + + class FLAC__Subframe_LPC(Structure): + _fields_ = [("entropy_coding_method", FLAC__EntropyCodingMethod), + ("order", c_uint), + ("qlp_coeff_precision", c_uint), + ("quantization_level", c_int), + ("qlp_coeff", FLAC__int32 * FLAC__MAX_LPC_ORDER), + ("warmup", FLAC__int32 * FLAC__MAX_LPC_ORDER), + ("residual", FLAC__int32_p)] + + + FLAC__SUBFRAME_LPC_QLP_COEFF_PRECISION_LEN = c_uint.in_dll(libflac, "FLAC__SUBFRAME_LPC_QLP_COEFF_PRECISION_LEN") + + FLAC__SUBFRAME_LPC_QLP_SHIFT_LEN = c_uint.in_dll(libflac, "FLAC__SUBFRAME_LPC_QLP_SHIFT_LEN") + + + + class FLAC__Subframe_data(Union): + _fields_ = [("constant", FLAC__Subframe_Constant), + ("fixed", FLAC__Subframe_Fixed), + ("lpc", FLAC__Subframe_LPC), + ("verbatim", FLAC__Subframe_Verbatim)] + + class FLAC__Subframe(Structure): + _fields_ = [("type", FLAC__SubframeType), + ("data", FLAC__Subframe_data), + ("wasted_bits", c_uint)] + + + FLAC__SUBFRAME_ZERO_PAD_LEN = c_uint.in_dll(libflac, "FLAC__SUBFRAME_ZERO_PAD_LEN") + + FLAC__SUBFRAME_TYPE_LEN = c_uint.in_dll(libflac, "FLAC__SUBFRAME_TYPE_LEN") + + FLAC__SUBFRAME_WASTED_BITS_FLAG_LEN = c_uint.in_dll(libflac, "FLAC__SUBFRAME_WASTED_BITS_FLAG_LEN") + + FLAC__SUBFRAME_TYPE_CONSTANT_BYTE_ALIGNED_MASK = c_uint.in_dll(libflac, "FLAC__SUBFRAME_TYPE_CONSTANT_BYTE_ALIGNED_MASK") + + FLAC__SUBFRAME_TYPE_VERBATIM_BYTE_ALIGNED_MASK = c_uint.in_dll(libflac, "FLAC__SUBFRAME_TYPE_VERBATIM_BYTE_ALIGNED_MASK") + + FLAC__SUBFRAME_TYPE_FIXED_BYTE_ALIGNED_MASK = c_uint.in_dll(libflac, "FLAC__SUBFRAME_TYPE_FIXED_BYTE_ALIGNED_MASK") + + FLAC__SUBFRAME_TYPE_LPC_BYTE_ALIGNED_MASK = c_uint.in_dll(libflac, "FLAC__SUBFRAME_TYPE_LPC_BYTE_ALIGNED_MASK") + + + FLAC__ChannelAssignment = c_int + + FLAC__CHANNEL_ASSIGNMENT_INDEPENDENT = 0 + FLAC__CHANNEL_ASSIGNMENT_LEFT_SIDE = 1 + FLAC__CHANNEL_ASSIGNMENT_RIGHT_SIDE = 2 + FLAC__CHANNEL_ASSIGNMENT_MID_SIDE = 3 + + + + libflac.FLAC__ChannelAssignmentString.restype = c_char_p + libflac.FLAC__ChannelAssignmentString.argtypes = [] + + def FLAC__ChannelAssignmentString(): + return libflac.FLAC__ChannelAssignmentString() + + FLAC__FrameNumberType = c_int + + + libflac.FLAC__FrameNumberTypeString.restype = c_char_p + libflac.FLAC__FrameNumberTypeString.argtypes = [] + + def FLAC__FrameNumberTypeString(): + return libflac.FLAC__FrameNumberTypeString() + + + class FLAC__FrameHeader_number(Union): + _fields_ =[("frame_number", FLAC__uint32), + ("sample_number", FLAC__uint64)] + + class FLAC__FrameHeader(Structure): + _fields_ = [("blocksize", c_uint), + ("sample_rate", c_uint), + ("channels", c_uint), + ("channel_assignment", FLAC__ChannelAssignment), + ("bits_per_sample", c_uint), + ("number_type", FLAC__FrameNumberType), + ("number", FLAC__FrameHeader_number), + ("crc", FLAC__uint8)] + + + FLAC__FRAME_HEADER_SYNC = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_SYNC") + + FLAC__FRAME_HEADER_RESERVED_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_RESERVED_LEN") + + FLAC__FRAME_HEADER_BLOCKING_STRATEGY_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_BLOCKING_STRATEGY_LEN") + + FLAC__FRAME_HEADER_BLOCK_SIZE_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_BLOCK_SIZE_LEN") + + FLAC__FRAME_HEADER_SAMPLE_RATE_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_SAMPLE_RATE_LEN") + + FLAC__FRAME_HEADER_CHANNEL_ASSIGNMENT_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_CHANNEL_ASSIGNMENT_LEN") + + FLAC__FRAME_HEADER_BITS_PER_SAMPLE_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_BITS_PER_SAMPLE_LEN") + + FLAC__FRAME_HEADER_ZERO_PAD_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_ZERO_PAD_LEN") + + FLAC__FRAME_HEADER_CRC_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_HEADER_CRC_LEN") + + + + class FLAC__FrameFooter(Structure): + _fields_ = [("crc", FLAC__uint16)] + + FLAC__FRAME_FOOTER_CRC_LEN = c_uint.in_dll(libflac, "FLAC__FRAME_FOOTER_CRC_LEN") + + + + class FLAC__Frame(Structure): + _fields_ = [("header", FLAC__FrameHeader), + ("subframes", FLAC__Subframe * FLAC__MAX_CHANNELS), + ("footer", FLAC__FrameFooter)] + + + FLAC__MetadataType = c_int + + FLAC__METADATA_TYPE_STREAMINFO = 0 + + FLAC__METADATA_TYPE_PADDING = 1 + + FLAC__METADATA_TYPE_APPLICATION = 2 + + FLAC__METADATA_TYPE_SEEKTABLE = 3 + + FLAC__METADATA_TYPE_VORBIS_COMMENT = 4 + + FLAC__METADATA_TYPE_CUESHEET = 5 + + FLAC__METADATA_TYPE_PICTURE = 6 + + FLAC__METADATA_TYPE_UNDEFINED = 7 + + FLAC__MAX_METADATA_TYPE = FLAC__MAX_METADATA_TYPE_CODE + + + + libflac.FLAC__MetadataTypeString.restype = c_char_p + libflac.FLAC__MetadataTypeString.argtypes = [] + + def FLAC__MetadataTypeString(): + return libflac.FLAC__MetadataTypeString() + + + + class FLAC__StreamMetadata_StreamInfo(Structure): + _fields_ = [("min_blocksize", c_uint), + ("max_framesize", c_uint), + ("min_framesize", c_uint), + ("max_framesize", c_uint), + ("sample_rate", c_uint), + ("channels", c_uint), + ("bits_per_sample", c_uint), + ("total_samples", FLAC__uint64), + ("md5sum", FLAC__byte*16)] + + FLAC__STREAM_METADATA_STREAMINFO_MIN_BLOCK_SIZE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_MIN_BLOCK_SIZE_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_MAX_BLOCK_SIZE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_MAX_BLOCK_SIZE_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_MIN_FRAME_SIZE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_MIN_FRAME_SIZE_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_MAX_FRAME_SIZE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_MAX_FRAME_SIZE_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_SAMPLE_RATE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_SAMPLE_RATE_LEN") + + + FLAC__STREAM_METADATA_STREAMINFO_CHANNELS_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_CHANNELS_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_BITS_PER_SAMPLE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_BITS_PER_SAMPLE_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_TOTAL_SAMPLES_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_TOTAL_SAMPLES_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_MD5SUM_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_STREAMINFO_MD5SUM_LEN") + + FLAC__STREAM_METADATA_STREAMINFO_LENGTH =(34) + + + class FLAC__StreamMetadata_Padding(Structure): + _fields_ = [("dummy", c_int)] + + + + class FLAC__StreamMetadata_Application(Structure): + _fields_ = [("id", FLAC__byte*4), + ("data", FLAC__byte_p)] + + FLAC__STREAM_METADATA_APPLICATION_ID_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_APPLICATION_ID_LEN") + + + class FLAC__StreamMetadata_SeekPoint(Structure): + _fields_ = [("sample_number", FLAC__uint64), + ("stream_offset", FLAC__uint64), + ("frame_samples", c_uint)] + + FLAC__STREAM_METADATA_SEEKPOINT_SAMPLE_NUMBER_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_SEEKPOINT_SAMPLE_NUMBER_LEN") + + FLAC__STREAM_METADATA_SEEKPOINT_STREAM_OFFSET_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_SEEKPOINT_STREAM_OFFSET_LEN") + + FLAC__STREAM_METADATA_SEEKPOINT_FRAME_SAMPLES_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_SEEKPOINT_FRAME_SAMPLES_LEN") + + FLAC__STREAM_METADATA_SEEKPOINT_LENGTH =(18) + + + FLAC__STREAM_METADATA_SEEKPOINT_PLACEHOLDER = FLAC__uint64.in_dll(libflac, "FLAC__STREAM_METADATA_SEEKPOINT_PLACEHOLDER") + + class FLAC__StreamMetadata_SeekTable(Structure): + _fields_ = [("num_points", c_uint), + ("points", POINTER(FLAC__StreamMetadata_SeekPoint))] + + class FLAC__StreamMetadata_VorbisComment_Entry(Structure): + _fields_ = [("length", FLAC__uint32), + ("entry", FLAC__byte_p)] + + FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN") + + + class FLAC__StreamMetadata_VorbisComment(Structure): + _fields_ = [("vendor_string", FLAC__StreamMetadata_VorbisComment_Entry), + ("num_comments", FLAC__uint32), + ("comments", POINTER(FLAC__StreamMetadata_VorbisComment_Entry))] + + FLAC__STREAM_METADATA_VORBIS_COMMENT_NUM_COMMENTS_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_VORBIS_COMMENT_NUM_COMMENTS_LEN") + + + class FLAC__StreamMetadata_CueSheet_Index(Structure): + _fields_ = [("offset", FLAC__uint64), + ("number", FLAC__byte)] + + + FLAC__STREAM_METADATA_CUESHEET_INDEX_OFFSET_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_INDEX_OFFSET_LEN") + + FLAC__STREAM_METADATA_CUESHEET_INDEX_NUMBER_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_INDEX_NUMBER_LEN") + + FLAC__STREAM_METADATA_CUESHEET_INDEX_RESERVED_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_INDEX_RESERVED_LEN") + + + class FLAC__StreamMetadata_CueSheet_Track(Structure): + _fields_ = [("offset", FLAC__uint64), + ("number", FLAC__byte), + ("isrc", c_char*13), + ("type", c_uint), + ("pre_emphasis", c_uint), + ("num_indices", FLAC__byte), + ("indices", POINTER(FLAC__StreamMetadata_CueSheet_Index))] + + FLAC__STREAM_METADATA_CUESHEET_TRACK_OFFSET_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_OFFSET_LEN") + + FLAC__STREAM_METADATA_CUESHEET_TRACK_NUMBER_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_NUMBER_LEN") + + FLAC__STREAM_METADATA_CUESHEET_TRACK_ISRC_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_ISRC_LEN") + + FLAC__STREAM_METADATA_CUESHEET_TRACK_TYPE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_TYPE_LEN") + + FLAC__STREAM_METADATA_CUESHEET_TRACK_PRE_EMPHASIS_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_PRE_EMPHASIS_LEN") + + FLAC__STREAM_METADATA_CUESHEET_TRACK_RESERVED_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_RESERVED_LEN") + + FLAC__STREAM_METADATA_CUESHEET_TRACK_NUM_INDICES_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_TRACK_NUM_INDICES_LEN") + + + class FLAC__StreamMetadata_CueSheet(Structure): + _fields_ = [("media_catalog_number", c_char*129), + ("lead_in", FLAC__uint64), + ("is_cd", FLAC__bool), + ("num_tracks", c_uint), + ("tracks", POINTER(FLAC__StreamMetadata_CueSheet_Track))] + + FLAC__STREAM_METADATA_CUESHEET_MEDIA_CATALOG_NUMBER_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_MEDIA_CATALOG_NUMBER_LEN") + + + FLAC__STREAM_METADATA_CUESHEET_LEAD_IN_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_LEAD_IN_LEN") + + FLAC__STREAM_METADATA_CUESHEET_IS_CD_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_IS_CD_LEN") + + FLAC__STREAM_METADATA_CUESHEET_RESERVED_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_RESERVED_LEN") + + FLAC__STREAM_METADATA_CUESHEET_NUM_TRACKS_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_CUESHEET_NUM_TRACKS_LEN") + + + FLAC__StreamMetadata_Picture_Type = c_int + FLAC__STREAM_METADATA_PICTURE_TYPE_OTHER = 0 + FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON_STANDARD = 1 + FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON = 2 + FLAC__STREAM_METADATA_PICTURE_TYPE_FRONT_COVER = 3 + FLAC__STREAM_METADATA_PICTURE_TYPE_BACK_COVER = 4 + FLAC__STREAM_METADATA_PICTURE_TYPE_LEAFLET_PAGE = 5 + FLAC__STREAM_METADATA_PICTURE_TYPE_MEDIA = 6 + FLAC__STREAM_METADATA_PICTURE_TYPE_LEAD_ARTIST = 7 + FLAC__STREAM_METADATA_PICTURE_TYPE_ARTIST = 8 + FLAC__STREAM_METADATA_PICTURE_TYPE_CONDUCTOR = 9 + FLAC__STREAM_METADATA_PICTURE_TYPE_BAND = 10 + FLAC__STREAM_METADATA_PICTURE_TYPE_COMPOSER = 11 + FLAC__STREAM_METADATA_PICTURE_TYPE_LYRICIST = 12 + FLAC__STREAM_METADATA_PICTURE_TYPE_RECORDING_LOCATION = 13 + FLAC__STREAM_METADATA_PICTURE_TYPE_DURING_RECORDING = 14 + FLAC__STREAM_METADATA_PICTURE_TYPE_DURING_PERFORMANCE = 15 + FLAC__STREAM_METADATA_PICTURE_TYPE_VIDEO_SCREEN_CAPTURE = 16 + FLAC__STREAM_METADATA_PICTURE_TYPE_FISH = 17 + FLAC__STREAM_METADATA_PICTURE_TYPE_ILLUSTRATION = 18 + FLAC__STREAM_METADATA_PICTURE_TYPE_BAND_LOGOTYPE = 19 + FLAC__STREAM_METADATA_PICTURE_TYPE_PUBLISHER_LOGOTYPE = 20 + + + libflac.FLAC__StreamMetadata_Picture_TypeString.restype = c_char_p + libflac.FLAC__StreamMetadata_Picture_TypeString.argtypes = [] + + def FLAC__StreamMetadata_Picture_TypeString(): + return libflac.FLAC__StreamMetadata_Picture_TypeString() + + + class FLAC__StreamMetadata_Picture(Structure): + _fields_ = [("type", FLAC__StreamMetadata_Picture_Type), + ("mime_type", c_char_p), + ("description", FLAC__byte_p), + ("width", FLAC__uint32), + ("height", FLAC__uint32), + ("depth", FLAC__uint32), + ("colors", FLAC__uint32), + ("data_length", FLAC__uint32), + ("data", FLAC__byte)] + + FLAC__STREAM_METADATA_PICTURE_TYPE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_TYPE_LEN") + + FLAC__STREAM_METADATA_PICTURE_MIME_TYPE_LENGTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_MIME_TYPE_LENGTH_LEN") + + FLAC__STREAM_METADATA_PICTURE_DESCRIPTION_LENGTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_DESCRIPTION_LENGTH_LEN") + + FLAC__STREAM_METADATA_PICTURE_WIDTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_WIDTH_LEN") + + FLAC__STREAM_METADATA_PICTURE_HEIGHT_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_HEIGHT_LEN") + + + FLAC__STREAM_METADATA_PICTURE_DEPTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_DEPTH_LEN") + + FLAC__STREAM_METADATA_PICTURE_COLORS_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_COLORS_LEN") + + FLAC__STREAM_METADATA_PICTURE_DATA_LENGTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_PICTURE_DATA_LENGTH_LEN") + + + class FLAC__StreamMetadata_Unknown(Structure): + _fields_ = [("data", FLAC__byte_p)] + + + class FLAC__StreamMetadata_data(Union): + _fields_ = [("stream_info", FLAC__StreamMetadata_StreamInfo), + ("padding", FLAC__StreamMetadata_Padding), + ("application", FLAC__StreamMetadata_Application), + ("seek_table", FLAC__StreamMetadata_SeekTable), + ("vorbis_comment", FLAC__StreamMetadata_VorbisComment), + ("cue_sheet", FLAC__StreamMetadata_CueSheet), + ("picture", FLAC__StreamMetadata_Picture), + ("unknown", FLAC__StreamMetadata_Unknown)] + + class FLAC__StreamMetadata(Structure): + _fields_ = [("type", FLAC__MetadataType), + ("is_last", FLAC__bool), + ("length", c_uint), + ("data", FLAC__StreamMetadata_data)] + + FLAC__STREAM_METADATA_IS_LAST_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_IS_LAST_LEN") + + FLAC__STREAM_METADATA_TYPE_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_TYPE_LEN") + + FLAC__STREAM_METADATA_LENGTH_LEN = c_uint.in_dll(libflac, "FLAC__STREAM_METADATA_LENGTH_LEN") + + FLAC__STREAM_METADATA_HEADER_LENGTH =(4) + + + + libflac.FLAC__format_sample_rate_is_valid.restype = FLAC__bool + libflac.FLAC__format_sample_rate_is_valid.argtypes = [c_uint] + + def FLAC__format_sample_rate_is_valid(sample_rate): + return libflac.FLAC__format_sample_rate_is_valid(sample_rate) + + + libflac.FLAC__format_blocksize_is_subset.restype = FLAC__bool + libflac.FLAC__format_blocksize_is_subset.argtypes = [c_uint, c_uint] + + def FLAC__format_blocksize_is_subset(blocksize, sample_rate): + return libflac.FLAC__format_blocksize_is_subset(blocksize, sample_rate) + + + libflac.FLAC__format_sample_rate_is_subset.restype = FLAC__bool + libflac.FLAC__format_sample_rate_is_subset.argtypes = [c_uint] + + def FLAC__format_sample_rate_is_subset(sample_rate): + return libflac.FLAC__format_sample_rate_is_subset(sample_rate) + + + libflac.FLAC__format_vorbiscomment_entry_name_is_legal.restype = FLAC__bool + libflac.FLAC__format_vorbiscomment_entry_name_is_legal.argtypes = [c_char_p] + + def FLAC__format_vorbiscomment_entry_name_is_legal(name): + return libflac.FLAC__format_vorbiscomment_entry_name_is_legal(name) + + libflac.FLAC__format_vorbiscomment_entry_value_is_legal.restype = FLAC__bool + libflac.FLAC__format_vorbiscomment_entry_value_is_legal.argtypes = [FLAC__byte_p, c_uint] + + def FLAC__format_vorbiscomment_entry_value_is_legal(value, length): + return libflac.FLAC__format_vorbiscomment_entry_value_is_legal(value, length) + + libflac.FLAC__format_vorbiscomment_entry_is_legal.restype = FLAC__bool + libflac.FLAC__format_vorbiscomment_entry_is_legal.argtypes = [FLAC__byte_p, c_uint] + + def FLAC__format_vorbiscomment_entry_is_legal(entry, length): + return libflac.FLAC__format_vorbiscomment_entry_is_legal(entry, length) + + libflac.FLAC__format_seektable_is_legal.restype = FLAC__bool + libflac.FLAC__format_seektable_is_legal.argtypes = [POINTER(FLAC__StreamMetadata_SeekTable)] + + def FLAC__format_seektable_is_legal(seek_table): + return libflac.FLAC__format_seektable_is_legal(seek_table) + + + libflac.FLAC__format_seektable_sort.restype = FLAC__bool + libflac.FLAC__format_seektable_sort.argtypes = [POINTER(FLAC__StreamMetadata_SeekTable)] + + def FLAC__format_seektable_sort(seek_table): + return libflac.FLAC__format_seektable_sort(seek_table) + + libflac.FLAC__format_cuesheet_is_legal.restype = FLAC__bool + libflac.FLAC__format_cuesheet_is_legal.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet), FLAC__bool, c_char_p_p] + + def FLAC__format_cuesheet_is_legal(cue_sheet, check_cd_da_subset, violation): + return libflac.FLAC__format_cuesheet_is_legal(cue_sheet, check_cd_da_subset, violation) + + # /format + + # metadata + + libflac.FLAC__metadata_get_streaminfo.restype = FLAC__bool + libflac.FLAC__metadata_get_streaminfo.argtypes = [c_char_p, POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_get_streaminfo(filename, streaminfo): + return libflac.FLAC__metadata_get_streaminfo(filename, streaminfo) + + libflac.FLAC__metadata_get_tags.restype = FLAC__bool + libflac.FLAC__metadata_get_tags.argtypes = [c_char_p, POINTER(POINTER(FLAC__StreamMetadata))] + + def FLAC__metadata_get_tags(filename, tags): + return libflac.FLAC__metadata_get_tags(filename, tags) + + libflac.FLAC__metadata_get_cuesheet.restype = FLAC__bool + libflac.FLAC__metadata_get_cuesheet.argtypes = [c_char_p, POINTER(POINTER(FLAC__StreamMetadata))] + + def FLAC__metadata_get_cuesheet(filename, cuesheet): + return libflac.FLAC__metadata_get_cuesheet(filename, cuesheet) + + libflac.FLAC__metadata_get_picture.restype = FLAC__bool + libflac.FLAC__metadata_get_picture.argtypes = [c_char_p, POINTER(POINTER(FLAC__StreamMetadata)), FLAC__StreamMetadata_Picture_Type, c_char_p, FLAC__byte_p, c_uint, c_uint, c_uint, c_uint] + + def FLAC__metadata_get_picture(filename, picture, type, mime_type, description, max_width, max_height, max_depth, max_colors): + return libflac.FLAC__metadata_get_picture(filename, picture, type, mime_type, description, max_width, max_height, max_depth, max_colors) + + + class FLAC__Metadata_SimpleIterator(Structure): + _fields_ = [("dummy", c_int)] + + FLAC__Metadata_SimpleIteratorStatus = c_int + + FLAC__METADATA_SIMPLE_ITERATOR_STATUS_OK = 0 + + + libflac.FLAC__Metadata_SimpleIteratorStatusString.restype = c_char_p + libflac.FLAC__Metadata_SimpleIteratorStatusString.argtypes = [] + + def FLAC__Metadata_SimpleIteratorStatusString(): + return libflac.FLAC__Metadata_SimpleIteratorStatusString() + + + libflac.FLAC__metadata_simple_iterator_new.restype = POINTER(FLAC__Metadata_SimpleIterator) + libflac.FLAC__metadata_simple_iterator_new.argtypes = [] + + def FLAC__metadata_simple_iterator_new(): + return libflac.FLAC__metadata_simple_iterator_new() + + + libflac.FLAC__metadata_simple_iterator_delete.restype = None + libflac.FLAC__metadata_simple_iterator_delete.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_delete(iterator): + return libflac.FLAC__metadata_simple_iterator_delete(iterator) + + + libflac.FLAC__metadata_simple_iterator_status.restype = FLAC__Metadata_SimpleIteratorStatus + libflac.FLAC__metadata_simple_iterator_status.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_status(iterator): + return libflac.FLAC__metadata_simple_iterator_status(iterator) + + libflac.FLAC__metadata_simple_iterator_init.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_init.argtypes = [POINTER(FLAC__Metadata_SimpleIterator), c_char_p, FLAC__bool, FLAC__bool] + + def FLAC__metadata_simple_iterator_init(iterator, filename, read_only, preserve_file_stats): + return libflac.FLAC__metadata_simple_iterator_init(iterator, filename, read_only, preserve_file_stats) + + libflac.FLAC__metadata_simple_iterator_is_writable.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_is_writable.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_is_writable(iterator): + return libflac.FLAC__metadata_simple_iterator_is_writable(iterator) + + libflac.FLAC__metadata_simple_iterator_next.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_next.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_next(iterator): + return libflac.FLAC__metadata_simple_iterator_next(iterator) + + libflac.FLAC__metadata_simple_iterator_prev.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_prev.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_prev(iterator): + return libflac.FLAC__metadata_simple_iterator_prev(iterator) + + libflac.FLAC__metadata_simple_iterator_is_last.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_is_last.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_is_last(iterator): + return libflac.FLAC__metadata_simple_iterator_is_last(iterator) + + libflac.FLAC__metadata_simple_iterator_get_block_offset.restype = c_off_t + libflac.FLAC__metadata_simple_iterator_get_block_offset.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_get_block_offset(iterator): + return libflac.FLAC__metadata_simple_iterator_get_block_offset(iterator) + + libflac.FLAC__metadata_simple_iterator_get_block_type.restype = FLAC__MetadataType + libflac.FLAC__metadata_simple_iterator_get_block_type.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_get_block_type(iterator): + return libflac.FLAC__metadata_simple_iterator_get_block_type(iterator) + + libflac.FLAC__metadata_simple_iterator_get_block_length.restype = c_uint + libflac.FLAC__metadata_simple_iterator_get_block_length.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_get_block_length(iterator): + return libflac.FLAC__metadata_simple_iterator_get_block_length(iterator) + + libflac.FLAC__metadata_simple_iterator_get_application_id.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_get_application_id.argtypes = [POINTER(FLAC__Metadata_SimpleIterator), FLAC__byte_p] + + def FLAC__metadata_simple_iterator_get_application_id(iterator, id): + return libflac.FLAC__metadata_simple_iterator_get_application_id(iterator, id) + + libflac.FLAC__metadata_simple_iterator_get_block.restype = POINTER(FLAC__StreamMetadata) + libflac.FLAC__metadata_simple_iterator_get_block.argtypes = [POINTER(FLAC__Metadata_SimpleIterator)] + + def FLAC__metadata_simple_iterator_get_block(iterator): + return libflac.FLAC__metadata_simple_iterator_get_block(iterator) + + libflac.FLAC__metadata_simple_iterator_set_block.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_set_block.argtypes = [POINTER(FLAC__Metadata_SimpleIterator), POINTER(FLAC__StreamMetadata), FLAC__bool] + + def FLAC__metadata_simple_iterator_set_block(iterator, block, use_padding): + return libflac.FLAC__metadata_simple_iterator_set_block(iterator, block, use_padding) + + libflac.FLAC__metadata_simple_iterator_insert_block_after.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_insert_block_after.argtypes = [POINTER(FLAC__Metadata_SimpleIterator), POINTER(FLAC__StreamMetadata), FLAC__bool] + + def FLAC__metadata_simple_iterator_insert_block_after(iterator, block, use_padding): + return libflac.FLAC__metadata_simple_iterator_insert_block_after(iterator, block, use_padding) + + libflac.FLAC__metadata_simple_iterator_delete_block.restype = FLAC__bool + libflac.FLAC__metadata_simple_iterator_delete_block.argtypes = [POINTER(FLAC__Metadata_SimpleIterator), FLAC__bool] + + def FLAC__metadata_simple_iterator_delete_block(iterator, use_padding): + return libflac.FLAC__metadata_simple_iterator_delete_block(iterator, use_padding) + + class FLAC__Metadata_Chain(Structure): + _fields_ = [("dummy", c_int)] + + class FLAC__Metadata_Iterator(Structure): + _fields_ = [("dummy", c_int)] + + FLAC__Metadata_ChainStatus = c_int + + FLAC__METADATA_CHAIN_STATUS_OK = 0 + + libflac.FLAC__Metadata_ChainStatusString.restype = c_char_p + libflac.FLAC__Metadata_ChainStatusString.argtypes = [] + + def FLAC__Metadata_ChainStatusString(): + return libflac.FLAC__Metadata_ChainStatusString() + + libflac.FLAC__metadata_chain_new.restype = POINTER(FLAC__Metadata_Chain) + libflac.FLAC__metadata_chain_new.argtypes = [] + + def FLAC__metadata_chain_new(): + return libflac.FLAC__metadata_chain_new() + + libflac.FLAC__metadata_chain_delete.restype = None + libflac.FLAC__metadata_chain_delete.argtypes = [POINTER(FLAC__Metadata_Chain)] + + def FLAC__metadata_chain_delete(chain): + return libflac.FLAC__metadata_chain_delete(chain) + + libflac.FLAC__metadata_chain_status.restype = FLAC__Metadata_ChainStatus + libflac.FLAC__metadata_chain_status.argtypes = [POINTER(FLAC__Metadata_Chain)] + + def FLAC__metadata_chain_status(chain): + return libflac.FLAC__metadata_chain_status(chain) + + libflac.FLAC__metadata_chain_read.restype = FLAC__bool + libflac.FLAC__metadata_chain_read.argtypes = [POINTER(FLAC__Metadata_Chain), c_char_p] + + def FLAC__metadata_chain_read(chain, filename): + return libflac.FLAC__metadata_chain_read(chain, filename) + + libflac.FLAC__metadata_chain_read_ogg.restype = FLAC__bool + libflac.FLAC__metadata_chain_read_ogg.argtypes = [POINTER(FLAC__Metadata_Chain), c_char_p] + + def FLAC__metadata_chain_read_ogg(chain, filename): + return libflac.FLAC__metadata_chain_read_ogg(chain, filename) + + libflac.FLAC__metadata_chain_read_with_callbacks.restype = FLAC__bool + libflac.FLAC__metadata_chain_read_with_callbacks.argtypes = [POINTER(FLAC__Metadata_Chain), FLAC__IOHandle, FLAC__IOCallbacks] + + def FLAC__metadata_chain_read_with_callbacks(chain, handle, callbacks): + return libflac.FLAC__metadata_chain_read_with_callbacks(chain, handle, callbacks) + + libflac.FLAC__metadata_chain_read_ogg_with_callbacks.restype = FLAC__bool + libflac.FLAC__metadata_chain_read_ogg_with_callbacks.argtypes = [POINTER(FLAC__Metadata_Chain), FLAC__IOHandle, FLAC__IOCallbacks] + + def FLAC__metadata_chain_read_ogg_with_callbacks(chain, handle, callbacks): + return libflac.FLAC__metadata_chain_read_ogg_with_callbacks(chain, handle, callbacks) + + libflac.FLAC__metadata_chain_check_if_tempfile_needed.restype = FLAC__bool + libflac.FLAC__metadata_chain_check_if_tempfile_needed.argtypes = [POINTER(FLAC__Metadata_Chain), FLAC__bool] + + def FLAC__metadata_chain_check_if_tempfile_needed(chain, use_padding): + return libflac.FLAC__metadata_chain_check_if_tempfile_needed(chain, use_padding) + + libflac.FLAC__metadata_chain_write.restype = FLAC__bool + libflac.FLAC__metadata_chain_write.argtypes = [POINTER(FLAC__Metadata_Chain), FLAC__bool, FLAC__bool] + + def FLAC__metadata_chain_write(chain, use_padding, preserve_file_stats): + return libflac.FLAC__metadata_chain_write(chain, use_padding, preserve_file_stats) + + libflac.FLAC__metadata_chain_write_with_callbacks.restype = FLAC__bool + libflac.FLAC__metadata_chain_write_with_callbacks.argtypes = [POINTER(FLAC__Metadata_Chain), FLAC__bool, FLAC__IOHandle, FLAC__IOCallbacks] + + def FLAC__metadata_chain_write_with_callbacks(chain, use_padding, handle, callbacks): + return libflac.FLAC__metadata_chain_write_with_callbacks(chain, use_padding, handle, callbacks) + + libflac.FLAC__metadata_chain_write_with_callbacks_and_tempfile.restype = FLAC__bool + libflac.FLAC__metadata_chain_write_with_callbacks_and_tempfile.argtypes = [POINTER(FLAC__Metadata_Chain), FLAC__bool, FLAC__IOHandle, FLAC__IOCallbacks, FLAC__IOHandle, FLAC__IOCallbacks] + + def FLAC__metadata_chain_write_with_callbacks_and_tempfile(chain, use_padding, handle, callbacks, temp_handle, temp_callbacks): + return libflac.FLAC__metadata_chain_write_with_callbacks_and_tempfile(chain, use_padding, handle, callbacks, temp_handle, temp_callbacks) + + libflac.FLAC__metadata_chain_merge_padding.restype = None + libflac.FLAC__metadata_chain_merge_padding.argtypes = [POINTER(FLAC__Metadata_Chain)] + + def FLAC__metadata_chain_merge_padding(chain): + return libflac.FLAC__metadata_chain_merge_padding(chain) + + libflac.FLAC__metadata_chain_sort_padding.restype = None + libflac.FLAC__metadata_chain_sort_padding.argtypes = [POINTER(FLAC__Metadata_Chain)] + + def FLAC__metadata_chain_sort_padding(chain): + return libflac.FLAC__metadata_chain_sort_padding(chain) + + libflac.FLAC__metadata_iterator_new.restype = POINTER(FLAC__Metadata_Iterator) + libflac.FLAC__metadata_iterator_new.argtypes = [] + + def FLAC__metadata_iterator_new(): + return libflac.FLAC__metadata_iterator_new() + + libflac.FLAC__metadata_iterator_delete.restype = None + libflac.FLAC__metadata_iterator_delete.argtypes = [POINTER(FLAC__Metadata_Iterator)] + + def FLAC__metadata_iterator_delete(iterator): + return libflac.FLAC__metadata_iterator_delete(iterator) + + libflac.FLAC__metadata_iterator_init.restype = None + libflac.FLAC__metadata_iterator_init.argtypes = [POINTER(FLAC__Metadata_Iterator), POINTER(FLAC__Metadata_Chain)] + + def FLAC__metadata_iterator_init(iterator, chain): + return libflac.FLAC__metadata_iterator_init(iterator, chain) + + libflac.FLAC__metadata_iterator_next.restype = FLAC__bool + libflac.FLAC__metadata_iterator_next.argtypes = [POINTER(FLAC__Metadata_Iterator)] + + def FLAC__metadata_iterator_next(iterator): + return libflac.FLAC__metadata_iterator_next(iterator) + + libflac.FLAC__metadata_iterator_prev.restype = FLAC__bool + libflac.FLAC__metadata_iterator_prev.argtypes = [POINTER(FLAC__Metadata_Iterator)] + + def FLAC__metadata_iterator_prev(iterator): + return libflac.FLAC__metadata_iterator_prev(iterator) + + libflac.FLAC__metadata_iterator_get_block_type.restype = FLAC__MetadataType + libflac.FLAC__metadata_iterator_get_block_type.argtypes = [POINTER(FLAC__Metadata_Iterator)] + + def FLAC__metadata_iterator_get_block_type(iterator): + return libflac.FLAC__metadata_iterator_get_block_type(iterator) + + libflac.FLAC__metadata_iterator_get_block_type.restype = POINTER(FLAC__StreamMetadata) + libflac.FLAC__metadata_iterator_get_block_type.argtypes = [POINTER(FLAC__Metadata_Iterator)] + + def FLAC__metadata_iterator_get_block_type(iterator): + return libflac.FLAC__metadata_iterator_get_block_type(iterator) + + libflac.FLAC__metadata_iterator_set_block.restype = FLAC__bool + libflac.FLAC__metadata_iterator_set_block.argtypes = [POINTER(FLAC__Metadata_Iterator), POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_iterator_set_block(iterator, block): + return libflac.FLAC__metadata_iterator_set_block(iterator, block) + + libflac.FLAC__metadata_iterator_delete_block.restype = FLAC__bool + libflac.FLAC__metadata_iterator_delete_block.argtypes = [POINTER(FLAC__Metadata_Iterator), FLAC__bool] + + def FLAC__metadata_iterator_delete_block(iterator, replace_with_padding): + return libflac.FLAC__metadata_iterator_delete_block(iterator, replace_with_padding) + + libflac.FLAC__metadata_iterator_insert_block_before.restype = FLAC__bool + libflac.FLAC__metadata_iterator_insert_block_before.argtypes = [POINTER(FLAC__Metadata_Iterator), POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_iterator_insert_block_before(iterator, block): + return libflac.FLAC__metadata_iterator_insert_block_before(iterator, block) + + libflac.FLAC__metadata_iterator_insert_block_after.restype = FLAC__bool + libflac.FLAC__metadata_iterator_insert_block_after.argtypes = [POINTER(FLAC__Metadata_Iterator), POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_iterator_insert_block_after(iterator, block): + return libflac.FLAC__metadata_iterator_insert_block_after(iterator, block) + + libflac.FLAC__metadata_object_new.restype = POINTER(FLAC__StreamMetadata) + libflac.FLAC__metadata_object_new.argtypes = [POINTER(FLAC__MetadataType)] + + def FLAC__metadata_object_new(type): + return libflac.FLAC__metadata_object_new(type) + + libflac.FLAC__metadata_object_clone.restype = POINTER(FLAC__StreamMetadata) + libflac.FLAC__metadata_object_clone.argtypes = [POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_object_clone(object): + return libflac.FLAC__metadata_object_clone(object) + + libflac.FLAC__metadata_object_delete.restype = None + libflac.FLAC__metadata_object_delete.argtypes = [POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_object_delete(object): + return libflac.FLAC__metadata_object_delete(object) + + libflac.FLAC__metadata_object_is_equal.restype = FLAC__bool + libflac.FLAC__metadata_object_is_equal.argtypes = [POINTER(FLAC__StreamMetadata), POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_object_is_equal(block1, block2): + return libflac.FLAC__metadata_object_is_equal(block1, block2) + + libflac.FLAC__metadata_object_application_set_data.restype = FLAC__bool + libflac.FLAC__metadata_object_application_set_data.argtypes = [POINTER(FLAC__StreamMetadata), FLAC__byte_p, c_uint, FLAC__bool] + + def FLAC__metadata_object_application_set_data(object, data, length, copy): + return libflac.FLAC__metadata_object_application_set_data(object, data, length, copy) + + libflac.FLAC__metadata_object_seektable_resize_points.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_resize_points.argtypes = [POINTER(FLAC__StreamMetadata),c_uint] + + def FLAC__metadata_object_seektable_resize_points(object, new_num_points): + return libflac.FLAC__metadata_object_seektable_resize_points(object, new_num_points) + + libflac.FLAC__metadata_object_seektable_set_point.restype = None + libflac.FLAC__metadata_object_seektable_set_point.argtypes = [POINTER(FLAC__StreamMetadata),c_uint, FLAC__StreamMetadata_SeekPoint] + + def FLAC__metadata_object_seektable_set_point(object, point_num, point): + return libflac.FLAC__metadata_object_seektable_set_point(object, point_num, point) + + libflac.FLAC__metadata_object_seektable_insert_point.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_insert_point.argtypes = [POINTER(FLAC__StreamMetadata),c_uint, FLAC__StreamMetadata_SeekPoint] + + def FLAC__metadata_object_seektable_insert_point(object, point_num, point): + return libflac.FLAC__metadata_object_seektable_insert_point(object, point_num, point) + + libflac.FLAC__metadata_object_seektable_delete_point.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_delete_point.argtypes = [POINTER(FLAC__StreamMetadata),c_uint] + + def FLAC__metadata_object_seektable_delete_point(object, point_num): + return libflac.FLAC__metadata_object_seektable_delete_point(object, point_num) + + libflac.FLAC__metadata_object_seektable_is_legal.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_is_legal.argtypes = [POINTER(FLAC__StreamMetadata)] + + def FLAC__metadata_object_seektable_is_legal(object): + return libflac.FLAC__metadata_object_seektable_is_legal(object) + + libflac.FLAC__metadata_object_seektable_template_append_placeholders.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_template_append_placeholders.argtypes = [POINTER(FLAC__StreamMetadata), c_uint] + + def FLAC__metadata_object_seektable_template_append_placeholders(object, num): + return libflac.FLAC__metadata_object_seektable_template_append_placeholders(object, num) + + libflac.FLAC__metadata_object_seektable_template_append_point.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_template_append_point.argtypes = [POINTER(FLAC__StreamMetadata), FLAC__uint64] + + def FLAC__metadata_object_seektable_template_append_point(object, sample_number): + return libflac.FLAC__metadata_object_seektable_template_append_point(object, sample_number) + + libflac.FLAC__metadata_object_seektable_template_append_points.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_template_append_points.argtypes = [POINTER(FLAC__StreamMetadata), POINTER(FLAC__uint64*0), c_uint] + + def FLAC__metadata_object_seektable_template_append_points(object, sample_numbers, num): + return libflac.FLAC__metadata_object_seektable_template_append_points(object, sample_numbers, num) + + libflac.FLAC__metadata_object_seektable_template_append_spaced_points.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_template_append_spaced_points.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, FLAC__uint64] + + def FLAC__metadata_object_seektable_template_append_spaced_points(object, num, total_samples): + return libflac.FLAC__metadata_object_seektable_template_append_spaced_points(object, num, total_samples) + + libflac.FLAC__metadata_object_seektable_template_append_spaced_points_by_samples.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_template_append_spaced_points_by_samples.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, FLAC__uint64] + + def FLAC__metadata_object_seektable_template_append_spaced_points_by_samples(object, samples, total_samples): + return libflac.FLAC__metadata_object_seektable_template_append_spaced_points_by_samples(object, samples, total_samples) + + libflac.FLAC__metadata_object_seektable_template_sort.restype = FLAC__bool + libflac.FLAC__metadata_object_seektable_template_sort.argtypes = [POINTER(FLAC__StreamMetadata), FLAC__bool] + + def FLAC__metadata_object_seektable_template_sort(object, compact): + return libflac.FLAC__metadata_object_seektable_template_sort(object, compact) + + libflac.FLAC__metadata_object_vorbiscomment_set_vendor_string.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_set_vendor_string.argtypes = [POINTER(FLAC__StreamMetadata), FLAC__StreamMetadata_VorbisComment_Entry, FLAC__bool] + + def FLAC__metadata_object_vorbiscomment_set_vendor_string(object, entry, copy): + return libflac.FLAC__metadata_object_vorbiscomment_set_vendor_string(object, entry, copy) + + libflac.FLAC__metadata_object_vorbiscomment_resize_comments.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_resize_comments.argtypes = [POINTER(FLAC__StreamMetadata), c_uint] + + def FLAC__metadata_object_vorbiscomment_resize_comments(object, new_num_comments): + return libflac.FLAC__metadata_object_vorbiscomment_resize_comments(object, new_num_comments) + + libflac.FLAC__metadata_object_vorbiscomment_set_comment.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_set_comment.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, FLAC__StreamMetadata_VorbisComment_Entry, FLAC__bool] + + def FLAC__metadata_object_vorbiscomment_set_comment(object, comment_num, entry, copy): + return libflac.FLAC__metadata_object_vorbiscomment_set_comment(object, comment_num, entry, copy) + + libflac.FLAC__metadata_object_vorbiscomment_insert_comment.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_insert_comment.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, FLAC__StreamMetadata_VorbisComment_Entry, FLAC__bool] + + def FLAC__metadata_object_vorbiscomment_insert_comment(object, comment_num, entry, copy): + return libflac.FLAC__metadata_object_vorbiscomment_insert_comment(object, comment_num, entry, copy) + + libflac.FLAC__metadata_object_vorbiscomment_append_comment.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_append_comment.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, FLAC__StreamMetadata_VorbisComment_Entry, FLAC__bool] + + def FLAC__metadata_object_vorbiscomment_append_comment(object, entry, copy): + return libflac.FLAC__metadata_object_vorbiscomment_append_comment(object,entry, copy) + + libflac.FLAC__metadata_object_vorbiscomment_replace_comment.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_replace_comment.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, FLAC__StreamMetadata_VorbisComment_Entry, FLAC__bool, FLAC__bool] + + def FLAC__metadata_object_vorbiscomment_replace_comment(object, entry, all, copy): + return libflac.FLAC__metadata_object_vorbiscomment_replace_comment(object,entry, all, copy) + + libflac.FLAC__metadata_object_vorbiscomment_delete_comment.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_delete_comment.argtypes = [POINTER(FLAC__StreamMetadata), c_uint] + + def FLAC__metadata_object_vorbiscomment_delete_comment(object, comment_num): + return libflac.FLAC__metadata_object_vorbiscomment_delete_comment(object,comment_num) + + libflac.FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair.argtypes = [POINTER(FLAC__StreamMetadata_VorbisComment_Entry), c_char_p, c_char_p] + + def FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(entry, field_name, field_value): + return libflac.FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(entry, field_name, field_value) + + libflac.FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair.argtypes = [POINTER(FLAC__StreamMetadata_VorbisComment_Entry), c_char_p_p, c_char_p_p] + + def FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(entry, field_name, field_value): + return libflac.FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(entry, field_name, field_value) + + libflac.FLAC__metadata_object_vorbiscomment_entry_matches.restype = FLAC__bool + libflac.FLAC__metadata_object_vorbiscomment_entry_matches.argtypes = [POINTER(FLAC__StreamMetadata_VorbisComment_Entry), c_char_p, c_uint] + + def FLAC__metadata_object_vorbiscomment_entry_matches(entry, field_name, field_value): + return libflac.FLAC__metadata_object_vorbiscomment_entry_matches(entry, field_name, field_value) + + libflac.FLAC__metadata_object_vorbiscomment_find_entry_from.restype = c_int + libflac.FLAC__metadata_object_vorbiscomment_find_entry_from.argtypes = [POINTER(FLAC__StreamMetadata), c_uint, c_char_p] + + def FLAC__metadata_object_vorbiscomment_find_entry_from(object, offset, field_name): + return libflac.FLAC__metadata_object_vorbiscomment_find_entry_from(object, offset, field_name) + + libflac.FLAC__metadata_object_vorbiscomment_remove_entry_matching.restype = c_int + libflac.FLAC__metadata_object_vorbiscomment_remove_entry_matching.argtypes = [POINTER(FLAC__StreamMetadata), c_char_p] + + def FLAC__metadata_object_vorbiscomment_remove_entry_matching(object, field_name): + return libflac.FLAC__metadata_object_vorbiscomment_remove_entry_matching(object, field_name) + + libflac.FLAC__metadata_object_vorbiscomment_remove_entries_matching.restype = c_int + libflac.FLAC__metadata_object_vorbiscomment_remove_entries_matching.argtypes = [POINTER(FLAC__StreamMetadata), c_char_p] + + def FLAC__metadata_object_vorbiscomment_remove_entries_matching(object, field_name): + return libflac.FLAC__metadata_object_vorbiscomment_remove_entries_matching(object, field_name) + + libflac.FLAC__metadata_object_cuesheet_track_new.restype = POINTER(FLAC__StreamMetadata_CueSheet_Track) + libflac.FLAC__metadata_object_cuesheet_track_new.argtypes = [] + + def FLAC__metadata_object_cuesheet_track_new(): + return libflac.FLAC__metadata_object_cuesheet_track_new() + + libflac.FLAC__metadata_object_cuesheet_track_delete.restype = None + libflac.FLAC__metadata_object_cuesheet_track_delete.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track)] + + def FLAC__metadata_object_cuesheet_track_delete(object): + return libflac.FLAC__metadata_object_cuesheet_track_delete(object) + + libflac.FLAC__metadata_object_cuesheet_track_resize_indices.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_track_resize_indices.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint, c_uint] + + def FLAC__metadata_object_cuesheet_track_resize_indices(object, track_num, new_num_indices): + return libflac.FLAC__metadata_object_cuesheet_track_resize_indices(object, track_num, new_num_indices) + + libflac.FLAC__metadata_object_cuesheet_track_insert_index.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_track_insert_index.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint, c_uint, FLAC__StreamMetadata_CueSheet_Index] + + def FLAC__metadata_object_cuesheet_track_insert_index(object, track_num, index_num, index): + return libflac.FLAC__metadata_object_cuesheet_track_insert_index(object, track_num, index_num, index) + + libflac.FLAC__metadata_object_cuesheet_track_insert_blank_index.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_track_insert_blank_index.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint, c_uint] + + def FLAC__metadata_object_cuesheet_track_insert_blank_index(object, track_num, index_num): + return libflac.FLAC__metadata_object_cuesheet_track_insert_blank_index(object, track_num, index_num) + + libflac.FLAC__metadata_object_cuesheet_track_delete_index.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_track_delete_index.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint, c_uint] + + def FLAC__metadata_object_cuesheet_track_delete_index(object, track_num, index_num): + return libflac.FLAC__metadata_object_cuesheet_track_delete_index(object, track_num, index_num) + + libflac.FLAC__metadata_object_cuesheet_resize_tracks.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_resize_tracks.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint] + + def FLAC__metadata_object_cuesheet_resize_tracks(object, new_num_tracks): + return libflac.FLAC__metadata_object_cuesheet_resize_tracks(object, new_num_tracks) + + libflac.FLAC__metadata_object_cuesheet_set_track.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_set_track.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint, POINTER(FLAC__StreamMetadata_CueSheet_Track), FLAC__bool] + + def FLAC__metadata_object_cuesheet_set_track(object, new_num_tracks, track, copy): + return libflac.FLAC__metadata_object_cuesheet_set_track(object, new_num_tracks, track, copy) + + libflac.FLAC__metadata_object_cuesheet_insert_track.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_insert_track.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint, POINTER(FLAC__StreamMetadata_CueSheet_Track), FLAC__bool] + + def FLAC__metadata_object_cuesheet_insert_track(object, track_num, track, copy): + return libflac.FLAC__metadata_object_cuesheet_insert_track(object, track_num, track, copy) + + libflac.FLAC__metadata_object_cuesheet_insert_blank_track.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_insert_blank_track.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint] + + def FLAC__metadata_object_cuesheet_insert_blank_track(object, track_num): + return libflac.FLAC__metadata_object_cuesheet_insert_blank_track(object, track_num) + + libflac.FLAC__metadata_object_cuesheet_delete_track.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_delete_track.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_uint] + + def FLAC__metadata_object_cuesheet_delete_track(object, track_num): + return libflac.FLAC__metadata_object_cuesheet_delete_track(object, track_num) + + libflac.FLAC__metadata_object_cuesheet_is_legal.restype = FLAC__bool + libflac.FLAC__metadata_object_cuesheet_is_legal.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), FLAC__bool, c_char_p_p] + + def FLAC__metadata_object_cuesheet_is_legal(object, check_cd_da_subset, violation): + return libflac.FLAC__metadata_object_cuesheet_is_legal(object, check_cd_da_subset, violation) + + libflac.FLAC__metadata_object_cuesheet_calculate_cddb_id.restype = FLAC__uint32 + libflac.FLAC__metadata_object_cuesheet_calculate_cddb_id.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track)] + + def FLAC__metadata_object_cuesheet_calculate_cddb_id(object): + return libflac.FLAC__metadata_object_cuesheet_calculate_cddb_id(object) + + libflac.FLAC__metadata_object_picture_set_mime_type.restype = FLAC__bool + libflac.FLAC__metadata_object_picture_set_mime_type.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_char_p, FLAC__bool] + + def FLAC__metadata_object_picture_set_mime_type(object, mime_type, copy): + return libflac.FLAC__metadata_object_picture_set_mime_type(object, mime_type, copy) + + libflac.FLAC__metadata_object_picture_set_description.restype = FLAC__bool + libflac.FLAC__metadata_object_picture_set_description.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), FLAC__byte_p, FLAC__bool] + + def FLAC__metadata_object_picture_set_description(object, description, copy): + return libflac.FLAC__metadata_object_picture_set_description(object, mime_type, copy) + + libflac.FLAC__metadata_object_picture_set_data.restype = FLAC__bool + libflac.FLAC__metadata_object_picture_set_data.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), FLAC__byte_p,FLAC__uint32, FLAC__bool] + + def FLAC__metadata_object_picture_set_data(object, data, length, copy): + return libflac.FLAC__metadata_object_picture_set_data(object, mime_type, copy) + + libflac.FLAC__metadata_object_picture_is_legal.restype = FLAC__bool + libflac.FLAC__metadata_object_picture_is_legal.argtypes = [POINTER(FLAC__StreamMetadata_CueSheet_Track), c_char_p] + + def FLAC__metadata_object_picture_is_legal(object, violation): + return libflac.FLAC__metadata_object_picture_is_legal(object, violation) + + # /metadata + + # stream_decoder + + FLAC__StreamDecoderState = c_int + FLAC__StreamDecoderStateEnum = ["FLAC__STREAM_DECODER_SEARCH_FOR_METADATA", + "FLAC__STREAM_DECODER_READ_METADATA", + "FLAC__STREAM_DECODER_SEARCH_FOR_FRAME_SYNC", + "FLAC__STREAM_DECODER_READ_FRAME", + "FLAC__STREAM_DECODER_END_OF_STREAM", + "FLAC__STREAM_DECODER_OGG_ERROR", + "FLAC__STREAM_DECODER_SEEK_ERROR", + "FLAC__STREAM_DECODER_ABORTED", + "FLAC__STREAM_DECODER_MEMORY_ALLOCATION_ERROR", + "FLAC__STREAM_DECODER_UNINITIALIZED"] + + libflac.FLAC__StreamDecoderStateString.restype = c_char_p + libflac.FLAC__StreamDecoderStateString.argtypes = [] + + def FLAC__StreamDecoderStateString(): + return libflac.FLAC__StreamDecoderStateString() + + + FLAC__StreamDecoderInitStatus = c_int + FLAC__StreamDecoderInitStatusEnum = ["FLAC__STREAM_DECODER_INIT_STATUS_OK", + "FLAC__STREAM_DECODER_INIT_STATUS_UNSUPPORTED_CONTAINER", + "FLAC__STREAM_DECODER_INIT_STATUS_INVALID_CALLBACKS", + "FLAC__STREAM_DECODER_INIT_STATUS_MEMORY_ALLOCATION_ERROR", + "FLAC__STREAM_DECODER_INIT_STATUS_ERROR_OPENING_FILE", + "FLAC__STREAM_DECODER_INIT_STATUS_ALREADY_INITIALIZED"] + + libflac.FLAC__StreamDecoderInitStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderInitStatusString.argtypes = [] + + def FLAC__StreamDecoderInitStatusString(): + return libflac.FLAC__StreamDecoderInitStatusString() + + + FLAC__StreamDecoderReadStatus = c_int + FLAC__StreamDecoderReadStatusEnum = ["FLAC__STREAM_DECODER_READ_STATUS_CONTINUE", + "FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM", + "FLAC__STREAM_DECODER_READ_STATUS_ABORT"] + + libflac.FLAC__StreamDecoderReadStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderReadStatusString.argtypes = [] + + def FLAC__StreamDecoderReadStatusString(): + return libflac.FLAC__StreamDecoderReadStatusString() + + + FLAC__StreamDecoderSeekStatus = c_int + FLAC__StreamDecoderSeekStatusEnum = ["FLAC__STREAM_DECODER_SEEK_STATUS_OK", + "FLAC__STREAM_DECODER_SEEK_STATUS_ERROR", + "FLAC__STREAM_DECODER_SEEK_STATUS_UNSUPPORTED"] + + libflac.FLAC__StreamDecoderSeekStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderSeekStatusString.argtypes = [] + + def FLAC__StreamDecoderSeekStatusString(): + return libflac.FLAC__StreamDecoderSeekStatusString() + + + FLAC__StreamDecoderTellStatus = c_int + FLAC__StreamDecoderTellStatusEnum = ["FLAC__STREAM_DECODER_TELL_STATUS_OK", + "FLAC__STREAM_DECODER_TELL_STATUS_ERROR", + "FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED"] + + libflac.FLAC__StreamDecoderTellStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderTellStatusString.argtypes = [] + + def FLAC__StreamDecoderTellStatusString(): + return libflac.FLAC__StreamDecoderTellStatusString() + + + FLAC__StreamDecoderLengthStatus = c_int + FLAC__StreamDecoderLengthStatusEnum = ["FLAC__STREAM_DECODER_LENGTH_STATUS_OK", + "FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR", + "FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED"] + + libflac.FLAC__StreamDecoderLengthStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderLengthStatusString.argtypes = [] + + def FLAC__StreamDecoderLengthStatusString(): + return libflac.FLAC__StreamDecoderLengthStatusString() + + + FLAC__StreamDecoderWriteStatus = c_int + FLAC__StreamDecoderWriteStatusEnum = ["FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE", "FLAC__STREAM_DECODER_WRITE_STATUS_ABORT"] + + libflac.FLAC__StreamDecoderWriteStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderWriteStatusString.argtypes = [] + + def FLAC__StreamDecoderWriteStatusString(): + return libflac.FLAC__StreamDecoderWriteStatusString() + + FLAC__StreamDecoderErrorStatus = c_int + FLAC__StreamDecoderErrorStatusEnum = ["FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC", + "FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER", + "FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH", + "FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"] + + libflac.FLAC__StreamDecoderErrorStatusString.restype = c_char_p + libflac.FLAC__StreamDecoderErrorStatusString.argtypes = [] + + def FLAC__StreamDecoderErrorStatusString(): + return libflac.FLAC__StreamDecoderErrorStatusString() + + + + class FLAC__StreamDecoderProtected(Structure): + _fields_ = [("dummy", c_int)] + + class FLAC__StreamDecoderPrivate(Structure): + _fields_ = [("dummy", c_int)] + + class FLAC__StreamDecoder(Structure): + _fields_ = [("protected_", POINTER(FLAC__StreamDecoderProtected)), + ("private_", POINTER(FLAC__StreamDecoderPrivate))] + + FLAC__StreamDecoderReadCallback = CFUNCTYPE( + FLAC__StreamDecoderReadStatus, + POINTER(FLAC__StreamDecoder), + POINTER(FLAC__byte*0), + c_size_t_p, + c_void_p + ) + + FLAC__StreamDecoderSeekCallback = CFUNCTYPE( + FLAC__StreamDecoderSeekStatus, + POINTER(FLAC__StreamDecoder), + FLAC__uint64, + c_void_p + ) + + FLAC__StreamDecoderTellCallback = CFUNCTYPE( + FLAC__StreamDecoderTellStatus, + POINTER(FLAC__StreamDecoder), + FLAC__uint64_p, + c_void_p + ) + + FLAC__StreamDecoderLengthCallback = CFUNCTYPE( + FLAC__StreamDecoderLengthStatus, + POINTER(FLAC__StreamDecoder), + FLAC__uint64_p, + c_void_p + ) + + FLAC__StreamDecoderEofCallback = CFUNCTYPE( + FLAC__bool, + POINTER(FLAC__StreamDecoder), + c_void_p + ) + + FLAC__StreamDecoderWriteCallback = CFUNCTYPE( + FLAC__StreamDecoderWriteStatus, + POINTER(FLAC__StreamDecoder), + POINTER(FLAC__Frame), + POINTER(FLAC__int32_p*0), + c_void_p + ) + + FLAC__StreamDecoderMetadataCallback = CFUNCTYPE( + None, + POINTER(FLAC__StreamDecoder), + POINTER(FLAC__StreamMetadata), + c_void_p + ) + + FLAC__StreamDecoderErrorCallback = CFUNCTYPE( + None, + POINTER(FLAC__StreamDecoder), + FLAC__StreamDecoderErrorStatus, + c_void_p + ) + + + libflac.FLAC__stream_decoder_new.restype = POINTER(FLAC__StreamDecoder) + libflac.FLAC__stream_decoder_new.argtypes = [] + + def FLAC__stream_decoder_new(): + return libflac.FLAC__stream_decoder_new() + + libflac.FLAC__stream_decoder_delete.restype = None + libflac.FLAC__stream_decoder_delete.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_delete(decoder): + return libflac.FLAC__stream_decoder_delete(decoder) + + + libflac.FLAC__stream_decoder_set_ogg_serial_number.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_ogg_serial_number.argtypes = [POINTER(FLAC__StreamDecoder), c_long] + + def FLAC__stream_decoder_set_ogg_serial_number(decoder, serial_number): + return libflac.FLAC__stream_decoder_set_ogg_serial_number(decoder, serial_number) + + libflac.FLAC__stream_decoder_set_md5_checking.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_md5_checking.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__bool] + + def FLAC__stream_decoder_set_md5_checking(decoder, value): + return libflac.FLAC__stream_decoder_set_md5_checking(decoder, value) + + libflac.FLAC__stream_decoder_set_metadata_respond.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_metadata_respond.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__MetadataType] + + def FLAC__stream_decoder_set_metadata_respond(decoder, type): + return libflac.FLAC__stream_decoder_set_metadata_respond(decoder, type) + + libflac.FLAC__stream_decoder_set_metadata_respond_application.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_metadata_respond_application.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__byte*4] + + def FLAC__stream_decoder_set_metadata_respond_application(decoder, id): + return libflac.FLAC__stream_decoder_set_metadata_respond_application(decoder, id) + + libflac.FLAC__stream_decoder_set_metadata_respond_all.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_metadata_respond_all.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_set_metadata_respond_all(decoder): + return libflac.FLAC__stream_decoder_set_metadata_respond_all(decoder) + + libflac.FLAC__stream_decoder_set_metadata_ignore.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_metadata_ignore.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__MetadataType] + + def FLAC__stream_decoder_set_metadata_ignore(decoder, type): + return libflac.FLAC__stream_decoder_set_metadata_ignore(decoder, type) + + libflac.FLAC__stream_decoder_set_metadata_ignore_application.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_metadata_ignore_application.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__byte*4] + + def FLAC__stream_decoder_set_metadata_ignore_application(decoder, id): + return libflac.FLAC__stream_decoder_set_metadata_ignore_application(decoder, id) + + libflac.FLAC__stream_decoder_set_metadata_ignore_all.restype = FLAC__bool + libflac.FLAC__stream_decoder_set_metadata_ignore_all.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_set_metadata_ignore_all(decoder): + return libflac.FLAC__stream_decoder_set_metadata_ignore_all(decoder) + + libflac.FLAC__stream_decoder_get_state.restype = FLAC__StreamDecoderState + libflac.FLAC__stream_decoder_get_state.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_state(decoder): + return libflac.FLAC__stream_decoder_get_state(decoder) + + libflac.FLAC__stream_decoder_get_resolved_state_string.restype = c_char_p + libflac.FLAC__stream_decoder_get_resolved_state_string.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_resolved_state_string(decoder): + return libflac.FLAC__stream_decoder_get_resolved_state_string(decoder) + + libflac.FLAC__stream_decoder_get_md5_checking.restype = FLAC__bool + libflac.FLAC__stream_decoder_get_md5_checking.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_md5_checking(decoder): + return libflac.FLAC__stream_decoder_get_md5_checking(decoder) + + libflac.FLAC__stream_decoder_get_total_samples.restype = FLAC__uint64 + libflac.FLAC__stream_decoder_get_total_samples.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_total_samples(decoder): + return libflac.FLAC__stream_decoder_get_total_samples(decoder) + + libflac.FLAC__stream_decoder_get_channels.restype = c_uint + libflac.FLAC__stream_decoder_get_channels.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_channels(decoder): + return libflac.FLAC__stream_decoder_get_channels(decoder) + + libflac.FLAC__stream_decoder_get_channel_assignment.restype = FLAC__ChannelAssignment + libflac.FLAC__stream_decoder_get_channel_assignment.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_channel_assignment(decoder): + return libflac.FLAC__stream_decoder_get_channel_assignment(decoder) + + libflac.FLAC__stream_decoder_get_bits_per_sample.restype = c_uint + libflac.FLAC__stream_decoder_get_bits_per_sample.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_bits_per_sample(decoder): + return libflac.FLAC__stream_decoder_get_bits_per_sample(decoder) + + libflac.FLAC__stream_decoder_get_sample_rate.restype = c_uint + libflac.FLAC__stream_decoder_get_sample_rate.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_sample_rate(decoder): + return libflac.FLAC__stream_decoder_get_sample_rate(decoder) + + libflac.FLAC__stream_decoder_get_blocksize.restype = c_uint + libflac.FLAC__stream_decoder_get_blocksize.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_get_blocksize(decoder): + return libflac.FLAC__stream_decoder_get_blocksize(decoder) + + libflac.FLAC__stream_decoder_get_decode_position.restype = FLAC__bool + libflac.FLAC__stream_decoder_get_decode_position.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__uint64_p] + + def FLAC__stream_decoder_get_decode_position(decoder, position): + return libflac.FLAC__stream_decoder_get_decode_position(decoder, position) + + libflac.FLAC__stream_decoder_init_stream.restype = FLAC__StreamDecoderInitStatus + libflac.FLAC__stream_decoder_init_stream.argtypes = [POINTER(FLAC__StreamDecoder), + FLAC__StreamDecoderReadCallback, + FLAC__StreamDecoderSeekCallback, + FLAC__StreamDecoderTellCallback, + FLAC__StreamDecoderLengthCallback, + FLAC__StreamDecoderEofCallback, + FLAC__StreamDecoderWriteCallback, + FLAC__StreamDecoderMetadataCallback, + FLAC__StreamDecoderErrorCallback, + c_void_p] + + def FLAC__stream_decoder_init_stream(decoder, read_callback, seek_callback, tell_callback, length_callback, eof_callback, write_callback, metadata_callback, error_callback, client_data): + return libflac.FLAC__stream_decoder_init_stream(decoder, read_callback, seek_callback, tell_callback, length_callback, eof_callback, write_callback, metadata_callback, error_callback, client_data) + + + libflac.FLAC__stream_decoder_init_ogg_stream.restype = FLAC__StreamDecoderInitStatus + libflac.FLAC__stream_decoder_init_ogg_stream.argtypes = [POINTER(FLAC__StreamDecoder), + FLAC__StreamDecoderReadCallback, + FLAC__StreamDecoderSeekCallback, + FLAC__StreamDecoderTellCallback, + FLAC__StreamDecoderLengthCallback, + FLAC__StreamDecoderEofCallback, + FLAC__StreamDecoderWriteCallback, + FLAC__StreamDecoderMetadataCallback, + FLAC__StreamDecoderErrorCallback, + c_void_p] + + def FLAC__stream_decoder_init_ogg_stream(decoder, read_callback, seek_callback, tell_callback, length_callback, eof_callback, write_callback, metadata_callback, error_callback, client_data): + return libflac.FLAC__stream_decoder_init_ogg_stream(decoder, read_callback, seek_callback, tell_callback, length_callback, eof_callback, write_callback, metadata_callback, error_callback, client_data) + + libflac.FLAC__stream_decoder_init_file.restype = FLAC__StreamDecoderInitStatus + libflac.FLAC__stream_decoder_init_file.argtypes = [POINTER(FLAC__StreamDecoder), + c_char_p, + FLAC__StreamDecoderWriteCallback, + FLAC__StreamDecoderMetadataCallback, + FLAC__StreamDecoderErrorCallback, + c_void_p] + + def FLAC__stream_decoder_init_file(decoder, filename, write_callback, metadata_callback, error_callback, client_data): + return libflac.FLAC__stream_decoder_init_file(decoder, filename, write_callback, metadata_callback, error_callback, client_data) + + libflac.FLAC__stream_decoder_init_ogg_file.restype = FLAC__StreamDecoderInitStatus + libflac.FLAC__stream_decoder_init_ogg_file.argtypes = [POINTER(FLAC__StreamDecoder), + c_char_p, + FLAC__StreamDecoderWriteCallback, + FLAC__StreamDecoderMetadataCallback, + FLAC__StreamDecoderErrorCallback, + c_void_p] + + def FLAC__stream_decoder_init_ogg_file(decoder, filename, write_callback, metadata_callback, error_callback, client_data): + return libflac.FLAC__stream_decoder_init_ogg_file(decoder, filename, write_callback, metadata_callback, error_callback, client_data) + + libflac.FLAC__stream_decoder_finish.restype = FLAC__bool + libflac.FLAC__stream_decoder_finish.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_finish(decoder): + return libflac.FLAC__stream_decoder_finish(decoder) + + libflac.FLAC__stream_decoder_flush.restype = FLAC__bool + libflac.FLAC__stream_decoder_flush.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_flush(decoder): + return libflac.FLAC__stream_decoder_flush(decoder) + + libflac.FLAC__stream_decoder_reset.restype = FLAC__bool + libflac.FLAC__stream_decoder_reset.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_reset(decoder): + return libflac.FLAC__stream_decoder_reset(decoder) + + libflac.FLAC__stream_decoder_process_single.restype = FLAC__bool + libflac.FLAC__stream_decoder_process_single.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_process_single(decoder): + return libflac.FLAC__stream_decoder_process_single(decoder) + + libflac.FLAC__stream_decoder_process_until_end_of_metadata.restype = FLAC__bool + libflac.FLAC__stream_decoder_process_until_end_of_metadata.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_process_until_end_of_metadata(decoder): + return libflac.FLAC__stream_decoder_process_until_end_of_metadata(decoder) + + libflac.FLAC__stream_decoder_process_until_end_of_stream.restype = FLAC__bool + libflac.FLAC__stream_decoder_process_until_end_of_stream.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_process_until_end_of_stream(decoder): + return libflac.FLAC__stream_decoder_process_until_end_of_stream(decoder) + + libflac.FLAC__stream_decoder_skip_single_frame.restype = FLAC__bool + libflac.FLAC__stream_decoder_skip_single_frame.argtypes = [POINTER(FLAC__StreamDecoder)] + + def FLAC__stream_decoder_skip_single_frame(decoder): + return libflac.FLAC__stream_decoder_skip_single_frame(decoder) + + libflac.FLAC__stream_decoder_seek_absolute.restype = FLAC__bool + libflac.FLAC__stream_decoder_seek_absolute.argtypes = [POINTER(FLAC__StreamDecoder), FLAC__uint64] + + def FLAC__stream_decoder_seek_absolute(decoder, sample): + return libflac.FLAC__stream_decoder_seek_absolute(decoder, sample) + + # /stream_decoder + + # stream_encoder + + FLAC__StreamEncoderState = c_int + + libflac.FLAC__StreamEncoderStateString.restype = c_char_p + libflac.FLAC__StreamEncoderStateString.argtypes = [] + + def FLAC__StreamEncoderStateString(): + return libflac.FLAC__StreamEncoderStateString() + + + FLAC__StreamEncoderInitStatus = c_int + + libflac.FLAC__StreamEncoderInitStatusString.restype = c_char_p + libflac.FLAC__StreamEncoderInitStatusString.argtypes = [] + + def FLAC__StreamEncoderInitStatusString(): + return libflac.FLAC__StreamEncoderInitStatusString() + + + FLAC__StreamEncoderReadStatus = c_int + + libflac.FLAC__StreamEncoderReadStatusString.restype = c_char_p + libflac.FLAC__StreamEncoderReadStatusString.argtypes = [] + + def FLAC__StreamEncoderReadStatusString(): + return libflac.FLAC__StreamEncoderReadStatusString() + + + FLAC__StreamEncoderWriteStatus = c_int + + libflac.FLAC__StreamEncoderWriteStatusString.restype = c_char_p + libflac.FLAC__StreamEncoderWriteStatusString.argtypes = [] + + def FLAC__StreamEncoderWriteStatusString(): + return libflac.FLAC__StreamEncoderWriteStatusString() + + + FLAC__StreamEncoderSeekStatus = c_int + + libflac.FLAC__StreamEncoderSeekStatusString.restype = c_char_p + libflac.FLAC__StreamEncoderSeekStatusString.argtypes = [] + + def FLAC__StreamEncoderSeekStatusString(): + return libflac.FLAC__StreamEncoderSeekStatusString() + + + FLAC__StreamEncoderTellStatus = c_int + + libflac.FLAC__StreamEncoderTellStatusString.restype = c_char_p + libflac.FLAC__StreamEncoderTellStatusString.argtypes = [] + + def FLAC__StreamEncoderTellStatusString(): + return libflac.FLAC__StreamEncoderTellStatusString() + + + class FLAC__StreamEncoderProtected(Structure): + _fields_ = [("dummy", c_int)] + + class FLAC__StreamEncoderPrivate(Structure): + _fields_ = [("dummy", c_int)] + + class FLAC__StreamEncoder(Structure): + _fields_ = [("protected_", POINTER(FLAC__StreamEncoderProtected)), + ("private_", POINTER(FLAC__StreamEncoderPrivate))] + + FLAC__StreamEncoderReadCallback = CFUNCTYPE(FLAC__StreamEncoderReadStatus, POINTER(FLAC__StreamEncoder), POINTER(FLAC__byte*0), c_size_t_p, c_void_p) + + FLAC__StreamEncoderWriteCallback = CFUNCTYPE(FLAC__StreamEncoderWriteStatus, POINTER(FLAC__StreamEncoder), POINTER(FLAC__byte*0), c_size_t, c_uint, c_uint, c_void_p) + + FLAC__StreamEncoderSeekCallback = CFUNCTYPE(FLAC__StreamEncoderSeekStatus, POINTER(FLAC__StreamEncoder), FLAC__uint64, c_void_p) + + FLAC__StreamEncoderTellCallback = CFUNCTYPE(FLAC__StreamEncoderTellStatus, POINTER(FLAC__StreamEncoder), FLAC__uint64_p, c_void_p) + + FLAC__StreamEncoderMetadataCallback = CFUNCTYPE(None, POINTER(FLAC__StreamEncoder), POINTER(FLAC__StreamMetadata), c_void_p) + + FLAC__StreamEncoderProgressCallback = CFUNCTYPE(None, POINTER(FLAC__StreamEncoder), FLAC__uint64,FLAC__uint64, c_uint, c_uint, c_void_p) + + + libflac.FLAC__stream_encoder_new.restype = POINTER(FLAC__StreamEncoder) + libflac.FLAC__stream_encoder_new.argtypes = [] + + def FLAC__stream_encoder_new(): + return libflac.FLAC__stream_encoder_new() + + libflac.FLAC__stream_encoder_delete.restype = None + libflac.FLAC__stream_encoder_delete.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_delete(encoder): + return libflac.FLAC__stream_encoder_delete(encoder) + + + libflac.FLAC__stream_encoder_set_ogg_serial_number.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_ogg_serial_number.argtypes = [POINTER(FLAC__StreamEncoder), c_long] + + def FLAC__stream_encoder_set_ogg_serial_number(encoder, serial_number): + return libflac.FLAC__stream_encoder_set_ogg_serial_number(encoder, serial_number) + + libflac.FLAC__stream_encoder_set_verify.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_verify.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_verify(encoder, value): + return libflac.FLAC__stream_encoder_set_verify(encoder, value) + + libflac.FLAC__stream_encoder_set_streamable_subset.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_streamable_subset.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_streamable_subset(encoder, value): + return libflac.FLAC__stream_encoder_set_streamable_subset(encoder, value) + + libflac.FLAC__stream_encoder_set_channels.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_channels.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_channels(encoder, value): + return libflac.FLAC__stream_encoder_set_channels(encoder, value) + + libflac.FLAC__stream_encoder_set_bits_per_sample.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_bits_per_sample.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_bits_per_sample(encoder, value): + return libflac.FLAC__stream_encoder_set_bits_per_sample(encoder, value) + + libflac.FLAC__stream_encoder_set_sample_rate.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_sample_rate.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_sample_rate(encoder, value): + return libflac.FLAC__stream_encoder_set_sample_rate(encoder, value) + + libflac.FLAC__stream_encoder_set_compression_level.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_compression_level.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_compression_level(encoder, value): + return libflac.FLAC__stream_encoder_set_compression_level(encoder, value) + + libflac.FLAC__stream_encoder_set_blocksize.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_blocksize.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_blocksize(encoder, value): + return libflac.FLAC__stream_encoder_set_blocksize(encoder, value) + + libflac.FLAC__stream_encoder_set_do_mid_side_stereo.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_do_mid_side_stereo.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_do_mid_side_stereo(encoder, value): + return libflac.FLAC__stream_encoder_set_do_mid_side_stereo(encoder, value) + + libflac.FLAC__stream_encoder_set_loose_mid_side_stereo.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_loose_mid_side_stereo.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_loose_mid_side_stereo(encoder, value): + return libflac.FLAC__stream_encoder_set_loose_mid_side_stereo(encoder, value) + + libflac.FLAC__stream_encoder_set_apodization.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_apodization.argtypes = [POINTER(FLAC__StreamEncoder), c_char_p] + + def FLAC__stream_encoder_set_apodization(encoder, specification): + return libflac.FLAC__stream_encoder_set_apodization(encoder, specification) + + libflac.FLAC__stream_encoder_set_max_lpc_order.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_max_lpc_order.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_max_lpc_order(encoder, value): + return libflac.FLAC__stream_encoder_set_max_lpc_order(encoder, value) + + libflac.FLAC__stream_encoder_set_qlp_coeff_precision.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_qlp_coeff_precision.argtypes = [POINTER(FLAC__StreamEncoder), c_uint] + + def FLAC__stream_encoder_set_qlp_coeff_precision(encoder, value): + return libflac.FLAC__stream_encoder_set_qlp_coeff_precision(encoder, value) + + libflac.FLAC__stream_encoder_set_do_qlp_coeff_prec_search.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_do_qlp_coeff_prec_search.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_do_qlp_coeff_prec_search(encoder, value): + return libflac.FLAC__stream_encoder_set_do_qlp_coeff_prec_search(encoder, value) + + libflac.FLAC__stream_encoder_set_do_escape_coding.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_do_escape_coding.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_do_escape_coding(encoder, value): + return libflac.FLAC__stream_encoder_set_do_escape_coding(encoder, value) + + libflac.FLAC__stream_encoder_set_do_exhaustive_model_search.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_do_exhaustive_model_search.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_do_exhaustive_model_search(encoder, value): + return libflac.FLAC__stream_encoder_set_do_exhaustive_model_search(encoder, value) + + libflac.FLAC__stream_encoder_set_min_residual_partition_order.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_min_residual_partition_order.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_min_residual_partition_order(encoder, value): + return libflac.FLAC__stream_encoder_set_min_residual_partition_order(encoder, value) + + libflac.FLAC__stream_encoder_set_max_residual_partition_order.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_max_residual_partition_order.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_max_residual_partition_order(encoder, value): + return libflac.FLAC__stream_encoder_set_max_residual_partition_order(encoder, value) + + libflac.FLAC__stream_encoder_set_rice_parameter_search_dist.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_rice_parameter_search_dist.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__bool] + + def FLAC__stream_encoder_set_rice_parameter_search_dist(encoder, value): + return libflac.FLAC__stream_encoder_set_rice_parameter_search_dist(encoder, value) + + libflac.FLAC__stream_encoder_set_total_samples_estimate.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_total_samples_estimate.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__uint64] + + def FLAC__stream_encoder_set_total_samples_estimate(encoder, value): + return libflac.FLAC__stream_encoder_set_total_samples_estimate(encoder, value) + + libflac.FLAC__stream_encoder_set_metadata.restype = FLAC__bool + libflac.FLAC__stream_encoder_set_metadata.argtypes = [POINTER(FLAC__StreamEncoder), POINTER(POINTER(FLAC__StreamMetadata)), c_uint] + + def FLAC__stream_encoder_set_metadata(encoder, metadata, num_blocks): + return libflac.FLAC__stream_encoder_set_metadata(encoder, metadata, num_blocks) + + libflac.FLAC__stream_encoder_get_state.restype = FLAC__StreamEncoderState + libflac.FLAC__stream_encoder_get_state.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_state(encoder): + return libflac.FLAC__stream_encoder_get_state(encoder) + + libflac.FLAC__stream_encoder_get_verify_decoder_state.restype = FLAC__StreamEncoderState + libflac.FLAC__stream_encoder_get_verify_decoder_state.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_verify_decoder_state(encoder): + return libflac.FLAC__stream_encoder_get_verify_decoder_state(encoder) + + libflac.FLAC__stream_encoder_get_resolved_state_string.restype = c_char_p + libflac.FLAC__stream_encoder_get_resolved_state_string.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_resolved_state_string(encoder): + return libflac.FLAC__stream_encoder_get_resolved_state_string(encoder) + + libflac.FLAC__stream_encoder_get_verify_decoder_error_stats.restype = None + libflac.FLAC__stream_encoder_get_verify_decoder_error_stats.argtypes = [POINTER(FLAC__StreamEncoder), FLAC__uint64_p, c_uint_p, c_uint_p, c_uint_p, FLAC__int32_p, FLAC__int32_p] + + def FLAC__stream_encoder_get_verify_decoder_error_stats(encoder, absolute_sample, frame_number, channel, sample, expected, got): + return libflac.FLAC__stream_encoder_get_verify_decoder_error_stats(encoder, absolute_sample, frame_number, channel, sample, expected, got) + + libflac.FLAC__stream_encoder_get_verify.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_verify.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_verify(encoder): + return libflac.FLAC__stream_encoder_get_verify(encoder) + + libflac.FLAC__stream_encoder_get_streamable_subset.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_streamable_subset.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_streamable_subset(encoder): + return libflac.FLAC__stream_encoder_get_streamable_subset(encoder) + + libflac.FLAC__stream_encoder_get_channels.restype = c_uint + libflac.FLAC__stream_encoder_get_channels.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_channels(encoder): + return libflac.FLAC__stream_encoder_get_channels(encoder) + + libflac.FLAC__stream_encoder_get_bits_per_sample.restype = c_uint + libflac.FLAC__stream_encoder_get_bits_per_sample.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_bits_per_sample(encoder): + return libflac.FLAC__stream_encoder_get_bits_per_sample(encoder) + + libflac.FLAC__stream_encoder_get_sample_rate.restype = c_uint + libflac.FLAC__stream_encoder_get_sample_rate.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_sample_rate(encoder): + return libflac.FLAC__stream_encoder_get_sample_rate(encoder) + + libflac.FLAC__stream_encoder_get_blocksize.restype = c_uint + libflac.FLAC__stream_encoder_get_blocksize.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_blocksize(encoder): + return libflac.FLAC__stream_encoder_get_blocksize(encoder) + + libflac.FLAC__stream_encoder_get_do_mid_side_stereo.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_do_mid_side_stereo.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_do_mid_side_stereo(encoder): + return libflac.FLAC__stream_encoder_get_do_mid_side_stereo(encoder) + + libflac.FLAC__stream_encoder_get_loose_mid_side_stereo.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_loose_mid_side_stereo.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_loose_mid_side_stereo(encoder): + return libflac.FLAC__stream_encoder_get_loose_mid_side_stereo(encoder) + + libflac.FLAC__stream_encoder_get_max_lpc_order.restype = c_uint + libflac.FLAC__stream_encoder_get_max_lpc_order.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_max_lpc_order(encoder): + return libflac.FLAC__stream_encoder_get_max_lpc_order(encoder) + + libflac.FLAC__stream_encoder_get_qlp_coeff_precision.restype = c_uint + libflac.FLAC__stream_encoder_get_qlp_coeff_precision.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_qlp_coeff_precision(encoder): + return libflac.FLAC__stream_encoder_get_qlp_coeff_precision(encoder) + + libflac.FLAC__stream_encoder_get_do_qlp_coeff_prec_search.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_do_qlp_coeff_prec_search.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_do_qlp_coeff_prec_search(encoder): + return libflac.FLAC__stream_encoder_get_do_qlp_coeff_prec_search(encoder) + + libflac.FLAC__stream_encoder_get_do_escape_coding.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_do_escape_coding.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_do_escape_coding(encoder): + return libflac.FLAC__stream_encoder_get_do_escape_coding(encoder) + + libflac.FLAC__stream_encoder_get_do_exhaustive_model_search.restype = FLAC__bool + libflac.FLAC__stream_encoder_get_do_exhaustive_model_search.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_do_exhaustive_model_search(encoder): + return libflac.FLAC__stream_encoder_get_do_exhaustive_model_search(encoder) + + libflac.FLAC__stream_encoder_get_min_residual_partition_order.restype = c_uint + libflac.FLAC__stream_encoder_get_min_residual_partition_order.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_min_residual_partition_order(encoder): + return libflac.FLAC__stream_encoder_get_min_residual_partition_order(encoder) + + libflac.FLAC__stream_encoder_get_max_residual_partition_order.restype = c_uint + libflac.FLAC__stream_encoder_get_max_residual_partition_order.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_max_residual_partition_order(encoder): + return libflac.FLAC__stream_encoder_get_max_residual_partition_order(encoder) + + libflac.FLAC__stream_encoder_get_rice_parameter_search_dist.restype = c_uint + libflac.FLAC__stream_encoder_get_rice_parameter_search_dist.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_rice_parameter_search_dist(encoder): + return libflac.FLAC__stream_encoder_get_rice_parameter_search_dist(encoder) + + libflac.FLAC__stream_encoder_get_total_samples_estimate.restype = FLAC__uint64 + libflac.FLAC__stream_encoder_get_total_samples_estimate.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_get_total_samples_estimate(encoder): + return libflac.FLAC__stream_encoder_get_total_samples_estimate(encoder) + + libflac.FLAC__stream_encoder_init_stream.restype = FLAC__StreamEncoderInitStatus + libflac.FLAC__stream_encoder_init_stream.argtypes = [POINTER(FLAC__StreamEncoder), + FLAC__StreamEncoderWriteCallback, + FLAC__StreamEncoderSeekCallback, + FLAC__StreamEncoderTellCallback, + FLAC__StreamEncoderMetadataCallback, + c_void_p] + + def FLAC__stream_encoder_init_stream(encoder, write_callback, seek_callback, tell_callback, metadata_callback,client_data): + return libflac.FLAC__stream_encoder_init_stream(encoder, write_callback, seek_callback, tell_callback, metadata_callback,client_data) + + libflac.FLAC__stream_encoder_init_ogg_stream.restype = FLAC__StreamEncoderInitStatus + libflac.FLAC__stream_encoder_init_ogg_stream.argtypes = [POINTER(FLAC__StreamEncoder), + FLAC__StreamEncoderReadCallback, + FLAC__StreamEncoderWriteCallback, + FLAC__StreamEncoderSeekCallback, + FLAC__StreamEncoderTellCallback, + FLAC__StreamEncoderMetadataCallback, + c_void_p] + + def FLAC__stream_encoder_init_ogg_stream(encoder, read_callback, write_callback, seek_callback, tell_callback, metadata_callback,client_data): + return libflac.FLAC__stream_encoder_init_ogg_stream(encoder, read_callback, write_callback, seek_callback, tell_callback, metadata_callback,client_data) + + libflac.FLAC__stream_encoder_init_file.restype = FLAC__StreamEncoderInitStatus + libflac.FLAC__stream_encoder_init_file.argtypes = [POINTER(FLAC__StreamEncoder), + c_char_p, + FLAC__StreamEncoderProgressCallback, + c_void_p] + + def FLAC__stream_encoder_init_file(encoder, filename, progress_callback,client_data): + return libflac.FLAC__stream_encoder_init_file(encoder, filename, progress_callback,client_data) + + + libflac.FLAC__stream_encoder_init_ogg_file.restype = FLAC__StreamEncoderInitStatus + libflac.FLAC__stream_encoder_init_ogg_file.argtypes = [POINTER(FLAC__StreamEncoder), + c_char_p, + FLAC__StreamEncoderProgressCallback, + c_void_p] + + def FLAC__stream_encoder_init_ogg_file(encoder, filename, progress_callback,client_data): + return libflac.FLAC__stream_encoder_init_ogg_file(encoder, filename, progress_callback,client_data) + + libflac.FLAC__stream_encoder_finish.restype = FLAC__bool + libflac.FLAC__stream_encoder_finish.argtypes = [POINTER(FLAC__StreamEncoder)] + + def FLAC__stream_encoder_finish(encoder): + return libflac.FLAC__stream_encoder_finish(encoder) + + libflac.FLAC__stream_encoder_process.restype = FLAC__bool + libflac.FLAC__stream_encoder_process.argtypes = [POINTER(FLAC__StreamEncoder), POINTER(FLAC__int32_p*0), c_uint] + + def FLAC__stream_encoder_process(encoder, buffer, samples): + return libflac.FLAC__stream_encoder_process(encoder, buffer, samples) + + libflac.FLAC__stream_encoder_process_interleaved.restype = FLAC__bool + libflac.FLAC__stream_encoder_process_interleaved.argtypes = [POINTER(FLAC__StreamEncoder), POINTER(FLAC__int32*0), c_uint] + + def FLAC__stream_encoder_process_interleaved(encoder, buffer, samples): + return libflac.FLAC__stream_encoder_process_interleaved(encoder, buffer, samples) + + # /stream_encoder diff --git a/LXST/Codecs/libs/pyogg/flac_file.py b/LXST/Codecs/libs/pyogg/flac_file.py new file mode 100644 index 0000000..7e97ca7 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/flac_file.py @@ -0,0 +1,114 @@ +import ctypes +from itertools import chain + +from . import flac +from .audio_file import AudioFile +from .pyogg_error import PyOggError + +def _to_char_p(string): + try: + return ctypes.c_char_p(string.encode("utf-8")) + except: + return ctypes.c_char_p(string) + +def _resize_array(array, new_size): + return (array._type_*new_size).from_address(ctypes.addressof(array)) + + +class FlacFile(AudioFile): + def write_callback(self, decoder, frame, buffer, client_data): + multi_channel_buf = _resize_array(buffer.contents, self.channels) + arr_size = frame.contents.header.blocksize + if frame.contents.header.channels >= 2: + arrays = [] + for i in range(frame.contents.header.channels): + arr = ctypes.cast(multi_channel_buf[i], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents + arrays.append(arr[:]) + + arr = list(chain.from_iterable(zip(*arrays))) + + self.buffer[self.buffer_pos : self.buffer_pos + len(arr)] = arr[:] + self.buffer_pos += len(arr) + + else: + arr = ctypes.cast(multi_channel_buf[0], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents + self.buffer[self.buffer_pos : self.buffer_pos + arr_size] = arr[:] + self.buffer_pos += arr_size + return 0 + + def metadata_callback(self,decoder, metadata, client_data): + if not self.buffer: + self.total_samples = metadata.contents.data.stream_info.total_samples + self.channels = metadata.contents.data.stream_info.channels + Buffer = flac.FLAC__int16*(self.total_samples * self.channels) + self.buffer = Buffer() + self.frequency = metadata.contents.data.stream_info.sample_rate + + def error_callback(self,decoder, status, client_data): + raise PyOggError("An error occured during the process of decoding. Status enum: {}".format(flac.FLAC__StreamDecoderErrorStatusEnum[status])) + + def __init__(self, path): + self.decoder = flac.FLAC__stream_decoder_new() + + self.client_data = ctypes.c_void_p() + + #: Number of channels in audio file. + self.channels = None + + #: Number of samples per second (per channel). For + # example, 44100. + self.frequency = None + + self.total_samples = None + + #: Raw PCM data from audio file. + self.buffer = None + + self.buffer_pos = 0 + + write_callback_ = flac.FLAC__StreamDecoderWriteCallback(self.write_callback) + + metadata_callback_ = flac.FLAC__StreamDecoderMetadataCallback(self.metadata_callback) + + error_callback_ = flac.FLAC__StreamDecoderErrorCallback(self.error_callback) + + init_status = flac.FLAC__stream_decoder_init_file( + self.decoder, + _to_char_p(path), # This will have an issue with Unicode filenames + write_callback_, + metadata_callback_, + error_callback_, + self.client_data + ) + + if init_status: # error + error = flac.FLAC__StreamDecoderInitStatusEnum[init_status] + raise PyOggError( + "An error occured when trying to open '{}': {}".format(path, error) + ) + + metadata_status = (flac.FLAC__stream_decoder_process_until_end_of_metadata(self.decoder)) + if not metadata_status: # error + raise PyOggError("An error occured when trying to decode the metadata of {}".format(path)) + + stream_status = (flac.FLAC__stream_decoder_process_until_end_of_stream(self.decoder)) + if not stream_status: # error + raise PyOggError("An error occured when trying to decode the audio stream of {}".format(path)) + + flac.FLAC__stream_decoder_finish(self.decoder) + + #: Length of buffer + self.buffer_length = len(self.buffer) + + self.bytes_per_sample = ctypes.sizeof(flac.FLAC__int16) # See definition of Buffer in metadata_callback() + + # Cast buffer to one-dimensional array of chars + CharBuffer = ( + ctypes.c_byte * + (self.bytes_per_sample * len(self.buffer)) + ) + self.buffer = CharBuffer.from_buffer(self.buffer) + + # FLAC audio is always signed. See + # https://xiph.org/flac/api/group__flac__stream__decoder.html#gaf98a4f9e2cac5747da6018c3dfc8dde1 + self.signed = True diff --git a/LXST/Codecs/libs/pyogg/flac_file_stream.py b/LXST/Codecs/libs/pyogg/flac_file_stream.py new file mode 100644 index 0000000..f832c31 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/flac_file_stream.py @@ -0,0 +1,141 @@ +import ctypes +from itertools import chain + +from . import flac +from .pyogg_error import PyOggError + +def _to_char_p(string): + try: + return ctypes.c_char_p(string.encode("utf-8")) + except: + return ctypes.c_char_p(string) + +def _resize_array(array, new_size): + return (array._type_*new_size).from_address(ctypes.addressof(array)) + + +class FlacFileStream: + def write_callback(self,decoder, frame, buffer, client_data): + multi_channel_buf = _resize_array(buffer.contents, self.channels) + arr_size = frame.contents.header.blocksize + if frame.contents.header.channels >= 2: + arrays = [] + for i in range(frame.contents.header.channels): + arr = ctypes.cast(multi_channel_buf[i], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents + arrays.append(arr[:]) + + arr = list(chain.from_iterable(zip(*arrays))) + + self.buffer = (flac.FLAC__int16*len(arr))(*arr) + self.bytes_written = len(arr) * 2 + + else: + arr = ctypes.cast(multi_channel_buf[0], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents + self.buffer = (flac.FLAC__int16*len(arr))(*arr[:]) + self.bytes_written = arr_size * 2 + return 0 + + def metadata_callback(self,decoder, metadata, client_data): + self.total_samples = metadata.contents.data.stream_info.total_samples + self.channels = metadata.contents.data.stream_info.channels + self.frequency = metadata.contents.data.stream_info.sample_rate + + def error_callback(self,decoder, status, client_data): + raise PyOggError("An error occured during the process of decoding. Status enum: {}".format(flac.FLAC__StreamDecoderErrorStatusEnum[status])) + + def __init__(self, path): + self.decoder = flac.FLAC__stream_decoder_new() + + self.client_data = ctypes.c_void_p() + + #: Number of channels in audio file. + self.channels = None + + #: Number of samples per second (per channel). For + # example, 44100. + self.frequency = None + + self.total_samples = None + + self.buffer = None + + self.bytes_written = None + + self.write_callback_ = flac.FLAC__StreamDecoderWriteCallback(self.write_callback) + + self.metadata_callback_ = flac.FLAC__StreamDecoderMetadataCallback(self.metadata_callback) + + self.error_callback_ = flac.FLAC__StreamDecoderErrorCallback(self.error_callback) + + init_status = flac.FLAC__stream_decoder_init_file(self.decoder, + _to_char_p(path), + self.write_callback_, + self.metadata_callback_, + self.error_callback_, + self.client_data) + + if init_status: # error + raise PyOggError("An error occured when trying to open '{}': {}".format(path, flac.FLAC__StreamDecoderInitStatusEnum[init_status])) + + metadata_status = (flac.FLAC__stream_decoder_process_until_end_of_metadata(self.decoder)) + if not metadata_status: # error + raise PyOggError("An error occured when trying to decode the metadata of {}".format(path)) + + #: Bytes per sample + self.bytes_per_sample = 2 + + def get_buffer(self): + """Returns the buffer. + + Returns buffer (a bytes object) or None if all data has + been read from the file. + + """ + # Attempt to read a single frame of audio + stream_status = (flac.FLAC__stream_decoder_process_single(self.decoder)) + if not stream_status: # error + raise PyOggError("An error occured when trying to decode the audio stream of {}".format(path)) + + # Check if we encountered the end of the stream + if (flac.FLAC__stream_decoder_get_state(self.decoder) == 4): # end of stream + return None + + buffer_as_bytes = bytes(self.buffer) + return buffer_as_bytes + + def clean_up(self): + flac.FLAC__stream_decoder_finish(self.decoder) + + def get_buffer_as_array(self): + """Provides the buffer as a NumPy array. + + Note that the underlying data type is 16-bit signed + integers. + + Does not copy the underlying data, so the returned array + should either be processed or copied before the next call + to get_buffer() or get_buffer_as_array(). + + """ + import numpy # type: ignore + + # Read the next samples from the stream + buf = self.get_buffer() + + # Check if we've come to the end of the stream + if buf is None: + return None + + # Convert the bytes buffer to a NumPy array + array = numpy.frombuffer( + buf, + dtype=numpy.int16 + ) + + # Reshape the array + return array.reshape( + (len(buf) + // self.bytes_per_sample + // self.channels, + self.channels) + ) diff --git a/LXST/Codecs/libs/pyogg/library_loader.py b/LXST/Codecs/libs/pyogg/library_loader.py new file mode 100644 index 0000000..711b1ba --- /dev/null +++ b/LXST/Codecs/libs/pyogg/library_loader.py @@ -0,0 +1,147 @@ +import ctypes +import ctypes.util +import os +import sys +import platform +from typing import ( + Optional, + Dict, + List +) + +_here = os.path.dirname(__file__) + +class ExternalLibraryError(Exception): + pass + +architecture = platform.architecture()[0] + +_windows_styles = ["{}", "lib{}", "lib{}_dynamic", "{}_dynamic"] + +_other_styles = ["{}", "lib{}"] + +if architecture == "32bit": + for arch_style in ["32bit", "32" "86", "win32", "x86", "_x86", "_32", "_win32", "_32bit"]: + for style in ["{}", "lib{}"]: + _windows_styles.append(style.format("{}"+arch_style)) + +elif architecture == "64bit": + for arch_style in ["64bit", "64" "86_64", "amd64", "win_amd64", "x86_64", "_x86_64", "_64", "_amd64", "_64bit"]: + for style in ["{}", "lib{}"]: + _windows_styles.append(style.format("{}"+arch_style)) + + +run_tests = lambda lib, tests: [f(lib) for f in tests] + +# Get the appropriate directory for the shared libraries depending +# on the current platform and architecture +platform_ = platform.system() +lib_dir = None +if platform_ == "Darwin": + lib_dir = "libs/macos" +elif platform_ == "Windows": + if architecture == "32bit": + lib_dir = "libs/win32" + elif architecture == "64bit": + lib_dir = "libs/win_amd64" + + +class Library: + @staticmethod + def load(names: Dict[str, str], paths: Optional[List[str]] = None, tests = []) -> Optional[ctypes.CDLL]: + lib = InternalLibrary.load(names, tests) + if lib is None: + lib = ExternalLibrary.load(names["external"], paths, tests) + return lib + + +class InternalLibrary: + @staticmethod + def load(names: Dict[str, str], tests) -> Optional[ctypes.CDLL]: + # If we do not have a library directory, give up immediately + if lib_dir is None: + return None + + # Get the appropriate library filename given the platform + try: + name = names[platform_] + except KeyError: + return None + + # Attempt to load the library from here + path = _here + "/" + lib_dir + "/" + name + try: + lib = ctypes.CDLL(path) + except OSError as e: + return None + + # Check that the library passes the tests + if tests and all(run_tests(lib, tests)): + return lib + + # Library failed tests + return None + +# Cache of libraries that have already been loaded +_loaded_libraries: Dict[str, ctypes.CDLL] = {} + +class ExternalLibrary: + @staticmethod + def load(name, paths = None, tests = []): + if name in _loaded_libraries: + return _loaded_libraries[name] + if sys.platform == "win32": + lib = ExternalLibrary.load_windows(name, paths, tests) + _loaded_libraries[name] = lib + return lib + else: + lib = ExternalLibrary.load_other(name, paths, tests) + _loaded_libraries[name] = lib + return lib + + @staticmethod + def load_other(name, paths = None, tests = []): + os.environ["PATH"] += ";" + ";".join((os.getcwd(), _here)) + if paths: os.environ["PATH"] += ";" + ";".join(paths) + + for style in _other_styles: + candidate = style.format(name) + library = ctypes.util.find_library(candidate) + if library: + try: + lib = ctypes.CDLL(library) + if tests and all(run_tests(lib, tests)): + return lib + except: + pass + + @staticmethod + def load_windows(name, paths = None, tests = []): + os.environ["PATH"] += ";" + ";".join((os.getcwd(), _here)) + if paths: os.environ["PATH"] += ";" + ";".join(paths) + + not_supported = [] # libraries that were found, but are not supported + for style in _windows_styles: + candidate = style.format(name) + library = ctypes.util.find_library(candidate) + if library: + try: + lib = ctypes.CDLL(library) + if tests and all(run_tests(lib, tests)): + return lib + not_supported.append(library) + except WindowsError: + pass + except OSError: + not_supported.append(library) + + + if not_supported: + raise ExternalLibraryError("library '{}' couldn't be loaded, because the following candidates were not supported:".format(name) + + ("\n{}" * len(not_supported)).format(*not_supported)) + + raise ExternalLibraryError("library '{}' couldn't be loaded".format(name)) + + + + diff --git a/LXST/Codecs/libs/pyogg/ogg.py b/LXST/Codecs/libs/pyogg/ogg.py new file mode 100644 index 0000000..08a944b --- /dev/null +++ b/LXST/Codecs/libs/pyogg/ogg.py @@ -0,0 +1,672 @@ +############################################################ +# Ogg license: # +############################################################ +""" +Copyright (c) 2002, Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import ctypes +from ctypes import c_int, c_int8, c_int16, c_int32, c_int64, c_uint, c_uint8, c_uint16, c_uint32, c_uint64, c_float, c_long, c_ulong, c_char, c_char_p, c_ubyte, c_longlong, c_ulonglong, c_size_t, c_void_p, c_double, POINTER, pointer, cast +import ctypes.util +import sys +from traceback import print_exc as _print_exc +import os + +from .library_loader import Library, ExternalLibrary, ExternalLibraryError + + +def get_raw_libname(name): + name = os.path.splitext(name)[0].lower() + for x in "0123456789._- ":name=name.replace(x,"") + return name + +# Define a function to convert strings to char-pointers. In Python 3 +# all strings are Unicode, while in Python 2 they were ASCII-encoded. +# FIXME: Does PyOgg even support Python 2? +if sys.version_info.major > 2: + to_char_p = lambda s: s.encode('utf-8') +else: + to_char_p = lambda s: s + +__here = os.getcwd() + +libogg = None + +try: + names = { + "Windows": "ogg.dll", + "Darwin": "libogg.0.dylib", + "external": "ogg" + } + libogg = Library.load(names, tests = [lambda lib: hasattr(lib, "oggpack_writeinit")]) +except ExternalLibraryError: + pass +except: + _print_exc() + +if libogg is not None: + PYOGG_OGG_AVAIL = True +else: + PYOGG_OGG_AVAIL = False + +if PYOGG_OGG_AVAIL: + # Sanity check also satisfies mypy type checking + assert libogg is not None + + # ctypes + c_ubyte_p = POINTER(c_ubyte) + c_uchar = c_ubyte + c_uchar_p = c_ubyte_p + c_float_p = POINTER(c_float) + c_float_p_p = POINTER(c_float_p) + c_float_p_p_p = POINTER(c_float_p_p) + c_char_p_p = POINTER(c_char_p) + c_int_p = POINTER(c_int) + c_long_p = POINTER(c_long) + + # os_types + ogg_int16_t = c_int16 + ogg_uint16_t = c_uint16 + ogg_int32_t = c_int32 + ogg_uint32_t = c_uint32 + ogg_int64_t = c_int64 + ogg_uint64_t = c_uint64 + ogg_int64_t_p = POINTER(ogg_int64_t) + + # ogg + class ogg_iovec_t(ctypes.Structure): + """ + Wrapper for: + typedef struct ogg_iovec_t; + """ + _fields_ = [("iov_base", c_void_p), + ("iov_len", c_size_t)] + + class oggpack_buffer(ctypes.Structure): + """ + Wrapper for: + typedef struct oggpack_buffer; + """ + _fields_ = [("endbyte", c_long), + ("endbit", c_int), + ("buffer", c_uchar_p), + ("ptr", c_uchar_p), + ("storage", c_long)] + + class ogg_page(ctypes.Structure): + """ + Wrapper for: + typedef struct ogg_page; + """ + _fields_ = [("header", c_uchar_p), + ("header_len", c_long), + ("body", c_uchar_p), + ("body_len", c_long)] + + class ogg_stream_state(ctypes.Structure): + """ + Wrapper for: + typedef struct ogg_stream_state; + """ + _fields_ = [("body_data", c_uchar_p), + ("body_storage", c_long), + ("body_fill", c_long), + ("body_returned", c_long), + + ("lacing_vals", c_int), + ("granule_vals", ogg_int64_t), + + ("lacing_storage", c_long), + ("lacing_fill", c_long), + ("lacing_packet", c_long), + ("lacing_returned", c_long), + + ("header", c_uchar*282), + ("header_fill", c_int), + + ("e_o_s", c_int), + ("b_o_s", c_int), + + ("serialno", c_long), + ("pageno", c_long), + ("packetno", ogg_int64_t), + ("granulepos", ogg_int64_t)] + + class ogg_packet(ctypes.Structure): + """ + Wrapper for: + typedef struct ogg_packet; + """ + _fields_ = [("packet", c_uchar_p), + ("bytes", c_long), + ("b_o_s", c_long), + ("e_o_s", c_long), + + ("granulepos", ogg_int64_t), + + ("packetno", ogg_int64_t)] + + def __str__(self): + bos = "" + if self.b_o_s: + bos = "beginning of stream, " + eos = "" + if self.e_o_s: + eos = "end of stream, " + + # Converting the data will cause a seg-fault if the memory isn't valid + data = bytes(self.packet[0:self.bytes]) + value = ( + f"Ogg Packet <{hex(id(self))}>: " + + f"number {self.packetno}, " + + f"granule position {self.granulepos}, " + + bos + eos + + f"{self.bytes} bytes" + ) + return value + + class ogg_sync_state(ctypes.Structure): + """ + Wrapper for: + typedef struct ogg_sync_state; + """ + _fields_ = [("data", c_uchar_p), + ("storage", c_int), + ("fill", c_int), + ("returned", c_int), + + ("unsynched", c_int), + ("headerbytes", c_int), + ("bodybytes", c_int)] + + b_p = POINTER(oggpack_buffer) + oy_p = POINTER(ogg_sync_state) + op_p = POINTER(ogg_packet) + og_p = POINTER(ogg_page) + os_p = POINTER(ogg_stream_state) + iov_p = POINTER(ogg_iovec_t) + + libogg.oggpack_writeinit.restype = None + libogg.oggpack_writeinit.argtypes = [b_p] + + def oggpack_writeinit(b): + libogg.oggpack_writeinit(b) + + try: + libogg.oggpack_writecheck.restype = c_int + libogg.oggpack_writecheck.argtypes = [b_p] + def oggpack_writecheck(b): + libogg.oggpack_writecheck(b) + except: + pass + + libogg.oggpack_writetrunc.restype = None + libogg.oggpack_writetrunc.argtypes = [b_p, c_long] + + def oggpack_writetrunc(b, bits): + libogg.oggpack_writetrunc(b, bits) + + libogg.oggpack_writealign.restype = None + libogg.oggpack_writealign.argtypes = [b_p] + + def oggpack_writealign(b): + libogg.oggpack_writealign(b) + + libogg.oggpack_writecopy.restype = None + libogg.oggpack_writecopy.argtypes = [b_p, c_void_p, c_long] + + def oggpack_writecopy(b, source, bits): + libogg.oggpack_writecopy(b, source, bits) + + libogg.oggpack_reset.restype = None + libogg.oggpack_reset.argtypes = [b_p] + + def oggpack_reset(b): + libogg.oggpack_reset(b) + + libogg.oggpack_writeclear.restype = None + libogg.oggpack_writeclear.argtypes = [b_p] + + def oggpack_writeclear(b): + libogg.oggpack_writeclear(b) + + libogg.oggpack_readinit.restype = None + libogg.oggpack_readinit.argtypes = [b_p, c_uchar_p, c_int] + + def oggpack_readinit(b, buf, bytes): + libogg.oggpack_readinit(b, buf, bytes) + + libogg.oggpack_write.restype = None + libogg.oggpack_write.argtypes = [b_p, c_ulong, c_int] + + def oggpack_write(b, value, bits): + libogg.oggpack_write(b, value, bits) + + libogg.oggpack_look.restype = c_long + libogg.oggpack_look.argtypes = [b_p, c_int] + + def oggpack_look(b, bits): + return libogg.oggpack_look(b, bits) + + libogg.oggpack_look1.restype = c_long + libogg.oggpack_look1.argtypes = [b_p] + + def oggpack_look1(b): + return libogg.oggpack_look1(b) + + libogg.oggpack_adv.restype = None + libogg.oggpack_adv.argtypes = [b_p, c_int] + + def oggpack_adv(b, bits): + libogg.oggpack_adv(b, bits) + + libogg.oggpack_adv1.restype = None + libogg.oggpack_adv1.argtypes = [b_p] + + def oggpack_adv1(b): + libogg.oggpack_adv1(b) + + libogg.oggpack_read.restype = c_long + libogg.oggpack_read.argtypes = [b_p, c_int] + + def oggpack_read(b, bits): + return libogg.oggpack_read(b, bits) + + libogg.oggpack_read1.restype = c_long + libogg.oggpack_read1.argtypes = [b_p] + + def oggpack_read1(b): + return libogg.oggpack_read1(b) + + libogg.oggpack_bytes.restype = c_long + libogg.oggpack_bytes.argtypes = [b_p] + + def oggpack_bytes(b): + return libogg.oggpack_bytes(b) + + libogg.oggpack_bits.restype = c_long + libogg.oggpack_bits.argtypes = [b_p] + + def oggpack_bits(b): + return libogg.oggpack_bits(b) + + libogg.oggpack_get_buffer.restype = c_uchar_p + libogg.oggpack_get_buffer.argtypes = [b_p] + + def oggpack_get_buffer(b): + return libogg.oggpack_get_buffer(b) + + + + libogg.oggpackB_writeinit.restype = None + libogg.oggpackB_writeinit.argtypes = [b_p] + + def oggpackB_writeinit(b): + libogg.oggpackB_writeinit(b) + + try: + libogg.oggpackB_writecheck.restype = c_int + libogg.oggpackB_writecheck.argtypes = [b_p] + + def oggpackB_writecheck(b): + return libogg.oggpackB_writecheck(b) + except: + pass + + libogg.oggpackB_writetrunc.restype = None + libogg.oggpackB_writetrunc.argtypes = [b_p, c_long] + + def oggpackB_writetrunc(b, bits): + libogg.oggpackB_writetrunc(b, bits) + + libogg.oggpackB_writealign.restype = None + libogg.oggpackB_writealign.argtypes = [b_p] + + def oggpackB_writealign(b): + libogg.oggpackB_writealign(b) + + libogg.oggpackB_writecopy.restype = None + libogg.oggpackB_writecopy.argtypes = [b_p, c_void_p, c_long] + + def oggpackB_writecopy(b, source, bits): + libogg.oggpackB_writecopy(b, source, bits) + + libogg.oggpackB_reset.restype = None + libogg.oggpackB_reset.argtypes = [b_p] + + def oggpackB_reset(b): + libogg.oggpackB_reset(b) + + libogg.oggpackB_reset.restype = None + libogg.oggpackB_writeclear.argtypes = [b_p] + + def oggpackB_reset(b): + libogg.oggpackB_reset(b) + + libogg.oggpackB_readinit.restype = None + libogg.oggpackB_readinit.argtypes = [b_p, c_uchar_p, c_int] + + def oggpackB_readinit(b, buf, bytes): + libogg.oggpackB_readinit(b, buf, bytes) + + libogg.oggpackB_write.restype = None + libogg.oggpackB_write.argtypes = [b_p, c_ulong, c_int] + + def oggpackB_write(b, value, bits): + libogg.oggpackB_write(b, value, bits) + + libogg.oggpackB_look.restype = c_long + libogg.oggpackB_look.argtypes = [b_p, c_int] + + def oggpackB_look(b, bits): + return libogg.oggpackB_look(b, bits) + + libogg.oggpackB_look1.restype = c_long + libogg.oggpackB_look1.argtypes = [b_p] + + def oggpackB_look1(b): + return libogg.oggpackB_look1(b) + + libogg.oggpackB_adv.restype = None + libogg.oggpackB_adv.argtypes = [b_p, c_int] + + def oggpackB_adv(b, bits): + libogg.oggpackB_adv(b, bits) + + libogg.oggpackB_adv1.restype = None + libogg.oggpackB_adv1.argtypes = [b_p] + + def oggpackB_adv1(b): + libogg.oggpackB_adv1(b) + + libogg.oggpackB_read.restype = c_long + libogg.oggpackB_read.argtypes = [b_p, c_int] + + def oggpackB_read(b, bits): + return libogg.oggpackB_read(b, bits) + + libogg.oggpackB_read1.restype = c_long + libogg.oggpackB_read1.argtypes = [b_p] + + def oggpackB_read1(b): + return libogg.oggpackB_read1(b) + + libogg.oggpackB_bytes.restype = c_long + libogg.oggpackB_bytes.argtypes = [b_p] + + def oggpackB_bytes(b): + return libogg.oggpackB_bytes(b) + + libogg.oggpackB_bits.restype = c_long + libogg.oggpackB_bits.argtypes = [b_p] + + def oggpackB_bits(b): + return libogg.oggpackB_bits(b) + + libogg.oggpackB_get_buffer.restype = c_uchar_p + libogg.oggpackB_get_buffer.argtypes = [b_p] + + def oggpackB_get_buffer(b): + return libogg.oggpackB_get_buffer(b) + + + + libogg.ogg_stream_packetin.restype = c_int + libogg.ogg_stream_packetin.argtypes = [os_p, op_p] + + def ogg_stream_packetin(os, op): + return libogg.ogg_stream_packetin(os, op) + + try: + libogg.ogg_stream_iovecin.restype = c_int + libogg.ogg_stream_iovecin.argtypes = [os_p, iov_p, c_int, c_long, ogg_int64_t] + + def ogg_stream_iovecin(os, iov, count, e_o_s, granulepos): + return libogg.ogg_stream_iovecin(os, iov, count, e_o_s, granulepos) + except: + pass + + libogg.ogg_stream_pageout.restype = c_int + libogg.ogg_stream_pageout.argtypes = [os_p, og_p] + + def ogg_stream_pageout(os, og): + return libogg.ogg_stream_pageout(os, og) + + try: + libogg.ogg_stream_pageout_fill.restype = c_int + libogg.ogg_stream_pageout_fill.argtypes = [os_p, og_p, c_int] + def ogg_stream_pageout_fill(os, og, nfill): + return libogg.ogg_stream_pageout_fill(os, og, nfill) + except: + pass + + libogg.ogg_stream_flush.restype = c_int + libogg.ogg_stream_flush.argtypes = [os_p, og_p] + + def ogg_stream_flush(os, og): + return libogg.ogg_stream_flush(os, og) + + try: + libogg.ogg_stream_flush_fill.restype = c_int + libogg.ogg_stream_flush_fill.argtypes = [os_p, og_p, c_int] + def ogg_stream_flush_fill(os, og, nfill): + return libogg.ogg_stream_flush_fill(os, og, nfill) + except: + pass + + + + libogg.ogg_sync_init.restype = c_int + libogg.ogg_sync_init.argtypes = [oy_p] + + def ogg_sync_init(oy): + return libogg.ogg_sync_init(oy) + + libogg.ogg_sync_clear.restype = c_int + libogg.ogg_sync_clear.argtypes = [oy_p] + + def ogg_sync_clear(oy): + return libogg.ogg_sync_clear(oy) + + libogg.ogg_sync_reset.restype = c_int + libogg.ogg_sync_reset.argtypes = [oy_p] + + def ogg_sync_reset(oy): + return libogg.ogg_sync_reset(oy) + + libogg.ogg_sync_destroy.restype = c_int + libogg.ogg_sync_destroy.argtypes = [oy_p] + + def ogg_sync_destroy(oy): + return libogg.ogg_sync_destroy(oy) + + try: + libogg.ogg_sync_check.restype = c_int + libogg.ogg_sync_check.argtypes = [oy_p] + def ogg_sync_check(oy): + return libogg.ogg_sync_check(oy) + except: + pass + + + + libogg.ogg_sync_buffer.restype = c_char_p + libogg.ogg_sync_buffer.argtypes = [oy_p, c_long] + + def ogg_sync_buffer(oy, size): + return libogg.ogg_sync_buffer(oy, size) + + libogg.ogg_sync_wrote.restype = c_int + libogg.ogg_sync_wrote.argtypes = [oy_p, c_long] + + def ogg_sync_wrote(oy, bytes): + return libogg.ogg_sync_wrote(oy, bytes) + + libogg.ogg_sync_pageseek.restype = c_int + libogg.ogg_sync_pageseek.argtypes = [oy_p, og_p] + + def ogg_sync_pageseek(oy, og): + return libogg.ogg_sync_pageseek(oy, og) + + libogg.ogg_sync_pageout.restype = c_long + libogg.ogg_sync_pageout.argtypes = [oy_p, og_p] + + def ogg_sync_pageout(oy, og): + return libogg.ogg_sync_pageout(oy, og) + + libogg.ogg_stream_pagein.restype = c_int + libogg.ogg_stream_pagein.argtypes = [os_p, og_p] + + def ogg_stream_pagein(os, og): + return libogg.ogg_stream_pagein(oy, og) + + libogg.ogg_stream_packetout.restype = c_int + libogg.ogg_stream_packetout.argtypes = [os_p, op_p] + + def ogg_stream_packetout(os, op): + return libogg.ogg_stream_packetout(oy, op) + + libogg.ogg_stream_packetpeek.restype = c_int + libogg.ogg_stream_packetpeek.argtypes = [os_p, op_p] + + def ogg_stream_packetpeek(os, op): + return libogg.ogg_stream_packetpeek(os, op) + + + + libogg.ogg_stream_init.restype = c_int + libogg.ogg_stream_init.argtypes = [os_p, c_int] + + def ogg_stream_init(os, serialno): + return libogg.ogg_stream_init(os, serialno) + + libogg.ogg_stream_clear.restype = c_int + libogg.ogg_stream_clear.argtypes = [os_p] + + def ogg_stream_clear(os): + return libogg.ogg_stream_clear(os) + + libogg.ogg_stream_reset.restype = c_int + libogg.ogg_stream_reset.argtypes = [os_p] + + def ogg_stream_reset(os): + return libogg.ogg_stream_reset(os) + + libogg.ogg_stream_reset_serialno.restype = c_int + libogg.ogg_stream_reset_serialno.argtypes = [os_p, c_int] + + def ogg_stream_reset_serialno(os, serialno): + return libogg.ogg_stream_reset_serialno(os, serialno) + + libogg.ogg_stream_destroy.restype = c_int + libogg.ogg_stream_destroy.argtypes = [os_p] + + def ogg_stream_destroy(os): + return libogg.ogg_stream_destroy(os) + + try: + libogg.ogg_stream_check.restype = c_int + libogg.ogg_stream_check.argtypes = [os_p] + def ogg_stream_check(os): + return libogg.ogg_stream_check(os) + except: + pass + + libogg.ogg_stream_eos.restype = c_int + libogg.ogg_stream_eos.argtypes = [os_p] + + def ogg_stream_eos(os): + return libogg.ogg_stream_eos(os) + + + + libogg.ogg_page_checksum_set.restype = None + libogg.ogg_page_checksum_set.argtypes = [og_p] + + def ogg_page_checksum_set(og): + libogg.ogg_page_checksum_set(og) + + + + libogg.ogg_page_version.restype = c_int + libogg.ogg_page_version.argtypes = [og_p] + + def ogg_page_version(og): + return libogg.ogg_page_version(og) + + libogg.ogg_page_continued.restype = c_int + libogg.ogg_page_continued.argtypes = [og_p] + + def ogg_page_continued(og): + return libogg.ogg_page_continued(og) + + libogg.ogg_page_bos.restype = c_int + libogg.ogg_page_bos.argtypes = [og_p] + + def ogg_page_bos(og): + return libogg.ogg_page_bos(og) + + libogg.ogg_page_eos.restype = c_int + libogg.ogg_page_eos.argtypes = [og_p] + + def ogg_page_eos(og): + return libogg.ogg_page_eos(og) + + libogg.ogg_page_granulepos.restype = ogg_int64_t + libogg.ogg_page_granulepos.argtypes = [og_p] + + def ogg_page_granulepos(og): + return libogg.ogg_page_granulepos(og) + + libogg.ogg_page_serialno.restype = c_int + libogg.ogg_page_serialno.argtypes = [og_p] + + def ogg_page_serialno(og): + return libogg.ogg_page_serialno(og) + + libogg.ogg_page_pageno.restype = c_long + libogg.ogg_page_pageno.argtypes = [og_p] + + def ogg_page_pageno(og): + return libogg.ogg_page_pageno(og) + + libogg.ogg_page_packets.restype = c_int + libogg.ogg_page_packets.argtypes = [og_p] + + def ogg_page_packets(og): + return libogg.ogg_page_packets(og) + + + + libogg.ogg_packet_clear.restype = None + libogg.ogg_packet_clear.argtypes = [op_p] + + def ogg_packet_clear(op): + libogg.ogg_packet_clear(op) diff --git a/LXST/Codecs/libs/pyogg/ogg_opus_writer.py b/LXST/Codecs/libs/pyogg/ogg_opus_writer.py new file mode 100644 index 0000000..547d0f5 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/ogg_opus_writer.py @@ -0,0 +1,421 @@ +import builtins +import copy +import ctypes +import random +import struct +from typing import ( + Optional, + Union, + BinaryIO +) + +from . import ogg +from . import opus +from .opus_buffered_encoder import OpusBufferedEncoder +#from .opus_encoder import OpusEncoder +from .pyogg_error import PyOggError + +class OggOpusWriter(): + """Encodes PCM data into an OggOpus file.""" + + def __init__(self, + f: Union[BinaryIO, str], + encoder: OpusBufferedEncoder, + custom_pre_skip: Optional[int] = None) -> None: + """Construct an OggOpusWriter. + + f may be either a string giving the path to the file, or + an already-opened file handle. + + If f is an already-opened file handle, then it is the + user's responsibility to close the file when they are + finished with it. The file should be opened for writing + in binary (not text) mode. + + The encoder should be a + OpusBufferedEncoder and should be fully configured before the + first call to the `write()` method. + + The Opus encoder requires an amount of "warm up" and when + stored in an Ogg container that warm up can be skipped. When + `custom_pre_skip` is None, the required amount of warm up + silence is automatically calculated and inserted. If a custom + (non-silent) pre-skip is desired, then `custom_pre_skip` + should be specified as the number of samples (per channel). + It is then the user's responsibility to pass the non-silent + pre-skip samples to `encode()`. + + """ + # Store the Opus encoder + self._encoder = encoder + + # Store the custom pre skip + self._custom_pre_skip = custom_pre_skip + + # Create a new stream state with a random serial number + self._stream_state = self._create_stream_state() + + # Create a packet (reused for each pass) + self._ogg_packet = ogg.ogg_packet() + self._packet_valid = False + + # Create a page (reused for each pass) + self._ogg_page = ogg.ogg_page() + + # Counter for the number of packets written into Ogg stream + self._count_packets = 0 + + # Counter for the number of samples encoded into Opus + # packets + self._count_samples = 0 + + # Flag to indicate if the headers have been written + self._headers_written = False + + # Flag to indicate that the stream has been finished (the + # EOS bit was set in a final packet) + self._finished = False + + # Reference to the current encoded packet (written only + # when we know if it the last) + self._current_encoded_packet: Optional[bytes] = None + + # Open file if required. Given this may raise an exception, + # it should be the last step of initialisation. + self._i_opened_the_file = False + if isinstance(f, str): + self._file = builtins.open(f, 'wb') + self._i_opened_the_file = True + else: + # Assume it's already opened file + self._file = f + + def __del__(self) -> None: + if not self._finished: + self.close() + + # + # User visible methods + # + + def write(self, pcm: memoryview) -> None: + """Encode the PCM and write out the Ogg Opus stream. + + Encoders the PCM using the provided encoder. + + """ + # Check that the stream hasn't already been finished + if self._finished: + raise PyOggError( + "Stream has already ended. Perhaps close() was "+ + "called too early?") + + # If we haven't already written out the headers, do so + # now. Then, write a frame of silence to warm up the + # encoder. + if not self._headers_written: + pre_skip = self._write_headers(self._custom_pre_skip) + if self._custom_pre_skip is None: + self._write_silence(pre_skip) + + # Call the internal method to encode the bytes + self._write_to_oggopus(pcm) + + + def _write_to_oggopus(self, pcm: memoryview, flush: bool = False) -> None: + assert self._encoder is not None + + def handle_encoded_packet(encoded_packet: memoryview, + samples: int, + end_of_stream: bool) -> None: + # Cast memoryview to ctypes Array + Buffer = ctypes.c_ubyte * len(encoded_packet) + encoded_packet_ctypes = Buffer.from_buffer(encoded_packet) + + # Obtain a pointer to the encoded packet + encoded_packet_ptr = ctypes.cast( + encoded_packet_ctypes, + ctypes.POINTER(ctypes.c_ubyte) + ) + + # Increase the count of the number of samples written + self._count_samples += samples + + # Place data into the packet + self._ogg_packet.packet = encoded_packet_ptr + self._ogg_packet.bytes = len(encoded_packet) + self._ogg_packet.b_o_s = 0 + self._ogg_packet.e_o_s = end_of_stream + self._ogg_packet.granulepos = self._count_samples + self._ogg_packet.packetno = self._count_packets + + # Increase the counter of the number of packets + # in the stream + self._count_packets += 1 + + # Write the packet into the stream + self._write_packet() + + + # Encode the PCM data into an Opus packet + self._encoder.buffered_encode( + pcm, + flush=flush, + callback=handle_encoded_packet + ) + + def close(self) -> None: + # Check we haven't already closed this stream + if self._finished: + # We're attempting to close an already closed stream, + # do nothing more. + return + + # Flush the underlying buffered encoder + self._write_to_oggopus(memoryview(bytearray(b"")), flush=True) + + # The current packet must be the end of the stream, update + # the packet's details + self._ogg_packet.e_o_s = 1 + + # Write the packet to the stream + if self._packet_valid: + self._write_packet() + + # Flush the stream of any unwritten pages + self._flush() + + # Mark the stream as finished + self._finished = True + + # Close the file if we opened it + if self._i_opened_the_file: + self._file.close() + self._i_opened_the_file = False + + # Clean up the Ogg-related memory + ogg.ogg_stream_clear(self._stream_state) + + # Clean up the reference to the encoded packet (as it must + # now have been written) + del self._current_encoded_packet + + # + # Internal methods + # + + def _create_random_serial_no(self) -> ctypes.c_int: + sizeof_c_int = ctypes.sizeof(ctypes.c_int) + min_int = -2**(sizeof_c_int*8-1) + max_int = 2**(sizeof_c_int*8-1)-1 + serial_no = ctypes.c_int(random.randint(min_int, max_int)) + + return serial_no + + def _create_stream_state(self) -> ogg.ogg_stream_state: + # Create a random serial number + serial_no = self._create_random_serial_no() + + # Create an ogg_stream_state + ogg_stream_state = ogg.ogg_stream_state() + + # Initialise the stream state + ogg.ogg_stream_init( + ctypes.pointer(ogg_stream_state), + serial_no + ) + + return ogg_stream_state + + def _make_identification_header(self, pre_skip: int, input_sampling_rate: int = 0) -> bytes: + """Make the OggOpus identification header. + + An input_sampling rate may be set to zero to mean 'unspecified'. + + Only channel mapping family 0 is currently supported. + This allows mono and stereo signals. + + See https://tools.ietf.org/html/rfc7845#page-12 for more + details. + + """ + signature = b"OpusHead" + version = 1 + output_channels = self._encoder._channels + output_gain = 0 + channel_mapping_family = 0 + data = struct.pack( + " int: + """ Returns pre-skip. """ + if custom_pre_skip is not None: + # Use the user-specified amount of pre-skip + pre_skip = custom_pre_skip + else: + # Obtain the algorithmic delay of the Opus encoder. See + # https://tools.ietf.org/html/rfc7845#page-27 + delay_samples = self._encoder.get_algorithmic_delay() + + # Extra samples are recommended. See + # https://tools.ietf.org/html/rfc7845#page-27 + extra_samples = 120 + + # We will just fill a whole frame with silence. Calculate + # the minimum frame length, which we'll use as the + # pre-skip. + frame_durations = [2.5, 5, 10, 20, 40, 60] # milliseconds + frame_lengths = [ + x * self._encoder._samples_per_second // 1000 + for x in frame_durations + ] + for frame_length in frame_lengths: + if frame_length > delay_samples + extra_samples: + pre_skip = frame_length + break + + # Create the identification header + id_header = self._make_identification_header( + pre_skip = pre_skip + ) + + # Specify the packet containing the identification header + self._ogg_packet.packet = ctypes.cast(id_header, ogg.c_uchar_p) # type: ignore + self._ogg_packet.bytes = len(id_header) + self._ogg_packet.b_o_s = 1 + self._ogg_packet.e_o_s = 0 + self._ogg_packet.granulepos = 0 + self._ogg_packet.packetno = self._count_packets + self._count_packets += 1 + + # Write the identification header + result = ogg.ogg_stream_packetin( + self._stream_state, + self._ogg_packet + ) + + if result != 0: + raise PyOggError( + "Failed to write Opus identification header" + ) + + return pre_skip + + def _make_comment_header(self): + """Make the OggOpus comment header. + + See https://tools.ietf.org/html/rfc7845#page-22 for more + details. + + """ + signature = b"OpusTags" + vendor_string = b"ENCODER=PyOgg" + vendor_string_length = struct.pack(" None: + super().__init__() + + self._frame_size_ms: Optional[float] = None + self._frame_size_bytes: Optional[int] = None + + # Buffer contains the bytes required for the next + # frame. + self._buffer: Optional[ctypes.Array] = None + + # Location of the next free byte in the buffer + self._buffer_index = 0 + + + def set_frame_size(self, frame_size: float) -> None: + """ Set the desired frame duration (in milliseconds). + + Valid options are 2.5, 5, 10, 20, 40, or 60ms. + + """ + + # Ensure the frame size is valid. Compare frame size in + # units of 0.1ms to avoid floating point comparison + if int(frame_size*10) not in [25, 50, 100, 200, 400, 600]: + raise PyOggError( + "Frame size ({:f}) not one of ".format(frame_size)+ + "the acceptable values" + ) + + self._frame_size_ms = frame_size + + self._calc_frame_size() + + + def set_sampling_frequency(self, samples_per_second: int) -> None: + super().set_sampling_frequency(samples_per_second) + self._calc_frame_size() + + + def buffered_encode(self, + pcm_bytes: memoryview, + flush: bool = False, + callback: Callable[[memoryview,int,bool],None] = None + ) -> List[Tuple[memoryview, int, bool]]: + """Gets encoded packets and their number of samples. + + This method returns a list, where each item in the list is + a tuple. The first item in the tuple is an Opus-encoded + frame stored as a bytes-object. The second item in the + tuple is the number of samples encoded (excluding + silence). + + If `callback` is supplied then this method will instead + return an empty list but call the callback for every + Opus-encoded frame that would have been returned as a + list. This option has the desireable property of + eliminating the copying of the encoded packets, which is + required in order to form a list. The callback should + take two arguments, the encoded frame (a Python bytes + object) and the number of samples encoded per channel (an + int). The user must either process or copy the data as + the data may be overwritten once the callback terminates. + + """ + # If there's no work to do return immediately + if len(pcm_bytes) == 0 and flush == False: + return [] # no work to do + + # Sanity checks + if self._frame_size_ms is None: + raise PyOggError("Frame size must be set before encoding") + assert self._frame_size_bytes is not None + assert self._channels is not None + assert self._buffer is not None + assert self._buffer_index is not None + + # Local variable initialisation + results = [] + pcm_index = 0 + pcm_len = len(pcm_bytes) + + # 'Cast' memoryview of PCM to ctypes Array + Buffer = ctypes.c_ubyte * len(pcm_bytes) + try: + pcm_ctypes = Buffer.from_buffer(pcm_bytes) + except TypeError: + warnings.warn( + "Because PCM was read-only, an extra memory "+ + "copy was required; consider storing PCM in "+ + "writable memory (for example, bytearray "+ + "rather than bytes)." + ) + pcm_ctypes = Buffer.from_buffer(pcm_bytes) + + # Either store the encoded packet to return at the end of the + # method or immediately call the callback with the encoded + # packet. + def store_or_callback(encoded_packet: memoryview, + samples: int, + end_of_stream: bool = False) -> None: + if callback is None: + # Store the result + results.append(( + encoded_packet, + samples, + end_of_stream + )) + else: + # Call the callback + callback( + encoded_packet, + samples, + end_of_stream + ) + + # Fill the remainder of the buffer with silence and encode it. + # The associated number of samples are only that of actual + # data, not the added silence. + def flush_buffer() -> None: + # Sanity checks to satisfy mypy + assert self._buffer_index is not None + assert self._channels is not None + assert self._buffer is not None + + # If the buffer is already empty, we have no work to do + if self._buffer_index == 0: + return + + # Store the number of samples currently in the buffer + samples = ( + self._buffer_index + // self._channels + // ctypes.sizeof(opus.opus_int16) + ) + + # Fill the buffer with silence + ctypes.memset( + # destination + ctypes.byref(self._buffer, self._buffer_index), + # value + 0, + # count + len(self._buffer) - self._buffer_index + ) + + # Encode the PCM + # As at 2020-11-05, mypy is unaware that ctype Arrays + # support the buffer protocol. + encoded_packet = self.encode(memoryview(self._buffer)) # type: ignore + + # Either store the encoded packet or call the + # callback + store_or_callback(encoded_packet, samples, True) + + + # Copy the data remaining from the provided PCM into the + # buffer. Flush if required. + def copy_insufficient_data() -> None: + # Sanity checks to satisfy mypy + assert self._buffer is not None + + # Calculate remaining data + remaining_data = len(pcm_bytes) - pcm_index + + # Copy the data into the buffer. + ctypes.memmove( + # destination + ctypes.byref(self._buffer, self._buffer_index), + # source + ctypes.byref(pcm_ctypes, pcm_index), + # count + remaining_data + ) + + self._buffer_index += remaining_data + + # If we've been asked to flush the buffer then do so + if flush: + flush_buffer() + + # Loop through the provided PCM and the current buffer, + # encoding as we have full packets. + while True: + # There are two possibilities at this point: either we + # have previously unencoded data still in the buffer or we + # do not + if self._buffer_index == 0: + # We do not have unencoded data + + # We are free to progress through the PCM that has + # been provided encoding frames without copying any + # bytes. Once there is insufficient data remaining + # for a complete frame, that data should be copied + # into the buffer and we have finished. + if pcm_len - pcm_index > self._frame_size_bytes: + # We have enough data remaining in the provided + # PCM to encode more than an entire frame without + # copying any data. Unfortunately, splicing a + # ctypes array copies the array. To avoid the + # copy we use memoryview see + # https://mattgwwalker.wordpress.com/2020/12/12/python-ctypes-slicing/ + frame_data = memoryview(pcm_bytes)[ + pcm_index:pcm_index+self._frame_size_bytes + ] + + # Update the PCM index + pcm_index += self._frame_size_bytes + + # Store number of samples (per channel) of actual + # data + samples = ( + len(frame_data) + // self._channels + // ctypes.sizeof(opus.opus_int16) + ) + + # Encode the PCM + encoded_packet = super().encode(frame_data) + + # Either store the encoded packet or call the + # callback + store_or_callback(encoded_packet, samples) + + else: + # We do not have enough data to fill a frame while + # still having data left over. Copy the data into + # the buffer. + copy_insufficient_data() + return results + + else: + # We have unencoded data. + + # Copy the provided PCM into the buffer (up until the + # buffer is full). If we can fill it, then we can + # encode the filled buffer and continue. If we can't + # fill it then we've finished. + data_required = len(self._buffer) - self._buffer_index + if pcm_len > data_required: + # We have sufficient data to fill the buffer and + # have data left over. Copy data into the buffer. + assert pcm_index == 0 + remaining = len(self._buffer) - self._buffer_index + ctypes.memmove( + # destination + ctypes.byref(self._buffer, self._buffer_index), + # source + pcm_ctypes, + # count + remaining + ) + pcm_index += remaining + self._buffer_index += remaining + assert self._buffer_index == len(self._buffer) + + # Encode the PCM + encoded_packet = super().encode( + # Memoryviews of ctypes do work, even though + # mypy complains. + memoryview(self._buffer) # type: ignore + ) + + # Store number of samples (per channel) of actual + # data + samples = ( + self._buffer_index + // self._channels + // ctypes.sizeof(opus.opus_int16) + ) + + # We've now processed the buffer + self._buffer_index = 0 + + # Either store the encoded packet or call the + # callback + store_or_callback(encoded_packet, samples) + else: + # We have insufficient data to fill the buffer + # while still having data left over. Copy the + # data into the buffer. + copy_insufficient_data() + return results + + + def _calc_frame_size(self): + """Calculates the number of bytes in a frame. + + If the frame size (in milliseconds) and the number of + samples per seconds have already been specified, then the + frame size in bytes is set. Otherwise, this method does + nothing. + + The frame size is measured in bytes required to store the + sample. + + """ + if (self._frame_size_ms is None + or self._samples_per_second is None): + return + + self._frame_size_bytes = ( + self._frame_size_ms + * self._samples_per_second + // 1000 + * ctypes.sizeof(opus.opus_int16) + * self._channels + ) + + # Allocate space for the buffer + Buffer = ctypes.c_ubyte * self._frame_size_bytes + self._buffer = Buffer() + + + def _get_next_frame(self, add_silence=False): + """Gets the next Opus-encoded frame. + + Returns a tuple where the first item is the Opus-encoded + frame and the second item is the number of encoded samples + (per channel). + + Returns None if insufficient data is available. + + """ + next_frame = bytes() + samples = 0 + + # Ensure frame size has been specified + if self._frame_size_bytes is None: + raise PyOggError( + "Desired frame size hasn't been set. Perhaps "+ + "encode() was called before set_frame_size() "+ + "and set_sampling_frequency()?" + ) + + # Check if there's insufficient data in the buffer to fill + # a frame. + if self._frame_size_bytes > self._buffer_size: + if len(self._buffer) == 0: + # No data at all in buffer + return None + if add_silence: + # Get all remaining data + while len(self._buffer) != 0: + next_frame += self._buffer.popleft() + self._buffer_size = 0 + # Store number of samples (per channel) of actual + # data + samples = ( + len(next_frame) + // self._channels + // ctypes.sizeof(opus.opus_int16) + ) + # Fill remainder of frame with silence + bytes_remaining = self._frame_size_bytes - len(next_frame) + next_frame += b'\x00' * bytes_remaining + return (next_frame, samples) + else: + # Insufficient data to fill a frame and we're not + # adding silence + return None + + bytes_remaining = self._frame_size_bytes + while bytes_remaining > 0: + if len(self._buffer[0]) <= bytes_remaining: + # Take the whole first item + buffer_ = self._buffer.popleft() + next_frame += buffer_ + bytes_remaining -= len(buffer_) + self._buffer_size -= len(buffer_) + else: + # Take only part of the buffer + + # TODO: This could be more efficiently + # implemented. Rather than appending back the + # remaining data, we could just update an index + # saying where we were up to in regards to the + # first entry of the buffer. + buffer_ = self._buffer.popleft() + next_frame += buffer_[:bytes_remaining] + self._buffer_size -= bytes_remaining + # And put the unused part back into the buffer + self._buffer.appendleft(buffer_[bytes_remaining:]) + bytes_remaining = 0 + + # Calculate number of samples (per channel) + samples = ( + len(next_frame) + // self._channels + // ctypes.sizeof(opus.opus_int16) + ) + + return (next_frame, samples) diff --git a/LXST/Codecs/libs/pyogg/opus_decoder.py b/LXST/Codecs/libs/pyogg/opus_decoder.py new file mode 100644 index 0000000..8a1f4dd --- /dev/null +++ b/LXST/Codecs/libs/pyogg/opus_decoder.py @@ -0,0 +1,273 @@ +import ctypes + +from . import opus +from .pyogg_error import PyOggError + +class OpusDecoder: + def __init__(self): + self._decoder = None + self._channels = None + self._samples_per_second = None + self._pcm_buffer = None + self._pcm_buffer_ptr = None + self._pcm_buffer_size_int = None + + # TODO: Check if there is clean up that we need to do when + # closing a decoder. + + # + # User visible methods + # + + def set_channels(self, n): + + """Set the number of channels. + + n must be either 1 or 2. + + The decoder is capable of filling in either mono or + interleaved stereo pcm buffers. + + """ + if self._decoder is None: + if n < 0 or n > 2: + raise PyOggError( + "Invalid number of channels in call to "+ + "set_channels()" + ) + self._channels = n + else: + raise PyOggError( + "Cannot change the number of channels after "+ + "the decoder was created. Perhaps "+ + "set_channels() was called after decode()?" + ) + self._create_pcm_buffer() + + def set_sampling_frequency(self, samples_per_second): + """Set the number of samples (per channel) per second. + + samples_per_second must be one of 8000, 12000, 16000, + 24000, or 48000. + + Internally Opus stores data at 48000 Hz, so that should be + the default value for Fs. However, the decoder can + efficiently decode to buffers at 8, 12, 16, and 24 kHz so + if for some reason the caller cannot use data at the full + sample rate, or knows the compressed data doesn't use the + full frequency range, it can request decoding at a reduced + rate. + + """ + if self._decoder is None: + if samples_per_second in [8000, 12000, 16000, 24000, 48000]: + self._samples_per_second = samples_per_second + else: + raise PyOggError( + "Specified sampling frequency "+ + "({:d}) ".format(samples_per_second)+ + "was not one of the accepted values" + ) + else: + raise PyOggError( + "Cannot change the sampling frequency after "+ + "the decoder was created. Perhaps "+ + "set_sampling_frequency() was called after decode()?" + ) + self._create_pcm_buffer() + + def decode(self, encoded_bytes: memoryview): + """Decodes an Opus-encoded packet into PCM. + + """ + # If we haven't already created a decoder, do so now + if self._decoder is None: + self._decoder = self._create_decoder() + + # Create a ctypes array from the memoryview (without copying + # data) + Buffer = ctypes.c_char * len(encoded_bytes) + encoded_bytes_ctypes = Buffer.from_buffer(encoded_bytes) + + # Create pointer to encoded bytes + encoded_bytes_ptr = ctypes.cast( + encoded_bytes_ctypes, + ctypes.POINTER(ctypes.c_ubyte) + ) + + # Store length of encoded bytes into int32 + len_int32 = opus.opus_int32( + len(encoded_bytes) + ) + + # Check that we have a PCM buffer + if self._pcm_buffer is None: + raise PyOggError("PCM buffer was not configured.") + + # Decode the encoded frame + result = opus.opus_decode( + self._decoder, + encoded_bytes_ptr, + len_int32, + self._pcm_buffer_ptr, + self._pcm_buffer_size_int, + 0 # TODO: What's Forward Error Correction about? + ) + + # Check for any errors + if result < 0: + raise PyOggError( + "An error occurred while decoding an Opus-encoded "+ + "packet: "+ + opus.opus_strerror(result).decode("utf") + ) + + # Extract just the valid data as bytes + end_valid_data = ( + result + * ctypes.sizeof(opus.opus_int16) + * self._channels + ) + + # Create memoryview of PCM buffer to avoid copying data during slice. + mv = memoryview(self._pcm_buffer) + + # Cast memoryview to chars + mv = mv.cast('c') + + # Slice memoryview to extract only valid data + mv = mv[:end_valid_data] + + return mv + + + def decode_missing_packet(self, frame_duration): + """ Obtain PCM data despite missing a frame. + + frame_duration is in milliseconds. + + """ + + # Consider frame duration in units of 0.1ms in order to + # avoid floating-point comparisons. + if int(frame_duration*10) not in [25, 50, 100, 200, 400, 600]: + raise PyOggError( + "Frame duration ({:f}) is not one of the accepted values".format(frame_duration) + ) + + # Calculate frame size + frame_size = int( + frame_duration + * self._samples_per_second + // 1000 + ) + + # Store frame size as int + frame_size_int = ctypes.c_int(frame_size) + + # Decode missing packet + result = opus.opus_decode( + self._decoder, + None, + 0, + self._pcm_buffer_ptr, + frame_size_int, + 0 # TODO: What is this Forward Error Correction about? + ) + + # Check for any errors + if result < 0: + raise PyOggError( + "An error occurred while decoding an Opus-encoded "+ + "packet: "+ + opus.opus_strerror(result).decode("utf") + ) + + # Extract just the valid data as bytes + end_valid_data = ( + result + * ctypes.sizeof(opus.opus_int16) + * self._channels + ) + return bytes(self._pcm_buffer)[:end_valid_data] + + # + # Internal methods + # + + def _create_pcm_buffer(self): + if (self._samples_per_second is None + or self._channels is None): + # We cannot define the buffer yet + return + + # Create buffer to hold 120ms of samples. See "opus_decode()" at + # https://opus-codec.org/docs/opus_api-1.3.1/group__opus__decoder.html + max_duration = 120 # milliseconds + max_samples = max_duration * self._samples_per_second // 1000 + PCMBuffer = opus.opus_int16 * (max_samples * self._channels) + self._pcm_buffer = PCMBuffer() + self._pcm_buffer_ptr = ( + ctypes.cast(ctypes.pointer(self._pcm_buffer), + ctypes.POINTER(opus.opus_int16)) + ) + + # Store samples per channel in an int + self._pcm_buffer_size_int = ctypes.c_int(max_samples) + + def _create_decoder(self): + # To create a decoder, we must first allocate resources for it. + # We want Python to be responsible for the memory deallocation, + # and thus Python must be responsible for the initial memory + # allocation. + + # Check that the sampling frequency has been defined + if self._samples_per_second is None: + raise PyOggError( + "The sampling frequency was not specified before "+ + "attempting to create an Opus decoder. Perhaps "+ + "decode() was called before set_sampling_frequency()?" + ) + + # The sampling frequency must be passed in as a 32-bit int + samples_per_second = opus.opus_int32(self._samples_per_second) + + # Check that the number of channels has been defined + if self._channels is None: + raise PyOggError( + "The number of channels were not specified before "+ + "attempting to create an Opus decoder. Perhaps "+ + "decode() was called before set_channels()?" + ) + + # The number of channels must also be passed in as a 32-bit int + channels = opus.opus_int32(self._channels) + + # Obtain the number of bytes of memory required for the decoder + size = opus.opus_decoder_get_size(channels); + + # Allocate the required memory for the decoder + memory = ctypes.create_string_buffer(size) + + # Cast the newly-allocated memory as a pointer to a decoder. We + # could also have used opus.od_p as the pointer type, but writing + # it out in full may be clearer. + decoder = ctypes.cast(memory, ctypes.POINTER(opus.OpusDecoder)) + + # Initialise the decoder + error = opus.opus_decoder_init( + decoder, + samples_per_second, + channels + ); + + # Check that there hasn't been an error when initialising the + # decoder + if error != opus.OPUS_OK: + raise PyOggError( + "An error occurred while creating the decoder: "+ + opus.opus_strerror(error).decode("utf") + ) + + # Return our newly-created decoder + return decoder diff --git a/LXST/Codecs/libs/pyogg/opus_encoder.py b/LXST/Codecs/libs/pyogg/opus_encoder.py new file mode 100644 index 0000000..1da82da --- /dev/null +++ b/LXST/Codecs/libs/pyogg/opus_encoder.py @@ -0,0 +1,358 @@ +import ctypes +from typing import Optional, Union, ByteString + +from . import opus +from .pyogg_error import PyOggError + +class OpusEncoder: + """Encodes PCM data into Opus frames.""" + def __init__(self) -> None: + self._encoder: Optional[ctypes.pointer] = None + self._channels: Optional[int] = None + self._samples_per_second: Optional[int] = None + self._application: Optional[int] = None + self._max_bytes_per_frame: Optional[opus.opus_int32] = None + self._output_buffer: Optional[ctypes.Array] = None + self._output_buffer_ptr: Optional[ctypes.pointer] = None + + # An output buffer of 4,000 bytes is recommended in + # https://opus-codec.org/docs/opus_api-1.3.1/group__opus__encoder.html + self.set_max_bytes_per_frame(4000) + + # + # User visible methods + # + + def set_channels(self, n: int) -> None: + """Set the number of channels. + + n must be either 1 or 2. + + """ + if self._encoder is None: + if n < 0 or n > 2: + raise PyOggError( + "Invalid number of channels in call to "+ + "set_channels()" + ) + self._channels = n + else: + raise PyOggError( + "Cannot change the number of channels after "+ + "the encoder was created. Perhaps "+ + "set_channels() was called after encode()?" + ) + + def set_sampling_frequency(self, samples_per_second: int) -> None: + """Set the number of samples (per channel) per second. + + This must be one of 8000, 12000, 16000, 24000, or 48000. + + Regardless of the sampling rate and number of channels + selected, the Opus encoder can switch to a lower audio + bandwidth or number of channels if the bitrate selected is + too low. This also means that it is safe to always use 48 + kHz stereo input and let the encoder optimize the + encoding. + + """ + if self._encoder is None: + if samples_per_second in [8000, 12000, 16000, 24000, 48000]: + self._samples_per_second = samples_per_second + else: + raise PyOggError( + "Specified sampling frequency "+ + "({:d}) ".format(samples_per_second)+ + "was not one of the accepted values" + ) + else: + raise PyOggError( + "Cannot change the sampling frequency after "+ + "the encoder was created. Perhaps "+ + "set_sampling_frequency() was called after encode()?" + ) + + def set_application(self, application: str) -> None: + """Set the encoding mode. + + This must be one of 'voip', 'audio', or 'restricted_lowdelay'. + + 'voip': Gives best quality at a given bitrate for voice + signals. It enhances the input signal by high-pass + filtering and emphasizing formants and + harmonics. Optionally it includes in-band forward error + correction to protect against packet loss. Use this mode + for typical VoIP applications. Because of the enhancement, + even at high bitrates the output may sound different from + the input. + + 'audio': Gives best quality at a given bitrate for most + non-voice signals like music. Use this mode for music and + mixed (music/voice) content, broadcast, and applications + requiring less than 15 ms of coding delay. + + 'restricted_lowdelay': configures low-delay mode that + disables the speech-optimized mode in exchange for + slightly reduced delay. This mode can only be set on an + newly initialized encoder because it changes the codec + delay. + """ + if self._encoder is not None: + raise PyOggError( + "Cannot change the application after "+ + "the encoder was created. Perhaps "+ + "set_application() was called after encode()?" + ) + if application == "voip": + self._application = opus.OPUS_APPLICATION_VOIP + elif application == "audio": + self._application = opus.OPUS_APPLICATION_AUDIO + elif application == "restricted_lowdelay": + self._application = opus.OPUS_APPLICATION_RESTRICTED_LOWDELAY + else: + raise PyOggError( + "The application specification '{:s}' ".format(application)+ + "wasn't one of the accepted values." + ) + + def set_max_bytes_per_frame(self, max_bytes: int) -> None: + """Set the maximum number of bytes in an encoded frame. + + Size of the output payload. This may be used to impose an + upper limit on the instant bitrate, but should not be used + as the only bitrate control. + + TODO: Use OPUS_SET_BITRATE to control the bitrate. + + """ + self._max_bytes_per_frame = opus.opus_int32(max_bytes) + OutputBuffer = ctypes.c_ubyte * max_bytes + self._output_buffer = OutputBuffer() + self._output_buffer_ptr = ( + ctypes.cast(ctypes.pointer(self._output_buffer), + ctypes.POINTER(ctypes.c_ubyte)) + ) + + + def encode(self, pcm: Union[bytes, bytearray, memoryview]) -> memoryview: + """Encodes PCM data into an Opus frame. + + `pcm` must be formatted as bytes-like, with each sample taking + two bytes (signed 16-bit integers; interleaved left, then + right channels if in stereo). + + If `pcm` is not writeable, a copy of the array will be made. + + """ + # If we haven't already created an encoder, do so now + if self._encoder is None: + self._encoder = self._create_encoder() + + # Sanity checks also satisfy mypy type checking + assert self._channels is not None + assert self._samples_per_second is not None + assert self._output_buffer is not None + + # Calculate the effective frame duration of the given PCM + # data. Calculate it in units of 0.1ms in order to avoid + # floating point comparisons. + bytes_per_sample = 2 + frame_size = ( + len(pcm) # bytes + // bytes_per_sample + // self._channels + ) + frame_duration = ( + (10*frame_size) + // (self._samples_per_second//1000) + ) + + # Check that we have a valid frame size + if int(frame_duration) not in [25, 50, 100, 200, 400, 600]: + raise PyOggError( + "The effective frame duration ({:.1f} ms) " + .format(frame_duration/10)+ + "was not one of the acceptable values." + ) + + # Create a ctypes object sharing the memory of the PCM data + PcmCtypes = ctypes.c_ubyte * len(pcm) + try: + # Attempt to share the PCM memory + + # Unfortunately, as at 2020-09-27, the type hinting for + # read-only and writeable buffer protocols was a + # work-in-progress. The following only works for writable + # cases, but the method's parameters include a read-only + # possibility (bytes), thus we ignore mypy's error. + pcm_ctypes = PcmCtypes.from_buffer(pcm) # type: ignore[arg-type] + except TypeError: + # The data must be copied if it's not writeable + pcm_ctypes = PcmCtypes.from_buffer_copy(pcm) + + # Create a pointer to the PCM data + pcm_ptr = ctypes.cast( + pcm_ctypes, + ctypes.POINTER(opus.opus_int16) + ) + + # Create an int giving the frame size per channel + frame_size_int = ctypes.c_int(frame_size) + + # Encode PCM + result = opus.opus_encode( + self._encoder, + pcm_ptr, + frame_size_int, + self._output_buffer_ptr, + self._max_bytes_per_frame + ) + + # Check for any errors + if result < 0: + raise PyOggError( + "An error occurred while encoding to Opus format: "+ + opus.opus_strerror(result).decode("utf") + ) + + # Get memoryview of buffer so that the slice operation doesn't + # copy the data. + # + # Unfortunately, as at 2020-09-27, the type hints for + # memoryview do not include ctype arrays. This is because + # there is no currently accepted manner to label a class as + # supporting the buffer protocol. However, it's clearly a + # work in progress. For more information, see: + # * https://bugs.python.org/issue27501 + # * https://github.com/python/typing/issues/593 + # * https://github.com/python/typeshed/pull/4232 + mv = memoryview(self._output_buffer) # type: ignore + + # Cast the memoryview to char + mv = mv.cast('c') + + # Slice just the valid data from the memoryview + valid_data_as_bytes = mv[:result] + + # DEBUG + # Convert memoryview back to ctypes instance + Buffer = ctypes.c_ubyte * len(valid_data_as_bytes) + buf = Buffer.from_buffer( valid_data_as_bytes ) + + # Convert PCM back to pointer and dump 4,000-byte buffer + ptr = ctypes.cast( + buf, + ctypes.POINTER(ctypes.c_ubyte) + ) + + return valid_data_as_bytes + + + def get_algorithmic_delay(self): + """Gets the total samples of delay added by the entire codec. + + This can be queried by the encoder and then the provided + number of samples can be skipped on from the start of the + decoder's output to provide time aligned input and + output. From the perspective of a decoding application the + real data begins this many samples late. + + The decoder contribution to this delay is identical for all + decoders, but the encoder portion of the delay may vary from + implementation to implementation, version to version, or even + depend on the encoder's initial configuration. Applications + needing delay compensation should call this method rather than + hard-coding a value. + + """ + # If we haven't already created an encoder, do so now + if self._encoder is None: + self._encoder = self._create_encoder() + + # Obtain the algorithmic delay of the Opus encoder. See + # https://tools.ietf.org/html/rfc7845#page-27 + delay = opus.opus_int32() + + result = opus.opus_encoder_ctl( + self._encoder, + opus.OPUS_GET_LOOKAHEAD_REQUEST, + ctypes.pointer(delay) + ) + if result != opus.OPUS_OK: + raise PyOggError( + "Failed to obtain the algorithmic delay of "+ + "the Opus encoder: "+ + opus.opus_strerror(result).decode("utf") + ) + delay_samples = delay.value + return delay_samples + + + # + # Internal methods + # + + def _create_encoder(self) -> ctypes.pointer: + # To create an encoder, we must first allocate resources for it. + # We want Python to be responsible for the memory deallocation, + # and thus Python must be responsible for the initial memory + # allocation. + + # Check that the application has been defined + if self._application is None: + raise PyOggError( + "The application was not specified before "+ + "attempting to create an Opus encoder. Perhaps "+ + "encode() was called before set_application()?" + ) + application = self._application + + # Check that the sampling frequency has been defined + if self._samples_per_second is None: + raise PyOggError( + "The sampling frequency was not specified before "+ + "attempting to create an Opus encoder. Perhaps "+ + "encode() was called before set_sampling_frequency()?" + ) + + # The frequency must be passed in as a 32-bit int + samples_per_second = opus.opus_int32(self._samples_per_second) + + # Check that the number of channels has been defined + if self._channels is None: + raise PyOggError( + "The number of channels were not specified before "+ + "attempting to create an Opus encoder. Perhaps "+ + "encode() was called before set_channels()?" + ) + channels = self._channels + + # Obtain the number of bytes of memory required for the encoder + size = opus.opus_encoder_get_size(channels); + + # Allocate the required memory for the encoder + memory = ctypes.create_string_buffer(size) + + # Cast the newly-allocated memory as a pointer to an encoder. We + # could also have used opus.oe_p as the pointer type, but writing + # it out in full may be clearer. + encoder = ctypes.cast(memory, ctypes.POINTER(opus.OpusEncoder)) + + # Initialise the encoder + error = opus.opus_encoder_init( + encoder, + samples_per_second, + channels, + application + ) + + # Check that there hasn't been an error when initialising the + # encoder + if error != opus.OPUS_OK: + raise PyOggError( + "An error occurred while creating the encoder: "+ + opus.opus_strerror(error).decode("utf") + ) + + # Return our newly-created encoder + return encoder diff --git a/LXST/Codecs/libs/pyogg/opus_file.py b/LXST/Codecs/libs/pyogg/opus_file.py new file mode 100644 index 0000000..f8519f4 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/opus_file.py @@ -0,0 +1,106 @@ +import ctypes + +from . import ogg +from . import opus +from .pyogg_error import PyOggError +from .audio_file import AudioFile + +class OpusFile(AudioFile): + def __init__(self, path: str) -> None: + # Open the file + error = ctypes.c_int() + of = opus.op_open_file( + ogg.to_char_p(path), + ctypes.pointer(error) + ) + + # Check for errors + if error.value != 0: + raise PyOggError( + ("File '{}' couldn't be opened or doesn't exist. "+ + "Error code: {}").format(path, error.value) + ) + + # Extract the number of channels in the newly opened file + #: Number of channels in audio file. + self.channels = opus.op_channel_count(of, -1) + + # Allocate sufficient memory to store the entire PCM + pcm_size = opus.op_pcm_total(of, -1) + Buf = opus.opus_int16*(pcm_size*self.channels) + buf = Buf() + + # Create a pointer to the newly allocated memory. It + # seems we can only do pointer arithmetic on void + # pointers. See + # https://mattgwwalker.wordpress.com/2020/05/30/pointer-manipulation-in-python/ + buf_ptr = ctypes.cast( + ctypes.pointer(buf), + ctypes.c_void_p + ) + assert buf_ptr.value is not None # for mypy + buf_ptr_zero = buf_ptr.value + + #: Bytes per sample + self.bytes_per_sample = ctypes.sizeof(opus.opus_int16) + + # Read through the entire file, copying the PCM into the + # buffer + samples = 0 + while True: + # Calculate remaining buffer size + remaining_buffer = ( + len(buf) # int + - (buf_ptr.value + - buf_ptr_zero) // self.bytes_per_sample + ) + + # Convert buffer pointer to the desired type + ptr = ctypes.cast( + buf_ptr, + ctypes.POINTER(opus.opus_int16) + ) + + # Read the next section of PCM + ns = opus.op_read( + of, + ptr, + remaining_buffer, + ogg.c_int_p() + ) + + # Check for errors + if ns<0: + raise PyOggError( + "Error while reading OggOpus file. "+ + "Error code: {}".format(ns) + ) + + # Increment the pointer + buf_ptr.value += ( + ns + * self.bytes_per_sample + * self.channels + ) + assert buf_ptr.value is not None # for mypy + + samples += ns + + # Check if we've finished + if ns==0: + break + + # Close the open file + opus.op_free(of) + + # Opus files are always stored at 48k samples per second + #: Number of samples per second (per channel). Always 48,000. + self.frequency = 48000 + + # Cast buffer to a one-dimensional array of chars + #: Raw PCM data from audio file. + CharBuffer = ( + ctypes.c_byte + * (self.bytes_per_sample * self.channels * pcm_size) + ) + self.buffer = CharBuffer.from_buffer(buf) diff --git a/LXST/Codecs/libs/pyogg/opus_file_stream.py b/LXST/Codecs/libs/pyogg/opus_file_stream.py new file mode 100644 index 0000000..b3e1723 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/opus_file_stream.py @@ -0,0 +1,127 @@ +import ctypes + +from . import ogg +from . import opus +from .pyogg_error import PyOggError + +class OpusFileStream: + def __init__(self, path): + """Opens an OggOpus file as a stream. + + path should be a string giving the filename of the file to + open. Unicode file names may not work correctly. + + An exception will be raised if the file cannot be opened + correctly. + + """ + error = ctypes.c_int() + + self.of = opus.op_open_file(ogg.to_char_p(path), ctypes.pointer(error)) + + if error.value != 0: + self.of = None + raise PyOggError("file couldn't be opened or doesn't exist. Error code : {}".format(error.value)) + + #: Number of channels in audio file + self.channels = opus.op_channel_count(self.of, -1) + + #: Total PCM Length + self.pcm_size = opus.op_pcm_total(self.of, -1) + + #: Number of samples per second (per channel) + self.frequency = 48000 + + # The buffer size should be (per channel) large enough to + # hold 120ms (the largest possible Opus frame) at 48kHz. + # See https://opus-codec.org/docs/opusfile_api-0.7/group__stream__decoding.html#ga963c917749335e29bb2b698c1cb20a10 + self.buffer_size = self.frequency // 1000 * 120 * self.channels + self.Buf = opus.opus_int16 * self.buffer_size + self._buf = self.Buf() + self.buffer_ptr = ctypes.cast( + ctypes.pointer(self._buf), + opus.opus_int16_p + ) + + #: Bytes per sample + self.bytes_per_sample = ctypes.sizeof(opus.opus_int16) + + def __del__(self): + if self.of is not None: + opus.op_free(self.of) + + def get_buffer(self): + """Obtains the next frame of PCM samples. + + Returns an array of signed 16-bit integers. If the file + is in stereo, the left and right channels are interleaved. + + Returns None when all data has been read. + + The array that is returned should be either processed or + copied before the next call to :meth:`~get_buffer` or + :meth:`~get_buffer_as_array` as the array's memory is reused for + each call. + + """ + # Read the next frame + samples_read = opus.op_read( + self.of, + self.buffer_ptr, + self.buffer_size, + None + ) + + # Check for errors + if samples_read < 0: + raise PyOggError( + "Failed to read OpusFileStream. Error {:d}".format(samples_read) + ) + + # Check if we've reached the end of the stream + if samples_read == 0: + return None + + # Cast the pointer to opus_int16 to an array of the + # correct size + result_ptr = ctypes.cast( + self.buffer_ptr, + ctypes.POINTER(opus.opus_int16 * (samples_read*self.channels)) + ) + + # Convert the array to Python bytes + return bytes(result_ptr.contents) + + def get_buffer_as_array(self): + """Provides the buffer as a NumPy array. + + Note that the underlying data type is 16-bit signed + integers. + + Does not copy the underlying data, so the returned array + should either be processed or copied before the next call + to :meth:`~get_buffer` or :meth:`~get_buffer_as_array`. + + """ + import numpy # type: ignore + + # Read the next samples from the stream + buf = self.get_buffer() + + # Check if we've come to the end of the stream + if buf is None: + return None + + # Convert the bytes buffer to a NumPy array + array = numpy.frombuffer( + buf, + dtype=numpy.int16 + ) + + # Reshape the array + return array.reshape( + (len(buf) + // self.bytes_per_sample + // self.channels, + self.channels) + ) diff --git a/LXST/Codecs/libs/pyogg/py.typed b/LXST/Codecs/libs/pyogg/py.typed new file mode 100644 index 0000000..d4defd9 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. This package uses inline types. \ No newline at end of file diff --git a/LXST/Codecs/libs/pyogg/pyogg_error.py b/LXST/Codecs/libs/pyogg/pyogg_error.py new file mode 100644 index 0000000..35f28bf --- /dev/null +++ b/LXST/Codecs/libs/pyogg/pyogg_error.py @@ -0,0 +1,2 @@ +class PyOggError(Exception): + pass diff --git a/LXST/Codecs/libs/pyogg/vorbis.py b/LXST/Codecs/libs/pyogg/vorbis.py new file mode 100644 index 0000000..a8432ba --- /dev/null +++ b/LXST/Codecs/libs/pyogg/vorbis.py @@ -0,0 +1,855 @@ +############################################################ +# Vorbis license: # +############################################################ +""" +Copyright (c) 2002-2015 Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import ctypes +import ctypes.util +from traceback import print_exc as _print_exc +import os + +OV_EXCLUDE_STATIC_CALLBACKS = False + +__MINGW32__ = False + +_WIN32 = False + +from .ogg import * + +from .library_loader import ExternalLibrary, ExternalLibraryError + +__here = os.getcwd() + +libvorbis = None + +try: + names = { + "Windows": "libvorbis.dll", + "Darwin": "libvorbis.0.dylib", + "external": "vorbis" + } + libvorbis = Library.load(names, tests = [lambda lib: hasattr(lib, "vorbis_info_init")]) +except ExternalLibraryError: + pass +except: + _print_exc() + +libvorbisfile = None + +try: + names = { + "Windows": "libvorbisfile.dll", + "Darwin": "libvorbisfile.3.dylib", + "external": "vorbisfile" + } + libvorbisfile = Library.load(names, tests = [lambda lib: hasattr(lib, "ov_clear")]) +except ExternalLibraryError: + pass +except: + _print_exc() + +libvorbisenc = None + +# In some cases, libvorbis may also have the libvorbisenc functionality. +libvorbis_is_also_libvorbisenc = True + +for f in ("vorbis_encode_ctl", + "vorbis_encode_init", + "vorbis_encode_init_vbr", + "vorbis_encode_setup_init", + "vorbis_encode_setup_managed", + "vorbis_encode_setup_vbr"): + if not hasattr(libvorbis, f): + libvorbis_is_also_libvorbisenc = False + break + +if libvorbis_is_also_libvorbisenc: + libvorbisenc = libvorbis +else: + try: + names = { + "Windows": "libvorbisenc.dll", + "Darwin": "libvorbisenc.2.dylib", + "external": "vorbisenc" + } + libvorbisenc = Library.load(names, tests = [lambda lib: hasattr(lib, "vorbis_encode_init")]) + except ExternalLibraryError: + pass + except: + _print_exc() + +if libvorbis is None: + PYOGG_VORBIS_AVAIL = False +else: + PYOGG_VORBIS_AVAIL = True + +if libvorbisfile is None: + PYOGG_VORBIS_FILE_AVAIL = False +else: + PYOGG_VORBIS_FILE_AVAIL = True + +if libvorbisenc is None: + PYOGG_VORBIS_ENC_AVAIL = False +else: + PYOGG_VORBIS_ENC_AVAIL = True + +# FIXME: What's the story with the lack of checking for PYOGG_VORBIS_ENC_AVAIL? +# We just seem to assume that it's available. + +if PYOGG_OGG_AVAIL and PYOGG_VORBIS_AVAIL and PYOGG_VORBIS_FILE_AVAIL: + # Sanity check also satisfies mypy type checking + assert libogg is not None + assert libvorbis is not None + assert libvorbisfile is not None + + + # codecs + class vorbis_info(ctypes.Structure): + """ + Wrapper for: + typedef struct vorbis_info vorbis_info; + """ + _fields_ = [("version", c_int), + ("channels", c_int), + ("rate", c_long), + + ("bitrate_upper", c_long), + ("bitrate_nominal", c_long), + ("bitrate_lower", c_long), + ("bitrate_window", c_long), + ("codec_setup", c_void_p)] + + + + class vorbis_dsp_state(ctypes.Structure): + """ + Wrapper for: + typedef struct vorbis_dsp_state vorbis_dsp_state; + """ + _fields_ = [("analysisp", c_int), + ("vi", POINTER(vorbis_info)), + ("pcm", c_float_p_p), + ("pcmret", c_float_p_p), + ("pcm_storage", c_int), + ("pcm_current", c_int), + ("pcm_returned", c_int), + + ("preextrapolate", c_int), + ("eofflag", c_int), + + ("lW", c_long), + ("W", c_long), + ("nW", c_long), + ("centerW", c_long), + + ("granulepos", ogg_int64_t), + ("sequence", ogg_int64_t), + + ("glue_bits", ogg_int64_t), + ("time_bits", ogg_int64_t), + ("floor_bits", ogg_int64_t), + ("res_bits", ogg_int64_t), + + ("backend_state", c_void_p)] + + class alloc_chain(ctypes.Structure): + """ + Wrapper for: + typedef struct alloc_chain; + """ + pass + + alloc_chain._fields_ = [("ptr", c_void_p), + ("next", POINTER(alloc_chain))] + + class vorbis_block(ctypes.Structure): + """ + Wrapper for: + typedef struct vorbis_block vorbis_block; + """ + _fields_ = [("pcm", c_float_p_p), + ("opb", oggpack_buffer), + ("lW", c_long), + ("W", c_long), + ("nW", c_long), + ("pcmend", c_int), + ("mode", c_int), + + ("eofflag", c_int), + ("granulepos", ogg_int64_t), + ("sequence", ogg_int64_t), + ("vd", POINTER(vorbis_dsp_state)), + + ("localstore", c_void_p), + ("localtop", c_long), + ("localalloc", c_long), + ("totaluse", c_long), + ("reap", POINTER(alloc_chain)), + + ("glue_bits", c_long), + ("time_bits", c_long), + ("floor_bits", c_long), + ("res_bits", c_long), + + ("internal", c_void_p)] + + class vorbis_comment(ctypes.Structure): + """ + Wrapper for: + typedef struct vorbis_comment vorbis_comment; + """ + _fields_ = [("user_comments", c_char_p_p), + ("comment_lengths", c_int_p), + ("comments", c_int), + ("vendor", c_char_p)] + + + + vi_p = POINTER(vorbis_info) + vc_p = POINTER(vorbis_comment) + vd_p = POINTER(vorbis_dsp_state) + vb_p = POINTER(vorbis_block) + + libvorbis.vorbis_info_init.restype = None + libvorbis.vorbis_info_init.argtypes = [vi_p] + def vorbis_info_init(vi): + libvorbis.vorbis_info_init(vi) + + libvorbis.vorbis_info_clear.restype = None + libvorbis.vorbis_info_clear.argtypes = [vi_p] + def vorbis_info_clear(vi): + libvorbis.vorbis_info_clear(vi) + + libvorbis.vorbis_info_blocksize.restype = c_int + libvorbis.vorbis_info_blocksize.argtypes = [vi_p, c_int] + def vorbis_info_blocksize(vi, zo): + return libvorbis.vorbis_info_blocksize(vi, zo) + + libvorbis.vorbis_comment_init.restype = None + libvorbis.vorbis_comment_init.argtypes = [vc_p] + def vorbis_comment_init(vc): + libvorbis.vorbis_comment_init(vc) + + libvorbis.vorbis_comment_add.restype = None + libvorbis.vorbis_comment_add.argtypes = [vc_p, c_char_p] + def vorbis_comment_add(vc, comment): + libvorbis.vorbis_comment_add(vc, comment) + + libvorbis.vorbis_comment_add_tag.restype = None + libvorbis.vorbis_comment_add_tag.argtypes = [vc_p, c_char_p, c_char_p] + def vorbis_comment_add_tag(vc, tag, comment): + libvorbis.vorbis_comment_add_tag(vc, tag, comment) + + libvorbis.vorbis_comment_query.restype = c_char_p + libvorbis.vorbis_comment_query.argtypes = [vc_p, c_char_p, c_int] + def vorbis_comment_query(vc, tag, count): + libvorbis.vorbis_comment_query(vc, tag, count) + + libvorbis.vorbis_comment_query_count.restype = c_int + libvorbis.vorbis_comment_query_count.argtypes = [vc_p, c_char_p] + def vorbis_comment_query_count(vc, tag): + libvorbis.vorbis_comment_query_count(vc, tag) + + libvorbis.vorbis_comment_clear.restype = None + libvorbis.vorbis_comment_clear.argtypes = [vc_p] + def vorbis_comment_clear(vc): + libvorbis.vorbis_comment_clear(vc) + + + + libvorbis.vorbis_block_init.restype = c_int + libvorbis.vorbis_block_init.argtypes = [vd_p, vb_p] + def vorbis_block_init(v,vb): + return libvorbis.vorbis_block_init(v,vb) + + libvorbis.vorbis_block_clear.restype = c_int + libvorbis.vorbis_block_clear.argtypes = [vb_p] + def vorbis_block_clear(vb): + return libvorbis.vorbis_block_clear(vb) + + libvorbis.vorbis_dsp_clear.restype = None + libvorbis.vorbis_dsp_clear.argtypes = [vd_p] + def vorbis_dsp_clear(v): + return libvorbis.vorbis_dsp_clear(v) + + libvorbis.vorbis_granule_time.restype = c_double + libvorbis.vorbis_granule_time.argtypes = [vd_p, ogg_int64_t] + def vorbis_granule_time(v, granulepos): + return libvorbis.vorbis_granule_time(v, granulepos) + + + + libvorbis.vorbis_version_string.restype = c_char_p + libvorbis.vorbis_version_string.argtypes = [] + def vorbis_version_string(): + return libvorbis.vorbis_version_string() + + + + + + libvorbis.vorbis_analysis_init.restype = c_int + libvorbis.vorbis_analysis_init.argtypes = [vd_p, vi_p] + def vorbis_analysis_init(v, vi): + return libvorbis.vorbis_analysis_init(v, vi) + + libvorbis.vorbis_commentheader_out.restype = c_int + libvorbis.vorbis_commentheader_out.argtypes = [vc_p, op_p] + def vorbis_commentheader_out(vc, op): + return libvorbis.vorbis_commentheader_out(vc, op) + + libvorbis.vorbis_analysis_headerout.restype = c_int + libvorbis.vorbis_analysis_headerout.argtypes = [vd_p, vc_p, op_p, op_p, op_p] + def vorbis_analysis_headerout(v,vc, op, op_comm, op_code): + return libvorbis.vorbis_analysis_headerout(v,vc, op, op_comm, op_code) + + libvorbis.vorbis_analysis_buffer.restype = c_float_p_p + libvorbis.vorbis_analysis_buffer.argtypes = [vd_p, c_int] + def vorbis_analysis_buffer(v, vals): + return libvorbis.vorbis_analysis_buffer(v, vals) + + libvorbis.vorbis_analysis_wrote.restype = c_int + libvorbis.vorbis_analysis_wrote.argtypes = [vd_p, c_int] + def vorbis_analysis_wrote(v, vals): + return libvorbis.vorbis_analysis_wrote(v, vals) + + libvorbis.vorbis_analysis_blockout.restype = c_int + libvorbis.vorbis_analysis_blockout.argtypes = [vd_p, vb_p] + def vorbis_analysis_blockout(v, vb): + return libvorbis.vorbis_analysis_blockout(v, vb) + + libvorbis.vorbis_analysis.restype = c_int + libvorbis.vorbis_analysis.argtypes = [vb_p, op_p] + def vorbis_analysis(vb, op): + return libvorbis.vorbis_analysis(vb, op) + + + + + libvorbis.vorbis_bitrate_addblock.restype = c_int + libvorbis.vorbis_bitrate_addblock.argtypes = [vb_p] + def vorbis_bitrate_addblock(vb): + return libvorbis.vorbis_bitrate_addblock(vb) + + libvorbis.vorbis_bitrate_flushpacket.restype = c_int + libvorbis.vorbis_bitrate_flushpacket.argtypes = [vd_p, op_p] + def vorbis_bitrate_flushpacket(vd, op): + return libvorbis.vorbis_bitrate_flushpacket(vd, op) + + + + + libvorbis.vorbis_synthesis_idheader.restype = c_int + libvorbis.vorbis_synthesis_idheader.argtypes = [op_p] + def vorbis_synthesis_idheader(op): + return libvorbis.vorbis_synthesis_idheader(op) + + libvorbis.vorbis_synthesis_headerin.restype = c_int + libvorbis.vorbis_synthesis_headerin.argtypes = [vi_p, vc_p, op_p] + def vorbis_synthesis_headerin(vi, vc, op): + return libvorbis.vorbis_synthesis_headerin(vi, vc, op) + + + + + libvorbis.vorbis_synthesis_init.restype = c_int + libvorbis.vorbis_synthesis_init.argtypes = [vd_p, vi_p] + def vorbis_synthesis_init(v,vi): + return libvorbis.vorbis_synthesis_init(v,vi) + + libvorbis.vorbis_synthesis_restart.restype = c_int + libvorbis.vorbis_synthesis_restart.argtypes = [vd_p] + def vorbis_synthesis_restart(v): + return libvorbis.vorbis_synthesis_restart(v) + + libvorbis.vorbis_synthesis.restype = c_int + libvorbis.vorbis_synthesis.argtypes = [vb_p, op_p] + def vorbis_synthesis(vb, op): + return libvorbis.vorbis_synthesis(vb, op) + + libvorbis.vorbis_synthesis_trackonly.restype = c_int + libvorbis.vorbis_synthesis_trackonly.argtypes = [vb_p, op_p] + def vorbis_synthesis_trackonly(vb, op): + return libvorbis.vorbis_synthesis_trackonly(vb, op) + + libvorbis.vorbis_synthesis_blockin.restype = c_int + libvorbis.vorbis_synthesis_blockin.argtypes = [vd_p, vb_p] + def vorbis_synthesis_blockin(v, vb): + return libvorbis.vorbis_synthesis_blockin(v, vb) + + libvorbis.vorbis_synthesis_pcmout.restype = c_int + libvorbis.vorbis_synthesis_pcmout.argtypes = [vd_p, c_float_p_p_p] + def vorbis_synthesis_pcmout(v, pcm): + return libvorbis.vorbis_synthesis_pcmout(v, pcm) + + libvorbis.vorbis_synthesis_lapout.restype = c_int + libvorbis.vorbis_synthesis_lapout.argtypes = [vd_p, c_float_p_p_p] + def vorbis_synthesis_lapout(v, pcm): + return libvorbis.vorbis_synthesis_lapout(v, pcm) + + libvorbis.vorbis_synthesis_read.restype = c_int + libvorbis.vorbis_synthesis_read.argtypes = [vd_p, c_int] + def vorbis_synthesis_read(v, samples): + return libvorbis.vorbis_synthesis_read(v, samples) + + libvorbis.vorbis_packet_blocksize.restype = c_long + libvorbis.vorbis_packet_blocksize.argtypes = [vi_p, op_p] + def vorbis_packet_blocksize(vi, op): + return libvorbis.vorbis_packet_blocksize(vi, op) + + + + libvorbis.vorbis_synthesis_halfrate.restype = c_int + libvorbis.vorbis_synthesis_halfrate.argtypes = [vi_p, c_int] + def vorbis_synthesis_halfrate(v, flag): + return libvorbis.vorbis_synthesis_halfrate(v, flag) + + libvorbis.vorbis_synthesis_halfrate_p.restype = c_int + libvorbis.vorbis_synthesis_halfrate_p.argtypes = [vi_p] + def vorbis_synthesis_halfrate_p(vi): + return libvorbis.vorbis_synthesis_halfrate_p(vi) + + OV_FALSE = -1 + OV_EOF = -2 + OV_HOLE = -3 + + OV_EREAD = -128 + OV_EFAULT = -129 + OV_EIMPL =-130 + OV_EINVAL =-131 + OV_ENOTVORBIS =-132 + OV_EBADHEADER =-133 + OV_EVERSION =-134 + OV_ENOTAUDIO =-135 + OV_EBADPACKET =-136 + OV_EBADLINK =-137 + OV_ENOSEEK =-138 + # end of codecs + + # vorbisfile + read_func = ctypes.CFUNCTYPE(c_size_t, + c_void_p, + c_size_t, + c_size_t, + c_void_p) + + seek_func = ctypes.CFUNCTYPE(c_int, + c_void_p, + ogg_int64_t, + c_int) + + close_func = ctypes.CFUNCTYPE(c_int, + c_void_p) + + tell_func = ctypes.CFUNCTYPE(c_long, + c_void_p) + + class ov_callbacks(ctypes.Structure): + """ + Wrapper for: + typedef struct ov_callbacks; + """ + + _fields_ = [("read_func", read_func), + ("seek_func", seek_func), + ("close_func", close_func), + ("tell_func", tell_func)] + + NOTOPEN = 0 + PARTOPEN = 1 + OPENED = 2 + STREAMSET = 3 + INITSET = 4 + + class OggVorbis_File(ctypes.Structure): + """ + Wrapper for: + typedef struct OggVorbis_File OggVorbis_File; + """ + + _fields_ = [("datasource", c_void_p), + ("seekable", c_int), + ("offset", ogg_int64_t), + ("end", ogg_int64_t), + ("oy", ogg_sync_state), + + ("links", c_int), + ("offsets", ogg_int64_t_p), + ("dataoffsets", ogg_int64_t_p), + ("serialnos", c_long_p), + ("pcmlengths", ogg_int64_t_p), + ("vi", vi_p), + ("vc", vc_p), + + ("pcm_offset", ogg_int64_t), + ("ready_state", c_int), + ("current_serialno", c_long), + ("current_link", c_int), + + ("bittrack", c_double), + ("samptrack", c_double), + + ("os", ogg_stream_state), + + ("vd", vorbis_dsp_state), + ("vb", vorbis_block), + + ("callbacks", ov_callbacks)] + vf_p = POINTER(OggVorbis_File) + + libvorbisfile.ov_clear.restype = c_int + libvorbisfile.ov_clear.argtypes = [vf_p] + + def ov_clear(vf): + return libvorbisfile.ov_clear(vf) + + libvorbisfile.ov_fopen.restype = c_int + libvorbisfile.ov_fopen.argtypes = [c_char_p, vf_p] + + def ov_fopen(path, vf): + return libvorbisfile.ov_fopen(to_char_p(path), vf) + + libvorbisfile.ov_open_callbacks.restype = c_int + libvorbisfile.ov_open_callbacks.argtypes = [c_void_p, vf_p, c_char_p, c_long, ov_callbacks] + + def ov_open_callbacks(datasource, vf, initial, ibytes, callbacks): + return libvorbisfile.ov_open_callbacks(datasource, vf, initial, ibytes, callbacks) + + def ov_open(*args, **kw): + raise PyOggError("ov_open is not supported, please use ov_fopen instead") + + def ov_test(*args, **kw): + raise PyOggError("ov_test is not supported") + + libvorbisfile.ov_test_callbacks.restype = c_int + libvorbisfile.ov_test_callbacks.argtypes = [c_void_p, vf_p, c_char_p, c_long, ov_callbacks] + + def ov_test_callbacks(datasource, vf, initial, ibytes, callbacks): + return libvorbisfile.ov_test_callbacks(datasource, vf, initial, ibytes, callbacks) + + libvorbisfile.ov_test_open.restype = c_int + libvorbisfile.ov_test_open.argtypes = [vf_p] + + def ov_test_open(vf): + return libvorbisfile.ov_test_open(vf) + + + + + libvorbisfile.ov_bitrate.restype = c_long + libvorbisfile.ov_bitrate.argtypes = [vf_p, c_int] + + def ov_bitrate(vf, i): + return libvorbisfile.ov_bitrate(vf, i) + + libvorbisfile.ov_bitrate_instant.restype = c_long + libvorbisfile.ov_bitrate_instant.argtypes = [vf_p] + + def ov_bitrate_instant(vf): + return libvorbisfile.ov_bitrate_instant(vf) + + libvorbisfile.ov_streams.restype = c_long + libvorbisfile.ov_streams.argtypes = [vf_p] + + def ov_streams(vf): + return libvorbisfile.ov_streams(vf) + + libvorbisfile.ov_seekable.restype = c_long + libvorbisfile.ov_seekable.argtypes = [vf_p] + + def ov_seekable(vf): + return libvorbisfile.ov_seekable(vf) + + libvorbisfile.ov_serialnumber.restype = c_long + libvorbisfile.ov_serialnumber.argtypes = [vf_p, c_int] + + def ov_serialnumber(vf, i): + return libvorbisfile.ov_serialnumber(vf, i) + + + + libvorbisfile.ov_raw_total.restype = ogg_int64_t + libvorbisfile.ov_raw_total.argtypes = [vf_p, c_int] + + def ov_raw_total(vf, i): + return libvorbisfile.ov_raw_total(vf, i) + + libvorbisfile.ov_pcm_total.restype = ogg_int64_t + libvorbisfile.ov_pcm_total.argtypes = [vf_p, c_int] + + def ov_pcm_total(vf, i): + return libvorbisfile.ov_pcm_total(vf, i) + + libvorbisfile.ov_time_total.restype = c_double + libvorbisfile.ov_time_total.argtypes = [vf_p, c_int] + + def ov_time_total(vf, i): + return libvorbisfile.ov_time_total(vf, i) + + + + + libvorbisfile.ov_raw_seek.restype = c_int + libvorbisfile.ov_raw_seek.argtypes = [vf_p, ogg_int64_t] + + def ov_raw_seek(vf, pos): + return libvorbisfile.ov_raw_seek(vf, pos) + + libvorbisfile.ov_pcm_seek.restype = c_int + libvorbisfile.ov_pcm_seek.argtypes = [vf_p, ogg_int64_t] + + def ov_pcm_seek(vf, pos): + return libvorbisfile.ov_pcm_seek(vf, pos) + + libvorbisfile.ov_pcm_seek_page.restype = c_int + libvorbisfile.ov_pcm_seek_page.argtypes = [vf_p, ogg_int64_t] + + def ov_pcm_seek_page(vf, pos): + return libvorbisfile.ov_pcm_seek_page(vf, pos) + + libvorbisfile.ov_time_seek.restype = c_int + libvorbisfile.ov_time_seek.argtypes = [vf_p, c_double] + + def ov_time_seek(vf, pos): + return libvorbisfile.ov_time_seek(vf, pos) + + libvorbisfile.ov_time_seek_page.restype = c_int + libvorbisfile.ov_time_seek_page.argtypes = [vf_p, c_double] + + def ov_time_seek_page(vf, pos): + return libvorbisfile.ov_time_seek_page(vf, pos) + + + + + libvorbisfile.ov_raw_seek_lap.restype = c_int + libvorbisfile.ov_raw_seek_lap.argtypes = [vf_p, ogg_int64_t] + + def ov_raw_seek_lap(vf, pos): + return libvorbisfile.ov_raw_seek_lap(vf, pos) + + libvorbisfile.ov_pcm_seek_lap.restype = c_int + libvorbisfile.ov_pcm_seek_lap.argtypes = [vf_p, ogg_int64_t] + + def ov_pcm_seek_lap(vf, pos): + return libvorbisfile.ov_pcm_seek_lap(vf, pos) + + libvorbisfile.ov_pcm_seek_page_lap.restype = c_int + libvorbisfile.ov_pcm_seek_page_lap.argtypes = [vf_p, ogg_int64_t] + + def ov_pcm_seek_page_lap(vf, pos): + return libvorbisfile.ov_pcm_seek_page_lap(vf, pos) + + libvorbisfile.ov_time_seek_lap.restype = c_int + libvorbisfile.ov_time_seek_lap.argtypes = [vf_p, c_double] + + def ov_time_seek_lap(vf, pos): + return libvorbisfile.ov_time_seek_lap(vf, pos) + + libvorbisfile.ov_time_seek_page_lap.restype = c_int + libvorbisfile.ov_time_seek_page_lap.argtypes = [vf_p, c_double] + + def ov_time_seek_page_lap(vf, pos): + return libvorbisfile.ov_time_seek_page_lap(vf, pos) + + + + libvorbisfile.ov_raw_tell.restype = ogg_int64_t + libvorbisfile.ov_raw_tell.argtypes = [vf_p] + + def ov_raw_tell(vf): + return libvorbisfile.ov_raw_tell(vf) + + libvorbisfile.ov_pcm_tell.restype = ogg_int64_t + libvorbisfile.ov_pcm_tell.argtypes = [vf_p] + + def ov_pcm_tell(vf): + return libvorbisfile.ov_pcm_tell(vf) + + libvorbisfile.ov_time_tell.restype = c_double + libvorbisfile.ov_time_tell.argtypes = [vf_p] + + def ov_time_tell(vf): + return libvorbisfile.ov_time_tell(vf) + + + + libvorbisfile.ov_info.restype = vi_p + libvorbisfile.ov_info.argtypes = [vf_p, c_int] + + def ov_info(vf, link): + return libvorbisfile.ov_info(vf, link) + + libvorbisfile.ov_comment.restype = vc_p + libvorbisfile.ov_comment.argtypes = [vf_p, c_int] + + def ov_comment(vf, link): + return libvorbisfile.ov_comment(vf, link) + + + + libvorbisfile.ov_read_float.restype = c_long + libvorbisfile.ov_read_float.argtypes = [vf_p, c_float_p_p_p, c_int, c_int_p] + + def ov_read_float(vf, pcm_channels, samples, bitstream): + return libvorbisfile.ov_read_float(vf, pcm_channels, samples, bitstream) + + filter_ = ctypes.CFUNCTYPE(None, + c_float_p_p, + c_long, + c_long, + c_void_p) + + try: + libvorbisfile.ov_read_filter.restype = c_long + libvorbisfile.ov_read_filter.argtypes = [vf_p, c_char_p, c_int, c_int, c_int, c_int, c_int_p, filter_, c_void_p] + + def ov_read_filter(vf, buffer, length, bigendianp, word, sgned, bitstream, filter_, filter_param): + return libvorbisfile.ov_read_filter(vf, buffer, length, bigendianp, word, sgned, bitstream, filter_, filter_param) + except: + pass + + libvorbisfile.ov_read.restype = c_long + libvorbisfile.ov_read.argtypes = [vf_p, c_char_p, c_int, c_int, c_int, c_int, c_int_p] + + def ov_read(vf, buffer, length, bigendianp, word, sgned, bitstream): + return libvorbisfile.ov_read(vf, buffer, length, bigendianp, word, sgned, bitstream) + + libvorbisfile.ov_crosslap.restype = c_int + libvorbisfile.ov_crosslap.argtypes = [vf_p, vf_p] + + def ov_crosslap(vf1, cf2): + return libvorbisfile.ov_crosslap(vf1, vf2) + + + + + libvorbisfile.ov_halfrate.restype = c_int + libvorbisfile.ov_halfrate.argtypes = [vf_p, c_int] + + def ov_halfrate(vf, flag): + return libvorbisfile.ov_halfrate(vf, flag) + + libvorbisfile.ov_halfrate_p.restype = c_int + libvorbisfile.ov_halfrate_p.argtypes = [vf_p] + + def ov_halfrate_p(vf): + return libvorbisfile.ov_halfrate_p(vf) + # end of vorbisfile + + try: + # vorbisenc + + # Sanity check also satisfies mypy type checking + assert libvorbisenc is not None + + libvorbisenc.vorbis_encode_init.restype = c_int + libvorbisenc.vorbis_encode_init.argtypes = [vi_p, c_long, c_long, c_long, c_long, c_long] + + def vorbis_encode_init(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate): + return libvorbisenc.vorbis_encode_init(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate) + + libvorbisenc.vorbis_encode_setup_managed.restype = c_int + libvorbisenc.vorbis_encode_setup_managed.argtypes = [vi_p, c_long, c_long, c_long, c_long, c_long] + + def vorbis_encode_setup_managed(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate): + return libvorbisenc.vorbis_encode_setup_managed(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate) + + libvorbisenc.vorbis_encode_setup_vbr.restype = c_int + libvorbisenc.vorbis_encode_setup_vbr.argtypes = [vi_p, c_long, c_long, c_float] + + def vorbis_encode_setup_vbr(vi, channels, rate, quality): + return libvorbisenc.vorbis_encode_setup_vbr(vi, channels, rate, quality) + + libvorbisenc.vorbis_encode_init_vbr.restype = c_int + libvorbisenc.vorbis_encode_init_vbr.argtypes = [vi_p, c_long, c_long, c_float] + + def vorbis_encode_init_vbr(vi, channels, rate, quality): + return libvorbisenc.vorbis_encode_init_vbr(vi, channels, rate, quality) + + libvorbisenc.vorbis_encode_setup_init.restype = c_int + libvorbisenc.vorbis_encode_setup_init.argtypes = [vi_p] + + def vorbis_encode_setup_init(vi): + return libvorbisenc.vorbis_encode_setup_init(vi) + + libvorbisenc.vorbis_encode_ctl.restype = c_int + libvorbisenc.vorbis_encode_ctl.argtypes = [vi_p, c_int, c_void_p] + + def vorbis_encode_ctl(vi, number, arg): + return libvorbisenc.vorbis_encode_ctl(vi, number, arg) + + class ovectl_ratemanage_arg(ctypes.Structure): + _fields_ = [("management_active", c_int), + ("bitrate_hard_min", c_long), + ("bitrate_hard_max", c_long), + ("bitrate_hard_window", c_double), + ("bitrate_av_lo", c_long), + ("bitrate_av_hi", c_long), + ("bitrate_av_window", c_double), + ("bitrate_av_window_center", c_double)] + + class ovectl_ratemanage2_arg(ctypes.Structure): + _fields_ = [("management_active", c_int), + ("bitrate_limit_min_kbps", c_long), + ("bitrate_limit_max_kbps", c_long), + ("bitrate_limit_reservoir_bits", c_long), + ("bitrate_limit_reservoir_bias", c_double), + ("bitrate_average_kbps", c_long), + ("bitrate_average_damping", c_double)] + + OV_ECTL_RATEMANAGE2_GET =0x14 + + OV_ECTL_RATEMANAGE2_SET =0x15 + + OV_ECTL_LOWPASS_GET =0x20 + + OV_ECTL_LOWPASS_SET =0x21 + + OV_ECTL_IBLOCK_GET =0x30 + + OV_ECTL_IBLOCK_SET =0x31 + + OV_ECTL_COUPLING_GET =0x40 + + OV_ECTL_COUPLING_SET =0x41 + + OV_ECTL_RATEMANAGE_GET =0x10 + + OV_ECTL_RATEMANAGE_SET =0x11 + + OV_ECTL_RATEMANAGE_AVG =0x12 + + OV_ECTL_RATEMANAGE_HARD =0x13 + # end of vorbisenc + except: + pass diff --git a/LXST/Codecs/libs/pyogg/vorbis_file.py b/LXST/Codecs/libs/pyogg/vorbis_file.py new file mode 100644 index 0000000..918f1e8 --- /dev/null +++ b/LXST/Codecs/libs/pyogg/vorbis_file.py @@ -0,0 +1,161 @@ +import ctypes + +from . import vorbis +from .audio_file import AudioFile +from .pyogg_error import PyOggError + +# TODO: Issue #70: Vorbis files with multiple logical bitstreams could +# be supported by chaining VorbisFile instances (with say a 'next' +# attribute that points to the next VorbisFile that would contain the +# PCM for the next logical bitstream). A considerable constraint to +# implementing this was that examples files that demonstrated multiple +# logical bitstreams couldn't be found or created. Note that even +# Audacity doesn't handle multiple logical bitstreams (see +# https://wiki.audacityteam.org/wiki/OGG#Importing_multiple_stream_files). + +# TODO: Issue #53: Unicode file names are not well supported. +# They may work in macOS and Linux, they don't work under Windows. + +class VorbisFile(AudioFile): + def __init__(self, + path: str, + bytes_per_sample: int = 2, + signed:bool = True) -> None: + """Load an OggVorbis File. + + path specifies the location of the Vorbis file. Unicode + filenames may not work correctly under Windows. + + bytes_per_sample specifies the word size of the PCM. It may + be either 1 or 2. Specifying one byte per sample will save + memory but will likely decrease the quality of the decoded + audio. + + Only Vorbis files with a single logical bitstream are + supported. + + """ + # Sanity check the number of bytes per sample + assert bytes_per_sample==1 or bytes_per_sample==2 + + # Sanity check that the vorbis library is available (for mypy) + assert vorbis.libvorbisfile is not None + + #: Bytes per sample + self.bytes_per_sample = bytes_per_sample + + #: Samples are signed (rather than unsigned) + self.signed = signed + + # Create a Vorbis File structure + vf = vorbis.OggVorbis_File() + + # Attempt to open the Vorbis file + error = vorbis.libvorbisfile.ov_fopen( + vorbis.to_char_p(path), + ctypes.byref(vf) + ) + + # Check for errors during opening + if error != 0: + raise PyOggError( + ("File '{}' couldn't be opened or doesn't exist. "+ + "Error code : {}").format(path, error) + ) + + # Extract info from the Vorbis file + info = vorbis.libvorbisfile.ov_info( + ctypes.byref(vf), + -1 # the current logical bitstream + ) + + #: Number of channels in audio file. + self.channels = info.contents.channels + + #: Number of samples per second (per channel), 44100 for + # example. + self.frequency = info.contents.rate + + # Extract the total number of PCM samples for the first + # logical bitstream + pcm_length_samples = vorbis.libvorbisfile.ov_pcm_total( + ctypes.byref(vf), + 0 # to extract the length of the first logical bitstream + ) + + # Create a memory block to store the entire PCM + Buffer = ( + ctypes.c_char + * ( + pcm_length_samples + * self.bytes_per_sample + * self.channels + ) + ) + self.buffer = Buffer() + + # Create a pointer to the newly allocated memory. It + # seems we can only do pointer arithmetic on void + # pointers. See + # https://mattgwwalker.wordpress.com/2020/05/30/pointer-manipulation-in-python/ + buf_ptr = ctypes.cast( + ctypes.pointer(self.buffer), + ctypes.c_void_p + ) + + # Storage for the index of the logical bitstream + bitstream_previous = None + bitstream = ctypes.c_int() + + # Set bytes remaining to read into PCM + read_size = len(self.buffer) + + while True: + # Convert buffer pointer to the desired type + ptr = ctypes.cast( + buf_ptr, + ctypes.POINTER(ctypes.c_char) + ) + + # Attempt to decode PCM from the Vorbis file + result = vorbis.libvorbisfile.ov_read( + ctypes.byref(vf), + ptr, + read_size, + 0, # Little endian + self.bytes_per_sample, + int(self.signed), + ctypes.byref(bitstream) + ) + + # Check for errors + if result < 0: + raise PyOggError( + "An error occurred decoding the Vorbis file: "+ + f"Error code: {result}" + ) + + # Check that the bitstream hasn't changed as we only + # support Vorbis files with a single logical bitstream. + if bitstream_previous is None: + bitstream_previous = bitstream + else: + if bitstream_previous != bitstream: + raise PyOggError( + "PyOgg currently supports Vorbis files "+ + "with only one logical stream" + ) + + # Check for end of file + if result == 0: + break + + # Calculate the number of bytes remaining to read into PCM + read_size -= result + + # Update the pointer into the buffer + buf_ptr.value += result + + + # Close the file and clean up memory + vorbis.libvorbisfile.ov_clear(ctypes.byref(vf)) diff --git a/LXST/Codecs/libs/pyogg/vorbis_file_stream.py b/LXST/Codecs/libs/pyogg/vorbis_file_stream.py new file mode 100644 index 0000000..57677ba --- /dev/null +++ b/LXST/Codecs/libs/pyogg/vorbis_file_stream.py @@ -0,0 +1,110 @@ +import ctypes + +from . import vorbis +from .pyogg_error import PyOggError + +class VorbisFileStream: + def __init__(self, path, buffer_size=8192): + self.exists = False + self._buffer_size = buffer_size + + self.vf = vorbis.OggVorbis_File() + error = vorbis.ov_fopen(path, ctypes.byref(self.vf)) + if error != 0: + raise PyOggError("file couldn't be opened or doesn't exist. Error code : {}".format(error)) + + info = vorbis.ov_info(ctypes.byref(self.vf), -1) + + #: Number of channels in audio file. + self.channels = info.contents.channels + + #: Number of samples per second (per channel). Always + # 48,000. + self.frequency = info.contents.rate + + array = (ctypes.c_char*(self._buffer_size*self.channels))() + + self.buffer_ = ctypes.cast(ctypes.pointer(array), ctypes.c_char_p) + + self.bitstream = ctypes.c_int() + self.bitstream_pointer = ctypes.pointer(self.bitstream) + + self.exists = True # TODO: is this the best place for this statement? + + #: Bytes per sample + self.bytes_per_sample = 2 # TODO: Where is this defined? + + def __del__(self): + if self.exists: + vorbis.ov_clear(ctypes.byref(self.vf)) + self.exists = False + + def clean_up(self): + vorbis.ov_clear(ctypes.byref(self.vf)) + + self.exists = False + + def get_buffer(self): + """get_buffer() -> bytesBuffer, bufferLength + + Returns None when all data has been read from the file. + + """ + if not self.exists: + return None + buffer = [] + total_bytes_written = 0 + + while True: + new_bytes = vorbis.ov_read(ctypes.byref(self.vf), self.buffer_, self._buffer_size*self.channels - total_bytes_written, 0, 2, 1, self.bitstream_pointer) + + array_ = ctypes.cast(self.buffer_, ctypes.POINTER(ctypes.c_char*(self._buffer_size*self.channels))).contents + + buffer.append(array_.raw[:new_bytes]) + + total_bytes_written += new_bytes + + if new_bytes == 0 or total_bytes_written >= self._buffer_size*self.channels: + break + + out_buffer = b"".join(buffer) + + if total_bytes_written == 0: + self.clean_up() + return(None) + + return out_buffer + + def get_buffer_as_array(self): + """Provides the buffer as a NumPy array. + + Note that the underlying data type is 16-bit signed + integers. + + Does not copy the underlying data, so the returned array + should either be processed or copied before the next call + to get_buffer() or get_buffer_as_array(). + + """ + import numpy # type: ignore + + # Read the next samples from the stream + buf = self.get_buffer() + + # Check if we've come to the end of the stream + if buf is None: + return None + + # Convert the bytes buffer to a NumPy array + array = numpy.frombuffer( + buf, + dtype=numpy.int16 + ) + + # Reshape the array + return array.reshape( + (len(buf) + // self.bytes_per_sample + // self.channels, + self.channels) + ) diff --git a/LXST/Common.py b/LXST/Common.py new file mode 100644 index 0000000..6ba1e9c --- /dev/null +++ b/LXST/Common.py @@ -0,0 +1,2 @@ +def nop(): + pass \ No newline at end of file diff --git a/LXST/Generators.py b/LXST/Generators.py new file mode 100644 index 0000000..e505a50 --- /dev/null +++ b/LXST/Generators.py @@ -0,0 +1,135 @@ +import os +import RNS +import math +import time +import threading +import numpy as np +from collections import deque +from .Codecs import Codec, CodecError +from .Sources import LocalSource + +RNS.loglevel = RNS.LOG_DEBUG + +class ToneSource(LocalSource): + DEFAULT_FRAME_MS = 80 + DEFAULT_SAMPLERATE = 48000 + DEFAULT_FREQUENCY = 400 + EASE_TIME_MS = 20 + + def __init__(self, frequency=DEFAULT_FREQUENCY, gain=0.1, ease=True, ease_time_ms=EASE_TIME_MS, + target_frame_ms=DEFAULT_FRAME_MS, codec=None, sink=None, channels=1): + + self.target_frame_ms = target_frame_ms + self.samplerate = self.DEFAULT_SAMPLERATE + self.channels = channels + self.bitdepth = 32 + self.frequency = frequency + self._gain = gain + self.gain = self._gain + self.ease = ease + self.theta = 0 + self.ease_gain = 0 + self.ease_time_ms = ease_time_ms + self.ease_step = 0 + self.gain_step = 0 + self.easing_out = False + self.should_run = False + self.generate_thread = None + self.generate_lock = threading.Lock() + self._codec = None + self.codec = codec + self.sink = sink + + @property + def codec(self): + return self._codec + + @codec.setter + def codec(self, codec): + if codec == None: + self._codec = None + elif not issubclass(type(codec), Codec): + raise CodecError(f"Invalid codec specified for {self}") + else: + self._codec = codec + + if self.codec.preferred_samplerate: + self.samplerate = self.codec.preferred_samplerate + + if self.codec.frame_quanta_ms: + if self.target_frame_ms%self.codec.frame_quanta_ms != 0: + self.target_frame_ms = math.ceil(self.target_frame_ms/self.codec.frame_quanta_ms)*self.codec.frame_quanta_ms + RNS.log(f"{self} target frame time quantized to {self.target_frame_ms}ms due to codec frame quanta", RNS.LOG_DEBUG) + + if self.codec.frame_max_ms: + if self.target_frame_ms > self.codec.frame_max_ms: + self.target_frame_ms = self.codec.frame_max_ms + RNS.log(f"{self} target frame time clamped to {self.target_frame_ms}ms due to codec frame limit", RNS.LOG_DEBUG) + + if self.codec.valid_frame_ms: + if not self.target_frame_ms in self.codec.valid_frame_ms: + self.target_frame_ms = min(self.codec.valid_frame_ms, key=lambda t:abs(t-self.target_frame_ms)) + RNS.log(f"{self} target frame time clamped to closest valid value of {self.target_frame_ms}ms ", RNS.LOG_DEBUG) + + self.samples_per_frame = math.ceil((self.target_frame_ms/1000)*self.samplerate) + self.frame_time = self.samples_per_frame/self.samplerate + self.ease_step = 1/(self.samplerate*(self.ease_time_ms/1000)) + self.gain_step = 0.02/(self.samplerate*(self.ease_time_ms/1000)) + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting at {self.samples_per_frame} samples per frame, {self.channels} channels", RNS.LOG_DEBUG) + self.ease_gain = 0 if self.ease else 1 + self.should_run = True + self.generate_thread = threading.Thread(target=self.__generate_job, daemon=True) + self.generate_thread.start() + + def stop(self): + if not self.ease: + self.should_run = False + else: + self.easing_out = True + + @property + def running(self): + return self.should_run and not self.easing_out + + def __generate(self): + frame_samples = np.zeros((self.samples_per_frame, self.channels), dtype="float32") + step = (self.frequency * 2 * math.pi) / self.samplerate + for n in range(0, self.samples_per_frame): + self.theta += step + amplitude = math.sin(self.theta)*self._gain*self.ease_gain + for c in range(0, self.channels): + frame_samples[n, c] = amplitude + + if self.gain > self._gain: + self._gain += self.gain_step + if self._gain > self.gain: self._gain = self.gain + + if self.gain < self._gain: + self._gain -= self.gain_step + if self._gain < self.gain: self._gain = self.gain + + if self.ease: + if self.ease_gain < 1.0 and not self.easing_out: + self.ease_gain += self.ease_step + if self.ease_gain > 1.0: self.ease_gain = 1.0 + elif self.easing_out and self.ease_gain > 0.0: + self.ease_gain -= self.ease_step + if self.ease_gain <= 0.0: + self.ease_gain = 0.0 + self.easing_out = False + self.should_run = False + + return frame_samples + + def __generate_job(self): + with self.generate_lock: + while self.should_run: + if self.codec and self.sink and self.sink.can_receive(from_source=self): + frame_samples = self.__generate() + self.last_samples = frame_samples + frame = self.codec.encode(frame_samples) + self.sink.handle_frame(frame, self) + time.sleep(self.frame_time*0.1) diff --git a/LXST/Mixer.py b/LXST/Mixer.py new file mode 100644 index 0000000..62ed1e9 --- /dev/null +++ b/LXST/Mixer.py @@ -0,0 +1,149 @@ +import RNS +import LXST +import time +import math +import threading +import numpy as np +from collections import deque +from inspect import currentframe +from .Codecs import Codec, Raw +from .Codecs.Codec import resample +from .Sinks import LocalSink +from .Sources import LocalSource, Backend + +class Mixer(LocalSource, LocalSink): + MAX_FRAMES = 8 + TYPE_MAP_FACTOR = np.iinfo("int16").max + + def __init__(self, target_frame_ms=40, samplerate=None, codec=None, sink=None): + self.incoming_frames = {} + self.target_frame_ms = target_frame_ms + self.frame_time = self.target_frame_ms/1000 + self.should_run = False + self.mixer_thread = None + self.mixer_lock = threading.Lock() + self.insert_lock = threading.Lock() + self.bitdepth = 32 + self.channels = None + self.samplerate = None + self._sink = None + self._source = None + self._codec = None + + if samplerate: self.samplerate = samplerate + if sink: self.sink = sink + if codec: self.codec = codec + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting", RNS.LOG_DEBUG) + self.should_run = True + self.mixer_thread = threading.Thread(target=self._mixer_job, daemon=True) + self.mixer_thread.start() + + def stop(self): + self.should_run = False + + def can_receive(self, from_source): + if not from_source in self.incoming_frames: + return True + elif len(self.incoming_frames[from_source]) < self.MAX_FRAMES: + return True + else: + return False + + def handle_frame(self, frame, source, decoded=False): + with self.insert_lock: + if not source in self.incoming_frames: + self.incoming_frames[source] = deque(maxlen=self.MAX_FRAMES) + + if not self.channels: + self.channels = source.channels + + if not self.samplerate: + self.samplerate = source.samplerate + self.samples_per_frame = math.ceil((self.target_frame_ms/1000)*self.samplerate) + self.frame_time = self.samples_per_frame/self.samplerate + RNS.log(f"{self} samplerate set to {RNS.prettyfrequency(self.samplerate)}", RNS.LOG_DEBUG) + RNS.log(f"{self} frame time is {RNS.prettyshorttime(self.frame_time)}") + + if not decoded: frame_samples = source.codec.decode(frame) + else: frame_samples = frame + + # TODO: Add resampling for all source types + # if CODEC_OUTPUT_RATE != self.samplerate: + # frame_samples = resample(frame_samples, source.bitdepth, source.channels, CODEC_OUTPUT_RATE, self.samplerate) + + self.incoming_frames[source].append(frame_samples) + + def _mixer_job(self): + with self.mixer_lock: + while self.should_run: + if self.sink and self.sink.can_receive(): + source_count = 0 + mixed_frame = None + for source in self.incoming_frames.copy(): + if len(self.incoming_frames[source]) > 0: + next_frame = self.incoming_frames[source].popleft() + if source_count == 0: mixed_frame = next_frame + else: mixed_frame = mixed_frame + next_frame + source_count += 1 + + if source_count > 0: + if self.codec: self.sink.handle_frame(self.codec.encode(mixed_frame), self) + else: self.sink.handle_frame(mixed_frame, self) + else: + time.sleep(self.frame_time*0.1) + + else: + time.sleep(self.frame_time*0.1) + + @property + def codec(self): + return self._codec + + @codec.setter + def codec(self, codec): + if codec == None: + self._codec = None + elif not issubclass(type(codec), Codec): + raise CodecError(f"Invalid codec specified for {self}") + else: + self._codec = codec + + if self.codec.preferred_samplerate: + self.samplerate = self.codec.preferred_samplerate + else: + self.samplerate = Backend.SAMPLERATE + + if self.codec.frame_quanta_ms: + if self.target_frame_ms%self.codec.frame_quanta_ms != 0: + self.target_frame_ms = math.ceil(self.target_frame_ms/self.codec.frame_quanta_ms)*self.codec.frame_quanta_ms + RNS.log(f"{self} target frame time quantized to {self.target_frame_ms}ms due to codec frame quanta", RNS.LOG_DEBUG) + + if self.codec.frame_max_ms: + if self.target_frame_ms > self.codec.frame_max_ms: + self.target_frame_ms = self.codec.frame_max_ms + RNS.log(f"{self} target frame time clamped to {self.target_frame_ms}ms due to codec frame limit", RNS.LOG_DEBUG) + + if self.codec.valid_frame_ms: + if not self.target_frame_ms in self.codec.valid_frame_ms: + self.target_frame_ms = min(self.codec.valid_frame_ms, key=lambda t:abs(t-self.target_frame_ms)) + RNS.log(f"{self} target frame time clamped to closest valid value of {self.target_frame_ms}ms ", RNS.LOG_DEBUG) + + @property + def source(self): + return self._source + + @source.setter + def source(self, source): + self._source = source + + @property + def sink(self): + return self._sink + + @sink.setter + def sink(self, sink): + self._sink = sink + diff --git a/LXST/Network.py b/LXST/Network.py new file mode 100644 index 0000000..5c0f2a2 --- /dev/null +++ b/LXST/Network.py @@ -0,0 +1,149 @@ +import RNS +import time +import threading +from .Sinks import RemoteSink +from .Sources import RemoteSource +from .Codecs import Null, codec_header_byte, codec_type +from collections import deque +from RNS.vendor import umsgpack as mp + +FIELD_SIGNALLING = 0x00 +FIELD_FRAMES = 0x01 + +class SignallingReceiver(): + def __init__(self, proxy=None): + # TODO: Add inband signalling scheduler + self.outgoing_signals = deque() + self.proxy = proxy + + def handle_signalling_from(self, source): + source.set_packet_callback(self._packet) + + def signalling_received(self, signals, source): + if self.proxy: self.proxy.signalling_received(signals, source) + + def signal(self, signal, destination, immediate=True): + signalling_data = {FIELD_SIGNALLING:[signal]} + + if immediate: + signalling_packet = RNS.Packet(destination, mp.packb(signalling_data), create_receipt=False) + signalling_packet.send() + else: + # TODO: Add inband signalling scheduler + pass + + def _packet(self, data, packet, unpacked=None): + try: + if not unpacked: unpacked = mp.unpackb(data) + source = packet.link if hasattr(packet, "link") else None + if type(unpacked) == dict: + if FIELD_SIGNALLING in unpacked: + signalling = unpacked[FIELD_SIGNALLING] + if type(signalling) == list: + self.signalling_received(signalling, source) + else: + self.signalling_received([signalling], source) + + except Exception as e: + RNS.log(f"{self} could not process incoming packet: {e}", RNS.LOG_ERROR) + RNS.trace_exception(e) + +class Packetizer(RemoteSink): + def __init__(self, destination, failure_callback=None): + self.destination = destination + self.should_run = False + self.source = None + self.transmit_failure = False + self.__failure_calback = failure_callback + + def handle_frame(self, frame, source=None): + if type(self.destination) == RNS.Link and not self.destination.status == RNS.Link.ACTIVE: + return + + # TODO: Add inband signalling scheduler + frame = codec_header_byte(type(self.source.codec))+frame + packet_data = {FIELD_FRAMES:frame} + frame_packet = RNS.Packet(self.destination, mp.packb(packet_data), create_receipt=False) + if frame_packet.send() == False: + self.transmit_failure = True + if callable(self.__failure_calback): self.__failure_calback() + + # TODO: Remove testing + # if not hasattr(self, "frames"): + # self.frames = 0 + # self.frame_bytes = 0 + # self.total_bytes = 0 + # self.total_bytes = 0 + # self.overhead_bytes = 0 + # self.frames += 1 + # self.frame_bytes += len(frame) + # self.total_bytes += len(frame_packet.raw) + # self.overhead_bytes += len(frame_packet.raw)-len(frame) + # self.overhead_ratio = self.frame_bytes / self.total_bytes + # if not hasattr(self, "started"): + # self.started = time.time() + # rate = 0 + # codec_rate = 0 + # else: + # rate = (self.total_bytes*8)/(time.time()-self.started) + # codec_rate = (self.frame_bytes*8)/(time.time()-self.started) + # print(f"\rP={len(frame_packet.raw)}/{len(frame)}/{len(frame_packet.raw)-len(frame)} N={self.frames} E={round(self.overhead_ratio*100,0)}% O={RNS.prettysize(self.total_bytes)} F={RNS.prettysize(self.frame_bytes)} S={RNS.prettyspeed(rate)} C={RNS.prettyspeed(codec_rate)}", end=" ") + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting", RNS.LOG_DEBUG) + self.should_run = True + + def stop(self): + self.should_run = False + +class LinkSource(RemoteSource, SignallingReceiver): + def __init__(self, link, signalling_receiver, sink=None): + self.should_run = False + self.link = link + self.sink = sink + self.codec = Null() + self.pipeline = None + self.proxy = signalling_receiver + self.receive_lock = threading.Lock() + self.link.set_packet_callback(self._packet) + + def _packet(self, data, packet): + with self.receive_lock: + try: + unpacked = mp.unpackb(data) + if type(unpacked) == dict: + if FIELD_FRAMES in unpacked: + frames = unpacked[FIELD_FRAMES] + if type(frames) != list: frames = [frames] + for frame in frames: + frame_codec = codec_type(frame[0]) + if self.codec and self.sink: + if type(self.codec) != frame_codec: + RNS.log(f"Remote switched codec to {frame_codec}", RNS.LOG_DEBUG) + if self.pipeline: self.pipeline.codec = frame_codec() + else: self.codec = frame_codec(); self.codec.sink = self.sink + decoded_frame = self.codec.decode(frame[1:]) + if self.codec.channels: self.channels = self.codec.channels + else: + decoded_frame = self.codec.decode(frame[1:]) + + if self.pipeline: + self.sink.handle_frame(decoded_frame, self) + else: + self.sink.handle_frame(decoded_frame, self, decoded=True) + + if FIELD_SIGNALLING in unpacked: + super()._packet(data=None, packet=packet, unpacked=unpacked) + + except Exception as e: + RNS.log(f"{self} could not process incoming packet: {e}", RNS.LOG_ERROR) + RNS.trace_exception(e) + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting", RNS.LOG_DEBUG) + self.should_run = True + + def stop(self): + self.should_run = False \ No newline at end of file diff --git a/LXST/Pipeline.py b/LXST/Pipeline.py new file mode 100644 index 0000000..db1920d --- /dev/null +++ b/LXST/Pipeline.py @@ -0,0 +1,60 @@ +from .Sources import * +from .Sinks import * +from .Codecs import * +from .Mixer import Mixer +from .Network import Packetizer + +class PipelineError(Exception): + pass + +class Pipeline(): + def __init__(self, source, codec, sink, processor = None): + if not issubclass(type(source), Source): raise PipelineError("Audio pipeline initialised with invalid source") + if not issubclass(type(sink), Sink) : raise PipelineError("Audio pipeline initialised with invalid sink") + if not issubclass(type(codec), Codec) : raise PipelineError("Audio pipeline initialised with invalid codec") + self._codec = None + self.source = source + self.source.pipeline = self + self.source.sink = sink + self.codec = codec + + if isinstance(sink, Loopback): + sink.samplerate = source.samplerate + if isinstance(source, Loopback): + source._sink = sink + if isinstance(sink, Packetizer): + sink.source = source + + @property + def codec(self): + if self.source: + return self.source.codec + else: + return None + + @codec.setter + def codec(self, codec): + if not self._codec == codec: + self._codec = codec + self.source.codec = self._codec + self.source.codec.sink = self.sink + self.source.codec.source = self.source + + @property + def sink(self): + if self.source: + return self.source.sink + else: + return None + + @property + def running(self): + return self.source.should_run + + def start(self): + if not self.running: + self.source.start() + + def stop(self): + if self.running: + self.source.stop() \ No newline at end of file diff --git a/LXST/Primitives/Telephony.py b/LXST/Primitives/Telephony.py new file mode 100644 index 0000000..625a7b9 --- /dev/null +++ b/LXST/Primitives/Telephony.py @@ -0,0 +1,501 @@ +import os +import RNS +import LXST +import time +import threading + +from LXST import APP_NAME +from LXST import Mixer, Pipeline +from LXST.Codecs import Raw, Opus, Codec2, Null +from LXST.Sinks import LineSink +from LXST.Sources import LineSource, OpusFileSource +from LXST.Generators import ToneSource +from LXST.Network import SignallingReceiver, Packetizer, LinkSource + +PRIMITIVE_NAME = "telephony" + +class Signalling(): + STATUS_BUSY = 0x00 + STATUS_REJECTED = 0x01 + STATUS_CALLING = 0x02 + STATUS_AVAILABLE = 0x03 + STATUS_RINGING = 0x04 + STATUS_CONNECTING = 0x05 + STATUS_ESTABLISHED = 0x06 + AUTO_STATUS_CODES = [STATUS_CALLING, STATUS_AVAILABLE, STATUS_RINGING, + STATUS_CONNECTING, STATUS_ESTABLISHED] + +class Telephone(SignallingReceiver): + RING_TIME = 60 + WAIT_TIME = 70 + DIAL_TONE_FREQUENCY = 382 + DIAL_TONE_EASE_MS = 3.14159 + JOB_INTERVAL = 5 + ANNOUNCE_INTERVAL_MIN = 60*5 + ANNOUNCE_INTERVAL = 60*60*3 + ALLOW_ALL = 0xFF + ALLOW_NONE = 0xFE + + def __init__(self, identity, ring_time=RING_TIME, wait_time=WAIT_TIME, auto_answer=None, allowed=ALLOW_ALL): + super().__init__() + self.identity = identity + self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, PRIMITIVE_NAME) + self.destination.set_proof_strategy(RNS.Destination.PROVE_NONE) + self.destination.set_link_established_callback(self.__incoming_link_established) + self.allowed = allowed + self.blocked = None + self.last_announce = 0 + self.call_handler_lock = threading.Lock() + self.pipeline_lock = threading.Lock() + self.caller_pipeline_open_lock = threading.Lock() + self.links = {} + self.ring_time = ring_time + self.wait_time = wait_time + self.auto_answer = auto_answer + self.active_call = None + self.call_status = Signalling.STATUS_AVAILABLE + self._external_busy = False + self.__ringing_callback = None + self.__established_callback = None + self.__ended_callback = None + self.target_frame_time_ms = None + self.audio_output = None + self.audio_input = None + self.dial_tone = None + self.dial_tone_frequency = self.DIAL_TONE_FREQUENCY + self.dial_tone_ease_ms = self.DIAL_TONE_EASE_MS + self.transmit_codec = None + self.receive_codec = None + self.receive_mixer = None + self.transmit_mixer = None + self.receive_pipeline = None + self.transmit_pipeline = None + self.ringer_lock = threading.Lock() + self.ringer_output = None + self.ringer_pipeline = None + self.ringtone_path = None + self.speaker_device = None + self.microphone_device = None + self.ringer_device = None + + threading.Thread(target=self.__jobs, daemon=True).start() + RNS.log(f"{self} listening on {RNS.prettyhexrep(self.destination.hash)}", RNS.LOG_DEBUG) + + def teardown(self): + self.hangup() + RNS.Transport.deregister_destination(self.destination) + self.destination = None + + def announce(self): + self.destination.announce() + self.last_announce = time.time() + + def set_allowed(self, allowed): + valid_allowed = [self.ALLOW_ALL, self.ALLOW_NONE] + if callable(allowed) or type(allowed) == list or allowed in valid_allowed: self.allowed = allowed + else: raise TypeError(f"Invalid type for allowed callers: {type(allowed)}") + + def set_blocked(self, blocked): + if type(blocked) == list or blocked == None: self.blocked = blocked + else: raise TypeError(f"Invalid type for blocked callers: {type(blocked)}") + + def set_announce_interval(self, announce_interval): + if not type(announce_interval) == int: raise TypeError(f"Invalid type for announce interval: {announce_interval}") + else: + if announce_interval < self.ANNOUNCE_INTERVAL_MIN: announce_interval = self.ANNOUNCE_INTERVAL_MIN + self.announce_interval = announce_interval + + def set_ringing_callback(self, callback): + if not callable(callback): raise TypeError(f"Invalid callback, {callback} is not callable") + self.__ringing_callback = callback + + def set_established_callback(self, callback): + if not callable(callback): raise TypeError(f"Invalid callback, {callback} is not callable") + self.__established_callback = callback + + def set_ended_callback(self, callback): + if not callable(callback): raise TypeError(f"Invalid callback, {callback} is not callable") + self.__ended_callback = callback + + def set_speaker(self, device): + self.speaker_device = device + RNS.log(f"{self} speaker device set to {device}", RNS.LOG_DEBUG) + + def set_microphone(self, device): + self.microphone_device = device + RNS.log(f"{self} microphone device set to {device}", RNS.LOG_DEBUG) + + def set_ringer(self, device): + self.ringer_device = device + RNS.log(f"{self} ringer device set to {device}", RNS.LOG_DEBUG) + + def set_ringtone(self, ringtone_path, gain=1.0): + self.ringtone_path = ringtone_path + self.ringtone_gain = gain + RNS.log(f"{self} ringtone set to {self.ringtone_path}", RNS.LOG_DEBUG) + + def __jobs(self): + while self.destination != None: + time.sleep(self.JOB_INTERVAL) + if time.time() > self.last_announce+self.ANNOUNCE_INTERVAL: + if self.destination != None: self.announce() + + def __is_allowed(self, remote_identity): + identity_hash = remote_identity.hash + if type(self.blocked) == list and identity_hash in self.blocked: return False + elif self.allowed == self.ALLOW_ALL: return True + elif self.allowed == self.ALLOW_NONE: return False + elif type(self.allowed) == list: return identity_hash in self.allowed + elif callable(self.allowed): return self.allowed(identity_hash) + + def __timeout_incoming_call_at(self, call, timeout): + def job(): + while time.time() Signalling.STATUS_RINGING: + RNS.log(f"Incoming call from {RNS.prettyhexrep(identity.hash)} already answered and active") + return False + elif not self.active_call: + RNS.log(f"Answering call failed, no active incoming call", RNS.LOG_ERROR) + return False + elif not self.active_call.get_remote_identity(): + RNS.log(f"Answering call failed, active incoming call is not from {RNS.prettyhexrep(identity.hash)}", RNS.LOG_ERROR) + return False + else: + RNS.log(f"Answering call from {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG) + self.__open_pipelines(identity) + self.__start_pipelines() + RNS.log(f"Call setup complete for {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG) + if callable(self.__established_callback): self.__established_callback(self.active_call.get_remote_identity()) + return True + + def hangup(self): + if self.active_call: + with self.call_handler_lock: + terminating_call = self.active_call; self.active_call = None + remote_identity = terminating_call.get_remote_identity() + + if terminating_call.is_incoming and self.call_status == Signalling.STATUS_RINGING: + if not terminating_call.ring_timeout and terminating_call.status == RNS.Link.ACTIVE: + self.signal(Signalling.STATUS_REJECTED, terminating_call) + + if terminating_call.status == RNS.Link.ACTIVE: terminating_call.teardown() + self.__stop_pipelines() + self.receive_mixer = None + self.transmit_mixer = None + self.receive_pipeline = None + self.transmit_pipeline = None + self.audio_output = None + self.dial_tone = None + self.call_status = Signalling.STATUS_AVAILABLE + if remote_identity: + RNS.log(f"Call with {RNS.prettyhexrep(remote_identity.hash)} terminated", RNS.LOG_DEBUG) + else: + RNS.log(f"Outgoing call could not be connected, link establishment failed", RNS.LOG_DEBUG) + + if callable(self.__ended_callback): self.__ended_callback(remote_identity) + + def mute_receive(self): + pass + + def mute_transmit(self): + pass + + def select_call_codecs(self): + self.receive_codec = Null() + + # self.transmit_codec = Codec2(mode=Codec2.CODEC2_700C) + # self.transmit_codec = Codec2(mode=Codec2.CODEC2_1600) + # self.transmit_codec = Codec2(mode=Codec2.CODEC2_3200) + # self.transmit_codec = Opus(profile=Opus.PROFILE_VOICE_LOW) + self.transmit_codec = Opus(profile=Opus.PROFILE_VOICE_MEDIUM) + # self.transmit_codec = Opus(profile=Opus.PROFILE_VOICE_HIGH) + # self.transmit_codec = Opus(profile=Opus.PROFILE_VOICE_MAX) + # self.transmit_codec = Opus(profile=Opus.PROFILE_AUDIO_MIN) + # self.transmit_codec = Opus(profile=Opus.PROFILE_AUDIO_LOW) + # self.transmit_codec = Opus(profile=Opus.PROFILE_AUDIO_MEDIUM) + # self.transmit_codec = Opus(profile=Opus.PROFILE_AUDIO_HIGH) + # self.transmit_codec = Opus(profile=Opus.PROFILE_AUDIO_MAX) + # self.transmit_codec = Raw() + + def select_call_frame_time(self): + self.target_frame_time_ms = 60 + return self.target_frame_time_ms + + def __reset_dialling_pipelines(self): + with self.pipeline_lock: + if self.audio_output: self.audio_output.stop() + if self.dial_tone: self.dial_tone.stop() + if self.receive_pipeline: self.receive_pipeline.stop() + if self.receive_mixer: self.receive_mixer.stop() + self.audio_output = None + self.dial_tone = None + self.receive_pipeline = None + self.receive_mixer = None + self.__prepare_dialling_pipelines() + + def __prepare_dialling_pipelines(self): + self.select_call_frame_time() + self.select_call_codecs() + if self.audio_output == None: self.audio_output = LineSink(preferred_device=self.speaker_device) + if self.receive_mixer == None: self.receive_mixer = Mixer(target_frame_ms=self.target_frame_time_ms) + if self.dial_tone == None: self.dial_tone = ToneSource(frequency=self.dial_tone_frequency, gain=0.0, ease_time_ms=self.dial_tone_ease_ms, target_frame_ms=self.target_frame_time_ms, codec=Null(), sink=self.receive_mixer) + if self.receive_pipeline == None: self.receive_pipeline = Pipeline(source=self.receive_mixer, codec=Null(), sink=self.audio_output) + + def __activate_ring_tone(self): + if self.ringtone_path != None and os.path.isfile(self.ringtone_path): + if not self.ringer_pipeline: + if not self.ringer_output: self.ringer_output = LineSink(preferred_device=self.ringer_device) + self.ringer_source = OpusFileSource(self.ringtone_path, loop=True, target_frame_ms=60) + self.ringer_pipeline = Pipeline(source=self.ringer_source, codec=Null(), sink=self.ringer_output) + + def job(): + with self.ringer_lock: + while self.active_call and self.active_call.is_incoming and self.call_status == Signalling.STATUS_RINGING: + if not self.ringer_pipeline.running: self.ringer_pipeline.start() + time.sleep(0.1) + self.ringer_source.stop() + threading.Thread(target=job, daemon=True).start() + + def __play_busy_tone(self): + if self.audio_output == None or self.receive_mixer == None or self.dial_tone == None: self.__reset_dialling_pipelines() + with self.pipeline_lock: + window = 0.5; started = time.time() + while time.time()-started < 4.25: + elapsed = (time.time()-started)%window + if elapsed > 0.25: self.__enable_dial_tone() + else: self.__mute_dial_tone() + time.sleep(0.005) + time.sleep(0.5) + + def __activate_dial_tone(self): + def job(): + window = 7 + started = time.time() + while self.active_call and self.active_call.is_outgoing and self.call_status == Signalling.STATUS_RINGING: + elapsed = (time.time()-started)%window + if elapsed > 0.05 and elapsed < 2.05: self.__enable_dial_tone() + else: self.__mute_dial_tone() + time.sleep(0.2) + + threading.Thread(target=job, daemon=True).start() + + def __enable_dial_tone(self): + if not self.receive_mixer.should_run: self.receive_mixer.start() + self.dial_tone.gain = 0.04 + if not self.dial_tone.running: self.dial_tone.start() + + def __mute_dial_tone(self): + if not self.receive_mixer.should_run: self.receive_mixer.start() + if self.dial_tone.running and self.dial_tone.gain != 0: self.dial_tone.gain = 0.0 + if not self.dial_tone.running: self.dial_tone.start() + + def __disable_dial_tone(self): + if self.dial_tone and self.dial_tone.running: + self.dial_tone.stop() + + def __open_pipelines(self, identity): + with self.pipeline_lock: + if not self.active_call.get_remote_identity() == identity: + RNS.log("Identity mismatch while opening call pipelines, tearing down call", RNS.LOG_ERROR) + self.hangup() + else: + if not hasattr(self.active_call, "pipelines_opened"): self.active_call.pipelines_opened = False + if self.active_call.pipelines_opened: RNS.log(f"Pipelines already openened for call with {RNS.prettyhexrep(identity.hash)}", RNS.LOG_ERROR) + else: + RNS.log(f"Opening audio pipelines for call with {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG) + if self.active_call.is_incoming: self.signal(Signalling.STATUS_CONNECTING, self.active_call) + + self.__prepare_dialling_pipelines() + self.transmit_mixer = Mixer(target_frame_ms=self.target_frame_time_ms) + self.audio_input = LineSource(preferred_device=self.microphone_device, target_frame_ms=self.target_frame_time_ms, codec=Raw(), sink=self.transmit_mixer) + # self.audio_input = OpusFileSource("/home/markqvist/Information/Source/LXST/docs/425.opus", loop=True, target_frame_ms=self.target_frame_time_ms, codec=Raw(), sink=self.transmit_mixer, timed=True) + self.transmit_pipeline = Pipeline(source=self.transmit_mixer, + codec=self.transmit_codec, + sink=Packetizer(self.active_call, failure_callback=self.__packetizer_failure)) + + self.active_call.audio_source = LinkSource(link=self.active_call, signalling_receiver=self, sink=self.receive_mixer) + + self.signal(Signalling.STATUS_ESTABLISHED, self.active_call) + + def __packetizer_failure(self): + RNS.log(f"Frame packetization failed, terminating call", RNS.LOG_ERROR) + self.hangup() + + def __start_pipelines(self): + with self.pipeline_lock: + if self.receive_mixer: self.receive_mixer.start() + if self.transmit_mixer: self.transmit_mixer.start() + if self.audio_input: self.audio_input.start() + if self.transmit_pipeline: self.transmit_pipeline.start() + if not self.audio_input: RNS.log("No audio input was ready at call establishment", RNS.LOG_ERROR) + RNS.log(f"Audio pipelines started", RNS.LOG_DEBUG) + + def __stop_pipelines(self): + with self.pipeline_lock: + if self.receive_mixer: self.receive_mixer.stop() + if self.transmit_mixer: self.transmit_mixer.stop() + if self.audio_input: self.audio_input.stop() + if self.receive_pipeline: self.receive_pipeline.stop() + if self.transmit_pipeline: self.transmit_pipeline.stop() + RNS.log(f"Audio pipelines stopped", RNS.LOG_DEBUG) + + def call(self, identity): + with self.call_handler_lock: + if not self.active_call: + self.call_status = Signalling.STATUS_CALLING + outgoing_call_timeout = time.time()+self.wait_time + call_destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, PRIMITIVE_NAME) + if not RNS.Transport.has_path(call_destination.hash): + RNS.log(f"No path known for call to {RNS.prettyhexrep(call_destination.hash)}, requesting path...", RNS.LOG_DEBUG) + RNS.Transport.request_path(call_destination.hash) + while not RNS.Transport.has_path(call_destination.hash) and time.time() < outgoing_call_timeout: time.sleep(0.2) + + if not RNS.Transport.has_path(call_destination.hash) and time.time() >= outgoing_call_timeout: + self.hangup() + else: + RNS.log(f"Establishing link with {RNS.prettyhexrep(call_destination.hash)}...", RNS.LOG_DEBUG) + self.active_call = RNS.Link(call_destination, + established_callback=self.__outgoing_link_established, + closed_callback=self.__outgoing_link_closed) + + self.active_call.is_incoming = False + self.active_call.is_outgoing = True + self.active_call.ring_timeout = False + self.__timeout_outgoing_call_at(self.active_call, outgoing_call_timeout) + + def __outgoing_link_established(self, link): + RNS.log(f"Link established for call with {link.get_remote_identity()}", RNS.LOG_DEBUG) + link.set_link_closed_callback(self.__link_closed) + self.handle_signalling_from(link) + + def __outgoing_link_closed(self, link): + pass + + def signalling_received(self, signals, source): + for signal in signals: + if source != self.active_call: + RNS.log("Received signalling on non-active call, ignoring", RNS.LOG_DEBUG) + else: + if signal == Signalling.STATUS_BUSY: + RNS.log("Remote is busy, terminating", RNS.LOG_DEBUG) + self.__play_busy_tone() + self.__disable_dial_tone() + self.hangup() + elif signal == Signalling.STATUS_REJECTED: + RNS.log("Remote rejected call, terminating", RNS.LOG_DEBUG) + self.__play_busy_tone() + self.__disable_dial_tone() + self.hangup() + elif signal == Signalling.STATUS_AVAILABLE: + RNS.log("Line available, sending identification", RNS.LOG_DEBUG) + self.call_status = signal + source.identify(self.identity) + elif signal == Signalling.STATUS_RINGING: + RNS.log("Identification accepted, remote is now ringing", RNS.LOG_DEBUG) + self.call_status = signal + self.__prepare_dialling_pipelines() + if self.active_call and self.active_call.is_outgoing: + self.__activate_dial_tone() + elif signal == Signalling.STATUS_CONNECTING: + RNS.log("Call answered, remote is performing call setup, opening audio pipelines", RNS.LOG_DEBUG) + self.call_status = signal + with self.caller_pipeline_open_lock: + self.__reset_dialling_pipelines() + self.__open_pipelines(self.active_call.get_remote_identity()) + elif signal == Signalling.STATUS_ESTABLISHED: + if self.active_call and self.active_call.is_outgoing: + RNS.log("Remote call setup completed, starting audio pipelines", RNS.LOG_DEBUG) + with self.caller_pipeline_open_lock: + self.__start_pipelines() + self.__disable_dial_tone() + RNS.log(f"Call setup complete for {RNS.prettyhexrep(self.active_call.get_remote_identity().hash)}", RNS.LOG_DEBUG) + self.call_status = signal + if callable(self.__established_callback): self.__established_callback(self.active_call.get_remote_identity()) + + def __str__(self): + return f"" \ No newline at end of file diff --git a/LXST/Primitives/__init__.py b/LXST/Primitives/__init__.py new file mode 100644 index 0000000..e285a45 --- /dev/null +++ b/LXST/Primitives/__init__.py @@ -0,0 +1 @@ +from .Telephony import Telephone \ No newline at end of file diff --git a/LXST/Primitives/hardware/display_i2c_lcd1602.py b/LXST/Primitives/hardware/display_i2c_lcd1602.py new file mode 100644 index 0000000..a096c92 --- /dev/null +++ b/LXST/Primitives/hardware/display_i2c_lcd1602.py @@ -0,0 +1,100 @@ +import os +import time +import threading +from importlib.util import find_spec +if find_spec("smbus"): import smbus +else: raise OSError(f"No smbus module available, cannot use {os.path.basename(__file__)} driver") + +class LCD(): + DEFAULT_ADDR = 0x27 + DEFAULT_I2C_CH = 1 + COLS = 16 + ROWS = 2 + + MODE_CHR = 0x01 + MODE_CMD = 0x00 + + ROW_1 = 0x80 + ROW_2 = 0xC0 + + BACKLIGHT_ON = 0x08 + BACKLIGHT_OFF = 0x00 + FLAG_ENABLE = 0b00000100 + FLAG_RS = 0b00000001 + + T_PULSE = 0.5/1000 + T_DELAY = 0.5/1000 + + CMD_INIT1 = 0x33 + CMD_INIT2 = 0x32 + CMD_CLEAR = 0x01 + + SHARED_BUS = None + + def __init__(self, address=None): + if not LCD.SHARED_BUS: LCD.SHARED_BUS = smbus.SMBus(self.DEFAULT_I2C_CH) + self.address = address or self.DEFAULT_ADDR + self.bus = LCD.SHARED_BUS + self.row = LCD.ROW_1 + self.backlight = LCD.BACKLIGHT_ON + self.__init_display() + + def __init_display(self): + self.__send_command(LCD.CMD_INIT1) + self.__send_command(LCD.CMD_INIT2) + self.__send_command(0x28) # Data length, number of lines, font size + self.__send_command(0x0C) # Display on, cursor off, blink off + self.__send_command(LCD.CMD_CLEAR) + time.sleep(LCD.T_DELAY) + + def __send_command(self, command): + byte = command & 0xF0 # Transmit MSBs + byte |= 0x04; self.__send_byte(byte); time.sleep(LCD.T_PULSE) + byte &= 0xFB; self.__send_byte(byte) + + byte = (command & 0x0F) << 4 # Transmit LSBs + byte |= 0x04; self.__send_byte(byte); time.sleep(LCD.T_PULSE) + byte &= 0xFB; self.__send_byte(byte) + + def __send_data(self, data): + byte = data & 0xF0 # Transmit MSBs + byte |= 0x05; self.__send_byte(byte); time.sleep(LCD.T_PULSE) + byte &= 0xFB; self.__send_byte(byte) + + byte = (data & 0x0F) << 4 # Transmit LSBs + byte |= 0x05; self.__send_byte(byte); time.sleep(LCD.T_PULSE) + byte &= 0xFB; self.__send_byte(byte) + + def __send_byte(self, byte): + self.bus.write_byte(self.address, byte | self.backlight) + + def print(self, string, x=0, y=0): + string = string.ljust(LCD.COLS," ") + if x < 0: x = 0 + if x > 15: x = 15 + if y < 0: y = 0 + if y > 1: y = 1 + if self.is_sleeping: self.wake() + self.__send_command(0x80 + 0x40 * y + x) # Set cursor location + for i in range(LCD.COLS): self.__send_data(ord(string[i])) + + def clear(self): + self.__init_display() + + @property + def is_sleeping(self): + return self.backlight == LCD.BACKLIGHT_OFF + + def sleep(self): + self.backlight = LCD.BACKLIGHT_OFF + self.__send_command(LCD.CMD_CLEAR) + + def wake(self): + self.backlight = LCD.BACKLIGHT_ON + self.__init_display() + + def close(self): + self.sleep() + self.bus.close() + self.bus = None + LCD.SHARED_BUS = None \ No newline at end of file diff --git a/LXST/Primitives/hardware/keypad_gpio_4x4.py b/LXST/Primitives/hardware/keypad_gpio_4x4.py new file mode 100644 index 0000000..dd977fe --- /dev/null +++ b/LXST/Primitives/hardware/keypad_gpio_4x4.py @@ -0,0 +1,130 @@ +import os +import time +import threading +from importlib.util import find_spec +if find_spec("RPi"): import RPi.GPIO as GPIO +else: raise OSError(f"No GPIO module available, cannot use {os.path.basename(__file__)} driver") + +class Event: + UP = 0x00 + DOWN = 0x01 + +class Keypad(): + ROWS = 4 + COLS = 4 + SCAN_INTERVAL_MS = 20 + + LOW = 0x00 + HIGH = 0x01 + + DEFAULT_MAP = [["1", "2", "3", "A"], + ["4", "5", "6", "B"], + ["7", "8", "9", "C"], + ["*", "0", "#", "D"]] + + DEFAULT_ROWPINS = [21, 20, 16, 12] + DEFAULT_COLPINS = [26, 19, 13, 6] + DEFAULT_HOOKPIN = 5 + HOOK_DEBOUNCE_MS = 150 + + def __init__(self, row_pins=None, col_pins=None, key_map=None, callback=None): + if not row_pins == None and (not type(row_pins) == list or len(row_pins) != 4): + raise ValueError("Invalid row pins specification") + if not col_pins == None and (not type(col_pins) == list or len(col_pins) != 4): + raise ValueError("Invalid row pins specification") + + self.row_pins = row_pins or self.DEFAULT_ROWPINS + self.col_pins = col_pins or self.DEFAULT_COLPINS + self.scan_lock = threading.Lock() + self.callback = callback + self.hook_time = 0 + self.hook_pin = None + self.on_hook = True + self.check_hook = False + self.should_run = False + self.ec = Event + self.set_key_map(key_map) + + def enable_hook(self, pin=None): + if pin == None: pin = self.DEFAULT_HOOKPIN + self.hook_pin = pin + GPIO.setup(self.hook_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) + self.key_states["hook"] = False + self.check_hook = True + + def set_key_map(self, key_map): + self.key_map = key_map or self.DEFAULT_MAP + self.key_states = {} + for row in self.key_map: + for key in row: self.key_states[key] = False + + def is_down(self, key): + if not key in self.key_states: return False + else: + return self.key_states[key] + + def is_up(self, key): + if not key in self.key_states: return False + else: + return not self.key_states[key] + + def __job(self): + while self.should_run: + self.__scan() + time.sleep(self.SCAN_INTERVAL_MS/1000) + + def __handle(self, active_keys): + events = [] + for key in self.key_states: + if self.key_states[key] == False: + if key in active_keys: + self.key_states[key] = True + events.append((key, Event.DOWN)) + + elif self.key_states[key] == True: + if not key in active_keys: + self.key_states[key] = False + events.append((key, Event.UP)) + + if callable(self.callback): + for event in events: + self.callback(self, event) + + def __scan(self): + active_keys = [] + for row in range(0, self.ROWS): + GPIO.setup(self.row_pins[row], GPIO.OUT) + GPIO.output(self.row_pins[row], GPIO.HIGH) + for col in range(0, self.COLS): + if GPIO.input(self.col_pins[col]): + active_keys.append(self.key_map[row][col]) + + GPIO.output(self.row_pins[row], GPIO.LOW) + GPIO.setup(self.row_pins[row], GPIO.IN, pull_up_down=GPIO.PUD_OFF) + + if self.check_hook: + on_hook = GPIO.input(self.hook_pin) == GPIO.LOW + + if on_hook: + active_keys.append("hook") + self.hook_time = time.time() + + if self.key_states["hook"] == True and not on_hook: + if time.time()-self.hook_time < self.HOOK_DEBOUNCE_MS/1000: + active_keys.append("hook") + else: + self.hook_time = time.time() + + if len(active_keys) >= 0 and len(active_keys) <= 4: self.__handle(active_keys) + + def start(self): + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + for row_pin in self.row_pins: GPIO.setup(row_pin, GPIO.OUT) + for col_pin in self.col_pins: GPIO.setup(col_pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + + self.should_run = True + threading.Thread(target=self.__job, daemon=True).start() + + def stop(self): + self.should_run = False \ No newline at end of file diff --git a/LXST/Processing.py b/LXST/Processing.py new file mode 100644 index 0000000..e69de29 diff --git a/LXST/Sinks.py b/LXST/Sinks.py new file mode 100644 index 0000000..997d650 --- /dev/null +++ b/LXST/Sinks.py @@ -0,0 +1,136 @@ +import RNS +import math +import time +import threading +from collections import deque + +class LinuxBackend(): + SAMPLERATE = 48000 + + def __init__(self, preferred_device=None, samplerate=SAMPLERATE): + import soundcard + self.samplerate = samplerate + self.soundcard = soundcard + if preferred_device: + try: self.device = self.soundcard.get_speaker(preferred_device) + except: self.device = soundcard.default_speaker() + else: self.device = soundcard.default_speaker() + RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG) + + def flush(self): + self.recorder.flush() + + def get_player(self, samples_per_frame=None): + return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame) + +def get_backend(): + if RNS.vendor.platformutils.is_linux(): + return LinuxBackend + else: + return None + +Backend = get_backend() + +class Sink(): + def handle_frame(self, frame, source): + pass + + def can_receive(self, from_source=None): + return True + +class RemoteSink(Sink): + pass + +class LocalSink(Sink): + pass + +class LineSink(LocalSink): + MAX_FRAMES = 6 + AUTOSTART_MIN = 1 + FRAME_TIMEOUT = 8 + + def __init__(self, preferred_device=None, autodigest=True): + self.preferred_device = preferred_device + self.frame_deque = deque(maxlen=self.MAX_FRAMES) + self.should_run = False + self.digest_thread = None + self.digest_lock = threading.Lock() + self.insert_lock = threading.Lock() + self.frame_deque = deque(maxlen=self.MAX_FRAMES) + self.underrun_at = None + self.frame_timeout = self.FRAME_TIMEOUT + self.autodigest = autodigest + self.autostart_min = self.AUTOSTART_MIN + self.buffer_max_height = self.MAX_FRAMES-3 + + self.preferred_samplerate = Backend.SAMPLERATE + self.backend = Backend(preferred_device=self.preferred_device, samplerate=self.preferred_samplerate) + self.samplerate = self.backend.samplerate + self.channels = self.backend.device.channels + + self.samples_per_frame = None + self.frame_time = None + self.output_latency = 0 + self.max_latency = 0 + + def can_receive(self, from_source=None): + with self.insert_lock: + if len(self.frame_deque) < self.buffer_max_height: + return True + else: + return False + + def handle_frame(self, frame, source=None): + with self.insert_lock: + self.frame_deque.append(frame) + + if self.samples_per_frame == None: + self.samples_per_frame = frame.shape[0] + self.frame_time = self.samples_per_frame*(1/self.backend.samplerate) + RNS.log(f"{self} starting at {self.samples_per_frame} samples per frame, {self.channels} channels", RNS.LOG_DEBUG) + + if self.autodigest and not self.should_run: + if len(self.frame_deque) >= self.autostart_min: + self.start() + + def start(self): + if not self.should_run: + self.should_run = True + self.digest_thread = threading.Thread(target=self.__digest_job, daemon=True) + self.digest_thread.start() + + def stop(self): + self.should_run = False + + def __digest_job(self): + with self.digest_lock: + with self.backend.get_player(samples_per_frame=self.samples_per_frame) as player: + while self.should_run: + frames_ready = len(self.frame_deque) + if frames_ready: + self.output_latency = len(self.frame_deque)*self.frame_time + self.max_latency = self.buffer_max_height*self.frame_time + self.underrun_at = None + + with self.insert_lock: frame = self.frame_deque.popleft() + if frame.shape[1] > self.channels: frame = frame[:, 0:self.channels] + player.play(frame) + + if len(self.frame_deque) > self.buffer_max_height: + RNS.log(f"Buffer lag on {self} (height {len(self.frame_deque)}), dropping one frame", RNS.LOG_DEBUG) + self.frame_deque.popleft() + + else: + if self.underrun_at == None: + # TODO: Remove debug + # RNS.log(f"Buffer underrun on {self}", RNS.LOG_DEBUG) + self.underrun_at = time.time() + else: + if time.time() > self.underrun_at+(self.frame_time*self.frame_timeout): + RNS.log(f"No frames available on {self}, stopping playback", RNS.LOG_DEBUG) + self.should_run = False + else: + time.sleep(self.frame_time*0.1) + +class PacketSink(RemoteSink): + pass \ No newline at end of file diff --git a/LXST/Sources.py b/LXST/Sources.py new file mode 100644 index 0000000..23e208c --- /dev/null +++ b/LXST/Sources.py @@ -0,0 +1,270 @@ +import os +import RNS +import math +import time +import threading +import numpy as np +from collections import deque +from .Sinks import LocalSink +from .Codecs import Codec, CodecError +from .Codecs.libs.pyogg import OpusFile + +RNS.loglevel = RNS.LOG_DEBUG + +class LinuxBackend(): + SAMPLERATE = 48000 + + def __init__(self, preferred_device=None, samplerate=SAMPLERATE): + import soundcard + self.samplerate = samplerate + self.soundcard = soundcard + if preferred_device: + try: self.device = self.soundcard.get_microphone(preferred_device) + except: self.device = self.soundcard.default_microphone() + else: self.device = self.soundcard.default_microphone() + self.channels = self.device.channels + self.bitdepth = 32 + RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG) + + def flush(self): + self.recorder.flush() + + def get_recorder(self, samples_per_frame): + return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame) + +def get_backend(): + if RNS.vendor.platformutils.is_linux(): + return LinuxBackend + else: + return None + +Backend = get_backend() + +class Source(): + pass + +class LocalSource(Source): + pass + +class RemoteSource(Source): + pass + +class Loopback(LocalSource, LocalSink): + MAX_FRAMES = 128 + + def __init__(self, target_frame_ms=70, codec=None, sink=None): + self.frame_deque = deque(maxlen=self.MAX_FRAMES) + self.should_run = False + self.loopback_thread = None + self.loopback_lock = threading.Lock() + self.codec = codec + self._sink = sink + self._source = None + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting", RNS.LOG_DEBUG) + self.should_run = True + + def stop(self): + self.should_run = False + + def can_receive(self, from_source=None): + if self._sink: + return self._sink.can_receive(from_source) + else: + return True + + def handle_frame(self, frame, source): + with self.loopback_lock: + if self.codec and self.sink: + self.sink.handle_frame(self.codec.decode(frame), self) + + @property + def source(self): + return self._source + + @source.setter + def source(self, source): + self._source = source + +class LineSource(LocalSource): + MAX_FRAMES = 128 + DEFAULT_FRAME_MS = 80 + + def __init__(self, preferred_device=None, target_frame_ms=DEFAULT_FRAME_MS, codec=None, sink=None): + self.preferred_device = preferred_device + self.frame_deque = deque(maxlen=self.MAX_FRAMES) + self.target_frame_ms = target_frame_ms + self.samplerate = None + self.channels = None + self.bitdepth = None + self.should_run = False + self.ingest_thread = None + self.recording_lock = threading.Lock() + self._codec = None + self.codec = codec + self.sink = sink + + @property + def codec(self): + return self._codec + + @codec.setter + def codec(self, codec): + if codec == None: + self._codec = None + elif not issubclass(type(codec), Codec): + raise CodecError(f"Invalid codec specified for {self}") + else: + self._codec = codec + + if self.codec.preferred_samplerate: + self.preferred_samplerate = self.codec.preferred_samplerate + else: + self.preferred_samplerate = Backend.SAMPLERATE + + if self.codec.frame_quanta_ms: + if self.target_frame_ms%self.codec.frame_quanta_ms != 0: + self.target_frame_ms = math.ceil(self.target_frame_ms/self.codec.frame_quanta_ms)*self.codec.frame_quanta_ms + RNS.log(f"{self} target frame time quantized to {self.target_frame_ms}ms due to codec frame quanta", RNS.LOG_DEBUG) + + if self.codec.frame_max_ms: + if self.target_frame_ms > self.codec.frame_max_ms: + self.target_frame_ms = self.codec.frame_max_ms + RNS.log(f"{self} target frame time clamped to {self.target_frame_ms}ms due to codec frame limit", RNS.LOG_DEBUG) + + if self.codec.valid_frame_ms: + if not self.target_frame_ms in self.codec.valid_frame_ms: + self.target_frame_ms = min(self.codec.valid_frame_ms, key=lambda t:abs(t-self.target_frame_ms)) + RNS.log(f"{self} target frame time clamped to closest valid value of {self.target_frame_ms}ms ", RNS.LOG_DEBUG) + + self.backend = Backend(preferred_device=self.preferred_device, samplerate=self.preferred_samplerate) + self.samplerate = self.backend.samplerate + self.bitdepth = self.backend.bitdepth + self.channels = self.backend.channels + self.samples_per_frame = math.ceil((self.target_frame_ms/1000)*self.samplerate) + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting at {self.samples_per_frame} samples per frame, {self.channels} channels", RNS.LOG_DEBUG) + self.should_run = True + self.ingest_thread = threading.Thread(target=self.__ingest_job, daemon=True) + self.ingest_thread.start() + + def stop(self): + self.should_run = False + + def __ingest_job(self): + with self.recording_lock: + frame_samples = None + with self.backend.get_recorder(samples_per_frame=self.samples_per_frame) as recorder: + while self.should_run: + frame_samples = recorder.record(numframes=self.samples_per_frame) + if self.codec: + frame = self.codec.encode(frame_samples) + if self.sink and self.sink.can_receive(from_source=self): + self.sink.handle_frame(frame, self) + +class OpusFileSource(LocalSource): + MAX_FRAMES = 128 + DEFAULT_FRAME_MS = 70 + TYPE_MAP_FACTOR = np.iinfo("int16").max + + def __init__(self, file_path, target_frame_ms=DEFAULT_FRAME_MS, loop=False, codec=None, sink=None, timed=False): + self.target_frame_ms = target_frame_ms + self.loop = loop + self.timed = timed + self.read_lock = threading.Lock() + self.should_run = False + self.ingest_thread = None + self.next_frame = None + self._codec = None + + if file_path == None: + raise TypeError(f"{self} initialised with invalid file path: {file_path}") + elif os.path.isfile(file_path): + self.file = OpusFile(file_path) + self.samplerate = self.file.frequency + self.channels = self.file.channels + self.bitdepth = 16 + self.samples = self.file.as_array()/self.TYPE_MAP_FACTOR + self.sample_count = self.samples.shape[0] + self.length_ms = (self.sample_count/self.samplerate)*1000 + RNS.log(f"{self} loaded {RNS.prettytime(self.length_ms/1000)} of audio from {file_path}", RNS.LOG_DEBUG) + RNS.log(f"{self} samplerate is {RNS.prettyfrequency(self.samplerate)}, {self.channels} channels, {self.sample_count} samples in total", RNS.LOG_DEBUG) + else: + raise OSError(f"{self} file {file_path} not found") + + self.codec = codec + self.sink = sink + + @property + def codec(self): + return self._codec + + @codec.setter + def codec(self, codec): + if codec == None: + self._codec = None + elif not issubclass(type(codec), Codec): + raise CodecError(f"Invalid codec specified for {self}") + else: + self._codec = codec + + if self.codec.frame_quanta_ms: + if self.target_frame_ms%self.codec.frame_quanta_ms != 0: + self.target_frame_ms = math.ceil(self.target_frame_ms/self.codec.frame_quanta_ms)*self.codec.frame_quanta_ms + RNS.log(f"{self} target frame time quantized to {self.target_frame_ms}ms due to codec frame quanta", RNS.LOG_DEBUG) + + if self.codec.frame_max_ms: + if self.target_frame_ms > self.codec.frame_max_ms: + self.target_frame_ms = self.codec.frame_max_ms + RNS.log(f"{self} target frame time clamped to {self.target_frame_ms}ms due to codec frame limit", RNS.LOG_DEBUG) + + if self.codec.valid_frame_ms: + if not self.target_frame_ms in self.codec.valid_frame_ms: + self.target_frame_ms = min(self.codec.valid_frame_ms, key=lambda t:abs(t-self.target_frame_ms)) + RNS.log(f"{self} target frame time clamped to closest valid value of {self.target_frame_ms}ms ", RNS.LOG_DEBUG) + + self.samples_per_frame = math.ceil((self.target_frame_ms/1000)*self.samplerate) + self.frame_time = self.samples_per_frame/self.samplerate + RNS.log(f"{self} frame time is {RNS.prettyshorttime(self.frame_time)}", RNS.LOG_DEBUG) + + def start(self): + if not self.should_run: + RNS.log(f"{self} starting at {self.samples_per_frame} samples per frame, {self.channels} channels", RNS.LOG_DEBUG) + self.should_run = True + self.ingest_thread = threading.Thread(target=self.__ingest_job, daemon=True) + self.ingest_thread.start() + + def stop(self): + self.should_run = False + + def __ingest_job(self): + with self.read_lock: + self.next_frame = time.time() + fi = 0; spf = self.samples_per_frame; sc = self.sample_count + while self.should_run: + if self.sink and self.sink.can_receive(from_source=self) and (not self.timed or time.time() >= self.next_frame): + self.next_frame = time.time()+self.frame_time + fi += 1 + fs = (fi-1)*spf; fe = min(fi*spf, sc) + frame_samples = self.samples[fs:fe, :] + if len(frame_samples) < 1: + if self.loop: + RNS.log(f"{self} exhausted file samples, looping...", RNS.LOG_DEBUG) + fi = 0 + else: + RNS.log(f"{self} exhausted file samples, stopping...", RNS.LOG_DEBUG) + self.should_run = False + else: + if self.codec: + frame = self.codec.encode(frame_samples) + if self.sink and self.sink.can_receive(from_source=self): + self.sink.handle_frame(frame, self) + else: + time.sleep(self.frame_time*0.1) + +class PacketSource(RemoteSource): + pass \ No newline at end of file diff --git a/LXST/Utilities/rnphone.py b/LXST/Utilities/rnphone.py new file mode 100644 index 0000000..e1d8814 --- /dev/null +++ b/LXST/Utilities/rnphone.py @@ -0,0 +1,787 @@ +#!/usr/bin/env python3 + +import RNS +import os +import sys +import time +import signal +import threading +import argparse + +from LXST._version import __version__ +from LXST.Primitives.Telephony import Telephone +from RNS.vendor.configobj import ConfigObj + +class ReticulumTelephone(): + STATE_AVAILABLE = 0x00 + STATE_CONNECTING = 0x01 + STATE_RINGING = 0x02 + STATE_IN_CALL = 0x03 + + HW_SLEEP_TIMEOUT = 15 + HW_STATE_IDLE = 0x00 + HW_STATE_DIAL = 0x01 + HW_STATE_SLEEP = 0xFF + KPD_NUMBERS = ["0","1","2","3","4","5","6","7","8","9"] + KPD_HEX_ALPHA = ["A","B","C","D","E","F"] + KPD_SYMBOLS = ["*","#"] + + RING_TIME = 30 + WAIT_TIME = 60 + PATH_TIME = 10 + + def __init__(self, configdir, rnsconfigdir, verbosity = 0, service = False): + self.service = service + self.configdir = configdir + self.config = None + self.should_run = False + self.telephone = None + self.state = self.STATE_AVAILABLE + self.hw_state = self.HW_STATE_IDLE + self.hw_last_event = time.time() + self.hw_input = "" + self.direction = None + self.last_input = None + self.first_run = False + self.ringtone_path = None + self.speaker_device = None + self.microphone_device = None + self.ringer_device = None + self.keypad = None + self.display = None + self.allowed = Telephone.ALLOW_ALL + self.allow_phonebook = False + self.allowed_list = [] + self.blocked_list = [] + self.phonebook = {} + self.aliases = {} + self.names = {} + self.reload_config() + self.main_menu() + + reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=3+verbosity) + self.telephone = Telephone(self.identity, ring_time=self.ring_time, wait_time=self.wait_time) + self.telephone.set_ringtone(self.ringtone_path) + self.telephone.set_ringing_callback(self.ringing) + self.telephone.set_established_callback(self.call_established) + self.telephone.set_ended_callback(self.call_ended) + self.telephone.set_speaker(self.speaker_device) + self.telephone.set_microphone(self.microphone_device) + self.telephone.set_ringer(self.ringer_device) + self.telephone.set_allowed(self.allowed) + self.telephone.set_blocked(self.blocked_list) + + def create_default_config(self): + rnphone_config = ConfigObj(__default_rnphone_config__.splitlines()) + rnphone_config.filename = self.configpath + rnphone_config.write() + + def reload_config(self): + if self.service: RNS.log("Loading configuration...", RNS.LOG_DEBUG) + if self.configdir == None: + if os.path.isdir("/etc/rnphone") and os.path.isfile("/etc/rnphone/config"): + self.configdir = "/etc/rnphone" + elif os.path.isdir(RNS.Reticulum.userdir+"/.config/rnphone") and os.path.isfile(Reticulum.userdir+"/.config/rnphone/config"): + self.configdir = RNS.Reticulum.userdir+"/.config/rnphone" + else: + self.configdir = RNS.Reticulum.userdir+"/.rnphone" + + self.configpath = self.configdir+"/config" + self.ignoredpath = self.configdir+"/ignored" + self.allowedpath = self.configdir+"/allowed" + self.identitypath = self.configdir+"/identity" + self.storagedir = self.configdir+"/storage" + + self.ring_time = ReticulumTelephone.RING_TIME + self.wait_time = ReticulumTelephone.WAIT_TIME + self.path_time = ReticulumTelephone.PATH_TIME + + if not os.path.isdir(self.storagedir): + os.makedirs(self.storagedir) + + if not os.path.isfile(self.configpath): + self.create_default_config() + self.first_run = True + + if os.path.isfile(self.configpath): + try: + self.config = ConfigObj(self.configpath) + except Exception as e: + RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) + RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) + RNS.panic() + + # Generate or load primary identity + if os.path.isfile(self.identitypath): + try: + self.identity = RNS.Identity.from_file(self.identitypath) + if self.identity != None: + pass + else: + RNS.log("Could not load the Primary Identity from "+self.identitypath, RNS.LOG_ERROR) + exit(1) + except Exception as e: + RNS.log("Could not load the Primary Identity from "+self.identitypath, RNS.LOG_ERROR) + RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR) + exit(1) + else: + try: + print("No primary identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identitypath) + print("Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + RNS.log("Could not create and save a new Primary Identity", RNS.LOG_ERROR) + RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR) + exit(1) + + self.apply_config() + + def __is_allowed(self, identity_hash): + if identity_hash in self.allowed_list: return True + else: return False + + def load_phonebook(self, phonebook): + if self.service: RNS.log("Loading phonebook...", RNS.LOG_DEBUG) + for name in phonebook: + alias = None + identity_hash = phonebook[name] + if type(identity_hash) == list: + components = identity_hash + identity_hash = components[0] + alias_input = components[1] + alias = "" + for c in alias_input: + if c in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]: + alias += c + if len(alias) == 0: alias = None + + if len(identity_hash) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: + if identity_hash != RNS.hexrep(self.identity.hash, delimit=False): + try: + hash_bytes = bytes.fromhex(identity_hash) + self.phonebook[name] = identity_hash + self.names[identity_hash] = name + if alias: self.aliases[identity_hash] = alias + if self.allow_phonebook: self.allowed_list.append(hash_bytes) + except Exception as e: + RNS.log(f"Could not load phonebook entry for {name}: {e}", RNS.LOG_ERROR) + + def apply_config(self): + if "telephone" in self.config: + config = self.config["telephone"] + if "ringtone" in config: self.ringtone_path = os.path.join(self.configdir, config["ringtone"]) + if "speaker" in config: self.speaker_device = config["speaker"] + if "microphone" in config: self.microphone_device = config["microphone"] + if "ringer" in config: self.ringer_device = config["ringer"] + if "allowed_callers" in config: + allowed_callers = config["allowed_callers"] + if str(allowed_callers).lower() == "all": self.allowed = Telephone.ALLOW_ALL + elif str(allowed_callers).lower() == "none": self.allowed = Telephone.ALLOW_NONE + elif str(allowed_callers).lower() == "phonebook": + self.allow_phonebook = True + self.allowed = self.__is_allowed + elif type(config["allowed_callers"]) == list: + self.allowed = self.__is_allowed + for identity_hash in config["allowed_callers"]: + if len(identity_hash) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: + if identity_hash != RNS.hexrep(self.identity.hash, delimit=False): + try: hash_bytes = bytes.fromhex(identity_hash) + except Exception as e: RNS.log(f"Could not load allowed caller entry {identity_hash}: {e}", RNS.LOG_ERROR) + self.allowed_list.append(hash_bytes) + + if "blocked_callers" in config: + blocked_callers = config["blocked_callers"] + if not type(blocked_callers) == list: blocked_callers = [blocked_callers] + if len(blocked_callers) > 0: + for identity_hash in blocked_callers: + if len(identity_hash) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: + if identity_hash != RNS.hexrep(self.identity.hash, delimit=False): + try: hash_bytes = bytes.fromhex(identity_hash) + except Exception as e: RNS.log(f"Could not load blocked caller entry {identity_hash}: {e}", RNS.LOG_ERROR) + self.blocked_list.append(hash_bytes) + + if "phonebook" in self.config: + self.load_phonebook(self.config["phonebook"]) + + if "hardware" in self.config: + config = self.config["hardware"] + if "keypad" in config: + self.enable_keypad(config["keypad"].lower()) + if "keypad_hook_pin" in config: self.enable_hook(pin = config.as_int("keypad_hook_pin")) + if "display" in config: self.enable_display(config["display"].lower()) + + self.last_dialled_identity_hash = None + + def enable_keypad(self, driver): + if self.service: RNS.log(f"Starting keypad: {driver}", RNS.LOG_DEBUG) + if driver == "gpio_4x4": + from LXST.Primitives.hardware.keypad_gpio_4x4 import Keypad + self.keypad = Keypad(callback=self._keypad_event) + self.keypad.start() + else: raise OSError("Unknown keypad driver specified") + + def enable_hook(self, pin=None): + if self.keypad: self.keypad.enable_hook(pin=pin) + + def enable_display(self, driver): + if self.service: RNS.log(f"Starting display: {driver}", RNS.LOG_DEBUG) + if self.display == None: + if driver == "i2c_lcd1602": + from LXST.Primitives.hardware.display_i2c_lcd1602 import LCD + self.display = LCD() + else: raise OSError("Unknown display driver specified") + + if self.display: + threading.Thread(target=self._display_job, daemon=True).start() + + @property + def is_available(self): + return self.state == self.STATE_AVAILABLE + + @property + def is_in_call(self): + return self.state == self.STATE_IN_CALL + + @property + def is_ringing(self): + return self.state == self.STATE_RINGING + + @property + def call_is_connecting(self): + return self.state == self.STATE_CONNECTING + + @property + def hw_is_idle(self): + return self.hw_state == self.HW_STATE_IDLE + + @property + def hw_is_dialing(self): + return self.hw_state == self.HW_STATE_DIAL + + def start(self): + if not self.should_run: + signal.signal(signal.SIGINT, self.sigint_handler) + signal.signal(signal.SIGTERM, self.sigterm_handler) + self.telephone.announce() + self.should_run = True + self.run() + + def stop(self): + self.should_run = False + + def dial(self, identity_hash): + self.last_dialled_identity_hash = identity_hash + self.telephone.set_busy(True) + identity_hash = bytes.fromhex(identity_hash) + destination_hash = RNS.Destination.hash_from_name_and_identity("lxst.telephony", identity_hash) + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + if self.display: self.display.print("Finding path...", x=0, y=0) + def spincheck(): + return RNS.Transport.has_path(destination_hash) + self.__spin(spincheck, "Requesting path for call to "+RNS.prettyhexrep(identity_hash), self.path_time) + if not spincheck(): + print("Path request timed out") + if self.display: + self.display.print("Finding path", x=0, y=0) + self.display.print("timed out", x=0, y=1) + time.sleep(1.5) + self.became_available() + + self.telephone.set_busy(False) + if RNS.Transport.has_path(destination_hash): + call_hops = RNS.Transport.hops_to(destination_hash) + cs = "" if call_hops == 1 else "s" + print(f"Connecting call over {call_hops} hop{cs}...") + if self.display: + call_hops_str = f"({call_hops}h{cs})" + call_str = "Calling"; ns = self.display.COLS-(len(call_str)+len(call_hops_str)); s = " "*ns + disp_str = f"{call_str}{s}{call_hops_str}" + self.display.print(disp_str, x=0, y=0) + + identity = RNS.Identity.recall(destination_hash) + self.call(identity) + else: + self.became_available() + + def redial(self, args=None): + if self.last_dialled_identity_hash: self.dial(self.last_dialled_identity_hash) + + def call(self, remote_identity): + print(f"Calling {RNS.prettyhexrep(remote_identity.hash)}...") + self.state = self.STATE_CONNECTING + self.caller = remote_identity + self.direction = "to" + self.telephone.call(self.caller) + + def ringing(self, remote_identity): + if self.hw_state == self.HW_STATE_SLEEP: self.hw_state = self.HW_STATE_IDLE + self.state = self.STATE_RINGING + self.caller = remote_identity + self.direction = "from" if self.direction == None else "to" + print(f"\n\nIncoming call from {RNS.prettyhexrep(self.caller.hash)}") + print(f"Hit enter to answer, {Terminal.BOLD}r{Terminal.END} to reject") + if self.display: + hash_str = RNS.hexrep(self.caller.hash, delimit=False) + if hash_str in self.aliases: + remote_alias = self.aliases[hash_str] + remote_name = self.names[hash_str] + self.display.print(remote_name, x=0, y=0) + self.display.print(f"({remote_alias})".rjust(self.display.COLS," "), x=0, y=1) + + else: + self.display.print(hash_str[:16], x=0, y=0) + self.display.print(hash_str[16:], x=0, y=1) + + def call_ended(self, remote_identity): + if self.is_in_call or self.is_ringing or self.call_is_connecting: + if self.is_in_call: print(f"Call with {RNS.prettyhexrep(self.caller.hash)} ended\n") + if self.is_ringing: print(f"Call {self.direction} {RNS.prettyhexrep(self.caller.hash)} was not answered\n") + if self.call_is_connecting: print(f"Call to {RNS.prettyhexrep(self.caller.hash)} could not be connected\n") + self.direction = None + self.state = self.STATE_AVAILABLE + self.became_available() + + def call_established(self, remote_identity): + if self.call_is_connecting or self.is_ringing: + self.state = self.STATE_IN_CALL + print(f"Call established with {RNS.prettyhexrep(self.caller.hash)}") + self.display_call_status() + + def display_call_status(self): + def job(): + started = time.time() + erase_str = "" + while self.state == self.STATE_IN_CALL: + elapsed = round(time.time()-started) + time_string = RNS.prettytime(elapsed) + stat_string = f"In call for {time_string}, hit enter to hang up " + print(f"\r{stat_string}", end="") + erase_string = " "*len(stat_string) + sys.stdout.flush() + print(f"\r{erase_str}", end="") + + if self.display: + self.display.print("Call connected", x=0, y=0) + self.display.print(f"{time_string}", x=0, y=1) + time.sleep(1.00) + else: + time.sleep(0.25) + + print(f"\r{erase_str}> ", end="") + + threading.Thread(target=job, daemon=True).start() + + def became_available(self): + if not self.service: + if self.is_available and self.first_run: + hs = "" + if not hasattr(self, "first_prompt"): hs = " (or ? for help)"; self.first_prompt = True + print(f"Enter identity hash and hit enter to call{hs}\n", end="") + print("> ", end="") + sys.stdout.flush() + + if self.display: + self.display.clear() + self.display.print("Telephone Ready", x=0, y=0) + self.display.print("", x=0, y=1) + + if self.display or self.keypad: + self.hw_last_event = time.time() + self.hw_input = "" + self.hw_state = self.HW_STATE_IDLE + + def print_identity(self, args): + print(f"Identity hash of this telephone: {RNS.prettyhexrep(self.identity.hash)}\n") + + def print_destination(self, args): + print(f"Destination hash of this telephone: {RNS.prettyhexrep(self.telephone.destination.hash)}\n") + + def phonebook_menu(self, args=None): + if len(self.phonebook) < 1: + print("\nNo entries in phonebook\n") + else: + def exit_menu(args=None): + print("Phonebook closed") + self.main_menu() + + def dial_factory(identity_hash): + def x(args=None): self.dial(identity_hash) + return x + + print("") + print(f"{Terminal.UNDERLINE}Phonebook{Terminal.END}") + + self.active_menu = {} + maxaliaslen = 0 + for identity_hash in self.aliases: maxaliaslen = max(maxaliaslen, len(self.aliases[identity_hash])) + maxlen = 0; maxnlen = max(maxaliaslen, len(str(len(self.phonebook)))); n = 0 + for name in self.phonebook: maxlen = max(maxlen, len(name)) + for name in self.phonebook: + n += 1; identity_hash = self.phonebook[name] + alias = n + if identity_hash in self.aliases: + alias = self.aliases[identity_hash] + spaces = maxlen-len(name); nspaces = maxnlen-len(str(alias)); s = " " + print(f" {Terminal.BOLD}{s*nspaces}{alias}{Terminal.END} {name}{s*spaces} : <{identity_hash}>") + self.active_menu[f"{alias}"] = dial_factory(identity_hash) + + print(f" {Terminal.BOLD}b{Terminal.END}ack{s*(max(0, maxlen+maxnlen-2))}: Back to main menu\n") + self.active_menu["b"] = exit_menu + self.active_menu["back"] = exit_menu + self.active_menu["q"] = exit_menu + self.active_menu["quit"] = exit_menu + + def main_menu(self, args=None): + def m_help(argv): + print("") + print(f"{Terminal.UNDERLINE}Available commands{Terminal.END}") + print(f" {Terminal.BOLD}p{Terminal.END}honebook : Open the phonebook") + print(f" {Terminal.BOLD}r{Terminal.END}edial : Call the last called identity again") + print(f" {Terminal.BOLD}i{Terminal.END}dentity : Display the identity hash of this telephone") + print(f" {Terminal.BOLD}d{Terminal.END}esthash : Display the destination hash of this telephone") + print(f" {Terminal.BOLD}a{Terminal.END}nnounce : Send an announce from this telephone") + print(f" {Terminal.BOLD}q{Terminal.END}uit : Exit the program") + print(f" {Terminal.BOLD}h{Terminal.END}elp : This help menu") + print("") + + def m_quit(argv): + self.quit() + + def m_announce(argv): + self.telephone.announce() + print(f"Announce sent") + + self.active_menu = {"help": m_help, + "h": m_help, + "?": m_help, + "p": self.phonebook_menu, + "phonebook": self.phonebook_menu, + "r": self.redial, + "i": self.print_identity, + "identity": self.print_identity, + "d": self.print_destination, + "desthash": self.print_destination, + "a": m_announce, + "anounce": m_announce, + "redial": self.redial, + "exit": m_quit, + "quit": m_quit, + "q": m_quit} + + def run(self): + if self.service: + print(f"Reticulum Telephone Service is ready") + print(f"Identity hash: {RNS.prettyhexrep(self.identity.hash)}") + else: + print(f"\n{Terminal.BOLD}Reticulum Telephone Utility is ready{Terminal.END}") + print(f" Identity hash: {RNS.prettyhexrep(self.identity.hash)}\n") + + if self.service: + self.became_available() + while self.should_run: + time.sleep(0.5) + + else: + while self.should_run: + if self.is_available: + if self.last_input and len(self.last_input) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: + if self.is_available: + try: + self.dial(self.last_input) + + except Exception as e: + print(f"Invalid identity hash: {e}\n") + RNS.trace_exception(e) + + elif self.last_input and self.last_input.split(" ")[0] in self.active_menu: + self.active_menu[self.last_input.split(" ")[0]](self.last_input.split(" ")[1:]) + self.became_available() + + else: + self.became_available() + + elif self.is_ringing: + if self.last_input == "": + print(f"Answering call from {RNS.prettyhexrep(self.caller.hash)}") + if not self.telephone.answer(self.caller): + print(f"Could not answer call from {RNS.prettyhexrep(self.caller.hash)}") + else: + print(f"Rejecting call from {RNS.prettyhexrep(self.caller.hash)}") + self.telephone.hangup() + + elif self.is_in_call or self.call_is_connecting: + print(f"Hanging up call with {RNS.prettyhexrep(self.caller.hash)}") + self.telephone.hangup() + + self.last_input = input() + + def cleanup(self): + if self.display: self.display.close() + if self.keypad: self.keypad.stop() + + def quit(self): + self.cleanup() + exit(0) + + def __spin(self, until=None, msg=None, timeout=None): + i = 0 + syms = "⢄⢂⢁⡁⡈⡐⡠" + if timeout != None: + timeout = time.time()+timeout + + print(msg+" ", end=" ") + while (timeout == None or time.time() timeout: + return False + else: + return True + + def _display_job(self): + while self.display: + now = time.time() + if self.is_available and self.hw_is_idle and (self.telephone and not self.telephone.busy): + if now - self.hw_last_event >= self.HW_SLEEP_TIMEOUT: + self.hw_state = self.HW_STATE_SLEEP + self._sleep_display() + + time.sleep(1) + + def _sleep_display(self): + if self.display: self.display.sleep() + + def _wake_display(self): + if self.display: self.display.wake() + + def _update_display(self): + if self.display: + if self.hw_is_dialing: + if len(self.hw_input) == 0: lookup_name = "Enter number" + else: lookup_name = "Unknown" + + for identity_hash in self.aliases: + alias = self.aliases[identity_hash] + if self.hw_input == alias: lookup_name = self.names[identity_hash] + + self.display.print(f"{self.hw_input}", x=0, y=0) + self.display.print(f"{lookup_name}", x=0, y=1) + + + + def _keypad_event(self, keypad, event): + self.hw_last_event = time.time() + if self.hw_state == self.HW_STATE_SLEEP: + self.hw_state = self.HW_STATE_IDLE + self._wake_display() + self.became_available() + + if self.is_ringing: + answer_events = event[0] == "D" and event[1] == self.keypad.ec.DOWN + answer_events |= event[0] == "hook" and event[1] == self.keypad.ec.UP + if answer_events: + print(f"Answering call from {RNS.prettyhexrep(self.caller.hash)}") + if not self.telephone.answer(self.caller): + print(f"Could not answer call from {RNS.prettyhexrep(self.caller.hash)}") + elif event[0] == "C" and event[1] == self.keypad.ec.DOWN: + print(f"Rejecting call from {RNS.prettyhexrep(self.caller.hash)}") + self.telephone.hangup() + + elif self.is_in_call or self.call_is_connecting: + hangup_events = event[0] == "D" and event[1] == self.keypad.ec.DOWN + hangup_events |= event[0] == "hook" and event[1] == self.keypad.ec.DOWN + if hangup_events: + print(f"Hanging up call with {RNS.prettyhexrep(self.caller.hash)}") + self.telephone.hangup() + + elif self.is_available and self.hw_is_idle: + if event[0] == "A" and event[1] == self.keypad.ec.DOWN: + self.hw_input = ""; self.hw_state = self.HW_STATE_DIAL + self._update_display() + + if event[0] in self.KPD_NUMBERS and event[1] == self.keypad.ec.DOWN: + self.hw_input += event[0]; self.hw_state = self.HW_STATE_DIAL + self._update_display() + + elif self.is_available and self.hw_is_dialing: + dial_event = False + if event[1] == self.keypad.ec.DOWN: + if event[0] in self.KPD_NUMBERS: self.hw_input += event[0] + if event[0] == "A": self.became_available() + if event[0] == "B": self.hw_input = self.hw_input[:-1] + if event[0] == "C": self.hw_input = "" + if event[0] == "D": dial_event = True + + if event[0] == "hook" and event[1] == self.keypad.ec.UP: dial_event = True + + if dial_event: + for identity_hash in self.aliases: + alias = self.aliases[identity_hash] + if self.hw_input == alias: + self.hw_input = "" + self.hw_state = self.HW_STATE_IDLE + self.dial(identity_hash) + + self._update_display() + + def sigint_handler(self, signal, frame): + self.cleanup() + exit(0) + + def sigterm_handler(self, signal, frame): + self.cleanup() + exit(0) + +def main(): + app = None + try: + parser = argparse.ArgumentParser(description="Reticulum Telephone Utility") + + parser.add_argument("-l", "--list-devices", action="store_true", help="list available audio devices", default=False) + parser.add_argument("--config", action="store", default=None, help="path to config directory", type=str) + parser.add_argument("--rnsconfig", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + parser.add_argument("-s", "--service", action="store_true", help="run as a service", default=False) + parser.add_argument("--systemd", action="store_true", help="display example systemd unit", default=False) + parser.add_argument("--version", action="version", version="rnprobe {version}".format(version=__version__)) + parser.add_argument('-v', '--verbose', action='count', default=0) + + args = parser.parse_args() + + if args.list_devices: + import LXST + RNS.loglevel = 0 + print("\nAvailable audio devices:") + for device in LXST.Sources.Backend().soundcard.all_speakers(): print(f" Output : {device}") + for device in LXST.Sinks.Backend().soundcard.all_microphones(): print(f" Input : {device}") + exit(0) + + if args.systemd: + print("To install rnphone as a system service, paste the") + print("systemd unit configuration below into a new file at:\n") + print("/etc/systemd/system/rnphone.service\n") + print("Then enable the service at boot by running:\n\nsudo systemctl enable rnphone\n") + print("--- begin systemd unit snipped ---\n") + print(__systemd_unit__.replace("USERNAME", os.getlogin())) + print("--- end systemd unit snipped ---\n") + exit(0) + + ReticulumTelephone(configdir = args.config, + rnsconfigdir = args.rnsconfig, + verbosity = args.verbose, + service = args.service).start() + + except KeyboardInterrupt: + if app: app.quit() + print("") + exit() + +__default_rnphone_config__ = """# This is an example rnphone config file. +# You should probably edit it to suit your +# intended usage. + +[telephone] + # You can define the ringtone played when the + # phone is ringing. Must be in OPUS format, and + # located in the rnphone config directory. + + ringtone = ringtone.opus + + # You can define the preferred audio devices + # to use as the speaker output, ringer output + # and microphone input. The names do not have + # to be an exact match to your full soundcard + # device name, but will be fuzzy matched. + # You can list available device names with: + # rnphone -l + + # speaker = device name + # microphone = device name + # ringer = device name + + # You can configure who is allowed to call + # this telephone. This can be set to either + # "all", "none", "phonebook" or a list of + # identity hashes. See examples below. + + # allowed_callers = all + # allowed_callers = none + # allowed_callers = phonebook + # allowed_callers = b8d80b1b7a9d3147880b366995422a45, fcfb80d4cd3aab7c8710541fb2317974 + + # It is also possible to block specific + # callers on a per-identity basis. + + # blocked_callers = f3e8c3359b39d36f3baff0a616a73d3e, 5d2d14619dfa0ff06278c17347c14331 + +[phonebook] + # You can add entries to the phonebook for + # quick dialling by adding them here + + # Mary = f3e8c3359b39d36f3baff0a616a73d3e + # Jake = b8d80b1b7a9d3147880b366995422a45 + # Dean = 05d4c6697bb38e5458a3077571157bfa + + # You can optionally specify a numerical + # alias for calling with a physical keypad + + # Rudy = 5d2d14619dfa0ff06278c17347c14331, 241 + # Josh = fcfb80d4cd3aab7c8710541fb2317974, 7907 + +[hardware] + # If the required hardware is connected, and + # the neccessary modules installed, you can + # enable various hardware components. + + # keypad = gpio_4x4 + # display = i2c_lcd1602 + + # If you have a keypad connected, you can + # also enable a GPIO pin for detecting + # on-hook/off-hook status + + # keypad_hook_pin = 5 + + # You can configure a pin for muting the + # ringer amplifier, if available + + # amp_mute_pin = 25 + # amp_mute_level = high +""" + +__systemd_unit__ = """# This systemd unit allows installing rnphone +# as a system service on Linux-based devices +[Unit] +Description=Reticulum Telephone Service +After=sound.target + +[Service] +# Wait 30 seconds for WiFi and audio +# hardware to initialise. +ExecStartPre=/bin/sleep 30 +Type=simple +Environment="DISPLAY=:0" +Environment="XAUTHORITY=/home/USERNAME/.Xauthority" +Environment="XDG_RUNTIME_DIR=/run/user/1000" +Restart=always +RestartSec=5 +User=USERNAME +ExecStart=/home/USERNAME/.local/bin/rnphone --service -vvv + +[Install] +WantedBy=graphical.target +""" + +class Terminal(): + UNDERLINE = "\033[4m" + BOLD = "\033[1m" + END = "\033[0m" + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/LXST/__init__.py b/LXST/__init__.py new file mode 100644 index 0000000..97a0d90 --- /dev/null +++ b/LXST/__init__.py @@ -0,0 +1,7 @@ +APP_NAME = "lxst" + +from .Pipeline import Pipeline +from .Mixer import Mixer +from .Sources import * +from .Generators import * +from .Primitives import * \ No newline at end of file diff --git a/LXST/_version.py b/LXST/_version.py new file mode 100644 index 0000000..7a17bdd --- /dev/null +++ b/LXST/_version.py @@ -0,0 +1 @@ +__version__ = "0.2.4" \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b4b0a82 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +all: release + +clean: + @echo Cleaning... + -rm -r ./build + -rm -r ./dist + +remove_symlinks: + @echo Removing symlinks for build... + -rm ./RNS + -rm ./LXST/Utilities/LXST + -rm ./examples/LXST + +create_symlinks: + @echo Creating symlinks... + -ln -s ../Reticulum/RNS ./ + -ln -s ../../LXST/ ./LXST/Utilities/LXST + -ln -s ../LXST/ ./examples/LXST + +build_wheel: + python3 setup.py sdist bdist_wheel + +release: remove_symlinks build_wheel create_symlinks + +upload: + @echo Ready to publish release, hit enter to continue + @read VOID + @echo Uploading to PyPi... + twine upload dist/* + @echo Release published diff --git a/README.md b/README.md new file mode 100644 index 0000000..84ac2b4 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Lightweight Extensible Signal Transport + +LXST is a simple and flexible real-time streaming format and delivery protocol that allows a wide variety of implementations, while using as little bandwidth as possible. It is built on top of [Reticulum](https://reticulum.network) and offers zero-conf stream routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. + +- Provides a variety of ready-to-use primitives, for easily creating applications such as: + - Telephony and live voice calls + - Two-way radio systems + - Direct peer-to-peer radio communications + - Trunked and routed real-time radio systems + - Media streaming + - Broadcast radio + - Public address systems +- Can handle real-time signal streams with end-to-end latencies below 10 milliseconds +- Supports encoding and decoding stream contents with a range of different codecs + - Raw and lossless streams with arbitrary sample rates + - Up to 32 channels + - Up to 128-bit sample precision + - Efficient, high-quality voice and audio with OPUS + - Many different built-in profiles, from ~4.5kbps to ~96kbps + - Profiles are pre-tuned for different applications, such as: + - Low-bandwidth voice + - Medium quality voice + - High quality, perceptually lossless voice + - Media content such as podcasts + - Perceptually lossless stereo music + - Ultra low-bandwidth voice communications with Codec2 + - Provides intelligible voice between 700bps and 3200bps +- Can dynamically switch codecs mid-stream without stream re-initialization or frame loss +- Has in-band signalling support for call signalling, communications, metadata embedding, media and stream management +- Uses a fully staged signal pipelining, allowing arbitrary stream routing +- Provides built-in signal mixing support for any number of channels + +## Transport Encryption + +LXST uses encryption provided by [Reticulum](https://reticulum.network), and thus provides end-to-end encryption, guaranteed data integrity and authenticity, as well as forward secrecy by default. + +## Project Status & License + +This software is in a very early alpha state, and will change rapidly with ongoing development. Consider no APIs stable. Consider everything explosive. + +While under early development, the project is kept under a `CC BY-NC-ND 4.0` license. + +## Installation + +If you want to try out LXST, you can install it with pip: + +```bash +pip install lxst +``` diff --git a/docs/425.opus b/docs/425.opus new file mode 100644 index 0000000..5dc13a9 Binary files /dev/null and b/docs/425.opus differ diff --git a/docs/rns_audio_call_calc.py b/docs/rns_audio_call_calc.py new file mode 100644 index 0000000..8319923 --- /dev/null +++ b/docs/rns_audio_call_calc.py @@ -0,0 +1,140 @@ +import os +import math +import RNS +import RNS.vendor.umsgpack as mp + +def simulate(link_speed=2735, audio_slot_ms=400, codec_rate=1200, + signalling_bytes=14, method="msgpack"): + # Simulated on-air link speed + LINK_SPEED = link_speed + + # Packing method, can be "msgpack" or "protobuf" + PACKING_METHOD = method + + # The target audio slot time + TARGET_MS = audio_slot_ms + + # Packets needed per second for half-duplex audio + PACKETS_PER_SECOND = 1000/TARGET_MS + + # Effective audio encoder bitrate + CODEC_RATE = codec_rate + + # Per-packet overhead on a established link is 19 + # bytes, 3 for header and context, 16 for link ID + RNS_OVERHEAD = 19 + + # Physical-layer overhead. For RNode, this is 1 + # byte per RNS packet. + PHY_OVERHEAD = 1 + + # Total transport overhead + TRANSPORT_OVERHEAD = PHY_OVERHEAD+RNS_OVERHEAD + + # Calculate parameters + AUDIO_LEN = int(math.ceil(CODEC_RATE/(1000/TARGET_MS)/8)) + PER_BYTE_LATENCY_MS = 1000/(LINK_SPEED/8) + + # Pack the message with msgpack to get real- + # world packed message size + + if PACKING_METHOD == "msgpack": + # Calculate msgpack overhead + PL_LEN = len(mp.packb([os.urandom(signalling_bytes), os.urandom(AUDIO_LEN)])) + PACKING_OVERHEAD = PL_LEN-AUDIO_LEN + elif PACKING_METHOD == "protobuf": + # For protobuf, assume the 8 bytes of stated overhead + PACKING_OVERHEAD = 8 + PL_LEN = AUDIO_LEN+PACKING_OVERHEAD + else: + print("Unsupported packing method") + exit(1) + + # Calculate required encrypted token blocks + BLOCKSIZE = 16 + REQUIRED_BLOCKS = math.ceil((PL_LEN+1)/BLOCKSIZE) + ENCRYPTED_PAYLOAD_LEN = REQUIRED_BLOCKS*BLOCKSIZE + BLOCK_HEADROOM = (REQUIRED_BLOCKS*BLOCKSIZE) - PL_LEN - 1 + + # The complete on-air packet length + PACKET_LEN = PHY_OVERHEAD+RNS_OVERHEAD+ENCRYPTED_PAYLOAD_LEN + PACKET_LATENCY = round(PACKET_LEN*PER_BYTE_LATENCY_MS, 1) + + # TODO: This should include any additional + # airtime consumption such as preamble and TX-tail. + PACKET_AIRTIME = PACKET_LEN*PER_BYTE_LATENCY_MS + AIRTIME_PCT = (PACKET_AIRTIME/TARGET_MS) * 100 + + # Maximum amount of concurrent full-duplex + # calls that can coexist on the same channel + CONCURRENT_CALLS = math.floor(100/AIRTIME_PCT) + + # Calculate latencies + TRANSPORT_LATENCY = round((PHY_OVERHEAD+RNS_OVERHEAD)*PER_BYTE_LATENCY_MS, 1) + + PAYLOAD_LATENCY = round(ENCRYPTED_PAYLOAD_LEN*PER_BYTE_LATENCY_MS, 1) + RAW_DATA_LATENCY = round(AUDIO_LEN*PER_BYTE_LATENCY_MS, 1) + PACKING_LATENCY = round(PACKING_OVERHEAD*PER_BYTE_LATENCY_MS, 1) + + DATA_LATENCY = round(ENCRYPTED_PAYLOAD_LEN*PER_BYTE_LATENCY_MS, 1) + ENCRYPTION_LATENCY = round((ENCRYPTED_PAYLOAD_LEN-PL_LEN)*PER_BYTE_LATENCY_MS, 1) + if ENCRYPTED_PAYLOAD_LEN-PL_LEN == 1: + E_OPT_STR = "(optimal)" + else: + E_OPT_STR = "(sub-optimal)" + + TOTAL_LATENCY = round(TARGET_MS+PACKET_LATENCY, 1) + + print( "\n===== Simulation Parameters ===\n") + print(f" Packing method : {method}") + print(f" Sampling delay : {TARGET_MS}ms") + print(f" Codec bitrate : {CODEC_RATE} bps") + print(f" Audio data : {AUDIO_LEN} bytes") + print(f" Packing overhead : {PACKING_OVERHEAD} bytes") + print(f" Payload length : {PL_LEN} bytes") + print(f" AES blocks needed : {REQUIRED_BLOCKS}") + print(f" Encrypted payload : {ENCRYPTED_PAYLOAD_LEN} bytes") + print(f" Transport overhead : {TRANSPORT_OVERHEAD} bytes ({RNS_OVERHEAD} from RNS, {PHY_OVERHEAD} from PHY)") + print(f" On-air length : {PACKET_LEN} bytes") + print(f" Packet airtime : {round(PACKET_AIRTIME,2)}ms") + print(f" Transport bitrate : {RNS.prettyspeed((PACKET_LEN*8)/(TARGET_MS/1000))}") + + print( "\n===== Results for "+RNS.prettyspeed(LINK_SPEED)+" Link Speed ===\n") + print(f" Final latency : {TOTAL_LATENCY}ms") + print(f" Recording latency : contributes {TARGET_MS}ms") + print(f" Packet transport : contributes {PACKET_LATENCY}ms") + print(f" Payload : contributes {PAYLOAD_LATENCY}ms") + print(f" Audio data : contributes {RAW_DATA_LATENCY}ms") + print(f" Packing format : contributes {PACKING_LATENCY}ms") + print(f" Encryption : contributes {ENCRYPTION_LATENCY}ms {E_OPT_STR}") + print(f" RNS+PHY overhead : contributes {TRANSPORT_LATENCY}ms") + print(f"") + print(f" Half-duplex airtime : {round(AIRTIME_PCT, 2)}% of link capacity") + print(f" Concurrent calls : {int(CONCURRENT_CALLS)}\n") + print(f" Full-duplex airtime : {round(AIRTIME_PCT*2, 2)}% of link capacity") + print(f" Concurrent calls : {int(CONCURRENT_CALLS/2)}") + + if BLOCK_HEADROOM != 0: + print("") + print(f" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + print(f" Unaligned AES block! Each packet could fit") + print(f" {BLOCK_HEADROOM} bytes of additional audio data") + print(f" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + +#print( "\n= With mspack =================") +#simulate(method="msgpack") + +# simulate(link_speed=10980, audio_slot_ms=180, codec_rate=3200, + # signalling_bytes=18, method="msgpack") + +# simulate(link_speed=4690, audio_slot_ms=480, codec_rate=1200, +# signalling_bytes=18, method="msgpack") + +#simulate(link_speed=60e3, audio_slot_ms=270, codec_rate=12000, +# signalling_bytes=18, method="msgpack") + +simulate(link_speed=9600, audio_slot_ms=300, codec_rate=3200, + signalling_bytes=2, method="msgpack") + +#print("\n\n= With protobuf ===============") +#simulate(method="protobuf") diff --git a/docs/speech.opus b/docs/speech.opus new file mode 100644 index 0000000..42786a4 Binary files /dev/null and b/docs/speech.opus differ diff --git a/docs/speech_stereo.opus b/docs/speech_stereo.opus new file mode 100644 index 0000000..8bd8bf8 Binary files /dev/null and b/docs/speech_stereo.opus differ diff --git a/examples/mixer.py b/examples/mixer.py new file mode 100644 index 0000000..7d5f20f --- /dev/null +++ b/examples/mixer.py @@ -0,0 +1,51 @@ +import RNS +import LXST +import sys +import time +RNS.loglevel = RNS.LOG_DEBUG + +target_frame_ms = 20 +pipelined_output = True +raw = LXST.Codecs.Raw() + +# Pipelined mixer example +if pipelined_output: + opus = LXST.Codecs.Opus(profile=LXST.Codecs.Opus.PROFILE_AUDIO_HIGH) + codec2 = LXST.Codecs.Codec2(mode=LXST.Codecs.Codec2.CODEC2_3200) + line_sink = LXST.Sinks.LineSink() + mixer = LXST.Mixer(target_frame_ms=target_frame_ms) + loopback = LXST.Sources.Loopback() + + codec = opus + + file_source1 = LXST.Sources.OpusFileSource("./docs/speech_stereo.opus", codec=raw, sink=mixer, loop=True, target_frame_ms=target_frame_ms) + file_source2 = LXST.Sources.OpusFileSource("./docs/podcast.opus", codec=raw, sink=mixer, loop=True, target_frame_ms=target_frame_ms) + line_source = LXST.Sources.LineSource(target_frame_ms=target_frame_ms, codec=raw, sink=mixer) + + input_pipeline = LXST.Pipeline(source=mixer, codec=codec, sink=loopback) + output_pipeline = LXST.Pipeline(source=loopback, codec=codec, sink=line_sink) + input_pipeline.start(); output_pipeline.start() + +# Simple mixer example with output directly to sink +else: + line_sink = LXST.Sinks.LineSink() + mixer = LXST.Mixer(target_frame_ms=target_frame_ms, sink=line_sink) + file_source1 = LXST.Sources.OpusFileSource("./docs/speech_stereo.opus", codec=raw, sink=mixer, loop=True, target_frame_ms=target_frame_ms) + file_source2 = LXST.Sources.OpusFileSource("./docs/podcast.opus", codec=raw, sink=mixer, loop=True, target_frame_ms=target_frame_ms) + line_source = LXST.Sources.LineSource(target_frame_ms=target_frame_ms, codec=raw, sink=mixer) + +mixer.start() +line_source.start() +print("Hit enter to add another source"); input() + +file_source1.start() +print("Hit enter to add another source"); input() + +file_source2.start() +print("Hit enter to stop all sources"); input() + +file_source1.stop() +file_source2.stop() +line_source.stop() + +time.sleep(0.5) \ No newline at end of file diff --git a/examples/pipelines.py b/examples/pipelines.py new file mode 100644 index 0000000..00b8ae7 --- /dev/null +++ b/examples/pipelines.py @@ -0,0 +1,48 @@ +import RNS +import LXST +import sys +import time +RNS.loglevel = RNS.LOG_DEBUG + +if len(sys.argv) < 2: + print("No codec specified") + sys.exit(0) +else: + selected_codec = sys.argv[1] + +if len(sys.argv) >= 4: + target_frame_ms = int(sys.argv[3]) +else: + target_frame_ms = 40 + +if len(sys.argv) >= 3 and sys.argv[2].lower() == "file": + selected_source = LXST.Sources.OpusFileSource("./docs/speech_stereo.opus", loop=True, target_frame_ms=target_frame_ms) + # selected_source = LXST.Sources.OpusFileSource("./docs/music_stereo.opus", loop=True, target_frame_ms=target_frame_ms) + # selected_source = LXST.Sources.OpusFileSource("./docs/podcast.opus", loop=True, target_frame_ms=target_frame_ms) +else: + selected_source = LXST.Sources.LineSource(target_frame_ms=target_frame_ms) + +line_sink = LXST.Sinks.LineSink() +loopback = LXST.Sources.Loopback() + +if selected_codec.lower() == "raw": + raw = LXST.Codecs.Raw() + input_pipeline = LXST.Pipeline(source=selected_source, codec=raw, sink=loopback) + output_pipeline = LXST.Pipeline(source=loopback, codec=raw, sink=line_sink) +elif selected_codec.lower() == "codec2": + codec2 = LXST.Codecs.Codec2(mode=LXST.Codecs.Codec2.CODEC2_1600) + input_pipeline = LXST.Pipeline(source=selected_source, codec=codec2, sink=loopback) + output_pipeline = LXST.Pipeline(source=loopback, codec=codec2, sink=line_sink) +elif selected_codec.lower() == "opus": + opus = LXST.Codecs.Opus(profile=LXST.Codecs.Opus.PROFILE_VOICE_LOW) + input_pipeline = LXST.Pipeline(source=selected_source, codec=opus, sink=loopback) + output_pipeline = LXST.Pipeline(source=loopback, codec=opus, sink=line_sink) +else: + print("No valid codec selected") + sys.exit(0) + +input_pipeline.start(); output_pipeline.start() +input() +input_pipeline.stop() + +time.sleep(1) \ No newline at end of file diff --git a/examples/tone_generator.py b/examples/tone_generator.py new file mode 100644 index 0000000..a7128f5 --- /dev/null +++ b/examples/tone_generator.py @@ -0,0 +1,15 @@ +import RNS +import LXST +import sys +import time +RNS.loglevel = RNS.LOG_DEBUG + +target_frame_ms = 40 +tone = LXST.Generators.ToneSource(frequency=388, ease_time_ms=3.14159, target_frame_ms=target_frame_ms) +line_sink = LXST.Sinks.LineSink() +output_pipeline = LXST.Pipeline(source=tone, codec=LXST.Codecs.Null(), sink=line_sink) + +output_pipeline.start(); input() +tone.stop() + +time.sleep(1) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b7fd8e6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +soundcard +numpy +pycodec2 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fa9c5e2 --- /dev/null +++ b/setup.py @@ -0,0 +1,42 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +exec(open("LXST/_version.py", "r").read()) + +packages = setuptools.find_packages(exclude=[]) +packages.append("LXST.Utilities") +packages.append("LXST.Primitives.hardware") +packages.append("LXST.Codecs.libs.pydub") +packages.append("LXST.Codecs.libs.pyogg") +print("Packages:") +print(packages) + +setuptools.setup( + name="lxst", + version=__version__, + author="Mark Qvist", + author_email="mark@unsigned.io", + description="Lightweight Extensible Signal Transport for Reticulum", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://git.unsigned.io/markqvist/lxst", + packages=packages, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: Other/Proprietary License", + "Operating System :: OS Independent", + ], + entry_points= { + 'console_scripts': [ + 'rnphone=LXST.Utilities.rnphone:main', + ] + }, + install_requires=["rns>=0.9.2", + "soundcard", + "numpy", + "pycodec2", + "audioop-lts>=0.2.1;python_version>='3.13'"], + python_requires=">=3.7", +) \ No newline at end of file