diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json new file mode 100644 index 0000000000..0932520418 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationAudioSlideToCancel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png new file mode 100644 index 0000000000..890ef7345a Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/Contents.json deleted file mode 100644 index 80cfcaecaa..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 3, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 32, - "top" : 32, - "right" : 33, - "left" : 41 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleIncomingFullPad@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/ModernBubbleIncomingFullPad@2x.png b/Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/ModernBubbleIncomingFullPad@2x.png deleted file mode 100644 index d9f6fd76ee..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleIncoming.imageset/ModernBubbleIncomingFullPad@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/Contents.json deleted file mode 100644 index 08c8c83b11..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 33, - "top" : 32, - "right" : 34, - "left" : 44 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleIncomingPartialPad@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/ModernBubbleIncomingPartialPad@2x.png b/Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/ModernBubbleIncomingPartialPad@2x.png deleted file mode 100644 index 11a5c80fa4..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleIncomingMerged.imageset/ModernBubbleIncomingPartialPad@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/Contents.json deleted file mode 100644 index 0e38a88e8b..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 32, - "top" : 33, - "right" : 49, - "left" : 30 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleIncomingMergedBoth@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/ModernBubbleIncomingMergedBoth@2x.png b/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/ModernBubbleIncomingMergedBoth@2x.png deleted file mode 100644 index 2bca41e962..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBoth.imageset/ModernBubbleIncomingMergedBoth@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/Contents.json deleted file mode 100644 index a79bb7b0f8..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 32, - "top" : 33, - "right" : 35, - "left" : 44 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleIncomingMergedBottom@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/ModernBubbleIncomingMergedBottom@2x.png b/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/ModernBubbleIncomingMergedBottom@2x.png deleted file mode 100644 index 1d511b792b..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedBottom.imageset/ModernBubbleIncomingMergedBottom@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/Contents.json deleted file mode 100644 index 00b5220dbf..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 33, - "top" : 32, - "right" : 36, - "left" : 43 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleIncomingMergedTop@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/ModernBubbleIncomingMergedTop@2x.png b/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/ModernBubbleIncomingMergedTop@2x.png deleted file mode 100644 index 69cfa208f6..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleIncomingMergedTop.imageset/ModernBubbleIncomingMergedTop@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/Contents.json deleted file mode 100644 index 843f329871..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 33, - "top" : 31, - "right" : 43, - "left" : 34 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleOutgoingFullPad@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/ModernBubbleOutgoingFullPad@2x.png b/Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/ModernBubbleOutgoingFullPad@2x.png deleted file mode 100644 index 51946b74b0..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleOutgoing.imageset/ModernBubbleOutgoingFullPad@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/Contents.json b/Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/Contents.json deleted file mode 100644 index 227b099c2d..0000000000 --- a/Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "resizing" : { - "mode" : "9-part", - "center" : { - "mode" : "stretch", - "width" : 1, - "height" : 1 - }, - "cap-insets" : { - "bottom" : 33, - "top" : 32, - "right" : 44, - "left" : 34 - } - }, - "idiom" : "universal", - "filename" : "ModernBubbleOutgoingPartialPad@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/ModernBubbleOutgoingPartialPad@2x.png b/Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/ModernBubbleOutgoingPartialPad@2x.png deleted file mode 100644 index 982d3b1a4e..0000000000 Binary files a/Images.xcassets/Chat/Message/Background/BubbleOutgoingMerged.imageset/ModernBubbleOutgoingPartialPad@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/Message/Background/Contents.json b/Images.xcassets/Chat/Message/Background/Contents.json deleted file mode 100644 index 38f0c81fc2..0000000000 --- a/Images.xcassets/Chat/Message/Background/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "provides-namespace" : true - } -} \ No newline at end of file diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 2a8b095a49..68b74f8c35 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */; }; D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */; }; D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */; }; + D01F66131DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */; }; D021E0CE1DB4135500C6B04F /* ChatMediaInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */; }; D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */; }; D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */; }; @@ -35,14 +36,25 @@ D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */; }; D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0761D9190EF000889C2 /* GridMessageItem.swift */; }; D03120F61DA534C1006A2A60 /* PeerInfoActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03120F51DA534C1006A2A60 /* PeerInfoActionItem.swift */; }; + D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */; }; + D039EB081DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */; }; + D039EB0A1DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */; }; D03ADB481D703268005A521C /* ChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB471D703268005A521C /* ChatInterfaceState.swift */; }; D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; }; D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; }; D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; }; D04B66B81DD672D00049C3D2 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B66B71DD672D00049C3D2 /* GeoLocation.swift */; }; + D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */; }; + D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; }; D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; }; D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; }; D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; }; + D0736F211DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */; }; + D0736F231DF496D000F2C02A /* PeerMediaAudioPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */; }; + D0736F251DF4D0E500F2C02A /* TelegramController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F241DF4D0E500F2C02A /* TelegramController.swift */; }; + D0736F2A1DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F291DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift */; }; + D0736F2C1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */; }; + D0736F2E1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F2D1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift */; }; D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; }; D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */; }; D073CE711DCBF23F007511FD /* DeclareEncodables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE701DCBF23F007511FD /* DeclareEncodables.swift */; }; @@ -68,6 +80,12 @@ D08D452F1D5E340300A7428A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452A1D5E340300A7428A /* Display.framework */; }; D08D45301D5E340300A7428A /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452B1D5E340300A7428A /* Postbox.framework */; }; D08D45311D5E340300A7428A /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452C1D5E340300A7428A /* SwiftSignalKit.framework */; }; + D099EA1F1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */; }; + D099EA211DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */; }; + D099EA271DE765DB001AF5A8 /* ManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */; }; + D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */; }; + D099EA2D1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */; }; + D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */; }; D0AB0BB11D6718DA002C78E7 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */; }; D0AB0BB31D6718EB002C78E7 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB21D6718EB002C78E7 /* libz.tbd */; }; D0AB0BB51D6718F1002C78E7 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */; }; @@ -88,6 +106,36 @@ D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; }; D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; }; D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; }; + D0D03AE31DECACB700220C46 /* ManagedAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */; }; + D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */; }; + D0D03B081DECB0FE00220C46 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE81DECB0FE00220C46 /* diag_range.c */; }; + D0D03B091DECB0FE00220C46 /* diag_range.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AE91DECB0FE00220C46 /* diag_range.h */; }; + D0D03B0A1DECB0FE00220C46 /* opus_header.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AEA1DECB0FE00220C46 /* opus_header.c */; }; + D0D03B0B1DECB0FE00220C46 /* opus_header.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AEB1DECB0FE00220C46 /* opus_header.h */; }; + D0D03B0C1DECB0FE00220C46 /* opusenc.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AEC1DECB0FE00220C46 /* opusenc.h */; }; + D0D03B0D1DECB0FE00220C46 /* opusenc.m in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AED1DECB0FE00220C46 /* opusenc.m */; }; + D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AEE1DECB0FE00220C46 /* picture.c */; }; + D0D03B0F1DECB0FE00220C46 /* picture.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AEF1DECB0FE00220C46 /* picture.h */; }; + D0D03B101DECB0FE00220C46 /* wav_io.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AF01DECB0FE00220C46 /* wav_io.c */; }; + D0D03B111DECB0FE00220C46 /* wav_io.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AF11DECB0FE00220C46 /* wav_io.h */; }; + D0D03B121DECB0FE00220C46 /* bitwise.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AF41DECB0FE00220C46 /* bitwise.c */; }; + D0D03B131DECB0FE00220C46 /* framing.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AF51DECB0FE00220C46 /* framing.c */; }; + D0D03B141DECB0FE00220C46 /* ogg.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AF61DECB0FE00220C46 /* ogg.h */; }; + D0D03B151DECB0FE00220C46 /* os_types.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AF71DECB0FE00220C46 /* os_types.h */; }; + D0D03B161DECB0FE00220C46 /* opus.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AFB1DECB0FE00220C46 /* opus.h */; }; + D0D03B171DECB0FE00220C46 /* opus_defines.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AFC1DECB0FE00220C46 /* opus_defines.h */; }; + D0D03B181DECB0FE00220C46 /* opus_multistream.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AFD1DECB0FE00220C46 /* opus_multistream.h */; }; + D0D03B191DECB0FE00220C46 /* opus_types.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03AFE1DECB0FE00220C46 /* opus_types.h */; }; + D0D03B1B1DECB0FE00220C46 /* info.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B021DECB0FE00220C46 /* info.c */; }; + D0D03B1C1DECB0FE00220C46 /* internal.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B031DECB0FE00220C46 /* internal.c */; }; + D0D03B1D1DECB0FE00220C46 /* internal.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03B041DECB0FE00220C46 /* internal.h */; }; + D0D03B1E1DECB0FE00220C46 /* opusfile.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B051DECB0FE00220C46 /* opusfile.c */; }; + D0D03B1F1DECB0FE00220C46 /* opusfile.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03B061DECB0FE00220C46 /* opusfile.h */; }; + D0D03B201DECB0FE00220C46 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B071DECB0FE00220C46 /* stream.c */; }; + D0D03B231DECB1AD00220C46 /* TGDataItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D03B211DECB1AD00220C46 /* TGDataItem.h */; }; + D0D03B241DECB1AD00220C46 /* TGDataItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B221DECB1AD00220C46 /* TGDataItem.m */; }; + D0D03B261DECB26D00220C46 /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D03B251DECB26D00220C46 /* libopus.a */; }; + D0D03B2C1DED9B8900220C46 /* AudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */; }; D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */; }; D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */; }; D0D2686C1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2686B1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift */; }; @@ -203,7 +251,6 @@ D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */; }; D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */; }; D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */; }; - D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */; }; D0F69E461D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */; }; D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */; }; D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E4A1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift */; }; @@ -278,6 +325,7 @@ D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageActionButtonsNode.swift; sourceTree = ""; }; D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditAccessoryPanelNode.swift; sourceTree = ""; }; + D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingButton.swift; sourceTree = ""; }; D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputNode.swift; sourceTree = ""; }; D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputNode.swift; sourceTree = ""; }; D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputNodes.swift; sourceTree = ""; }; @@ -297,14 +345,25 @@ D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; D03120F51DA534C1006A2A60 /* PeerInfoActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoActionItem.swift; sourceTree = ""; }; + D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = ""; }; + D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = ""; }; + D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = ""; }; D03ADB471D703268005A521C /* ChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceState.swift; sourceTree = ""; }; D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = ""; }; D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = ""; }; D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = ""; }; D04B66B71DD672D00049C3D2 /* GeoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoLocation.swift; sourceTree = ""; }; + D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = ""; }; + D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; + D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioPlaylistPlayer.swift; sourceTree = ""; }; + D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaAudioPlaylist.swift; sourceTree = ""; }; + D0736F241DF4D0E500F2C02A /* TelegramController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramController.swift; sourceTree = ""; }; + D0736F291DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryPanel.swift; sourceTree = ""; }; + D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryContainerNode.swift; sourceTree = ""; }; + D0736F2D1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryHeaderNode.swift; sourceTree = ""; }; D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = ""; }; D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = ""; }; D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = ""; }; @@ -331,6 +390,12 @@ D08D452B1D5E340300A7428A /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Postbox.framework"; sourceTree = ""; }; D08D452C1D5E340300A7428A /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/SwiftSignalKit.framework"; sourceTree = ""; }; D08D452D1D5E340300A7428A /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCore.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/TelegramCore.framework"; sourceTree = ""; }; + D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; + D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = ""; }; + D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedMediaId.swift; sourceTree = ""; }; + D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedVideoNode.swift; sourceTree = ""; }; + D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = ""; }; + D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = ""; }; D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; D0AB0BB21D6718EB002C78E7 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; @@ -353,6 +418,36 @@ D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = ""; }; D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = ""; }; D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = ""; }; + D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = ""; }; + D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = ""; }; + D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; + D0D03AE91DECB0FE00220C46 /* diag_range.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = diag_range.h; sourceTree = ""; }; + D0D03AEA1DECB0FE00220C46 /* opus_header.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opus_header.c; sourceTree = ""; }; + D0D03AEB1DECB0FE00220C46 /* opus_header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_header.h; sourceTree = ""; }; + D0D03AEC1DECB0FE00220C46 /* opusenc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opusenc.h; sourceTree = ""; }; + D0D03AED1DECB0FE00220C46 /* opusenc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = opusenc.m; sourceTree = ""; }; + D0D03AEE1DECB0FE00220C46 /* picture.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = picture.c; sourceTree = ""; }; + D0D03AEF1DECB0FE00220C46 /* picture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = picture.h; sourceTree = ""; }; + D0D03AF01DECB0FE00220C46 /* wav_io.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = wav_io.c; sourceTree = ""; }; + D0D03AF11DECB0FE00220C46 /* wav_io.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wav_io.h; sourceTree = ""; }; + D0D03AF41DECB0FE00220C46 /* bitwise.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = bitwise.c; path = ogg/bitwise.c; sourceTree = ""; }; + D0D03AF51DECB0FE00220C46 /* framing.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = framing.c; path = ogg/framing.c; sourceTree = ""; }; + D0D03AF61DECB0FE00220C46 /* ogg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ogg.h; path = ogg/ogg.h; sourceTree = ""; }; + D0D03AF71DECB0FE00220C46 /* os_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = os_types.h; path = ogg/os_types.h; sourceTree = ""; }; + D0D03AFB1DECB0FE00220C46 /* opus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = opus.h; path = include/opus/opus.h; sourceTree = ""; }; + D0D03AFC1DECB0FE00220C46 /* opus_defines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = opus_defines.h; path = include/opus/opus_defines.h; sourceTree = ""; }; + D0D03AFD1DECB0FE00220C46 /* opus_multistream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = opus_multistream.h; path = include/opus/opus_multistream.h; sourceTree = ""; }; + D0D03AFE1DECB0FE00220C46 /* opus_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = opus_types.h; path = include/opus/opus_types.h; sourceTree = ""; }; + D0D03B021DECB0FE00220C46 /* info.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = info.c; sourceTree = ""; }; + D0D03B031DECB0FE00220C46 /* internal.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = internal.c; sourceTree = ""; }; + D0D03B041DECB0FE00220C46 /* internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = internal.h; sourceTree = ""; }; + D0D03B051DECB0FE00220C46 /* opusfile.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opusfile.c; sourceTree = ""; }; + D0D03B061DECB0FE00220C46 /* opusfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opusfile.h; sourceTree = ""; }; + D0D03B071DECB0FE00220C46 /* stream.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = stream.c; sourceTree = ""; }; + D0D03B211DECB1AD00220C46 /* TGDataItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGDataItem.h; sourceTree = ""; }; + D0D03B221DECB1AD00220C46 /* TGDataItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGDataItem.m; sourceTree = ""; }; + D0D03B251DECB26D00220C46 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = "third-party/opus/lib/libopus.a"; sourceTree = ""; }; + D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveform.swift; sourceTree = ""; }; D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateNavigationButtons.swift; sourceTree = ""; }; D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatAvatarNavigationNode.swift; sourceTree = ""; }; D0D2686B1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTitleAccessoryPanelNode.swift; sourceTree = ""; }; @@ -468,7 +563,6 @@ D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageWebpageBubbleContentNode.swift; sourceTree = ""; }; D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnreadItem.swift; sourceTree = ""; }; D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputPanelNode.swift; sourceTree = ""; }; - D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResizeableTextInputView.swift; sourceTree = ""; }; D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtonNode.swift; sourceTree = ""; }; D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetRollImageItem.swift; sourceTree = ""; }; D0F69E4A1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaActionSheetController.swift; sourceTree = ""; }; @@ -531,6 +625,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D0D03B261DECB26D00220C46 /* libopus.a in Frameworks */, D07551911DDA4FC70073E051 /* libc++.tbd in Frameworks */, D075518F1DDA4F9E0073E051 /* SSignalKit.framework in Frameworks */, D07551881DDA4BB50073E051 /* TelegramLegacyComponents.framework in Frameworks */, @@ -636,6 +731,17 @@ name = "Accessory Panels"; sourceTree = ""; }; + D0736F261DF4D2F300F2C02A /* Telegram Controller */ = { + isa = PBXGroup; + children = ( + D0736F241DF4D0E500F2C02A /* TelegramController.swift */, + D0736F291DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift */, + D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */, + D0736F2D1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift */, + ); + name = "Telegram Controller"; + sourceTree = ""; + }; D073CE611DCBBE09007511FD /* Sounds */ = { isa = PBXGroup; children = ( @@ -656,6 +762,8 @@ D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */, D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */, D0EFD8951DDE8249009E508A /* LegacyLocationPicker.swift */, + D0D03B211DECB1AD00220C46 /* TGDataItem.h */, + D0D03B221DECB1AD00220C46 /* TGDataItem.m */, ); name = "Legacy Components"; sourceTree = ""; @@ -678,6 +786,7 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D0D03B251DECB26D00220C46 /* libopus.a */, D07551901DDA4FC70073E051 /* libc++.tbd */, D075518E1DDA4F9E0073E051 /* SSignalKit.framework */, D07551871DDA4BB50073E051 /* TelegramLegacyComponents.framework */, @@ -700,6 +809,34 @@ name = Frameworks; sourceTree = ""; }; + D099EA1D1DE744EE001AF5A8 /* Horizontal List */ = { + isa = PBXGroup; + children = ( + D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */, + D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */, + ); + name = "Horizontal List"; + sourceTree = ""; + }; + D099EA251DE76585001AF5A8 /* Media Manager */ = { + isa = PBXGroup; + children = ( + D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */, + D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */, + D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */, + D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */, + D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, + D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */, + D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */, + D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */, + D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */, + D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */, + D0D03AE61DECB0D200220C46 /* Audio Recorder */, + D0F69DBC1D6B886C0046BCD6 /* Player */, + ); + name = "Media Manager"; + sourceTree = ""; + }; D0B7F8DF1D8A17D20045D939 /* Collection */ = { isa = PBXGroup; children = ( @@ -732,6 +869,73 @@ name = "Input Panels"; sourceTree = ""; }; + D0D03AE61DECB0D200220C46 /* Audio Recorder */ = { + isa = PBXGroup; + children = ( + D0D03AE71DECB0FE00220C46 /* opusenc */, + D0D03AF21DECB0FE00220C46 /* ogg */, + D0D03AF81DECB0FE00220C46 /* opus */, + D0D03B011DECB0FE00220C46 /* opusfile */, + ); + name = "Audio Recorder"; + sourceTree = ""; + }; + D0D03AE71DECB0FE00220C46 /* opusenc */ = { + isa = PBXGroup; + children = ( + D0D03AE81DECB0FE00220C46 /* diag_range.c */, + D0D03AE91DECB0FE00220C46 /* diag_range.h */, + D0D03AEA1DECB0FE00220C46 /* opus_header.c */, + D0D03AEB1DECB0FE00220C46 /* opus_header.h */, + D0D03AEC1DECB0FE00220C46 /* opusenc.h */, + D0D03AED1DECB0FE00220C46 /* opusenc.m */, + D0D03AEE1DECB0FE00220C46 /* picture.c */, + D0D03AEF1DECB0FE00220C46 /* picture.h */, + D0D03AF01DECB0FE00220C46 /* wav_io.c */, + D0D03AF11DECB0FE00220C46 /* wav_io.h */, + ); + name = opusenc; + path = "third-party/opusenc"; + sourceTree = SOURCE_ROOT; + }; + D0D03AF21DECB0FE00220C46 /* ogg */ = { + isa = PBXGroup; + children = ( + D0D03AF41DECB0FE00220C46 /* bitwise.c */, + D0D03AF51DECB0FE00220C46 /* framing.c */, + D0D03AF61DECB0FE00220C46 /* ogg.h */, + D0D03AF71DECB0FE00220C46 /* os_types.h */, + ); + name = ogg; + path = "third-party/ogg"; + sourceTree = SOURCE_ROOT; + }; + D0D03AF81DECB0FE00220C46 /* opus */ = { + isa = PBXGroup; + children = ( + D0D03AFB1DECB0FE00220C46 /* opus.h */, + D0D03AFC1DECB0FE00220C46 /* opus_defines.h */, + D0D03AFD1DECB0FE00220C46 /* opus_multistream.h */, + D0D03AFE1DECB0FE00220C46 /* opus_types.h */, + ); + name = opus; + path = "third-party/opus"; + sourceTree = SOURCE_ROOT; + }; + D0D03B011DECB0FE00220C46 /* opusfile */ = { + isa = PBXGroup; + children = ( + D0D03B021DECB0FE00220C46 /* info.c */, + D0D03B031DECB0FE00220C46 /* internal.c */, + D0D03B041DECB0FE00220C46 /* internal.h */, + D0D03B051DECB0FE00220C46 /* opusfile.c */, + D0D03B061DECB0FE00220C46 /* opusfile.h */, + D0D03B071DECB0FE00220C46 /* stream.c */, + ); + name = opusfile; + path = "third-party/opusfile"; + sourceTree = SOURCE_ROOT; + }; D0D2686A1D788F6600C422DA /* Title Accessory Panels */ = { isa = PBXGroup; children = ( @@ -824,6 +1028,7 @@ isa = PBXGroup; children = ( D0E35A051DE4801600BC6096 /* Vertical List */, + D099EA1D1DE744EE001AF5A8 /* Horizontal List */, ); name = "Context Request Results"; sourceTree = ""; @@ -883,9 +1088,8 @@ D0F69DBB1D6B88330046BCD6 /* Media */ = { isa = PBXGroup; children = ( - D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, + D099EA251DE76585001AF5A8 /* Media Manager */, D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */, - D0F69DBC1D6B886C0046BCD6 /* Player */, D0F69E9D1D6B8E240046BCD6 /* Resources */, ); name = Media; @@ -946,6 +1150,7 @@ D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */, D0F69DCA1D6B89F20046BCD6 /* Search */, D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */, + D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */, ); name = Nodes; sourceTree = ""; @@ -964,6 +1169,7 @@ D0F69DD31D6B8A160046BCD6 /* Controllers */ = { isa = PBXGroup; children = ( + D0736F261DF4D2F300F2C02A /* Telegram Controller */, D0F69DD71D6B8A300046BCD6 /* List */, D0F69DD41D6B8A240046BCD6 /* Alert */, ); @@ -1108,7 +1314,10 @@ isa = PBXGroup; children = ( D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */, - D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */, + D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */, + D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */, + D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */, + D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */, ); name = "Text Input"; sourceTree = ""; @@ -1231,6 +1440,7 @@ D04B66B71DD672D00049C3D2 /* GeoLocation.swift */, D00219031DDCC86400BE708A /* PerformanceSpinner.swift */, D023836F1DDF0462004018B6 /* UrlHandling.swift */, + D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */, ); name = Utils; sourceTree = ""; @@ -1301,11 +1511,25 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + D0D03B161DECB0FE00220C46 /* opus.h in Headers */, + D0D03B141DECB0FE00220C46 /* ogg.h in Headers */, D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */, + D0D03B231DECB1AD00220C46 /* TGDataItem.h in Headers */, + D0D03B1F1DECB0FE00220C46 /* opusfile.h in Headers */, + D0D03B1D1DECB0FE00220C46 /* internal.h in Headers */, D0FC40901D5B8E7500261D9D /* TelegramUI.h in Headers */, + D0D03B0B1DECB0FE00220C46 /* opus_header.h in Headers */, + D0D03B181DECB0FE00220C46 /* opus_multistream.h in Headers */, + D0D03B111DECB0FE00220C46 /* wav_io.h in Headers */, D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */, + D0D03B0C1DECB0FE00220C46 /* opusenc.h in Headers */, + D0D03B0F1DECB0FE00220C46 /* picture.h in Headers */, D0F69E8A1D6B8C850046BCD6 /* FFMpegSwResample.h in Headers */, D0F69E881D6B8C850046BCD6 /* FastBlur.h in Headers */, + D0D03B151DECB0FE00220C46 /* os_types.h in Headers */, + D0D03B191DECB0FE00220C46 /* opus_types.h in Headers */, + D0D03B171DECB0FE00220C46 /* opus_defines.h in Headers */, + D0D03B091DECB0FE00220C46 /* diag_range.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1419,6 +1643,7 @@ D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */, D0F7AB391DCFF87B009AD9A1 /* ChatMessageDateHeader.swift in Sources */, D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */, + D0D03B1B1DECB0FE00220C46 /* info.c in Sources */, D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */, D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */, D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */, @@ -1427,12 +1652,17 @@ D07CFF7F1DCA308500761F81 /* ChatListNodeLocation.swift in Sources */, D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */, + D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */, + D0D03B1E1DECB0FE00220C46 /* opusfile.c in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, + D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */, D0D2686C1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift in Sources */, D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */, + D01F66131DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift in Sources */, D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */, + D0736F251DF4D0E500F2C02A /* TelegramController.swift in Sources */, D0F69E561D6B8BDA0046BCD6 /* GalleryControllerNode.swift in Sources */, D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */, D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */, @@ -1453,7 +1683,9 @@ D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */, D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */, D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */, + D0D03B241DECB1AD00220C46 /* TGDataItem.m in Sources */, D0F69DA41D6B87EC0046BCD6 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, + D099EA2D1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift in Sources */, D0F69E161D6B8ACF0046BCD6 /* ChatHistoryEntry.swift in Sources */, D021E0CE1DB4135500C6B04F /* ChatMediaInputNode.swift in Sources */, D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */, @@ -1461,21 +1693,25 @@ D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */, D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */, D003702E1DA43052004308D3 /* PeerInfoAvatarAndNameItem.swift in Sources */, + D0D03B1C1DECB0FE00220C46 /* internal.c in Sources */, D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */, - D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */, D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */, + D0736F231DF496D000F2C02A /* PeerMediaAudioPlaylist.swift in Sources */, D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */, + D0D03B131DECB0FE00220C46 /* framing.c in Sources */, D0F69E651D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift in Sources */, D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */, D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */, D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */, D07CFF871DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift in Sources */, + D0D03B0A1DECB0FE00220C46 /* opus_header.c in Sources */, D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */, D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */, D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */, D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */, D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */, + D099EA1F1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */, D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */, D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */, D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, @@ -1486,6 +1722,7 @@ D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, + D0D03B121DECB0FE00220C46 /* bitwise.c in Sources */, D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */, D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */, D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */, @@ -1493,17 +1730,21 @@ D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */, D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */, D07551931DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift in Sources */, + D039EB0A1DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */, + D0736F2A1DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift in Sources */, D0E7A1BD1D8C246D00C37A6F /* ChatHistoryListNode.swift in Sources */, D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */, + D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */, D023ED2E1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift in Sources */, D02383771DDF16B2004018B6 /* ChatControllerTitlePanelNodeContainer.swift in Sources */, D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */, D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */, D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */, + D099EA211DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */, D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */, D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */, D075518D1DDA4E0B0073E051 /* LegacyControllerNode.swift in Sources */, @@ -1513,6 +1754,7 @@ D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */, D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */, D02383731DDF0D8A004018B6 /* ChatInfoTitlePanelNode.swift in Sources */, + D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */, D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */, D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */, D0DE77321D940295002B8809 /* ListMessageFileItemNode.swift in Sources */, @@ -1529,6 +1771,7 @@ D0F69E611D6B8BF90046BCD6 /* ChatDocumentGalleryItem.swift in Sources */, D0F69E0A1D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift in Sources */, D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */, + D0D03B0D1DECB0FE00220C46 /* opusenc.m in Sources */, D0B7F8E21D8A18070045D939 /* PeerMediaCollectionController.swift in Sources */, D0DE76FE1D92EFF2002B8809 /* PeerMediaCollectionTitleView.swift in Sources */, D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, @@ -1545,6 +1788,7 @@ D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, + D039EB081DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift in Sources */, D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */, D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */, D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */, @@ -1556,12 +1800,14 @@ D0E7A1C31D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift in Sources */, D0DC354A1DE366CD000195EB /* CommandChatInputContextPanelNode.swift in Sources */, D0F69E021D6B8A880046BCD6 /* ChatListHoleItem.swift in Sources */, + D0736F2E1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift in Sources */, D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */, D0F69E751D6B8C340046BCD6 /* ContactsPeerItem.swift in Sources */, D0F69DD61D6B8A2D0046BCD6 /* AlertController.swift in Sources */, D00370301DA43077004308D3 /* PeerInfoItem.swift in Sources */, D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */, D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, + D0D03B101DECB0FE00220C46 /* wav_io.c in Sources */, D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */, D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */, @@ -1576,6 +1822,7 @@ D023837E1DDF50FD004018B6 /* ChatToastAlertPanelNode.swift in Sources */, D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */, D0E7A1C11D8C258D00C37A6F /* ChatHistoryEntriesForView.swift in Sources */, + D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */, D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */, D073CE711DCBF23F007511FD /* DeclareEncodables.swift in Sources */, D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */, @@ -1593,12 +1840,15 @@ D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */, D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */, D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */, + D0D03AE31DECACB700220C46 /* ManagedAudioSession.swift in Sources */, D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */, + D0D03B2C1DED9B8900220C46 /* AudioWaveform.swift in Sources */, D0F69E771D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift in Sources */, D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */, D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */, D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */, D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */, + D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */, D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */, D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */, @@ -1614,6 +1864,7 @@ D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */, D023ED321DDB60CF00BD496D /* LegacyNavigationController.swift in Sources */, D0F69E2D1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift in Sources */, + D0D03B081DECB0FE00220C46 /* diag_range.c in Sources */, D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */, D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */, D0F69DC91D6B89EB0046BCD6 /* ImageNode.swift in Sources */, @@ -1622,10 +1873,14 @@ D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */, D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */, D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */, + D099EA271DE765DB001AF5A8 /* ManagedMediaId.swift in Sources */, D07CFF741DCA207200761F81 /* PeerSelectionController.swift in Sources */, + D0736F2C1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift in Sources */, D0E35A071DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */, + D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */, D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */, D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */, + D0736F211DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift in Sources */, D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */, D0F69E6B1D6B8C160046BCD6 /* MapInputControllerNode.swift in Sources */, D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */, @@ -1637,6 +1892,7 @@ D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */, D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */, D0F69D6D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift in Sources */, + D0D03B201DECB0FE00220C46 /* stream.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1720,6 +1976,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "third-party/ogg"; INFOPLIST_FILE = TelegramUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1728,12 +1985,14 @@ "$(inherited)", "$(PROJECT_DIR)/third-party/libwebp/lib", "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + "$(PROJECT_DIR)/third-party/opus/lib", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0.1; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 3.0; }; name = Hockeyapp; }; @@ -1866,6 +2125,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "third-party/ogg"; INFOPLIST_FILE = TelegramUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1874,6 +2134,7 @@ "$(inherited)", "$(PROJECT_DIR)/third-party/libwebp/lib", "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + "$(PROJECT_DIR)/third-party/opus/lib", ); OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = "-DDEBUG"; @@ -1881,7 +2142,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0.1; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -1899,6 +2161,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "third-party/ogg"; INFOPLIST_FILE = TelegramUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1907,12 +2170,14 @@ "$(inherited)", "$(PROJECT_DIR)/third-party/libwebp/lib", "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + "$(PROJECT_DIR)/third-party/opus/lib", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0.1; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/TelegramUI/ApplicationSpecificData.swift b/TelegramUI/ApplicationSpecificData.swift index 8595777add..6d66cdf0b9 100644 --- a/TelegramUI/ApplicationSpecificData.swift +++ b/TelegramUI/ApplicationSpecificData.swift @@ -1,6 +1,6 @@ import Foundation import SwiftSignalKit -final class ApplicationSpecificData { - let sharedChatMediaInputNode = Atomic(value: nil) +public final class ApplicationSpecificData { + } diff --git a/TelegramUI/AudioWaveform.swift b/TelegramUI/AudioWaveform.swift new file mode 100644 index 0000000000..5ec0c7d992 --- /dev/null +++ b/TelegramUI/AudioWaveform.swift @@ -0,0 +1,70 @@ +import Foundation + +private func getBits(data: UnsafeRawPointer, bitOffset: Int, numBits: Int) -> Int32 { + let normalizedNumBits = Int(pow(2.0, Double(numBits))) - 1 + let normalizedData = data.advanced(by: bitOffset / 8) + let normalizedBitOffset = bitOffset % 8 + + return (normalizedData.assumingMemoryBound(to: Int32.self).pointee >> Int32(normalizedBitOffset)) & Int32(normalizedNumBits) +} + +private func setBits(data: UnsafeMutableRawPointer, bitOffset: Int, numBits: Int, value: Int32) { + let normalizedNumBits = Int(pow(2.0, Double(numBits))) - 1 + let normalizedData = data.advanced(by: bitOffset / 8) + let normalizedBitOffset = bitOffset % 8 + + normalizedData.assumingMemoryBound(to: Int32.self).pointee |= value << Int32(normalizedBitOffset) +} + +final class AudioWaveform: Equatable { + let samples: Data + let peak: Int32 + + init(samples: Data, peak: Int32) { + self.samples = samples + self.peak = peak + } + + convenience init(bitstream: Data, bitsPerSample: Int) { + let numSamples = Int(Float(bitstream.count * 8) / Float(bitsPerSample)) + var result = Data() + result.count = numSamples * 2 + + bitstream.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + let maxSample = (1 << bitsPerSample) - 1 + + result.withUnsafeMutableBytes { (samples: UnsafeMutablePointer) -> Void in + let norm = Int64((1 << bitsPerSample) - 1) + for i in 0 ..< numSamples { + samples[i] = Int16(Int64(getBits(data: bytes, bitOffset: i * 5, numBits: 5)) * norm / norm) + } + } + } + + self.init(samples: result, peak: 31) + } + + func makeBitstream() -> Data { + let numSamples = self.samples.count / 2 + let bitstreamLength = (numSamples * 5) / 8 + (((numSamples * 5) % 8) == 0 ? 0 : 1) + var result = Data() + result.count = bitstreamLength + + let maxSample: Int32 = self.peak + + self.samples.withUnsafeBytes { (samples: UnsafePointer) -> Void in + result.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + for i in 0 ..< numSamples { + let value: Int32 = min(Int32(31), abs(Int32(samples[i])) * 31 / maxSample) + setBits(data: bytes, bitOffset: i * 5, numBits: 5, value: value & Int32(31)) + } + } + } + + return result + } + + static func ==(lhs: AudioWaveform, rhs: AudioWaveform) -> Bool { + return lhs.peak == rhs.peak && lhs.samples == rhs.samples + } +} diff --git a/TelegramUI/AudioWaveformNode.swift b/TelegramUI/AudioWaveformNode.swift new file mode 100644 index 0000000000..d12976a6db --- /dev/null +++ b/TelegramUI/AudioWaveformNode.swift @@ -0,0 +1,132 @@ +import Foundation +import Display +import AsyncDisplayKit + +private final class AudioWaveformNodeParameters: NSObject { + let waveform: AudioWaveform? + let color: UIColor? + + init(waveform: AudioWaveform?, color: UIColor?) { + self.waveform = waveform + self.color = color + + super.init() + } +} + +final class AudioWaveformNode: ASDisplayNode { + private var waveform: AudioWaveform? + private var color: UIColor? + + override init() { + super.init() + + self.isOpaque = false + } + + override var frame: CGRect { + get { + return super.frame + } set(value) { + let redraw = value.size != self.frame.size + super.frame = value + + if redraw { + self.setNeedsDisplay() + } + } + } + + func setup(color: UIColor, waveform: AudioWaveform?) { + if self.color == nil || !self.color!.isEqual(color) || self.waveform != waveform { + self.color = color + self.waveform = waveform + self.setNeedsDisplay() + } + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return AudioWaveformNodeParameters(waveform: self.waveform, color: self.color) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: @escaping asdisplaynode_iscancelled_block_t, isRasterizing: Bool) { + + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? AudioWaveformNodeParameters { + let sampleWidth: CGFloat = 2.0 + let halfSampleWidth: CGFloat = 1.0 + let distance: CGFloat = 1.0 + + let size = bounds.size + + if let color = parameters.color { + context.setFillColor(color.cgColor) + } + + if let waveform = parameters.waveform { + waveform.samples.withUnsafeBytes { (samples: UnsafePointer) -> Void in + let peakHeight: CGFloat = 12.0 + let maxReadSamples = waveform.samples.count / 2 + + var maxSample: UInt16 = 0 + for i in 0 ..< maxReadSamples { + let sample = samples[i] + if maxSample < sample { + maxSample = sample + } + } + + let invScale = 1.0 / max(1.0, CGFloat(maxSample)) + let numSamples = Int(floor(size.width / (sampleWidth + distance))) + + let adjustedSamplesMemory = malloc(numSamples * 2)! + let adjustedSamples = adjustedSamplesMemory.assumingMemoryBound(to: UInt16.self) + defer { + free(adjustedSamplesMemory) + } + memset(adjustedSamplesMemory, 0, numSamples * 2) + + for i in 0 ..< maxReadSamples { + let index = i * numSamples / maxReadSamples + let sample = samples[i] + if adjustedSamples[index] < sample { + adjustedSamples[index] = sample + } + } + + for i in 0 ..< numSamples { + let offset = CGFloat(i) * (sampleWidth + distance) + let peakSample = adjustedSamples[i] + + var sampleHeight = CGFloat(peakSample) * peakHeight * invScale + if abs(sampleHeight) > peakHeight { + sampleHeight = peakHeight + } + + let adjustedSampleHeight = sampleHeight - sampleWidth + if adjustedSampleHeight.isLessThanOrEqualTo(sampleWidth) { + context.fillEllipse(in: CGRect(x: offset, y: size.height - sampleWidth, width: sampleWidth, height: sampleHeight)) + context.fill(CGRect(x: offset, y: size.height - halfSampleWidth, width: sampleWidth, height: halfSampleWidth)) + } else { + let adjustedRect = CGRect(x: offset, y: size.height - adjustedSampleHeight, width: sampleWidth, height: adjustedSampleHeight) + context.fill(adjustedRect) + context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.minY - halfSampleWidth, width: sampleWidth, height: sampleWidth)) + context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.maxY - halfSampleWidth, width: sampleWidth, height: sampleHeight)) + } + } + } + } else { + context.fill(CGRect(x: halfSampleWidth, y: size.height - sampleWidth, width: size.width - sampleWidth, height: sampleWidth)) + context.fillEllipse(in: CGRect(x: 0.0, y: size.height - sampleWidth, width: sampleWidth, height: sampleWidth)) + context.fillEllipse(in: CGRect(x: size.width - sampleWidth, y: size.height - sampleWidth, width: sampleWidth, height: sampleWidth)) + } + } + } +} diff --git a/TelegramUI/AuthorizationCodeController.swift b/TelegramUI/AuthorizationCodeController.swift index b929dcb37a..93e25db9bc 100644 --- a/TelegramUI/AuthorizationCodeController.swift +++ b/TelegramUI/AuthorizationCodeController.swift @@ -51,7 +51,7 @@ class AuthorizationCodeController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func nextPressed() { diff --git a/TelegramUI/AuthorizationPasswordController.swift b/TelegramUI/AuthorizationPasswordController.swift index 6b4dc29fc7..0cdce1c786 100644 --- a/TelegramUI/AuthorizationPasswordController.swift +++ b/TelegramUI/AuthorizationPasswordController.swift @@ -42,7 +42,7 @@ class AuthorizationPasswordController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func nextPressed() { diff --git a/TelegramUI/AuthorizationPhoneController.swift b/TelegramUI/AuthorizationPhoneController.swift index 65bbd43b24..a7ae5c09bd 100644 --- a/TelegramUI/AuthorizationPhoneController.swift +++ b/TelegramUI/AuthorizationPhoneController.swift @@ -42,7 +42,7 @@ class AuthorizationPhoneController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func nextPressed() { diff --git a/TelegramUI/ChatContextResultManagedMediaId.swift b/TelegramUI/ChatContextResultManagedMediaId.swift new file mode 100644 index 0000000000..8d228e1d3b --- /dev/null +++ b/TelegramUI/ChatContextResultManagedMediaId.swift @@ -0,0 +1,23 @@ +import Foundation +import TelegramCore +import Postbox + +struct ChatContextResultManagedMediaId: ManagedMediaId { + let result: ChatContextResult + + init(result: ChatContextResult) { + self.result = result + } + + var hashValue: Int { + return self.result.id.hashValue + } + + func isEqual(to: ManagedMediaId) -> Bool { + if let to = to as? ChatContextResultManagedMediaId { + return self.result == to.result + } else { + return false + } + } +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 481bcbbb87..76f89e53d3 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -6,7 +6,7 @@ import Display import AsyncDisplayKit import TelegramCore -public class ChatController: ViewController { +public class ChatController: TelegramController { private var containerLayout = ContainerViewLayout() private let account: Account @@ -48,6 +48,11 @@ public class ChatController: ViewController { private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? + private var audioRecorderValue: ManagedAudioRecorder? + private var audioRecorderFeedback: HapticFeedback? + private var audioRecorder = Promise() + private var audioRecorderDisposable: Disposable? + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId @@ -55,7 +60,7 @@ public class ChatController: ViewController { performanceSpinnerAcquire() - super.init() + super.init(account: account) self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) @@ -87,8 +92,12 @@ public class ChatController: ViewController { } if let galleryMedia = galleryMedia { - if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "audio/mpeg" { - //debugPlayMedia(account: strongSelf.account, file: file) + if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + let player = ManagedAudioPlaylistPlayer(postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + applicationContext.mediaManager.setPlaylistPlayer(player) + player.control(.navigation(.next)) + } } else { let gallery = GalleryController(account: strongSelf.account, messageId: id) @@ -376,6 +385,14 @@ public class ChatController: ViewController { } enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: [], media: nil, replyToMessageId: postAsReply ? messageId : nil)]).start() } + }, updateInputState: { [weak self] f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState { + return $0.withUpdatedEffectiveInputState(f($0.effectiveInputState)) + } + }) + } }) self.controllerInteraction = controllerInteraction @@ -477,6 +494,47 @@ public class ChatController: ViewController { }) } }) + + self.audioRecorderDisposable = (self.audioRecorder.get() |> deliverOnMainQueue).start(next: { [weak self] audioRecorder in + if let strongSelf = self { + if strongSelf.audioRecorderValue !== audioRecorder { + strongSelf.audioRecorderValue = audioRecorder + + if let audioRecorder = audioRecorder { + /*(audioRecorder.recordingState + |> filter { state in + switch state { + case .recording: + return true + case .paused: + return false + } + } |> take(1) |> deliverOnMainQueue).start(completed: { + self?.audioRecorderFeedback?.tap() + })*/ + } else { + strongSelf.audioRecorderFeedback = nil + } + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + if let audioRecorder = audioRecorder { + if panelState.audioRecordingState == nil { + return panelState.withUpdatedAudioRecordingState(ChatTextInputPanelAudioRecordingState(recorder: audioRecorder)) + } + } else { + return panelState.withUpdatedAudioRecordingState(nil) + } + return panelState + } + }) + + if let audioRecorder = audioRecorder { + audioRecorder.start() + } + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -496,6 +554,7 @@ public class ChatController: ViewController { self.resolvePeerByNameDisposable?.dispose() self.botCallbackAlertMessageDisposable?.dispose() self.contextQueryState?.1.dispose() + self.audioRecorderDisposable?.dispose() } var chatDisplayNode: ChatControllerNode { @@ -802,6 +861,30 @@ public class ChatController: ViewController { }, sendContextResult: { [weak self] results, result in self?.enqueueChatContextResult(results, result) + }, sendBotCommand: { [weak self] botPeer, command in + if let strongSelf = self { + if let peer = strongSelf.presentationInterfaceState.peer, let addressName = botPeer.addressName { + let messageText: String + if peer is TelegramUser { + messageText = command + } else { + messageText = command + "@" + addressName + } + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) } + }) + } + }) + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: messageText, attributes: [], media: nil, replyToMessageId: replyMessageId)]).start() + } + } + }, beginAudioRecording: { [weak self] in + self?.requestAudioRecorder() + }, finishAudioRecording: { [weak self] sendAudio in + self?.dismissAudioRecorder(sendAudio: sendAudio) }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get())) self.interfaceInteraction = interfaceInteraction @@ -869,14 +952,13 @@ public class ChatController: ViewController { self.containerLayout = layout - self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets in + self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) }) } func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { let temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) - let inputContextQuery = inputContextQueryForChatPresentationIntefaceState(temporaryChatPresentationInterfaceState, account: self.account) let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, account: self.account) var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) @@ -1050,4 +1132,50 @@ public class ChatController: ViewController { enqueueMessages(account: self.account, peerId: self.peerId, messages: [message.withUpdatedReplyToMessageId(replyMessageId)]).start() } } + + private func requestAudioRecorder() { + if self.audioRecorderValue == nil { + if let applicationContext = self.account.applicationContext as? TelegramApplicationContext { + if self.audioRecorderFeedback == nil { + //self.audioRecorderFeedback = HapticFeedback() + self.audioRecorderFeedback?.prepareTap() + } + self.audioRecorder.set(applicationContext.mediaManager.audioRecorder()) + } + } + } + + private func dismissAudioRecorder(sendAudio: Bool) { + if let audioRecorderValue = self.audioRecorderValue { + audioRecorderValue.stop() + if sendAudio { + (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in + if let strongSelf = self, let data = data { + if data.duration < 0.5 { + strongSelf.audioRecorderFeedback?.error() + strongSelf.audioRecorderFeedback = nil + } else { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + + let resource = LocalFileMediaResource(fileId: randomId) + + strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + + var waveformBuffer: MemoryBuffer? + if let waveform = data.waveform { + waveformBuffer = MemoryBuffer(data: waveform) + } + + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]).start() + + strongSelf.audioRecorderFeedback?.success() + strongSelf.audioRecorderFeedback = nil + } + } + }) + } + } + self.audioRecorder.set(.single(nil)) + } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index c31c11d247..c5ea69b98f 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -25,8 +25,9 @@ public final class ChatControllerInteraction { let shareCurrentLocation: () -> Void let shareAccountContact: () -> Void let sendBotCommand: (MessageId, String) -> Void + let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void - public init(openMessage: @escaping (MessageId) -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId, String) -> Void) { + public init(openMessage: @escaping (MessageId) -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -41,5 +42,6 @@ public final class ChatControllerInteraction { self.shareCurrentLocation = shareCurrentLocation self.shareAccountContact = shareAccountContact self.sendBotCommand = sendBotCommand + self.updateInputState = updateInputState } } diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index c0b15e9f18..6a8fe26875 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -18,21 +18,37 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return panel } case let .mentions(peers): - if let currentPanel = currentPanel as? MentionChatInputContextPanelNode { - currentPanel.updateResults(peers) - return currentPanel + if !peers.isEmpty { + if let currentPanel = currentPanel as? MentionChatInputContextPanelNode { + currentPanel.updateResults(peers) + return currentPanel + } else { + let panel = MentionChatInputContextPanelNode(account: account) + panel.interfaceInteraction = interfaceInteraction + panel.updateResults(peers) + return panel + } } else { - let panel = MentionChatInputContextPanelNode(account: account) - panel.interfaceInteraction = interfaceInteraction - panel.updateResults(peers) - return panel + return nil + } + case let .commands(commands): + if !commands.isEmpty { + if let currentPanel = currentPanel as? CommandChatInputContextPanelNode { + currentPanel.updateResults(commands) + return currentPanel + } else { + let panel = CommandChatInputContextPanelNode(account: account) + panel.interfaceInteraction = interfaceInteraction + panel.updateResults(commands) + return panel + } + } else { + return nil } - case let .commands(peersAndCommands): - return nil case let .contextRequestResult(peer, results): if let results = results, (!results.results.isEmpty || results.switchPeer != nil) { switch results.presentation { - case .list, .media: + case .list: if let currentPanel = currentPanel as? VerticalListContextResultsChatInputContextPanelNode { currentPanel.updateResults(results) return currentPanel @@ -42,6 +58,16 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa panel.updateResults(results) return panel } + case .media: + if let currentPanel = currentPanel as? HorizontalListContextResultsChatInputContextPanelNode { + currentPanel.updateResults(results) + return currentPanel + } else { + let panel = HorizontalListContextResultsChatInputContextPanelNode(account: account) + panel.interfaceInteraction = interfaceInteraction + panel.updateResults(results) + return panel + } } } else { return nil diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index a3dfdc18d8..7413d59ce8 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -1,6 +1,7 @@ import Foundation import TelegramCore import Postbox +import Display struct PossibleContextQueryTypes: OptionSet { var rawValue: Int32 @@ -15,7 +16,8 @@ struct PossibleContextQueryTypes: OptionSet { static let hashtag = PossibleContextQueryTypes(rawValue: (1 << 0)) static let mention = PossibleContextQueryTypes(rawValue: (1 << 1)) - static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 2)) + static let command = PossibleContextQueryTypes(rawValue: (1 << 2)) + static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 3)) } private func makeScalar(_ c: Character) -> Character { @@ -27,6 +29,7 @@ private let spaceScalar = makeScalar(" ") private let newlineScalar = makeScalar("\n") private let hashScalar = makeScalar("#") private let atScalar = makeScalar("@") +private let slashScalar = makeScalar("/") private let alphanumerics = CharacterSet.alphanumerics func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> (Range, PossibleContextQueryTypes, Range?)? { @@ -78,7 +81,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> var possibleQueryRange: Range? - var possibleTypes = PossibleContextQueryTypes([.mention]) + var possibleTypes = PossibleContextQueryTypes([.command, .mention]) var definedType = false while true { @@ -98,6 +101,12 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> index = inputText.index(after: index) possibleQueryRange = index ..< maxIndex break + } else if c == slashScalar { + possibleTypes = possibleTypes.intersection([.command]) + definedType = true + index = inputText.index(after: index) + possibleQueryRange = index ..< maxIndex + break } if index == inputText.startIndex { @@ -115,7 +124,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> return nil } -func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatPresentationInputQuery? { +func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> ChatPresentationInputQuery? { let inputState = chatPresentationInterfaceState.interfaceState.effectiveInputState if let (possibleQueryRange, possibleTypes, additionalStringRange) = textInputStateContextQueryRangeAndType(inputState) { let query = inputState.inputText.substring(with: possibleQueryRange) @@ -123,6 +132,8 @@ func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfa return .hashtag(query) } else if possibleTypes == [.mention] { return .mention(query) + } else if possibleTypes == [.command] { + return .command(query) } else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange { let additionalString = inputState.inputText.substring(with: additionalStringRange) return .contextRequest(addressName: query, query: additionalString) @@ -134,17 +145,30 @@ func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfa } func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatTextInputPanelState { + var contextPlaceholder: NSAttributedString? + if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult { + if case let .contextRequestResult(peer, _) = inputQueryResult, let botUser = peer as? TelegramUser, let botInfo = botUser.botInfo, let inlinePlaceholder = botInfo.inlinePlaceholder { + if let inputQuery = inputContextQueryForChatPresentationIntefaceState(chatPresentationInterfaceState) { + if case let .contextRequest(addressName, query) = inputQuery, query.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: "@" + addressName, font: Font.regular(17.0), textColor: UIColor.clear)) + string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(17.0), textColor: UIColor(0xC8C8CE))) + contextPlaceholder = string + } + } + } + } switch chatPresentationInterfaceState.inputMode { case .media: - return ChatTextInputPanelState(accessoryItems: [.keyboard]) + return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) case .none, .text: if let _ = chatPresentationInterfaceState.interfaceState.editMessage { - return ChatTextInputPanelState(accessoryItems: []) + return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) } else { if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty { - return ChatTextInputPanelState(accessoryItems: [.stickers]) + return ChatTextInputPanelState(accessoryItems: [.stickers], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) } else { - return ChatTextInputPanelState(accessoryItems: []) + return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) } } } diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index e73acf966a..03d1182937 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -4,7 +4,7 @@ import TelegramCore import Postbox func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: ChatPresentationInputQuery?) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { - if let inputQuery = inputContextQueryForChatPresentationIntefaceState(chatPresentationInterfaceState, account: account) { + if let inputQuery = inputContextQueryForChatPresentationIntefaceState(chatPresentationInterfaceState) { if inputQuery == currentQuery { return nil } else { @@ -61,7 +61,35 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation return (nil, .single({ _ in return nil })) } case let .command(query): - return (nil, .single({ _ in return nil })) + let normalizedQuery = query.lowercased() + + if let peer = chatPresentationInterfaceState.peer { + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let currentQuery = currentQuery { + switch currentQuery { + case .command: + break + default: + signal = .single({ _ in return nil }) + } + } + + let participants = peerCommands(account: account, id: peer.id) + |> map { commands -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let filteredCommands = commands.commands.filter { command in + if command.command.text.hasPrefix(normalizedQuery) { + return true + } + return false + } + let sortedCommands = filteredCommands + return { _ in return .commands(sortedCommands) } + } + + return (inputQuery, signal |> then(participants)) + } else { + return (nil, .single({ _ in return nil })) + } case let .contextRequest(addressName, query): guard let chatPeer = chatPresentationInterfaceState.peer else { return (nil, .single({ _ in return nil })) @@ -72,7 +100,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation if let currentQuery = currentQuery { switch currentQuery { case let .contextRequest(currentAddressName, currentContextQuery) where currentAddressName == addressName: - if currentContextQuery.isEmpty != query.isEmpty { + if query.isEmpty && !currentContextQuery.isEmpty { delayRequest = false } default: diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 25a6a093f2..d6aeb915b5 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -98,7 +98,7 @@ public class ChatListController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func editPressed() { diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 40d4eee5cb..e2a708af59 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -105,7 +105,7 @@ private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] { if let _ = media as? TelegramMediaImage { result.append(ChatMessageMediaBubbleContentNode.self) } else if let file = media as? TelegramMediaFile { - if file.isVideo { + if file.isVideo || (file.isAnimated && file.dimensions != nil) { result.append(ChatMessageMediaBubbleContentNode.self) } else { result.append(ChatMessageFileBubbleContentNode.self) @@ -222,6 +222,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.doNotWaitForDoubleTapAtPoint = { [weak self] point in if let strongSelf = self { + if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) { + if let item = strongSelf.item { + for attribute in item.message.attributes { + if let attribute = attribute as? InlineBotMessageAttribute { + return true + } + } + } + } if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) { return true } @@ -752,7 +761,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { + if let nameNode = self.nameNode, nameNode.frame.contains(location) { + if let item = self.item { + for attribute in item.message.attributes { + if let attribute = attribute as? InlineBotMessageAttribute, let botPeer = item.message.peers[attribute.peerId], let addressName = botPeer.addressName { + self.controllerInteraction?.updateInputState { textInputState in + return ChatTextInputState(inputText: "@" + addressName + " ") + } + return + } + } + } + } else if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index a7efad746f..466a0eff9a 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -97,6 +97,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.backgroundNode.image = backgroundImage(color: UIColor(0x748391, 0.45)) self.stickBackgroundNode.image = backgroundImage(color: UIColor(0x939fab, 0.5)) + self.stickBackgroundNode.alpha = 0.0 self.backgroundNode.addSubnode(self.stickBackgroundNode) self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 6cf36814bf..428d7f7f88 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -41,7 +41,25 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.message, selectedFile!, item.message.flags.contains(.Incoming), CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let incoming = item.message.effectivelyIncoming + let statusType: ChatMessageDateAndStatusType? + if case .None = position.bottom { + if incoming { + statusType = .BubbleIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if item.message.flags.contains(.Unsent) { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } + } + } else { + statusType = nil + } + + let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.message, selectedFile!, item.message.flags.contains(.Incoming), statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) return (initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index e65933039e..d1c7255dec 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -12,11 +12,14 @@ private struct FetchControls { private let titleFont = Font.regular(16.0) private let descriptionFont = Font.regular(13.0) +private let durationFont = Font.regular(11.0) private let incomingTitleColor = UIColor(0x0b8bed) private let outgoingTitleColor = UIColor(0x3faa3c) private let incomingDescriptionColor = UIColor(0x999999) private let outgoingDescriptionColor = UIColor(0x6fb26a) +private let incomingDurationColor = UIColor(0x525252, 0.6) +private let outgoingDurationColor = UIColor(0x008c09, 0.8) private let fileIconIncomingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentIncoming")?.precomposed() private let fileIconOutgoingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentOutgoing")?.precomposed() @@ -24,6 +27,8 @@ private let fileIconOutgoingImage = UIImage(bundleImageName: "Chat/Message/Radia final class ChatMessageInteractiveFileNode: ASTransformNode { private let titleNode: TextNode private let descriptionNode: TextNode + private let waveformNode: AudioWaveformNode + private let dateAndStatusNode: ChatMessageDateAndStatusNode private var iconNode: TransformImageNode? private var progressNode: RadialProgressNode? @@ -48,6 +53,10 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { self.descriptionNode.displaysAsynchronously = true self.descriptionNode.isLayerBacked = true + self.waveformNode = AudioWaveformNode() + + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + super.init(layerBacked: false) self.addSubnode(self.titleNode) @@ -92,14 +101,15 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) let descriptionAsyncLayout = TextNode.asyncLayout(self.descriptionNode) let currentMessageIdAndFlags = self.messageIdAndFlags + let statusLayout = self.dateAndStatusNode.asyncLayout() - return { account, message, file, incoming, constrainedSize in + return { account, message, file, incoming, dateAndStatusType, constrainedSize in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in //var updateImageSignal: Signal DrawingContext, NoError>? var updatedStatusSignal: Signal? @@ -128,47 +138,102 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } if statusUpdated { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(progress: pendingStatus.progress) - } else { - return resourceStatus + if message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus { + return .Fetching(progress: pendingStatus.progress) + } else { + return resourceStatus + } } + } else { + updatedStatusSignal = chatMessageFileStatus(account: account, file: file) + } + } + + var statusSize: CGSize? + var statusApply: ((Bool) -> Void)? + + if let statusType = dateAndStatusType { + var t = Int(message.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + var edited = false + var viewCount: Int? + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count } + } + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + if let viewCount = viewCount { + dateText = "\(viewCount) " + dateText + } + if edited { + dateText = "edited " + dateText + } + + let (size, apply) = statusLayout(dateText, statusType, constrainedSize) + statusSize = size + statusApply = apply } var candidateTitleString: NSAttributedString? var candidateDescriptionString: NSAttributedString? + var isAudio = false + var audioWaveform: AudioWaveform? + var isVoice = false + var audioDuration: Int32 = 0 + for attribute in file.attributes { - if case let .Audio(_, _, title, performer, _) = attribute { - candidateTitleString = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor) - let descriptionText: String - if let performer = performer { - descriptionText = performer - } else if let size = file.size { - descriptionText = dataSizeString(size) - } else { - descriptionText = "" + if case let .Audio(voice, duration, title, performer, waveform) = attribute { + isAudio = true + if let _ = updatedStatusSignal { + updatedStatusSignal = .single(.Local) + } + + audioDuration = Int32(duration) + if voice { + isVoice = true + candidateDescriptionString = NSAttributedString(string: String(format: "%d:%02d", duration / 60, duration % 60), font: durationFont, textColor:incoming ? incomingDurationColor : outgoingDurationColor) + if let waveform = waveform { + waveform.withDataNoCopy { data in + audioWaveform = AudioWaveform(bitstream: data, bitsPerSample: 5) + } + } + } else { + candidateTitleString = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor) + let descriptionText: String + if let performer = performer { + descriptionText = performer + } else if let size = file.size { + descriptionText = dataSizeString(size) + } else { + descriptionText = "" + } + candidateDescriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? incomingDescriptionColor : outgoingDescriptionColor) } - candidateDescriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? incomingDescriptionColor : outgoingDescriptionColor) break } } - var titleString: NSAttributedString - let descriptionString: NSAttributedString + var titleString: NSAttributedString? + var descriptionString: NSAttributedString? if let candidateTitleString = candidateTitleString { titleString = candidateTitleString - } else { + } else if !isVoice { titleString = NSAttributedString(string: file.fileName ?? "File", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor) } if let candidateDescriptionString = candidateDescriptionString { descriptionString = candidateDescriptionString - } else { + } else if !isVoice { let descriptionText: String if let size = file.size { descriptionText = dataSizeString(size) @@ -183,15 +248,50 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let (titleLayout, titleApply) = titleAsyncLayout(titleString, nil, 1, .middle, textConstrainedSize, nil) let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(descriptionString, nil, 1, .middle, textConstrainedSize, nil) - return (max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0, { boundingWidth in - let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 44.0, height: 44.0)) + var voiceWidth: CGFloat = 0.0 + let minVoiceWidth: CGFloat = 120.0 + let maxVoiceWidth = constrainedSize.width + let maxVoiceLength: CGFloat = 30.0 + + let minLayoutWidth: CGFloat + if isVoice { + //y = a exp bx + //b = log (y1/y2) / (x1-x2) + //a = y1 / exp bx1 + + let b = log(maxVoiceWidth / minVoiceWidth) / (maxVoiceLength - 0.0) + let a = minVoiceWidth / exp(CGFloat(0.0)) + + let y = a * exp(b * CGFloat(audioDuration)) + + minLayoutWidth = floor(y) + } else { + minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0 + } + + return (minLayoutWidth, { boundingWidth in + let progressDiameter: CGFloat = isVoice ? 37.0 : 44.0 + let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: isVoice ? -5.0 : 0.0), size: CGSize(width: progressDiameter, height: progressDiameter)) let titleAndDescriptionHeight = titleLayout.size.height - 1.0 + descriptionLayout.size.height let titleFrame = CGRect(origin: CGPoint(x: progressFrame.maxX + 8.0, y: floor((44.0 - titleAndDescriptionHeight) / 2.0)), size: titleLayout.size) - let descriptionFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY - 1.0), size: descriptionLayout.size) - return (titleFrame.union(descriptionFrame).union(progressFrame).size, { [weak self] in + let descriptionFrame: CGRect + if isVoice { + descriptionFrame = CGRect(origin: CGPoint(x: 43.0, y: 19.0), size: descriptionLayout.size) + } else { + descriptionFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY - 1.0), size: descriptionLayout.size) + } + + let fittedLayoutSize: CGSize + if isVoice { + fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0) + } else { + fittedLayoutSize = titleFrame.union(descriptionFrame).union(progressFrame).size + } + + return (fittedLayoutSize, { [weak self] in if let strongSelf = self { strongSelf.messageIdAndFlags = (message.id, message.flags) strongSelf.file = file @@ -202,6 +302,27 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.titleNode.frame = titleFrame strongSelf.descriptionNode.frame = descriptionFrame + if let statusApply = statusApply, let statusSize = statusSize { + if strongSelf.dateAndStatusNode.supernode == nil { + strongSelf.addSubnode(strongSelf.dateAndStatusNode) + } + + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: fittedLayoutSize.width - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) + statusApply(false) + } else if strongSelf.dateAndStatusNode.supernode != nil { + strongSelf.dateAndStatusNode.removeFromSupernode() + } + + if isVoice { + if strongSelf.waveformNode.supernode == nil { + strongSelf.addSubnode(strongSelf.waveformNode) + } + strongSelf.waveformNode.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: fittedLayoutSize.width - 41.0, height: 12.0)) + strongSelf.waveformNode.setup(color: UIColor(incoming ? 0x007ee5 : 0x3fc33b), waveform: audioWaveform) + } else if strongSelf.waveformNode.supernode != nil { + strongSelf.waveformNode.removeFromSupernode() + } + /*if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(account, signal: updateImageSignal) }*/ @@ -223,9 +344,17 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { case let .Fetching(progress): strongSelf.progressNode?.state = .Fetching(progress: progress) case .Local: - strongSelf.progressNode?.state = .Play + if isAudio { + strongSelf.progressNode?.state = .Play + } else { + strongSelf.progressNode?.state = .Icon + } case .Remote: - strongSelf.progressNode?.state = .Remote + if isAudio { + strongSelf.progressNode?.state = .Play + } else { + strongSelf.progressNode?.state = .Remote + } } } } @@ -244,12 +373,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, message, file, incoming, constrainedSize in + return { account, message, file, incoming, dateAndStatusType, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var fileLayout: (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ dateAnsStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -259,7 +388,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(account, message, file, incoming, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(account, message, file, incoming, dateAndStatusType, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 8d10d6fec2..cd372881c7 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -94,6 +94,9 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) + if file.isAnimated { + nativeSize = nativeSize.fitted(CGSize(width: 480.0, height: 480.0)) + } } else { nativeSize = CGSize(width: 54.0, height: 54.0) } @@ -101,7 +104,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { return (layoutConstants.image.maxDimensions.width, { constrainedSize in return (min(layoutConstants.image.maxDimensions.width, nativeSize.width), { boundingWidth in let drawingSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) - let boundingSize = CGSize(width: max(boundingWidth, drawingSize.width), height: drawingSize.height).cropped(layoutConstants.image.maxDimensions) + let boundingSize = CGSize(width: max(boundingWidth, drawingSize.width), height: drawingSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: layoutConstants.image.maxDimensions.height)) var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal? diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 524bb7ec7d..26d9f1de71 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -47,18 +47,22 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { localtime_r(&t, &timeinfo) var edited = false + var viewCount: Int? for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = true - break + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count } } - let dateText: String - if edited { - dateText = String(format: "edited %02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) - } else { - dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + if let viewCount = viewCount { + dateText = "\(viewCount) " + dateText } + if edited { + dateText = "edited " + dateText + } + //let dateText = "\(message.id.id)" let statusType: ChatMessageDateAndStatusType? diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index b2008cde61..21520e7ba3 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -85,6 +85,27 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } + var t = Int(item.message.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + var edited = false + var viewCount: Int? + for attribute in item.message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } + } + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + if let viewCount = viewCount { + dateText = "\(viewCount) " + dateText + } + if edited { + dateText = "edited " + dateText + } + var textString: NSAttributedString? var inlineImageDimensions: CGSize? var inlineImageSize: CGSize? @@ -127,7 +148,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else { - let (_, refineLayout) = contentFileLayout(item.account, item.message, file, item.message.flags.contains(.Incoming), CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(item.account, item.message, file, item.message.flags.contains(.Incoming), nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = webpage.image { @@ -154,13 +175,6 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } return (initialWidth, { constrainedSize in - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - - let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) - //let dateText = "\(message.id.id)" - let statusType: ChatMessageDateAndStatusType if item.message.flags.contains(.Incoming) { statusType = .BubbleIncoming diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 0617901561..13dafd412a 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -24,9 +24,12 @@ final class ChatPanelInterfaceInteraction { let openPeerInfo: () -> Void let togglePeerNotifications: () -> Void let sendContextResult: (ChatContextResultCollection, ChatContextResult) -> Void + let sendBotCommand: (Peer, String) -> Void + let beginAudioRecording: () -> Void + let finishAudioRecording: (Bool) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -39,6 +42,9 @@ final class ChatPanelInterfaceInteraction { self.openPeerInfo = openPeerInfo self.togglePeerNotifications = togglePeerNotifications self.sendContextResult = sendContextResult + self.sendBotCommand = sendBotCommand + self.beginAudioRecording = beginAudioRecording + self.finishAudioRecording = finishAudioRecording self.statuses = statuses } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index bb848c70b5..d16788e9ac 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -41,7 +41,7 @@ enum ChatPresentationInputQuery: Equatable { enum ChatPresentationInputQueryResult: Equatable { case hashtags([String]) case mentions([Peer]) - case commands([(Peer, String)]) + case commands([PeerCommand]) case contextRequestResult(Peer, ChatContextResultCollection?) static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool { @@ -69,16 +69,10 @@ enum ChatPresentationInputQueryResult: Equatable { } case let .commands(lhsCommands): if case let .commands(rhsCommands) = rhs { - if lhsCommands.count != rhsCommands.count { + if lhsCommands != rhsCommands { return false - } else { - for i in 0 ..< lhsCommands.count { - if !lhsCommands[i].0.isEqual(rhsCommands[i].0) || lhsCommands[i].1 != rhsCommands[i].1 { - return false - } - } - return true } + return true } else { return false } diff --git a/TelegramUI/ChatTextInputAudioRecordingButton.swift b/TelegramUI/ChatTextInputAudioRecordingButton.swift new file mode 100644 index 0000000000..166808d04c --- /dev/null +++ b/TelegramUI/ChatTextInputAudioRecordingButton.swift @@ -0,0 +1,127 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit + +private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: UIColor(0x9099A2)) +private let offsetThreshold: CGFloat = 10.0 +private let dismissOffsetThreshold: CGFloat = 70.0 + +final class ChatTextInputAudioRecordingButton: UIButton { + var account: Account? + var beginRecording: () -> Void = { } + var endRecording: (Bool) -> Void = { _ in } + var offsetRecordingControls: () -> Void = { } + + private var recordingOverlay: ChatTextInputAudioRecordingOverlay? + private var startTouchLocation: CGPoint? + private(set) var controlsOffset: CGFloat = 0.0 + + private var micLevelDisposable: MetaDisposable? + + var audioRecorder: ManagedAudioRecorder? { + didSet { + if self.audioRecorder !== oldValue { + if self.micLevelDisposable == nil { + micLevelDisposable = MetaDisposable() + } + if let audioRecorder = self.audioRecorder { + self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in + self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level)) + })) + } else { + self.micLevelDisposable?.set(nil) + } + } + } + } + + init() { + super.init(frame: CGRect()) + + self.isExclusiveTouch = true + self.setImage(micIcon, for: []) + self.adjustsImageWhenHighlighted = false + self.adjustsImageWhenDisabled = false + self.disablesInteractiveTransitionGestureRecognizer = true + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let micLevelDisposable = self.micLevelDisposable { + micLevelDisposable.dispose() + } + if let recordingOverlay = self.recordingOverlay { + recordingOverlay.dismiss() + } + } + + func cancelRecording() { + self.isEnabled = false + self.isEnabled = true + } + + override func beginTracking(_ touch: UITouch, with touchEvent: UIEvent?) -> Bool { + if super.beginTracking(touch, with: touchEvent) { + self.startTouchLocation = touch.location(in: self) + + self.controlsOffset = 0.0 + self.beginRecording() + let recordingOverlay: ChatTextInputAudioRecordingOverlay + if let currentRecordingOverlay = self.recordingOverlay { + recordingOverlay = currentRecordingOverlay + } else { + recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self) + self.recordingOverlay = recordingOverlay + } + if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.getTopWindow() { + recordingOverlay.present(in: topWindow) + } + return true + } else { + return false + } + } + + override func endTracking(_ touch: UITouch?, with touchEvent: UIEvent?) { + super.endTracking(touch, with: touchEvent) + + self.endRecording(self.controlsOffset < 40.0) + self.dismissRecordingOverlay() + } + + override func cancelTracking(with event: UIEvent?) { + super.cancelTracking(with: event) + + self.endRecording(false) + self.dismissRecordingOverlay() + } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + if super.continueTracking(touch, with: event) { + if let startTouchLocation = self.startTouchLocation { + let horiontalOffset = startTouchLocation.x - touch.location(in: self).x + let controlsOffset = max(0.0, horiontalOffset - offsetThreshold) + if !controlsOffset.isEqual(to: self.controlsOffset) { + self.recordingOverlay?.dismissFactor = 1.0 - controlsOffset / dismissOffsetThreshold + self.controlsOffset = controlsOffset + self.offsetRecordingControls() + } + } + return true + } else { + return false + } + } + + private func dismissRecordingOverlay() { + if let recordingOverlay = self.recordingOverlay { + self.recordingOverlay = nil + recordingOverlay.dismiss() + } + } +} diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift new file mode 100644 index 0000000000..6b3ea86e92 --- /dev/null +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -0,0 +1,36 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let arrowImage = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.precomposed() + +final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { + private let arrowNode: ASImageNode + private let labelNode: TextNode + + override init() { + self.arrowNode = ASImageNode() + self.arrowNode.isLayerBacked = true + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.image = arrowImage + + self.labelNode = TextNode() + self.labelNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.arrowNode) + self.addSubnode(self.labelNode) + + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: "Slide to cancel", font: Font.regular(14.0), textColor: UIColor(0xaaaab2)), nil, 1, .end, CGSize(width: 200.0, height: 100.0), nil) + labelApply() + + let arrowSize = arrowImage?.size ?? CGSize() + let height = max(arrowSize.height, labelLayout.size.height) + self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height)) + self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize) + self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: floor((height - labelLayout.size.height) / 2.0) - UIScreenPixel), size: labelLayout.size) + } +} diff --git a/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift b/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift new file mode 100644 index 0000000000..7a643b17c5 --- /dev/null +++ b/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift @@ -0,0 +1,171 @@ +import Foundation +import Display +import AsyncDisplayKit +import UIKit + +private let innerCircleDiameter: CGFloat = 110.0 +private let outerCircleDiameter = innerCircleDiameter + 50.0 +private let outerCircleMinScale = innerCircleDiameter / outerCircleDiameter +private let innerCircleImage = generateFilledCircleImage(diameter: innerCircleDiameter, color: UIColor(0x007ee5)) +private let outerCircleImage = generateFilledCircleImage(diameter: outerCircleDiameter, color: UIColor(0x007ee5, 0.2)) +private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white)! + +private final class ChatTextInputAudioRecordingOverlayDisplayLinkTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + + super.init() + } + + @objc func displayLinkEvent() { + self.f() + } +} + +final class ChatTextInputAudioRecordingOverlay { + private weak var anchorView: UIView? + + private let containerNode: ASDisplayNode + private let circleContainerNode: ASDisplayNode + private let innerCircleNode: ASImageNode + private let outerCircleNode: ASImageNode + private let iconNode: ASImageNode + + var animationStartTime: Double? + var displayLink: CADisplayLink? + var currentLevel: CGFloat = 0.0 + var inputLevel: CGFloat = 0.0 + var animatedIn = false + + var dismissFactor: CGFloat = 1.0 { + didSet { + let scale = max(0.3, min(self.dismissFactor, 1.0)) + self.circleContainerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + } + } + + init(anchorView: UIView) { + self.anchorView = anchorView + + self.containerNode = ASDisplayNode() + self.containerNode.isLayerBacked = true + + self.circleContainerNode = ASDisplayNode() + self.circleContainerNode.isLayerBacked = true + + self.outerCircleNode = ASImageNode() + self.outerCircleNode.displayWithoutProcessing = true + self.outerCircleNode.displaysAsynchronously = false + self.outerCircleNode.isLayerBacked = true + self.outerCircleNode.image = outerCircleImage + self.outerCircleNode.frame = CGRect(origin: CGPoint(x: -outerCircleDiameter / 2.0, y: -outerCircleDiameter / 2.0), size: CGSize(width: outerCircleDiameter, height: outerCircleDiameter)) + + self.innerCircleNode = ASImageNode() + self.innerCircleNode.displayWithoutProcessing = true + self.innerCircleNode.displaysAsynchronously = false + self.innerCircleNode.isLayerBacked = true + self.innerCircleNode.image = innerCircleImage + self.innerCircleNode.frame = CGRect(origin: CGPoint(x: -innerCircleDiameter / 2.0, y: -innerCircleDiameter / 2.0), size: CGSize(width: innerCircleDiameter, height: innerCircleDiameter)) + + self.iconNode = ASImageNode() + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + self.iconNode.isLayerBacked = true + self.iconNode.image = micIcon + self.iconNode.frame = CGRect(origin: CGPoint(x: -micIcon.size.width / 2.0, y: -micIcon.size.height / 2.0), size: micIcon.size) + + self.circleContainerNode.addSubnode(self.outerCircleNode) + self.circleContainerNode.addSubnode(self.innerCircleNode) + self.containerNode.addSubnode(self.circleContainerNode) + self.containerNode.addSubnode(self.iconNode) + } + + deinit { + self.displayLink?.invalidate() + } + + func present(in window: UIWindow) { + if let anchorView = self.anchorView, let anchorSuperview = anchorView.superview { + if let displayLink = self.displayLink { + displayLink.invalidate() + } + self.displayLink = CADisplayLink(target: ChatTextInputAudioRecordingOverlayDisplayLinkTarget({ [weak self] in + self?.displayLinkEvent() + }), selector: #selector(ChatTextInputAudioRecordingOverlayDisplayLinkTarget.displayLinkEvent)) + + let convertedCenter = anchorSuperview.convert(anchorView.center, to: window) + self.containerNode.position = CGPoint(x: convertedCenter.x, y: convertedCenter.y) + window.addSubnode(self.containerNode) + + self.innerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + self.outerCircleNode.layer.transform = CATransform3DMakeScale(outerCircleMinScale, outerCircleMinScale, 1.0) + self.outerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: outerCircleMinScale as NSNumber, keyPath: "transform.scale", duration: 0.5) + self.innerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15) + self.outerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15) + self.iconNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15) + + self.animatedIn = true + self.animationStartTime = CACurrentMediaTime() + self.displayLink?.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) + self.displayLink?.isPaused = false + } + } + + func dismiss() { + self.displayLink?.invalidate() + self.displayLink = nil + + var innerCompleted = false + var outerCompleted = false + var iconCompleted = false + + var containerNodeRef: ASDisplayNode? = self.containerNode + + var completion: () -> Void = { + if let containerNode = containerNodeRef, innerCompleted, outerCompleted, iconCompleted { + containerNode.removeFromSupernode() + containerNodeRef = nil + } + } + + self.innerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) + self.innerCircleNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in + innerCompleted = true + completion() + }) + + var currentScaleValue: CGFloat = outerCircleMinScale + if let currentScale = self.outerCircleNode.layer.value(forKeyPath: "transform.scale") as? AnyObject, currentScale.responds(to: Selector("floatValue")) { + currentScaleValue = CGFloat(currentScale.floatValue) + } + + self.outerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) + self.outerCircleNode.layer.animateScale(from: currentScaleValue, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in + outerCompleted = true + completion() + }) + + self.iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { _ in + iconCompleted = true + completion() + }) + } + + private func displayLinkEvent() { + let t = CACurrentMediaTime() + if let animationStartTime = self.animationStartTime { + if t > animationStartTime + 0.5 { + self.currentLevel = self.currentLevel * 0.8 + self.inputLevel * 0.2 + + let scale = outerCircleMinScale + self.currentLevel * (1.0 - outerCircleMinScale) + self.outerCircleNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + } + } + } + + func addImmediateMicLevel(_ level: CGFloat) { + self.inputLevel = level + } +} diff --git a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift new file mode 100644 index 0000000000..3d5d31cc7b --- /dev/null +++ b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift @@ -0,0 +1,87 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit + +private final class ChatTextInputAudioRecordingTimeNodeParameters: NSObject { + let timestamp: Double + + init(timestamp: Double) { + self.timestamp = timestamp + super.init() + } +} + +private let textFont = Font.regular(15.0) + +final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { + private let textNode: TextNode + + private var timestamp: Double = 0.0 { + didSet { + self.setNeedsDisplay() + } + } + private var stateDisposable = MetaDisposable() + + var audioRecorder: ManagedAudioRecorder? { + didSet { + if self.audioRecorder !== oldValue { + if let audioRecorder = self.audioRecorder { + self.stateDisposable.set(audioRecorder.recordingState.start(next: { [weak self] state in + if let strongSelf = self { + switch state { + case let .paused(duration): + strongSelf.timestamp = duration + case let .recording(duration, _): + strongSelf.timestamp = duration + } + } + })) + } else { + self.stateDisposable.set(nil) + } + } + } + } + + override init() { + self.textNode = TextNode() + super.init() + self.isOpaque = false + } + + deinit { + self.stateDisposable.dispose() + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + let makeLayout = TextNode.asyncLayout(self.textNode) + let (size, apply) = makeLayout(NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: 200.0, height: 100.0), nil) + apply() + self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size) + return size.size + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: @escaping asdisplaynode_iscancelled_block_t, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? ChatTextInputAudioRecordingTimeNodeParameters { + let currentAudioDurationSeconds = Int(parameters.timestamp) + let currentAudioDurationMilliseconds = Int(parameters.timestamp * 100.0) % 100 + let text = String(format: "%d:%02d,%02d", currentAudioDurationSeconds / 60, currentAudioDurationSeconds % 60, currentAudioDurationMilliseconds) + let string = NSAttributedString(string: text, font: textFont, textColor: .black) + string.draw(at: CGPoint()) + } + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 380b0deecb..8ce65d0d23 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -61,7 +61,6 @@ private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height }) private let attachmentIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: UIColor(0x9099A2)) -private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: UIColor(0x9099A2)) private let sendIcon = UIImage(bundleImageName: "Chat/Input/Text/IconSend")?.precomposed() enum ChatTextInputAccessoryItem { @@ -70,28 +69,59 @@ enum ChatTextInputAccessoryItem { case inputButtons } +struct ChatTextInputPanelAudioRecordingState: Equatable { + let recorder: ManagedAudioRecorder + + init(recorder: ManagedAudioRecorder) { + self.recorder = recorder + } + + static func ==(lhs: ChatTextInputPanelAudioRecordingState, rhs: ChatTextInputPanelAudioRecordingState) -> Bool { + return lhs.recorder === rhs.recorder + } +} + struct ChatTextInputPanelState: Equatable { let accessoryItems: [ChatTextInputAccessoryItem] + let contextPlaceholder: NSAttributedString? + let audioRecordingState: ChatTextInputPanelAudioRecordingState? - init(accessoryItems: [ChatTextInputAccessoryItem]) { + init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, audioRecordingState: ChatTextInputPanelAudioRecordingState?) { self.accessoryItems = accessoryItems + self.contextPlaceholder = contextPlaceholder + self.audioRecordingState = audioRecordingState } init() { self.accessoryItems = [] + self.contextPlaceholder = nil + self.audioRecordingState = nil } static func ==(lhs: ChatTextInputPanelState, rhs: ChatTextInputPanelState) -> Bool { if lhs.accessoryItems != rhs.accessoryItems { return false } + if let lhsContextPlaceholder = lhs.contextPlaceholder, let rhsContextPlaceholder = rhs.contextPlaceholder { + return lhsContextPlaceholder.isEqual(to: rhsContextPlaceholder) + } else if (lhs.contextPlaceholder != nil) != (rhs.contextPlaceholder != nil) { + return false + } + if lhs.audioRecordingState != rhs.audioRecordingState { + return false + } return true } + + func withUpdatedAudioRecordingState(_ audioRecordingState: ChatTextInputPanelAudioRecordingState?) -> ChatTextInputPanelState { + return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, audioRecordingState: audioRecordingState) + } } private let keyboardImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard")?.precomposed() private let stickersImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers")?.precomposed() private let inputButtonsImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconInputButtons")?.precomposed() +private let audioRecordingDotImage = generateFilledCircleImage(diameter: 9.0, color: UIColor(0xed2521)) private final class AccessoryItemIconButton: HighlightableButton { init(item: ChatTextInputAccessoryItem) { @@ -120,14 +150,19 @@ private final class AccessoryItemIconButton: HighlightableButton { class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textPlaceholderNode: TextNode + var contextPlaceholderNode: TextNode? var textInputNode: ASEditableTextNode? let textInputBackgroundView: UIImageView - let micButton: HighlightableButton + let micButton: ChatTextInputAudioRecordingButton let sendButton: HighlightableButton let attachmentButton: HighlightableButton let searchLayoutClearButton: HighlightableButton let searchLayoutProgressView: UIImageView + var audioRecordingInfoContainerNode: ASDisplayNode? + var audioRecordingDotNode: ASImageNode? + var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? + var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] @@ -154,6 +189,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + override var account: Account? { + didSet { + self.micButton.account = self.account + } + } + func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) { if !state.inputText.isEmpty && self.textInputNode == nil { self.loadTextInputNode() @@ -202,7 +243,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.searchLayoutClearButton = HighlightableButton() self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage) self.searchLayoutProgressView.isHidden = true - self.micButton = HighlightableButton() + self.micButton = ChatTextInputAudioRecordingButton() self.sendButton = HighlightableButton() super.init() @@ -211,8 +252,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) self.view.addSubview(self.attachmentButton) - self.micButton.setImage(micIcon, for: []) - self.micButton.addTarget(self, action: #selector(self.micButtonPressed), for: .touchUpInside) + self.micButton.beginRecording = { [weak self] in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.beginAudioRecording() + } + } + self.micButton.endRecording = { [weak self] sendAudio in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.finishAudioRecording(sendAudio) + } + } + self.micButton.offsetRecordingControls = { [weak self] in + if let strongSelf = self { + strongSelf.updateLayout(width: strongSelf.bounds.size.width, transition: .immediate, interfaceState: strongSelf.presentationInterfaceState) + } + } self.view.addSubview(self.micButton) self.sendButton.setImage(sendIcon, for: []) @@ -381,7 +435,139 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: width) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) - transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 2.0 - UIScreenPixel, y: panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) + var audioRecordingItemsVerticalOffset: CGFloat = 0.0 + if let audioRecordingState = interfaceState.inputTextPanelState.audioRecordingState { + self.micButton.audioRecorder = audioRecordingState.recorder + let audioRecordingInfoContainerNode: ASDisplayNode + if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { + audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode + } else { + audioRecordingInfoContainerNode = ASDisplayNode() + self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode + self.insertSubnode(audioRecordingInfoContainerNode, at: 0) + } + + audioRecordingItemsVerticalOffset = panelHeight * 2.0 + transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 0.0) + if let textInputNode = self.textInputNode { + transition.updateAlpha(node: textInputNode, alpha: 0.0) + } + for (_, button) in self.accessoryItemButtons { + transition.updateAlpha(layer: button.layer, alpha: 0.0) + } + + var animateCancelSlideIn = false + let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator + if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator { + audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator + } else { + animateCancelSlideIn = transition.isAnimated + + audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator() + self.audioRecordingCancelIndicator = audioRecordingCancelIndicator + self.insertSubnode(audioRecordingCancelIndicator, at: 0) + } + + audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: floor((width - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) + + if animateCancelSlideIn { + let position = audioRecordingCancelIndicator.layer.position + audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + + var animateTimeSlideIn = false + let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode + if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode { + audioRecordingTimeNode = currentAudioRecordingTimeNode + } else { + audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode() + self.audioRecordingTimeNode = audioRecordingTimeNode + audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode) + + if transition.isAnimated { + animateTimeSlideIn = true + } + } + + let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0)) + + audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(0.0, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: width, height: panelHeight)) + + audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 28.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize) + if animateTimeSlideIn { + let position = audioRecordingTimeNode.layer.position + audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 28.0 - audioRecordingTimeSize.width, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + audioRecordingTimeNode.audioRecorder = audioRecordingState.recorder + + var animateDotSlideIn = false + let audioRecordingDotNode: ASImageNode + if let currentAudioRecordingDotNode = self.audioRecordingDotNode { + audioRecordingDotNode = currentAudioRecordingDotNode + } else { + animateDotSlideIn = transition.isAnimated + + audioRecordingDotNode = ASImageNode() + audioRecordingDotNode.image = audioRecordingDotImage + self.audioRecordingDotNode = audioRecordingDotNode + audioRecordingInfoContainerNode.addSubnode(audioRecordingDotNode) + } + audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: audioRecordingTimeNode.frame.minX - 17.0, y: panelHeight - minimalHeight + floor((minimalHeight - 9.0) / 2.0)), size: CGSize(width: 9.0, height: 9.0)) + if animateDotSlideIn { + let position = audioRecordingDotNode.layer.position + audioRecordingDotNode.layer.animatePosition(from: CGPoint(x: position.x - 9.0 - 51.0, y: position.y), to: position, duration: 0.7, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak audioRecordingDotNode] finished in + if finished { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber] + animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] + animation.duration = 0.5 + animation.autoreverses = true + animation.repeatCount = Float.infinity + + audioRecordingDotNode?.layer.add(animation, forKey: "recording") + } + }) + } + } else { + self.micButton.audioRecorder = nil + transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 1.0) + if let textInputNode = self.textInputNode { + transition.updateAlpha(node: textInputNode, alpha: 1.0) + } + for (_, button) in self.accessoryItemButtons { + transition.updateAlpha(layer: button.layer, alpha: 1.0) + } + + if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { + self.audioRecordingInfoContainerNode = nil + transition.updateFrame(node: audioRecordingInfoContainerNode, frame: CGRect(origin: CGPoint(x: -width, y: 0.0), size: audioRecordingInfoContainerNode.bounds.size), completion: { [weak audioRecordingInfoContainerNode] _ in + audioRecordingInfoContainerNode?.removeFromSupernode() + }) + } + + if let audioRecordingDotNode = self.audioRecordingDotNode { + self.audioRecordingDotNode = nil + } + + if let audioRecordingTimeNode = self.audioRecordingTimeNode { + self.audioRecordingTimeNode = nil + } + + if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator { + self.audioRecordingCancelIndicator = nil + if transition.isAnimated { + let position = audioRecordingCancelIndicator.layer.position + audioRecordingCancelIndicator.layer.animatePosition(from: position, to: CGPoint(x: 0.0 - audioRecordingCancelIndicator.bounds.size.width, y: position.y), duration: 0.3, removeOnCompletion: false, completion: { [weak audioRecordingCancelIndicator] _ in + audioRecordingCancelIndicator?.removeFromSupernode() + }) + } else { + audioRecordingCancelIndicator.removeFromSupernode() + } + } + } + + transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 2.0 - UIScreenPixel, y: panelHeight - minimalHeight + audioRecordingItemsVerticalOffset), size: CGSize(width: 40.0, height: minimalHeight))) var composeButtonsOffset: CGFloat = 0.0 var textInputBackgroundWidthOffset: CGFloat = 0.0 @@ -399,14 +585,36 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition.updateFrame(layer: self.searchLayoutProgressView.layer, frame: CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - searchProgressSize.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - searchProgressSize.height) / 2.0)), size: searchProgressSize)) if let textInputNode = self.textInputNode { - transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) } - transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5), size: self.textPlaceholderNode.frame.size)) + if let contextPlaceholder = self.presentationInterfaceState.inputTextPanelState.contextPlaceholder { + let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode) + let (placeholderSize, placeholderApply) = placeholderLayout(contextPlaceholder, nil, 1, .end, CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), nil) + let contextPlaceholderNode = placeholderApply() + if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode { + self.contextPlaceholderNode = nil + currentContextPlaceholderNode.removeFromSupernode() + } + + if self.contextPlaceholderNode !== contextPlaceholderNode { + self.contextPlaceholderNode = contextPlaceholderNode + self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) + } + + placeholderApply() + + contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5 + audioRecordingItemsVerticalOffset), size: placeholderSize.size) + } else if let contextPlaceholderNode = self.contextPlaceholderNode { + self.contextPlaceholderNode = nil + contextPlaceholderNode.removeFromSupernode() + } - transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top, width: width - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom)) + transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5 + audioRecordingItemsVerticalOffset), size: self.textPlaceholderNode.frame.size)) - var nextButtonTopRight = CGPoint(x: width - self.textFieldInsets.right - accessoryButtonInset, y: panelHeight - self.textFieldInsets.bottom - minimalInputHeight) + transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom)) + + var nextButtonTopRight = CGPoint(x: width - self.textFieldInsets.right - accessoryButtonInset, y: panelHeight - self.textFieldInsets.bottom - minimalInputHeight + audioRecordingItemsVerticalOffset) for (_, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize) @@ -580,9 +788,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func micButtonPressed() { - } - @objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.ensureFocused() diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift index 4024b37c27..ea0070febd 100644 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ b/TelegramUI/ChatVideoGalleryItem.swift @@ -121,7 +121,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { /*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) self.videoNode.player = VideoPlayer(source: source)*/ - let player = MediaPlayer(account: account, resource: file.resource) + let player = MediaPlayer(postbox: account.postbox, resource: file.resource) player.attachPlayerNode(self.videoNode) self.player = player self.videoStatusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in diff --git a/TelegramUI/CommandChatInputContextPanelNode.swift b/TelegramUI/CommandChatInputContextPanelNode.swift index 2909561845..9aefcd8276 100644 --- a/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/TelegramUI/CommandChatInputContextPanelNode.swift @@ -4,26 +4,36 @@ import Postbox import TelegramCore import Display +private struct CommandChatInputContextPanelEntryStableId: Hashable { + let command: PeerCommand + + var hashValue: Int { + return command.command.text.hashValue ^ command.peer.id.hashValue + } + + static func ==(lhs: CommandChatInputContextPanelEntryStableId, rhs: CommandChatInputContextPanelEntryStableId) -> Bool { + return lhs.command == rhs.command + } +} + private struct CommandChatInputContextPanelEntry: Equatable, Comparable, Identifiable { let index: Int - let peer: Peer - let command: String - let text: String + let command: PeerCommand - var stableId: Int64 { - return self.peer.id.toInt64() + var stableId: CommandChatInputContextPanelEntryStableId { + return CommandChatInputContextPanelEntryStableId(command: self.command) } static func ==(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) && lhs.command == rhs.command && lhs.text == rhs.text + return lhs.index == rhs.index && lhs.command == rhs.command } static func <(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { - return CommandChatInputPanelItem(account: account, peer: self.peer, peerSelected: peerSelected) + func item(account: Account, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem { + return CommandChatInputPanelItem(account: account, command: self.command, commandSelected: commandSelected) } } @@ -33,12 +43,12 @@ private struct CommandChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], account: Account, peerSelected: @escaping (Peer) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], account: Account, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, commandSelected: commandSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, commandSelected: commandSelected), directionHint: nil) } return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -54,7 +64,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.stackFromBottomInsetItemFactor = 3.5 + self.listView.keepBottomItemOverscrollBackground = true self.listView.limitHitTestToNodes = true super.init(account: account) @@ -65,23 +75,31 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.addSubnode(self.listView) } - func updateResults(_ results: [(Peer, BotCommand)]) { + func updateResults(_ results: [PeerCommand]) { var entries: [CommandChatInputContextPanelEntry] = [] var index = 0 - for (peer, command) in results { - entries.append(CommandChatInputContextPanelEntry(index: index, peer: peer, command: command.text, text: command.description)) + var stableIds = Set() + for command in results { + let entry = CommandChatInputContextPanelEntry(index: index, command: command) + if stableIds.contains(entry.stableId) { + continue + } + stableIds.insert(entry.stableId) + entries.append(entry) index += 1 } let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, peerSelected: { [weak self] peer in + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, commandSelected: { [weak self] command, sendImmediately in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.updateTextInputState { textInputState in - if let (range, type, _) = textInputStateContextQueryRangeAndType(textInputState) { - var inputText = textInputState.inputText + if sendImmediately { + interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text) + } else { + interfaceInteraction.updateTextInputState { textInputState in + if let (range, type, _) = textInputStateContextQueryRangeAndType(textInputState) { + var inputText = textInputState.inputText - if let addressName = peer.addressName, !addressName.isEmpty { - let replacementText = addressName + " " + let replacementText = command.command.text + " " inputText.replaceSubrange(range, with: replacementText) let utfLowerIndex = inputText.utf16.distance(from: inputText.utf16.startIndex, to: range.lowerBound.samePosition(in: inputText.utf16)) @@ -92,8 +110,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { return ChatTextInputState(inputText: inputText, selectionRange: utfUpperPosition ..< utfUpperPosition) } + return textInputState } - return textInputState } } }) @@ -117,12 +135,18 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { var options = ListViewDeleteAndInsertOptions() if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) + //options.insert(.Synchronous) + //options.insert(.LowLatency) } else { - //options.insert(.AnimateInsertion) + options.insert(.AnimateTopItemPosition) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + + var insets = UIEdgeInsets() + insets.top = topInsetForLayout(size: self.listView.bounds.size) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { var topItemOffset: CGFloat? strongSelf.listView.forEachItemNode { itemNode in @@ -140,8 +164,15 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } } + private func topInsetForLayout(size: CGSize) -> CGFloat { + var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5) + + return max(size.height - minimumItemHeights, 0.0) + } + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { var insets = UIEdgeInsets() + insets.top = self.topInsetForLayout(size: size) transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) diff --git a/TelegramUI/CommandChatInputPanelItem.swift b/TelegramUI/CommandChatInputPanelItem.swift index ee7594ad51..35c9fde29c 100644 --- a/TelegramUI/CommandChatInputPanelItem.swift +++ b/TelegramUI/CommandChatInputPanelItem.swift @@ -7,15 +7,15 @@ import Postbox final class CommandChatInputPanelItem: ListViewItem { fileprivate let account: Account - fileprivate let peer: Peer - private let peerSelected: (Peer) -> Void + fileprivate let command: PeerCommand + fileprivate let commandSelected: (PeerCommand, Bool) -> Void let selectable: Bool = true - public init(account: Account, peer: Peer, peerSelected: @escaping (Peer) -> Void) { + public init(account: Account, command: PeerCommand, commandSelected: @escaping (PeerCommand, Bool) -> Void) { self.account = account - self.peer = peer - self.peerSelected = peerSelected + self.command = command + self.commandSelected = commandSelected } public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { @@ -64,21 +64,48 @@ final class CommandChatInputPanelItem: ListViewItem { } func selected(listView: ListView) { - self.peerSelected(self.peer) + self.commandSelected(self.command, true) } } private let avatarFont = Font.regular(16.0) private let textFont = Font.medium(14.0) +private let descriptionFont = Font.regular(14.0) +private let descriptionColor = UIColor(0x9099A2) + +private let arrowImage = generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setStrokeColor(UIColor(0xC7CCD0).cgColor) + context.setLineCap(.round) + context.setLineWidth(2.0) + context.setLineJoin(.round) + + context.beginPath() + context.move(to: CGPoint(x: 1.0, y: 2.0)) + context.addLine(to: CGPoint(x: 1.0, y: 10.0)) + context.addLine(to: CGPoint(x: 9.0, y: 10.0)) + context.strokePath() + + context.beginPath() + context.move(to: CGPoint(x: 1.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 1.0)) + context.strokePath() +}) final class CommandChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 + private var item: CommandChatInputPanelItem? private let avatarNode: AvatarNode private let textNode: TextNode private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private let arrowNode: ASButtonNode init() { self.avatarNode = AvatarNode(font: avatarFont) @@ -96,6 +123,9 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true + self.arrowNode = HighlightableButtonNode() + self.arrowNode.setImage(arrowImage, for: []) + super.init(layerBacked: false, dynamicBounce: false) self.backgroundColor = .white @@ -105,6 +135,9 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.avatarNode) self.addSubnode(self.textNode) + self.addSubnode(self.arrowNode) + + self.arrowNode.addTarget(self, action: #selector(self.arrowButtonPressed), forControlEvents: [.touchUpInside]) } override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { @@ -124,19 +157,31 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { let leftInset: CGFloat = 55.0 let rightInset: CGFloat = 10.0 - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.peer.displayTitle, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), nil) + let commandString = NSMutableAttributedString() + commandString.append(NSAttributedString(string: "/" + item.command.command.text, font: textFont, textColor: .black)) + + if !item.command.command.description.isEmpty { + commandString.append(NSAttributedString(string: " " + item.command.command.description, font: descriptionFont, textColor: descriptionColor)) + } + + let (textLayout, textApply) = makeTextLayout(commandString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), nil) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { - strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) + strongSelf.item = item + + strongSelf.avatarNode.setPeer(account: item.account, peer: item.command.peer) textApply() strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0)) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + let arrowSize = CGSize(width: 42.0, height: nodeLayout.contentSize.height) + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: nodeLayout.size.width - arrowSize.width, y: 0.0), size: arrowSize) + strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom @@ -174,4 +219,18 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { } } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.arrowNode.frame.contains(point) { + return self.arrowNode.view + } else { + return super.hitTest(point, with: event) + } + } + + @objc func arrowButtonPressed() { + if let item = self.item { + item.commandSelected(item.command, false) + } + } } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index 53d38743bf..30c591335e 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -295,7 +295,7 @@ public class ContactsController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } private func enqueueTransition(_ transition: ContactsListTransition, firstTime: Bool, animated: Bool) { diff --git a/TelegramUI/FFMpegMediaFrameSource.swift b/TelegramUI/FFMpegMediaFrameSource.swift index adcb3a1a8c..0963646611 100644 --- a/TelegramUI/FFMpegMediaFrameSource.swift +++ b/TelegramUI/FFMpegMediaFrameSource.swift @@ -67,7 +67,7 @@ private func contextForCurrentThread() -> FFMpegMediaFrameSourceContext? { final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { private let queue: Queue - private let account: Account + private let postbox: Postbox private let resource: MediaResource private let taskQueue: ThreadTaskQueue @@ -87,9 +87,9 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - init(queue: Queue, account: Account, resource: MediaResource) { + init(queue: Queue, postbox: Postbox, resource: MediaResource) { self.queue = queue - self.account = account + self.postbox = postbox self.resource = resource self.taskQueue = ThreadTaskQueue() @@ -136,11 +136,11 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { self.generatingFrames = true - let account = self.account + let postbox = self.postbox let resource = self.resource let queue = self.queue self.performWithContext { [weak self] context in - context.initializeState(account: account, resource: resource) + context.initializeState(postbox: postbox, resource: resource) let frames = context.takeFrames(until: timestamp) @@ -177,11 +177,11 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let disposable = MetaDisposable() let queue = self.queue - let account = self.account + let postbox = self.postbox let resource = self.resource self.performWithContext { [weak self] context in - context.initializeState(account: account, resource: resource) + context.initializeState(postbox: postbox, resource: resource) context.seek(timestamp: timestamp, completed: { [weak self] streamDescriptions, timestamp in queue.async { [weak self] in diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index f17f3300f0..503aecd881 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -12,9 +12,6 @@ private struct StreamContext { fileprivate let timebase: CMTime fileprivate let duration: CMTime fileprivate let decoder: MediaTrackFrameDecoder - - func close() { - } } struct FFMpegMediaFrameSourceDescription { @@ -33,11 +30,6 @@ private struct InitializedState { fileprivate let audioStream: StreamContext? fileprivate let videoStream: StreamContext? - - func close() { - self.videoStream?.close() - self.audioStream?.close() - } } struct FFMpegMediaFrameSourceStreamContextInfo { @@ -50,31 +42,9 @@ struct FFMpegMediaFrameSourceContextInfo { let videoStream: FFMpegMediaFrameSourceStreamContextInfo? } -/*private func getFormatCallback(codecContext: UnsafeMutablePointer?, formats: UnsafePointer?) -> AVPixelFormat { - var formats = formats! - while formats.pointee != AV_PIX_FMT_NONE { - let desc = av_pix_fmt_desc_get(formats.pointee)! - - if formats.pointee == AV_PIX_FMT_VIDEOTOOLBOX { - let result = av_videotoolbox_default_init(codecContext!) - if (result < 0) { - print("av_videotoolbox_default_init failed (\(result))") - formats = formats.successor() - continue - } - - return formats.pointee; - } else if (desc.pointee.flags & UInt64(AV_PIX_FMT_FLAG_HWACCEL)) == 0 { - return formats.pointee - } - formats = formats.successor() - } - return formats.pointee -}*/ - private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let account = context.account, let resource = context.resource else { + guard let postbox = context.postbox, let resource = context.resource else { return 0 } @@ -83,7 +53,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa let resourceSize: Int = resource.size ?? 0 let readCount = min(resourceSize - context.readingOffset, Int(bufferSize)) - let data = account.postbox.mediaBox.resourceData(resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) + let data = postbox.mediaBox.resourceData(resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) var fetchedData: Data? let semaphore = DispatchSemaphore(value: 0) let _ = data.start(next: { data in @@ -106,7 +76,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let account = context.account, let resource = context.resource else { + guard let postbox = context.postbox, let resource = context.resource else { return 0 } @@ -125,7 +95,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe if context.readingOffset >= resourceSize { context.fetchedDataDisposable.set(nil) } else { - context.fetchedDataDisposable.set(account.postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: context.readingOffset ..< resourceSize).start()) + context.fetchedDataDisposable.set(postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: context.readingOffset ..< resourceSize).start()) } } } @@ -138,7 +108,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { var closed = false - fileprivate var account: Account? + fileprivate var postbox: Postbox? fileprivate var resource: MediaResource? private let ioBufferSize = 64 * 1024 @@ -162,19 +132,19 @@ final class FFMpegMediaFrameSourceContext: NSObject { fetchedDataDisposable.dispose() } - func initializeState(account: Account, resource: MediaResource) { + func initializeState(postbox: Postbox, resource: MediaResource) { if self.readingError || self.initializedState != nil { return } let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals - self.account = account + self.postbox = postbox self.resource = resource let resourceSize: Int = resource.size ?? 0 - self.fetchedDataDisposable.set(account.postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: 0 ..< resourceSize).start()) + self.fetchedDataDisposable.set(postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: 0 ..< resourceSize).start()) var avFormatContextRef = avformat_alloc_context() guard let avFormatContext = avFormatContextRef else { @@ -210,7 +180,28 @@ final class FFMpegMediaFrameSourceContext: NSObject { let codecPar = avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.codecpar! - if codecPar.pointee.codec_id == AV_CODEC_ID_H264 { + if false { + if let codec = avcodec_find_decoder(codecPar.pointee.codec_id) { + if let codecContext = avcodec_alloc_context3(codec) { + if avcodec_parameters_to_context(codecContext, avFormatContext.pointee.streams[streamIndex]!.pointee.codecpar) >= 0 { + if avcodec_open2(codecContext, codec, nil) >= 0 { + let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 24)) + + let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale) + + videoStream = StreamContext(index: streamIndex, codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext)) + break + } else { + var codecContextRef: UnsafeMutablePointer? = codecContext + avcodec_free_context(&codecContextRef) + } + } else { + var codecContextRef: UnsafeMutablePointer? = codecContext + avcodec_free_context(&codecContextRef) + } + } + } + } else if codecPar.pointee.codec_id == AV_CODEC_ID_H264 { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromCodecData(UInt32(kCMVideoCodecType_H264), codecPar.pointee.width, codecPar.pointee.height, codecPar.pointee.extradata, codecPar.pointee.extradata_size, 0x43637661) { let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 1000)) diff --git a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift index ccaa213705..d78b94f1ea 100644 --- a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift +++ b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift @@ -1,10 +1,42 @@ +import TelegramUIPrivateModule +import CoreMedia final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { + private let codecContext: UnsafeMutablePointer + + private let videoFrame: UnsafeMutablePointer + private var resetDecoderOnNextFrame = true + + init(codecContext: UnsafeMutablePointer) { + self.codecContext = codecContext + self.videoFrame = av_frame_alloc() + } + + deinit { + av_frame_unref(self.videoFrame) + + var codecContextRef: UnsafeMutablePointer? = codecContext + avcodec_free_context(&codecContextRef) + } + func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? { + var status = avcodec_send_packet(self.codecContext, frame.packet) + if status == 0 { + status = avcodec_receive_frame(self.codecContext, self.videoFrame) + if status == 0 { + return convertVideoFrame(self.videoFrame, pts: frame.pts, duration: frame.duration) + } + } + + return nil + } + + private func convertVideoFrame(_ frame: UnsafeMutablePointer, pts: CMTime, duration: CMTime) -> MediaTrackFrame? { return nil } func reset() { - + avcodec_flush_buffers(self.codecContext) + self.resetDecoderOnNextFrame = true } } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index bcdf4fede7..ddc664b36c 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -336,6 +336,6 @@ class GalleryController: ViewController { super.containerLayoutUpdated(layout, transition: transition) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } } diff --git a/TelegramUI/HapticFeedback.swift b/TelegramUI/HapticFeedback.swift new file mode 100644 index 0000000000..2ed31829f1 --- /dev/null +++ b/TelegramUI/HapticFeedback.swift @@ -0,0 +1,82 @@ +import Foundation +import UIKit + +@available(iOSApplicationExtension 10.0, *) +private final class HapticFeedbackImpl { + private lazy var impactGenerator = { UIImpactFeedbackGenerator(style: .light) }() + private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }() + + func prepareTap() { + self.impactGenerator.prepare() + } + + func tap() { + self.impactGenerator.impactOccurred() + } + + func success() { + self.notificationGenerator.notificationOccurred(.success) + } + + func error() { + self.notificationGenerator.notificationOccurred(.error) + } + + dynamic func f() { + } +} + +final class HapticFeedback { + private var impl: AnyObject? + + deinit { + let impl = self.impl + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + if #available(iOSApplicationExtension 10.0, *) { + if let impl = impl as? HapticFeedbackImpl { + impl.f() + } + } + }) + } + + @available(iOSApplicationExtension 10.0, *) + private func withImpl(_ f: (HapticFeedbackImpl) -> Void) { + if self.impl == nil { + self.impl = HapticFeedbackImpl() + } + f(self.impl as! HapticFeedbackImpl) + } + + func prepareTap() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.prepareTap() + } + } + } + + func tap() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.tap() + } + } + } + + func success() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.success() + } + } + } + + func error() { + if #available(iOSApplicationExtension 10.0, *) { + self.withImpl { impl in + impl.error() + } + } + } +} diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift new file mode 100644 index 0000000000..7251487797 --- /dev/null +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -0,0 +1,211 @@ +import Foundation +import AsyncDisplayKit +import Postbox +import TelegramCore +import Display + +private struct ChatContextResultStableId: Hashable { + let result: ChatContextResult + + var hashValue: Int { + return result.id.hashValue + } + + static func ==(lhs: ChatContextResultStableId, rhs: ChatContextResultStableId) -> Bool { + return lhs.result == rhs.result + } +} + +private struct HorizontalListContextResultsChatInputContextPanelEntry: Equatable, Comparable, Identifiable { + let index: Int + let result: ChatContextResult + + var stableId: ChatContextResultStableId { + return ChatContextResultStableId(result: self.result) + } + + static func ==(lhs: HorizontalListContextResultsChatInputContextPanelEntry, rhs: HorizontalListContextResultsChatInputContextPanelEntry) -> Bool { + return lhs.index == rhs.index && lhs.result == rhs.result + } + + static func <(lhs: HorizontalListContextResultsChatInputContextPanelEntry, rhs: HorizontalListContextResultsChatInputContextPanelEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, resultSelected: @escaping (ChatContextResult) -> Void) -> ListViewItem { + return HorizontalListContextResultsChatInputPanelItem(account: account, result: self.result, resultSelected: resultSelected) + } +} + +private struct HorizontalListContextResultsChatInputContextPanelTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], account: Account, resultSelected: @escaping (ChatContextResult) -> Void) -> HorizontalListContextResultsChatInputContextPanelTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, resultSelected: resultSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, resultSelected: resultSelected), directionHint: nil) } + + return HorizontalListContextResultsChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) +} + +final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { + private let listView: ListView + private let separatorNode: ASDisplayNode + private var currentResults: ChatContextResultCollection? + private var currentEntries: [HorizontalListContextResultsChatInputContextPanelEntry]? + + private var enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = [] + private var hasValidLayout = false + + override init(account: Account) { + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = UIColor(0xbdc2c7) + self.separatorNode.isHidden = true + + self.listView = ListView() + self.listView.isOpaque = true + self.listView.backgroundColor = .white + self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.listView.isHidden = true + + super.init(account: account) + + self.isOpaque = false + self.clipsToBounds = true + + self.addSubnode(self.listView) + self.addSubnode(self.separatorNode) + } + + func updateResults(_ results: ChatContextResultCollection) { + self.currentResults = results + var entries: [HorizontalListContextResultsChatInputContextPanelEntry] = [] + var index = 0 + var resultIds = Set() + for result in results.results { + let entry = HorizontalListContextResultsChatInputContextPanelEntry(index: index, result: result) + if resultIds.contains(entry.stableId) { + continue + } else { + resultIds.insert(entry.stableId) + } + entries.append(entry) + index += 1 + } + + let firstTime = self.currentEntries == nil + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, resultSelected: { [weak self] result in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.sendContextResult(results, result) + } + }) + self.currentEntries = entries + self.enqueueTransition(transition, firstTime: firstTime) + } + + private func enqueueTransition(_ transition: HorizontalListContextResultsChatInputContextPanelTransition, firstTime: Bool) { + enqueuedTransitions.append((transition, firstTime)) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + let options = ListViewDeleteAndInsertOptions() + if firstTime { + //options.insert(.Synchronous) + //options.insert(.LowLatency) + } else { + //options.insert(.AnimateTopItemPosition) + } + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self, firstTime { + let position = strongSelf.listView.position + strongSelf.listView.isHidden = false + strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + strongSelf.listView.bounds.size.width), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + strongSelf.separatorNode.isHidden = false + let separatorPosition = strongSelf.separatorNode.layer.position + strongSelf.separatorNode.layer.animatePosition(from: CGPoint(x: separatorPosition.x, y: separatorPosition.y + strongSelf.listView.bounds.size.width), to: separatorPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + }) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let listHeight: CGFloat = 105.0 + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - listHeight), size: CGSize(width: size.width, height: UIScreenPixel))) + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: listHeight, height: size.width) + + //transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + + transition.updatePosition(node: self.listView, position: CGPoint(x: size.width / 2.0, y: size.height - listHeight / 2.0)) + + let insets = UIEdgeInsets() + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: listHeight, height: size.width), insets: insets, duration: duration, curve: listViewCurve) + + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hasValidLayout { + hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + override func animateOut(completion: @escaping () -> Void) { + let position = self.listView.layer.position + self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + self.listView.bounds.size.width), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + completion() + }) + let separatorPosition = self.separatorNode.layer.position + self.separatorNode.layer.animatePosition(from: separatorPosition, to: CGPoint(x: separatorPosition.x, y: separatorPosition.y + listView.bounds.size.width), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let listViewBounds = self.listView.bounds + let listViewPosition = self.listView.position + let listViewFrame = CGRect(origin: CGPoint(x: listViewPosition.x - listViewBounds.height / 2.0, y: listViewPosition.y - listViewBounds.width / 2.0), size: CGSize(width: listViewBounds.height, height: listViewBounds.width)) + if !listViewFrame.contains(point) { + return nil + } + return super.hitTest(point, with: event) + } +} diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift new file mode 100644 index 0000000000..1251cdd21b --- /dev/null +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -0,0 +1,248 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox + +final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { + fileprivate let account: Account + fileprivate let result: ChatContextResult + private let resultSelected: (ChatContextResult) -> Void + + let selectable: Bool = true + + public init(account: Account, result: ChatContextResult, resultSelected: @escaping (ChatContextResult) -> Void) { + self.account = account + self.result = result + self.resultSelected = resultSelected + } + + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + let configure = { () -> Void in + let node = HorizontalListContextResultsChatInputPanelItemNode() + + let nodeLayout = node.asyncLayout() + let (top, bottom) = (previousItem != nil, nextItem != nil) + let (layout, apply) = nodeLayout(self, width, top, bottom) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + apply(.None) + }) + } + if Thread.isMainThread { + async { + configure() + } + } else { + configure() + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? HorizontalListContextResultsChatInputPanelItemNode { + Queue.mainQueue().async { + let nodeLayout = node.asyncLayout() + + async { + let (top, bottom) = (previousItem != nil, nextItem != nil) + + let (layout, apply) = nodeLayout(self, width, top, bottom) + Queue.mainQueue().async { + completion(layout, { + apply(animation) + }) + } + } + } + } else { + assertionFailure() + } + } + + func selected(listView: ListView) { + self.resultSelected(self.result) + } +} + +private let titleFont = Font.medium(16.0) +private let textFont = Font.regular(15.0) +private let iconFont = Font.medium(25.0) +private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(0xdfdfdf)) + +final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode { + private let imageNodeBackground: ASDisplayNode + private let imageNode: TransformImageNode + private let videoNode: ManagedVideoNode + private var currentImageResource: TelegramMediaResource? + private var currentVideoResource: TelegramMediaResource? + + init() { + self.imageNodeBackground = ASDisplayNode() + self.imageNodeBackground.isLayerBacked = true + self.imageNodeBackground.backgroundColor = UIColor(white: 0.9, alpha: 1.0) + + self.imageNode = TransformImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + + self.videoNode = ManagedVideoNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.backgroundColor = .white + + self.addSubnode(self.imageNodeBackground) + + self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.imageNode.alphaTransitionOnFirstUpdate = true + self.addSubnode(self.imageNode) + + self.videoNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.videoNode.clipsToBounds = true + self.addSubnode(self.videoNode) + } + + override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = item as? HorizontalListContextResultsChatInputPanelItem { + let doLayout = self.asyncLayout() + let merged = (top: previousItem != nil, bottom: nextItem != nil) + let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + self.contentSize = layout.contentSize + self.insets = layout.insets + apply(.None) + } + } + + func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + let imageLayout = self.imageNode.asyncLayout() + let currentImageResource = self.currentImageResource + let currentVideoResource = self.currentVideoResource + + return { [weak self] item, height, mergedTop, mergedBottom in + let sideInset: CGFloat = 4.0 + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + + var imageResource: TelegramMediaResource? + var videoResource: TelegramMediaResource? + var imageDimensions: CGSize? + switch item.result { + case let .externalReference(_, _, title, _, url, thumbnailUrl, contentUrl, _, dimensions, _, _): + if let contentUrl = contentUrl { + imageResource = HttpReferenceMediaResource(url: contentUrl, size: nil) + } else if let thumbnailUrl = thumbnailUrl { + imageResource = HttpReferenceMediaResource(url: thumbnailUrl, size: nil) + } + imageDimensions = dimensions + case let .internalReference(_, _, title, _, image, file, _): + if let image = image { + if let largestRepresentation = largestImageRepresentation(image.representations) { + imageDimensions = largestRepresentation.dimensions + } + imageResource = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 100.0))?.resource + } else if let file = file { + if let dimensions = file.dimensions { + imageDimensions = dimensions + } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = largestRepresentation.dimensions + } + imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource + } + + if let file = file { + if file.isVideo && file.isAnimated { + videoResource = file.resource + } + } + } + + let fittedImageDimensions: CGSize + let croppedImageDimensions: CGSize + if let imageDimensions = imageDimensions { + fittedImageDimensions = imageDimensions.fitted(CGSize(width: 1000.0, height: height - sideInset - sideInset)) + } else { + fittedImageDimensions = CGSize(width: height - sideInset - sideInset, height: height - sideInset - sideInset) + } + croppedImageDimensions = fittedImageDimensions.cropped(CGSize(width: floor(height * 4.0 / 3.0), height: 1000.0)) + + var imageApply: (() -> Void)? + var transformArguments: TransformImageArguments? + if let imageResource = imageResource { + let imageCorners = ImageCorners() + let arguments = TransformImageArguments(corners: imageCorners, imageSize: fittedImageDimensions, boundingSize: croppedImageDimensions, intrinsicInsets: UIEdgeInsets()) + transformArguments = arguments + imageApply = imageLayout(arguments) + } + + var updatedImageResource = false + if let currentImageResource = currentImageResource, let imageResource = imageResource { + if !currentImageResource.isEqual(to: imageResource) { + updatedImageResource = true + } + } else if (currentImageResource != nil) != (imageResource != nil) { + updatedImageResource = true + } + + var updatedVideoResource = false + if let currentVideoResource = currentVideoResource, let videoResource = videoResource { + if !currentVideoResource.isEqual(to: videoResource) { + updatedVideoResource = true + } + } else if (currentVideoResource != nil) != (videoResource != nil) { + updatedVideoResource = true + } + + if updatedImageResource { + if let imageResource = imageResource { + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation]) + //updateImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + updateImageSignal = chatMessagePhoto(account: item.account, photo: tmpImage) + } else { + updateImageSignal = .complete() + } + } + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets()) + + return (nodeLayout, { _ in + if let strongSelf = self { + strongSelf.currentImageResource = imageResource + strongSelf.currentVideoResource = videoResource + + if let imageApply = imageApply { + if let updateImageSignal = updateImageSignal { + strongSelf.imageNode.setSignal(account: item.account, signal: updateImageSignal) + } + + strongSelf.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) + strongSelf.imageNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) + + strongSelf.videoNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) + strongSelf.videoNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) + + strongSelf.imageNodeBackground.frame = CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: croppedImageDimensions.height, height: croppedImageDimensions.width)) + + if updatedVideoResource { + if let videoResource = videoResource { + if let applicationContext = item.account.applicationContext as? TelegramApplicationContext { + strongSelf.videoNode.acquireContext(account: item.account, mediaManager: applicationContext.mediaManager, id: ChatContextResultManagedMediaId(result: item.result), resource: videoResource) + } + } else { + strongSelf.videoNode.clearContext() + } + } + + imageApply() + + strongSelf.videoNode.transformArguments = transformArguments + } + } + }) + } + } +} diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 55c0c0e4af..3fbebdc72b 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -159,7 +159,7 @@ class LegacyController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } public func dismiss() { diff --git a/TelegramUI/ListController.swift b/TelegramUI/ListController.swift index 250dadfba2..bd2c380451 100644 --- a/TelegramUI/ListController.swift +++ b/TelegramUI/ListController.swift @@ -27,6 +27,6 @@ public class ListController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.listDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.listDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } } diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift new file mode 100644 index 0000000000..dfa3f5fc32 --- /dev/null +++ b/TelegramUI/ManagedAudioPlaylistPlayer.swift @@ -0,0 +1,150 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +enum AudioPlaylistItemLabelInfo { + case music(title: String?, performer: String?) + case voice +} + +struct AudioPlaylistItemInfo { + let duration: Double + let labelInfo: AudioPlaylistItemLabelInfo +} + +protocol AudioPlaylistItemId { + var hashValue: Int { get } + func isEqual(other: AudioPlaylistItemId) -> Bool +} + +protocol AudioPlaylistItem { + var id: AudioPlaylistItemId { get } + var resource: MediaResource? { get } + var info: AudioPlaylistItemInfo? { get } + + func isEqual(other: AudioPlaylistItem) -> Bool +} + +enum AudioPlaylistNavigation { + case next + case previous +} + +enum AudioPlaylistPlayback { + case play + case pause +} + +enum AudioPlaylistControl { + case navigation(AudioPlaylistNavigation) + case playback(AudioPlaylistPlayback) +} + +protocol AudioPlaylistId { + func isEqual(other: AudioPlaylistId) -> Bool +} + +struct AudioPlaylist { + let id: AudioPlaylistId + let navigate: (AudioPlaylistItem?, AudioPlaylistNavigation) -> Signal +} + +struct AudioPlaylistState: Equatable { + let playlistId: AudioPlaylistId + let item: AudioPlaylistItem? + + static func ==(lhs: AudioPlaylistState, rhs: AudioPlaylistState) -> Bool { + if !lhs.playlistId.isEqual(other: rhs.playlistId) { + return false + } + + if let lhsItem = lhs.item, let rhsItem = rhs.item { + if !lhsItem.isEqual(other: rhsItem) { + return false + } + } else if (lhs.item != nil) != (rhs.item != nil) { + return false + } + return true + } +} + +private final class AudioPlaylistItemState { + let item: AudioPlaylistItem + let player: MediaPlayer? + + init(item: AudioPlaylistItem, player: MediaPlayer?) { + self.item = item + self.player = player + } +} + +private final class AudioPlaylistInternalState { + var currentItem: AudioPlaylistItemState? + let navigationDisposable = MetaDisposable() +} + +final class ManagedAudioPlaylistPlayer { + private let postbox: Postbox + let playlist: AudioPlaylist + + private let currentState = Atomic(value: AudioPlaylistInternalState()) + private let currentStateValue = Promise() + + var state: Signal { + return self.currentStateValue.get() + } + + init(postbox: Postbox, playlist: AudioPlaylist) { + self.postbox = postbox + self.playlist = playlist + } + + deinit { + self.currentState.with { state -> Void in + state.navigationDisposable.dispose() + } + } + + func control(_ control: AudioPlaylistControl) { + switch control { + case let .playback(playback): + self.currentState.with { state -> Void in + if let item = state.currentItem { + switch playback { + case .play: + item.player?.play() + case .pause: + item.player?.pause() + } + } + } + case let .navigation(navigation): + let disposable = MetaDisposable() + var currentItem: AudioPlaylistItem? + self.currentState.with { state -> Void in + state.navigationDisposable.set(disposable) + } + disposable.set(self.playlist.navigate(currentItem, navigation).start(next: { [weak self] item in + if let strongSelf = self { + let updatedState = strongSelf.currentState.with { state -> AudioPlaylistState in + if let item = item { + var player: MediaPlayer? + if let resource = item.resource { + player = MediaPlayer(postbox: strongSelf.postbox, resource: resource) + } + state.currentItem = AudioPlaylistItemState(item: item, player: player) + player?.play() + return AudioPlaylistState(playlistId: strongSelf.playlist.id, item: item) + } else { + state.currentItem = nil + return AudioPlaylistState(playlistId: strongSelf.playlist.id, item: nil) + } + } + strongSelf.currentStateValue.set(.single(updatedState)) + } + })) + } + } +} diff --git a/TelegramUI/ManagedAudioRecorder.swift b/TelegramUI/ManagedAudioRecorder.swift new file mode 100644 index 0000000000..172b1af61b --- /dev/null +++ b/TelegramUI/ManagedAudioRecorder.swift @@ -0,0 +1,589 @@ +import Foundation +import SwiftSignalKit +import TelegramUIPrivateModule +import CoreMedia +import AVFoundation +import TelegramCore + +private let kOutputBus: UInt32 = 0 +private let kInputBus: UInt32 = 1 + +private func audioRecorderNativeStreamDescription() -> AudioStreamBasicDescription { + var canonicalBasicStreamDescription = AudioStreamBasicDescription() + canonicalBasicStreamDescription.mSampleRate = 16000.0 + canonicalBasicStreamDescription.mFormatID = kAudioFormatLinearPCM + canonicalBasicStreamDescription.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked + canonicalBasicStreamDescription.mFramesPerPacket = 1 + canonicalBasicStreamDescription.mChannelsPerFrame = 1 + canonicalBasicStreamDescription.mBitsPerChannel = 16 + canonicalBasicStreamDescription.mBytesPerPacket = 2 + canonicalBasicStreamDescription.mBytesPerFrame = 2 + return canonicalBasicStreamDescription +} + +private var nextRecorderContextId: Int32 = 0 +private func getNextRecorderContextId() -> Int32 { + return OSAtomicIncrement32(&nextRecorderContextId) +} + +private final class RecorderContextHolder { + weak var context: ManagedAudioRecorderContext? + + init(context: ManagedAudioRecorderContext?) { + self.context = context + } +} + +private final class AudioUnitHolder { + let queue: Queue + let audioUnit: Atomic + + init(queue: Queue, audioUnit: Atomic) { + self.queue = queue + self.audioUnit = audioUnit + } +} + +private var audioRecorderContexts: [Int32: RecorderContextHolder] = [:] +private var audioUnitHolders = Atomic<[Int32: AudioUnitHolder]>(value: [:]) + +private func addAudioRecorderContext(_ id: Int32, _ context: ManagedAudioRecorderContext) { + audioRecorderContexts[id] = RecorderContextHolder(context: context) +} + +private func removeAudioRecorderContext(_ id: Int32) { + audioRecorderContexts.removeValue(forKey: id) +} + +private func addAudioUnitHolder(_ id: Int32, _ queue: Queue, _ audioUnit: Atomic) { + audioUnitHolders.modify { dict in + var dict = dict + dict[id] = AudioUnitHolder(queue: queue, audioUnit: audioUnit) + return dict + } +} + +private func removeAudioUnitHolder(_ id: Int32) { + audioUnitHolders.modify { dict in + var dict = dict + dict.removeValue(forKey: id) + return dict + } +} + +private func withAudioRecorderContext(_ id: Int32, _ f: (ManagedAudioRecorderContext?) -> Void) { + if let holder = audioRecorderContexts[id], let context = holder.context { + f(context) + } else { + f(nil) + } +} + +private func withAudioUnitHolder(_ id: Int32, _ f: (Atomic, Queue) -> Void) { + let audioUnitAndQueue = audioUnitHolders.with { dict -> (Atomic, Queue)? in + if let record = dict[id] { + return (record.audioUnit, record.queue) + } else { + return nil + } + } + if let (audioUnit, queue) = audioUnitAndQueue { + f(audioUnit, queue) + } +} + +private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp: UnsafePointer, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { + let id = Int32(unsafeBitCast(refCon, to: intptr_t.self)) + + withAudioUnitHolder(id, { (holder, queue) in + var buffer = AudioBuffer() + buffer.mNumberChannels = 1; + buffer.mDataByteSize = inNumberFrames * 2; + buffer.mData = malloc(Int(inNumberFrames) * 2) + + var bufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: buffer) + + var status = noErr + holder.with { audioUnit in + if let audioUnit = audioUnit { + status = AudioUnitRender(audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &bufferList) + } else { + status = kAudioUnitErr_FailedInitialization + } + } + + if status == noErr { + queue.async { + withAudioRecorderContext(id, { context in + if let context = context { + context.processAndDisposeAudioBuffer(buffer) + } else { + free(buffer.mData) + } + }) + } + } else { + free(buffer.mData) + trace("ManagedAudioRecorder", what: "AudioUnitRender returned \(status)") + } + }) + + /* + if (status == noErr) + { + [[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^ + { + TGOpusAudioRecorder *recorder = globalRecorder; + if (recorder != nil && recorder.recorderId == (int)(intptr_t)inRefCon && recorder->_recording) { + + if (recorder->_waitForTone) { + if (CACurrentMediaTime() - recorder->_waitForToneStart > 0.3) { + [recorder _processBuffer:&buffer]; + } + } else { + [recorder _processBuffer:&buffer]; + } + } + + free(buffer.mData); + }]; + } + + sharedQueue.async { + withAudioRecorderContext(Int32(unsafeBitCast(refCon, to: intptr_t.self)), { context in + if !context.isPaused { + + } + }) + }*/ + return noErr +} + +struct RecordedAudioData { + let compressedData: Data + let duration: Double + let waveform: Data? +} + +final class ManagedAudioRecorderContext { + private let id: Int32 + private let micLevel: ValuePromise + private let recordingState: ValuePromise + + private var paused = true + + private let queue: Queue + private let mediaManager: MediaManager + private let oggWriter: TGOggOpusWriter + private let dataItem: TGDataItem + private var audioBuffer = Data() + + private let audioUnit = Atomic(value: nil) + + private var waveformSamples = Data() + private var waveformPeak: Int16 = 0 + private var waveformPeakCount: Int = 0 + + private var micLevelPeak: Int16 = 0 + private var micLevelPeakCount: Int = 0 + + fileprivate var isPaused = false + + private var recordingStateUpdateTimestamp: Double? + + private var audioSessionDisposable: Disposable? + + init(queue: Queue, mediaManager: MediaManager, micLevel: ValuePromise, recordingState: ValuePromise) { + assert(queue.isCurrent()) + + self.id = getNextRecorderContextId() + self.micLevel = micLevel + self.recordingState = recordingState + + self.queue = queue + self.mediaManager = mediaManager + self.dataItem = TGDataItem() + self.oggWriter = TGOggOpusWriter() + + addAudioRecorderContext(self.id, self) + addAudioUnitHolder(self.id, queue, self.audioUnit) + + self.oggWriter.begin(with: self.dataItem) + } + + deinit { + assert(self.queue.isCurrent()) + + removeAudioRecorderContext(self.id) + removeAudioUnitHolder(self.id) + + self.stop() + + self.audioSessionDisposable?.dispose() + } + + func start() { + assert(self.queue.isCurrent()) + + self.paused = false + + var desc = AudioComponentDescription() + desc.componentType = kAudioUnitType_Output + desc.componentSubType = kAudioUnitSubType_RemoteIO + desc.componentFlags = 0 + desc.componentFlagsMask = 0 + desc.componentManufacturer = kAudioUnitManufacturer_Apple + guard let inputComponent = AudioComponentFindNext(nil, &desc) else { + return + } + var maybeAudioUnit: AudioUnit? = nil + AudioComponentInstanceNew(inputComponent, &maybeAudioUnit) + + guard let audioUnit = maybeAudioUnit else { + return + } + + var status = noErr + + var one: UInt32 = 1 + guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, 4) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var audioStreamDescription = audioRecorderNativeStreamDescription() + guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &audioStreamDescription, UInt32(MemoryLayout.size)) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioStreamDescription, UInt32(MemoryLayout.size)) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var callbackStruct = AURenderCallbackStruct() + callbackStruct.inputProc = rendererInputProc + callbackStruct.inputProcRefCon = unsafeBitCast(intptr_t(self.id), to: UnsafeMutableRawPointer.self) + guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &callbackStruct, UInt32(MemoryLayout.size)) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var zero: UInt32 = 1 + guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, 0, &zero, 4) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + guard AudioUnitInitialize(audioUnit) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + self.audioUnit.swap(audioUnit) + + if self.audioSessionDisposable == nil { + let queue = self.queue + self.audioSessionDisposable = self.mediaManager.audioSession.push(audioSessionType: .playAndRecord, activate: { [weak self] in + queue.async { + if let strongSelf = self, !strongSelf.paused { + strongSelf.audioSessionAcquired() + } + } + }, deactivate: { [weak self] in + return Signal { subscriber in + queue.async { + if let strongSelf = self { + strongSelf.stop() + subscriber.putCompletion() + } + } + + return EmptyDisposable + } + }) + } + } + + func audioSessionAcquired() { + self.audioUnit.with { audioUnit -> Void in + if let audioUnit = audioUnit { + guard AudioOutputUnitStart(audioUnit) == noErr else { + self.stop() + return + } + } + } + } + + func stop() { + assert(self.queue.isCurrent()) + + self.paused = true + + if let audioUnit = self.audioUnit.swap(nil) { + var status = noErr + + status = AudioOutputUnitStop(audioUnit) + if status != noErr { + trace("ManagedAudioRecorder", what: "AudioOutputUnitStop returned \(status)") + } + + status = AudioUnitUninitialize(audioUnit) + if status != noErr { + trace("ManagedAudioRecorder", what: "AudioUnitUninitialize returned \(status)") + } + + status = AudioComponentInstanceDispose(audioUnit) + if status != noErr { + trace("ManagedAudioRecorder", what: "AudioComponentInstanceDispose returned \(status)") + } + } + + let audioSessionDisposable = self.audioSessionDisposable + self.audioSessionDisposable = nil + audioSessionDisposable?.dispose() + } + + func processAndDisposeAudioBuffer(_ buffer: AudioBuffer) { + assert(self.queue.isCurrent()) + + defer { + free(buffer.mData) + } + + let millisecondsPerPacket = 60 + let encoderPacketSizeInBytes = 16000 / 1000 * millisecondsPerPacket * 2 + + let currentEncoderPacket = malloc(encoderPacketSizeInBytes)! + defer { + free(currentEncoderPacket) + } + + var bufferOffset = 0 + + while true { + var currentEncoderPacketSize = 0 + + while currentEncoderPacketSize < encoderPacketSizeInBytes { + if audioBuffer.count != 0 { + let takenBytes = min(self.audioBuffer.count, encoderPacketSizeInBytes - currentEncoderPacketSize) + if takenBytes != 0 { + self.audioBuffer.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + memcpy(currentEncoderPacket.advanced(by: currentEncoderPacketSize), bytes, takenBytes) + } + self.audioBuffer.replaceSubrange(0 ..< takenBytes, with: Data()) + currentEncoderPacketSize += takenBytes + } + } else if bufferOffset < Int(buffer.mDataByteSize) { + let takenBytes = min(Int(buffer.mDataByteSize) - bufferOffset, encoderPacketSizeInBytes - currentEncoderPacketSize) + if takenBytes != 0 { + self.audioBuffer.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + memcpy(currentEncoderPacket.advanced(by: currentEncoderPacketSize), buffer.mData?.advanced(by: bufferOffset), takenBytes) + } + bufferOffset += takenBytes + currentEncoderPacketSize += takenBytes + } + } else { + break + } + } + + if currentEncoderPacketSize < encoderPacketSizeInBytes { + self.audioBuffer.append(currentEncoderPacket.assumingMemoryBound(to: UInt8.self), count: currentEncoderPacketSize) + break + } else { + let previousBytesWritten = self.oggWriter.encodedBytes() + + self.processWaveformPreview(samples: currentEncoderPacket.assumingMemoryBound(to: Int16.self), count: currentEncoderPacketSize / 2) + + self.oggWriter.writeFrame(currentEncoderPacket.assumingMemoryBound(to: UInt8.self), frameByteCount: UInt(currentEncoderPacketSize)) + + let timestamp = CACurrentMediaTime() + if self.recordingStateUpdateTimestamp == nil || self.recordingStateUpdateTimestamp! < timestamp + 0.1 { + self.recordingStateUpdateTimestamp = timestamp + self.recordingState.set(.recording(duration: oggWriter.encodedDuration(), durationMediaTimestamp: timestamp)) + } + + /*NSUInteger currentBytesWritten = [_oggWriter encodedBytes]; + if (currentBytesWritten != previousBytesWritten) + { + [ActionStageInstance() dispatchOnStageQueue:^ + { + TGLiveUploadActor *actor = (TGLiveUploadActor *)[ActionStageInstance() executingActorWithPath:_liveUploadPath]; + [actor updateSize:currentBytesWritten]; + }]; + }*/ + } + } + } + + func processWaveformPreview(samples: UnsafePointer, count: Int) { + for i in 0 ..< count { + var sample = samples.advanced(by: i).pointee + if sample < 0 { + if sample == Int16.min { + sample = Int16.max + } else { + sample = -sample + } + } + if self.waveformPeak < sample { + self.waveformPeak = sample + } + self.waveformPeakCount += 1 + + if self.waveformPeakCount >= 100 { + self.waveformSamples.count += 2 + var waveformPeak = self.waveformPeak + withUnsafeBytes(of: &waveformPeak, { bytes -> Void in + self.waveformSamples.append(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 2) + }) + self.waveformPeak = 0 + self.waveformPeakCount = 0 + } + + if self.micLevelPeak < sample { + self.micLevelPeak = sample + } + self.micLevelPeakCount += 1 + + if self.micLevelPeakCount >= 1200 { + let level = Float(self.micLevelPeak) / 4000.0 + self.micLevel.set(level) + self.micLevelPeak = 0 + self.micLevelPeakCount = 0 + } + } + } + + func takeData() -> RecordedAudioData? { + if self.oggWriter.writeFrame(nil, frameByteCount: 0) { + var scaledSamplesMemory = malloc(100 * 2)! + var scaledSamples: UnsafeMutablePointer = scaledSamplesMemory.assumingMemoryBound(to: Int16.self) + defer { + free(scaledSamplesMemory) + } + memset(scaledSamples, 0, 100 * 2); + var waveform: Data? + + self.waveformSamples.withUnsafeMutableBytes { (samples: UnsafeMutablePointer) -> Void in + let count = self.waveformSamples.count / 2 + for i in 0 ..< count { + let sample = samples[i] + let index = i * 100 / count + if (scaledSamples[index] < sample) { + scaledSamples[index] = sample; + } + } + + var peak: Int16 = 0 + var sumSamples: Int64 = 0 + for i in 0 ..< 100 { + let sample = scaledSamples[i] + if peak < sample { + peak = sample + } + sumSamples += Int64(peak) + } + var calculatedPeak: UInt16 = 0 + calculatedPeak = UInt16((Double(sumSamples) * 1.8 / 100.0)) + + if calculatedPeak < 2500 { + calculatedPeak = 2500 + } + + for i in 0 ..< 100 { + var sample: UInt16 = UInt16(Int64(scaledSamples[i])) + if sample > calculatedPeak { + scaledSamples[i] = Int16(calculatedPeak) + } + } + + let resultWaveform = AudioWaveform(samples: Data(bytes: scaledSamplesMemory, count: 100 * 2), peak: Int32(calculatedPeak)) + let bitstream = resultWaveform.makeBitstream() + waveform = AudioWaveform(bitstream: bitstream, bitsPerSample: 5).makeBitstream() + } + + return RecordedAudioData(compressedData: self.dataItem.data(), duration: self.oggWriter.encodedDuration(), waveform: waveform) + } else { + return nil + } + } +} + +enum AudioRecordingState: Equatable { + case paused(duration: Double) + case recording(duration: Double, durationMediaTimestamp: Double) + + static func ==(lhs: AudioRecordingState, rhs: AudioRecordingState) -> Bool { + switch lhs { + case let .paused(duration): + if case .paused(duration) = rhs { + return true + } else { + return false + } + case let .recording(duration, durationMediaTimestamp): + if case .recording(duration, durationMediaTimestamp) = rhs { + return true + } else { + return false + } + } + } +} + +final class ManagedAudioRecorder { + private let queue = Queue() + private var contextRef: Unmanaged? + private let micLevelValue = ValuePromise(0.0) + private let recordingStateValue = ValuePromise(.paused(duration: 0.0)) + + var micLevel: Signal { + return self.micLevelValue.get() + } + + var recordingState: Signal { + return self.recordingStateValue.get() + } + + init(mediaManager: MediaManager) { + self.queue.async { + let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, micLevel: self.micLevelValue, recordingState: self.recordingStateValue) + self.contextRef = Unmanaged.passRetained(context) + } + } + + func start() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.start() + } + } + } + + func stop() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.stop() + } + } + } + + func takenRecordedData() -> Signal { + return Signal { subscriber in + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + subscriber.putNext(context.takeData()) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + } + return EmptyDisposable + } + } +} diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift new file mode 100644 index 0000000000..74eec0fba7 --- /dev/null +++ b/TelegramUI/ManagedAudioSession.swift @@ -0,0 +1,138 @@ +import Foundation +import SwiftSignalKit +import AVFoundation + +enum ManagedAudioSessionType { + case none + case play + case playAndRecord +} + +private func nativeCategoryForType(_ type: ManagedAudioSessionType) -> String { + switch type { + case .none: + return AVAudioSessionCategoryPlayback + case .play: + return AVAudioSessionCategoryPlayback + case .playAndRecord: + return AVAudioSessionCategoryPlayAndRecord + } +} + +private final class HolderRecord { + let id: Int32 + let audioSessionType: ManagedAudioSessionType + let activate: () -> Void + let deactivate: () -> Signal + var active: Bool = false + var deactivatingDisposable: Disposable? = nil + + init(id: Int32, audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal) { + self.id = id + self.audioSessionType = audioSessionType + self.activate = activate + self.deactivate = deactivate + } +} + +final class ManagedAudioSession { + private var nextId: Int32 = 0 + private let queue = Queue() + private var holders: [HolderRecord] = [] + private var currentType: ManagedAudioSessionType = .none + + func push(audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal) -> Disposable { + let id = OSAtomicIncrement32(&self.nextId) + self.queue.async { + self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, activate: activate, deactivate: deactivate)) + self.updateHolders() + } + return ActionDisposable { [weak self] in + if let strongSelf = self { + strongSelf.queue.async { + strongSelf.removeDeactivatedHolder(id: id) + } + } + } + } + + private func removeDeactivatedHolder(id: Int32) { + assert(self.queue.isCurrent()) + + for i in 0 ..< self.holders.count { + if self.holders[i].id == id { + self.holders[i].deactivatingDisposable?.dispose() + self.holders.remove(at: i) + self.updateHolders() + break + } + } + } + + private func updateHolders() { + assert(self.queue.isCurrent()) + + print("holder count \(self.holders.count)") + + if !self.holders.isEmpty { + var activeIndex: Int? + var deactivating = false + var index = 0 + for record in self.holders { + if record.active { + activeIndex = index + break + } + else if record.deactivatingDisposable != nil { + deactivating = true + } + index += 1 + } + if !deactivating { + if let activeIndex = activeIndex, activeIndex != self.holders.count - 1 { + self.holders[activeIndex].active = false + let id = self.holders[activeIndex].id + self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate() |> deliverOn(self.queue)).start(completed: { [weak self] in + if let strongSelf = self { + for currentRecord in strongSelf.holders { + if currentRecord.id == id { + currentRecord.deactivatingDisposable = nil + break + } + } + strongSelf.updateHolders() + } + }) + } else if activeIndex == nil { + let lastIndex = self.holders.count - 1 + self.holders[lastIndex].active = true + self.applyType(self.holders[lastIndex].audioSessionType) + self.holders[lastIndex].activate() + } + } + } else { + self.applyType(.none) + } + } + + private func applyType(_ type: ManagedAudioSessionType) { + if self.currentType != type { + self.currentType = type + + do { + if type != .none { + print("ManagedAudioSession setting category for \(type)") + try AVAudioSession.sharedInstance().setCategory(nativeCategoryForType(type)) + print("ManagedAudioSession setting active \(type != .none)") + try AVAudioSession.sharedInstance().setActive(type != .none) + } else { + print("ManagedAudioSession setting active false") + try AVAudioSession.sharedInstance().setActive(false) + } + } catch let error { + print("ManagedAudioSession applyType error \(error)") + } + //[[AVAudioSession sharedInstance] setCategory:[self nativeCategoryForType:type] withOptions:(type == TGAudioSessionTypePlayAndRecord || type == TGAudioSessionTypePlayAndRecordHeadphones) ? AVAudioSessionCategoryOptionAllowBluetooth : 0 error:&error]; + } + } +} diff --git a/TelegramUI/ManagedMediaId.swift b/TelegramUI/ManagedMediaId.swift new file mode 100644 index 0000000000..0029c53f39 --- /dev/null +++ b/TelegramUI/ManagedMediaId.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol ManagedMediaId { + var hashValue: Int { get } + func isEqual(to: ManagedMediaId) -> Bool +} diff --git a/TelegramUI/ManagedVideoNode.swift b/TelegramUI/ManagedVideoNode.swift new file mode 100644 index 0000000000..76401d1a33 --- /dev/null +++ b/TelegramUI/ManagedVideoNode.swift @@ -0,0 +1,53 @@ +import Foundation +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +class ManagedVideoNode: ASDisplayNode { + private var videoContext: ManagedVideoContext? + private let videoContextDisposable = MetaDisposable() + var transformArguments: TransformImageArguments? { + didSet { + self.videoContext?.playerNode.transformArguments = self.transformArguments + } + } + + deinit { + self.videoContextDisposable.dispose() + } + + func clearContext() { + self.videoContextDisposable.set(nil) + } + + func acquireContext(account: Account, mediaManager: MediaManager, id: ManagedMediaId, resource: MediaResource) { + self.videoContextDisposable.set((mediaManager.videoContext(account: account, id: id, resource: resource) |> deliverOnMainQueue).start(next: { [weak self] videoContext in + if let strongSelf = self { + if strongSelf.videoContext !== videoContext { + if let videoContext = strongSelf.videoContext { + if videoContext.playerNode.supernode == self { + videoContext.playerNode.removeFromSupernode() + } + } + + strongSelf.videoContext = videoContext + if let videoContext = videoContext { + strongSelf.addSubnode(videoContext.playerNode) + videoContext.playerNode.transformArguments = strongSelf.transformArguments + strongSelf.setNeedsLayout() + videoContext.mediaPlayer.play() + } + } + } + })) + } + + override func layout() { + super.layout() + + if let videoContext = videoContext { + videoContext.playerNode.frame = self.bounds + } + } +} diff --git a/TelegramUI/MapInputController.swift b/TelegramUI/MapInputController.swift index afcbd7bfb2..bff6172f18 100644 --- a/TelegramUI/MapInputController.swift +++ b/TelegramUI/MapInputController.swift @@ -17,8 +17,8 @@ final class MapInputController: ViewController { } } - override init() { - super.init() + init() { + super.init(navigationBar: NavigationBar()) self._ready.set(.single(true)) @@ -57,6 +57,6 @@ final class MapInputController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.mapInputNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.mapInputNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } } diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index 979481d462..82c6bf29a7 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -5,18 +5,159 @@ import AVFoundation import MobileCoreServices import TelegramCore -final class MediaManager { - let queue = Queue() +private final class ManagedAudioPlaylistPlayerStatusesContext { + let subscribers } -//private var globalPlayer: AudioStreamPlayer? - -func debugPlayMedia(account: Account, file: TelegramMediaFile) { - /*globalPlayer = nil - let player = AudioStreamPlayer(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) - globalPlayer = player*/ +private struct WrappedManagedMediaId: Hashable { + let id: ManagedMediaId - /*let player = STKAudioPlayer() - player.play("http://www.stephaniequinn.com/Music/Canon.mp3") - testPlayer = player*/ + var hashValue: Int { + return self.id.hashValue + } + + static func ==(lhs: WrappedManagedMediaId, rhs: WrappedManagedMediaId) -> Bool { + return lhs.id.isEqual(to: rhs.id) + } +} + +final class ManagedVideoContext { + let mediaPlayer: MediaPlayer + let playerNode: MediaPlayerNode + + init(mediaPlayer: MediaPlayer, playerNode: MediaPlayerNode) { + self.mediaPlayer = mediaPlayer + self.playerNode = playerNode + } +} + +private final class ActiveManagedVideoContext { + let context: ManagedVideoContext + let contextSubscribers = Bag<(ManagedVideoContext?) -> Void>() + + init(context: ManagedVideoContext) { + self.context = context + } +} + +final class MediaManager { + private let queue = Queue.mainQueue() + + let audioSession = ManagedAudioSession() + + private let playlistPlayer = Atomic(value: nil) + private let playlistPlayerStateValue = Promise(nil) + var playlistPlayerState: Signal { + return self.playlistPlayerStateValue.get() + } + private var playlistPlayerStateValueDisposable: Disposable? + + private var managedVideoContexts: [WrappedManagedMediaId: ActiveManagedVideoContext] = [:] + + init() { + } + + deinit { + self.playlistPlayerStateValueDisposable?.dispose() + } + + func videoContext(account: Account, id: ManagedMediaId, resource: MediaResource) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.queue.async { + let wrappedId = WrappedManagedMediaId(id: id) + let activeContext: ActiveManagedVideoContext + if let currentActiveContext = self.managedVideoContexts[wrappedId] { + activeContext = currentActiveContext + } else { + let mediaPlayer = MediaPlayer(postbox: account.postbox, resource: resource) + let playerNode = MediaPlayerNode() + mediaPlayer.attachPlayerNode(playerNode) + activeContext = ActiveManagedVideoContext(context: ManagedVideoContext(mediaPlayer: mediaPlayer, playerNode: playerNode)) + self.managedVideoContexts[wrappedId] = activeContext + } + + let index = activeContext.contextSubscribers.add({ context in + subscriber.putNext(context) + }) + + for (subscriberIndex, subscriberSink) in activeContext.contextSubscribers.copyItemsWithIndices() { + if subscriberIndex == index { + subscriberSink(activeContext.context) + } else { + subscriberSink(nil) + } + } + + disposable.set(ActionDisposable { + self.queue.async { + if let activeContext = self.managedVideoContexts[wrappedId] { + activeContext.contextSubscribers.remove(index) + + if activeContext.contextSubscribers.isEmpty { + self.managedVideoContexts.removeValue(forKey: wrappedId) + } else { + let lastSubscriber = activeContext.contextSubscribers.copyItemsWithIndices().last!.1 + lastSubscriber(activeContext.context) + } + } + } + }) + } + + return disposable + } + } + + func audioRecorder() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.queue.async { + let audioRecorder = ManagedAudioRecorder(mediaManager: self) + subscriber.putNext(audioRecorder) + + disposable.set(ActionDisposable { + }) + } + + return disposable + } + } + + func setPlaylistPlayer(_ player: ManagedAudioPlaylistPlayer?) { + var disposePlayer: ManagedAudioPlaylistPlayer? + var updatedPlayer = false + self.playlistPlayer.modify { currentPlayer in + if currentPlayer !== player { + disposePlayer = currentPlayer + updatedPlayer = true + return player + } else { + return currentPlayer + } + } + + if let disposePlayer = disposePlayer { + } + + if updatedPlayer { + if let player = player { + self.playlistPlayerStateValue.set(player.state) + } else { + self.playlistPlayerStateValue.set(.single(nil)) + } + } + } + + func playlistPlayerState(playlistId: AudioPlaylistId, itemId: AudioPlaylistItemId) -> Signal { + return Signal { subscriber in + + + return ActionDisposable { + + } + } + } } diff --git a/TelegramUI/MediaNavigationAccessoryContainerNode.swift b/TelegramUI/MediaNavigationAccessoryContainerNode.swift new file mode 100644 index 0000000000..188866c4fe --- /dev/null +++ b/TelegramUI/MediaNavigationAccessoryContainerNode.swift @@ -0,0 +1,29 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class MediaNavigationAccessoryContainerNode: ASDisplayNode { + private let separatorNode: ASDisplayNode + let headerNode: MediaNavigationAccessoryHeaderNode + + override init() { + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + + self.headerNode = MediaNavigationAccessoryHeaderNode() + + super.init() + + self.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) + + self.addSubnode(self.headerNode) + self.addSubnode(self.separatorNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 36.0 - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 36.0))) + self.headerNode.updateLayout(size: size, transition: transition) + } +} diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift new file mode 100644 index 0000000000..49e2acc0ef --- /dev/null +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -0,0 +1,54 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() +}) + +final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { + private let titleNode: TextNode + private let subtitleNode: TextNode + + private let closeButton: HighlightableButtonNode + + var close: (() -> Void)? + + override init() { + self.titleNode = TextNode() + self.subtitleNode = TextNode() + + self.closeButton = HighlightableButtonNode() + self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) + self.addSubnode(self.closeButton) + + self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width, y: 12.0), size: closeButtonSize)) + } + + @objc func closeButtonPressed() { + if let close = self.close { + close() + } + } +} diff --git a/TelegramUI/MediaNavigationAccessoryPanel.swift b/TelegramUI/MediaNavigationAccessoryPanel.swift new file mode 100644 index 0000000000..35b72e7bdf --- /dev/null +++ b/TelegramUI/MediaNavigationAccessoryPanel.swift @@ -0,0 +1,49 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class MediaNavigationAccessoryPanel: ASDisplayNode { + private let containerNode: MediaNavigationAccessoryContainerNode + + var close: (() -> Void)? + + override init() { + self.containerNode = MediaNavigationAccessoryContainerNode() + + super.init() + + self.addSubnode(self.containerNode) + + containerNode.headerNode.close = { [weak self] in + if let strongSelf = self, let close = strongSelf.close { + close() + } + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) + self.containerNode.updateLayout(size: size, transition: transition) + } + + func animateIn(transition: ContainedViewLayoutTransition) { + self.clipsToBounds = true + let contentPosition = self.containerNode.layer.position + transition.animatePosition(node: self.containerNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - self.containerNode.frame.size.height), completion: { [weak self] _ in + self?.clipsToBounds = false + }) + } + + func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + self.clipsToBounds = true + let contentPosition = self.containerNode.layer.position + transition.animatePosition(node: self.containerNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - self.containerNode.frame.size.height), removeOnCompletion: false, completion: { [weak self] _ in + self?.clipsToBounds = false + completion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } +} diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 262ec8655e..78e97125e1 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -3,6 +3,7 @@ import SwiftSignalKit import Postbox import CoreMedia import TelegramCore +import Postbox private struct MediaPlayerControlTimebase { let timebase: CMTimebase @@ -29,7 +30,7 @@ private enum MediaPlayerState { private final class MediaPlayerContext { private let queue: Queue - private let account: Account + private let postbox: Postbox private let resource: MediaResource private var state: MediaPlayerState = .empty @@ -61,11 +62,11 @@ private final class MediaPlayerContext { } } - init(queue: Queue, account: Account, resource: MediaResource) { + init(queue: Queue, postbox: Postbox, resource: MediaResource) { assert(queue.isCurrent()) self.queue = queue - self.account = account + self.postbox = postbox self.resource = resource } @@ -123,7 +124,7 @@ private final class MediaPlayerContext { } } - let frameSource = FFMpegMediaFrameSource(queue: self.queue, account: account, resource: resource) + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resource: resource) let disposable = MetaDisposable() self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: action) @@ -310,6 +311,8 @@ private final class MediaPlayerContext { duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration)) } + var loopNow = false + var worstStatus: MediaTrackFrameBufferStatus? for status in [videoStatus, audioStatus] { if let status = status { @@ -368,6 +371,7 @@ private final class MediaPlayerContext { let nextTickDelay = max(0.0, finishedAt - timestamp) if nextTickDelay.isLessThanOrEqualTo(0.0) { rate = 0.0 + loopNow = true } else { let tickTimer = SwiftSignalKit.Timer(timeout: nextTickDelay, repeat: false, completion: { [weak self] in self?.tick() @@ -426,6 +430,10 @@ private final class MediaPlayerContext { } let status = MediaPlayerStatus(duration: duration, timestamp: timestamp, status: playbackStatus) self.status.set(.single(status)) + + if loopNow { + self.seek(timestamp: 0.0, action: .play) + } } } @@ -467,9 +475,9 @@ final class MediaPlayer { } } - init(account: Account, resource: MediaResource) { + init(postbox: Postbox, resource: MediaResource) { self.queue.async { - let context = MediaPlayerContext(queue: self.queue, account: account, resource: resource) + let context = MediaPlayerContext(queue: self.queue, postbox: postbox, resource: resource) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/TelegramUI/MediaPlayerNode.swift b/TelegramUI/MediaPlayerNode.swift index df93336977..e6168fe090 100644 --- a/TelegramUI/MediaPlayerNode.swift +++ b/TelegramUI/MediaPlayerNode.swift @@ -34,6 +34,12 @@ final class MediaPlayerNode: ASDisplayNode { var queue: Queue? private var isRequestingFrames = false + var transformArguments: TransformImageArguments? { + didSet { + self.updateLayout() + } + } + override init() { super.init() @@ -41,15 +47,41 @@ final class MediaPlayerNode: ASDisplayNode { self.view.addSubview(self.displayView!) } + deinit { + //assert(Queue.mainQueue().isCurrent()) + } + override var frame: CGRect { didSet { if !oldValue.size.equalTo(self.frame.size) { - self.displayView?.frame = self.bounds - self.snapshotNode?.frame = self.bounds + self.updateLayout() } } } + private func updateLayout() { + let bounds = self.bounds + + let fittedRect: CGRect + if let arguments = self.transformArguments { + let drawingRect = bounds + var fittedSize = arguments.imageSize + if abs(fittedSize.width - bounds.size.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = bounds.size.width + } + if abs(fittedSize.height - bounds.size.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = bounds.size.height + } + + fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + } else { + fittedRect = bounds + } + + self.displayView?.frame = fittedRect + self.snapshotNode?.frame = fittedRect + } + func reset() { (self.displayView?.layer as? AVSampleBufferDisplayLayer)?.flush() } diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift index edc99845da..0a32ed06bd 100644 --- a/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -52,7 +52,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.stackFromBottomInsetItemFactor = 3.5 + self.listView.keepBottomItemOverscrollBackground = true self.listView.limitHitTestToNodes = true super.init(account: account) @@ -121,12 +121,18 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { var options = ListViewDeleteAndInsertOptions() if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) + //options.insert(.Synchronous) + //options.insert(.LowLatency) } else { - //options.insert(.AnimateInsertion) + options.insert(.AnimateTopItemPosition) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + + var insets = UIEdgeInsets() + insets.top = topInsetForLayout(size: self.listView.bounds.size) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { var topItemOffset: CGFloat? strongSelf.listView.forEachItemNode { itemNode in @@ -144,24 +150,31 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } } + private func topInsetForLayout(size: CGSize) -> CGFloat { + var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5) + + return max(size.height - minimumItemHeights, 0.0) + } + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { var insets = UIEdgeInsets() + insets.top = topInsetForLayout(size: size) transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) var duration: Double = 0.0 var curve: UInt = 0 switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: + case .immediate: break - case .spring: - curve = 7 - } + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } } let listViewCurve: ListViewAnimationCurve diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift new file mode 100644 index 0000000000..f8bde32bac --- /dev/null +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -0,0 +1,106 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +struct PeerMessageHistoryAudioPlaylistItemId: AudioPlaylistItemId { + let id: MessageId + + var hashValue: Int { + return self.id.hashValue + } + + func isEqual(other: AudioPlaylistItemId) -> Bool { + if let other = other as? PeerMessageHistoryAudioPlaylistItemId { + return self.id == other.id + } else { + return false + } + } +} + +private final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { + let entry: MessageHistoryEntry + + var id: AudioPlaylistItemId { + return PeerMessageHistoryAudioPlaylistItemId(id: self.entry.index.id) + } + + var resource: MediaResource? { + switch self.entry { + case let .MessageEntry(message, _, _): + for media in message.media { + if let file = media as? TelegramMediaFile { + return file.resource + } + } + return nil + case .HoleEntry: + return nil + } + } + + var info: AudioPlaylistItemInfo? { + switch self.entry { + case let .MessageEntry(message, _, _): + for media in message.media { + if let file = media as? TelegramMediaFile { + for attribute in file.attributes { + if case let .Audio(isVoice, duration, title, performer, waveform: nil) = attribute { + if isVoice { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) + } else { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) + } + } + } + return nil + } + } + case .HoleEntry: + break + } + + return nil + } + + init(entry: MessageHistoryEntry) { + self.entry = entry + } + + func isEqual(other: AudioPlaylistItem) -> Bool { + if let other = other as? PeerMessageHistoryAudioPlaylistItem { + return self.entry == other.entry + } else { + return false + } + } +} + +struct PeerMessageHistoryAudioPlaylistId: AudioPlaylistId { + let peerId: PeerId + + func isEqual(other: AudioPlaylistId) -> Bool { + if let other = other as? PeerMessageHistoryAudioPlaylistId { + if self.peerId != other.peerId { + return false + } + return true + } else { + return false + } + } +} + +func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> AudioPlaylist { + return AudioPlaylist(id: PeerMessageHistoryAudioPlaylistId(peerId: messageId.peerId), navigate: { item, navigation in + return account.postbox.messageAtId(messageId) + |> map { message -> AudioPlaylistItem? in + if let message = message { + return PeerMessageHistoryAudioPlaylistItem(entry: .MessageEntry(message, false, nil)) + } else { + return nil + } + } + }) +} diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index a7d380687d..65ce50bbab 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -76,7 +76,7 @@ public class PeerMediaCollectionController: ViewController { if let galleryMedia = galleryMedia { if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "audio/mpeg" { - //debugPlayMedia(account: strongSelf.account, file: file) + } else { let gallery = GalleryController(account: strongSelf.account, messageId: id) @@ -186,6 +186,7 @@ public class PeerMediaCollectionController: ViewController { }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in + }, updateInputState: { _ in }) self.controllerInteraction = controllerInteraction @@ -202,6 +203,9 @@ public class PeerMediaCollectionController: ViewController { }, openPeerInfo: { }, togglePeerNotifications: { }, sendContextResult: { _ in + }, sendBotCommand: {_ in + }, beginAudioRecording: { + }, finishAudioRecording: { _ in }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) @@ -267,7 +271,7 @@ public class PeerMediaCollectionController: ViewController { self.containerLayout = layout - self.mediaCollectionDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets in + self.mediaCollectionDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets in self.mediaCollectionDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) }) } diff --git a/TelegramUI/PeerMessageManagedMediaId.swift b/TelegramUI/PeerMessageManagedMediaId.swift new file mode 100644 index 0000000000..3bc5040bc4 --- /dev/null +++ b/TelegramUI/PeerMessageManagedMediaId.swift @@ -0,0 +1,22 @@ +import Foundation +import Postbox + +struct PeerMessageManagedMediaId: ManagedMediaId { + let messageId: MessageId + + init(messageId: MessageId) { + self.messageId = messageId + } + + var hashValue: Int { + return self.messageId.hashValue + } + + func isEqual(to: ManagedMediaId) -> Bool { + if let to = to as? PeerMessageManagedMediaId { + return self.messageId == to.messageId + } else { + return false + } + } +} diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 9efec538d3..955e77ac62 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -104,7 +104,7 @@ public final class PeerSelectionController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.peerSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + self.peerSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func cancelPressed() { diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 2c53e9cdc5..103f17f93c 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -447,7 +447,8 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr context.withFlippedContext { c in c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + //c.setFillColor(UIColor(white: 0.0, alpha: 0.4).cgColor) c.fill(arguments.drawingRect) } @@ -549,6 +550,107 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa } } +private struct ResourceImageSuitableForDisplayDataResult { + +} + +private func resourceImageSuitableForDisplayDatas(account: Account, resourcesOrderedBySize: [MediaResource], displayPixelSize: CGSize) -> Signal { + return .never() +} + +func resourceImageSuitableForDisplay(account: Account, resourcesOrderedBySize: [MediaResource], displayPixelSize: CGSize) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = resourceImageSuitableForDisplayDatas(account: account, resourcesOrderedBySize: resourcesOrderedBySize, displayPixelSize: displayPixelSize) + + return signal |> map { result in + return { arguments in + /*let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if fullSizeComplete { + /*let options = NSMutableDictionary() + options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + }*/ + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } else { + let imageSource = CGImageSourceCreateIncremental(nil) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } + } + + var thumbnailImage: CGImage? + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + thumbnailImage = image + } + + var blurredThumbnailImage: UIImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + //c.setFillColor(UIColor(white: 0.0, alpha: 0.4).cgColor) + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + } + + addCorners(context, arguments: arguments) + + return context + */ + + return nil + } + } +} + func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photo) { return account.postbox.mediaBox.resourceStatus(largestRepresentation.resource) @@ -707,7 +809,8 @@ func chatMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(Tra context.withFlippedContext { c in c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + //c.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor) c.fill(arguments.drawingRect) } diff --git a/TelegramUI/RadialProgressNode.swift b/TelegramUI/RadialProgressNode.swift index e4daeeb4b4..1693d8cd1c 100644 --- a/TelegramUI/RadialProgressNode.swift +++ b/TelegramUI/RadialProgressNode.swift @@ -67,7 +67,7 @@ private class RadialProgressOverlayNode: ASDisplayNode { //CGContextSetLineCap(context, .Round) switch parameters.state { - case .None, .Remote, .Play: + case .None, .Remote, .Play, .Icon: break case let .Fetching(progress): let startAngle = -CGFloat(M_PI_2) @@ -109,6 +109,7 @@ public enum RadialProgressState { case Remote case Fetching(progress: Float) case Play + case Icon } public struct RadialProgressTheme { @@ -162,6 +163,13 @@ class RadialProgressNode: ASControlNode { default: self.setNeedsDisplay() } + case .Icon: + switch self.state { + case .Icon: + break + default: + self.setNeedsDisplay() + } } } } @@ -244,10 +252,28 @@ class RadialProgressNode: ASControlNode { context.addLine(to: CGPoint(x: parameters.diameter / 2.0, y: parameters.diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) context.addLine(to: CGPoint(x: parameters.diameter / 2.0 + arrowHeadSize / 2.0, y: parameters.diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) context.strokePath() - case .Play: + case .Icon: if let icon = parameters.theme.icon { icon.draw(at: CGPoint(x: floor((parameters.diameter - icon.size.width) / 2.0), y: floor((parameters.diameter - icon.size.height) / 2.0))) } + case .Play: + context.setFillColor(parameters.theme.foregroundColor.cgColor) + + let size = CGSize(width: 15.0, height: 18.0) + context.translateBy(x: (parameters.diameter - size.width) / 2.0 + 1.5, y: (parameters.diameter - size.height) / 2.0) + if (parameters.diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 0.8, y: 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") + context.fillPath() + if (parameters.diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(parameters.diameter - size.width) / 2.0 - 1.5, y: -(parameters.diameter - size.height) / 2.0) } } } diff --git a/TelegramUI/ResizeableTextInputView.swift b/TelegramUI/ResizeableTextInputView.swift deleted file mode 100644 index 541643afb7..0000000000 --- a/TelegramUI/ResizeableTextInputView.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation -import UIKit -import Display - -class ResizeableTextInputViewImpl: UITextView { - override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { - super.setContentOffset(contentOffset, animated: false) - } -} - -class ResizeableTextInputView: UIView, UITextViewDelegate { - let textView: ResizeableTextInputViewImpl - let shadowTextView: ResizeableTextInputViewImpl - let placeholderLabel: UILabel - var updateHeight: () -> Void = { } - var maxHeightForLines: CGFloat - var heightForSingleLine: CGFloat - let insets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 4.0, right: 0.0) - - var placeholder: String { - get { - return self.placeholderLabel.text ?? "" - } set(value) { - self.placeholderLabel.text = value - self.placeholderLabel.sizeToFit() - let placeholderSize = self.placeholderLabel.bounds.size - self.placeholderLabel.frame = CGRect(x: 2.0, y: self.insets.top, width: placeholderSize.width, height: placeholderSize.height) - } - } - - init() { - self.textView = ResizeableTextInputViewImpl() - self.textView.layoutManager.allowsNonContiguousLayout = true - self.textView.textContainerInset = UIEdgeInsets(top: 0.0, left: self.insets.left, bottom: 0.0, right: self.insets.right) - self.textView.backgroundColor = UIColor.clear - self.textView.textColor = UIColor.black - self.textView.isOpaque = false - self.textView.font = Font.regular(16.0) - - self.shadowTextView = ResizeableTextInputViewImpl() - self.shadowTextView.font = self.textView.font - self.shadowTextView.textContainerInset = self.textView.textContainerInset - self.shadowTextView.layoutManager.allowsNonContiguousLayout = true - self.shadowTextView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: CGFloat.greatestFiniteMagnitude) - - self.shadowTextView.text = "A" - self.shadowTextView.layoutManager.ensureLayout(for: shadowTextView.textContainer) - let singleLineHeight = ceil(shadowTextView.layoutManager.usedRect(for: shadowTextView.textContainer).size.height) - self.heightForSingleLine = singleLineHeight + 2.0 + self.insets.top + self.insets.bottom - - self.shadowTextView.text = "\n\n\n" - self.shadowTextView.layoutManager.ensureLayout(for: shadowTextView.textContainer) - let maxHeight = ceil(shadowTextView.layoutManager.usedRect(for: shadowTextView.textContainer).size.height) - self.maxHeightForLines = maxHeight + 2.0 + self.insets.top + self.insets.bottom - - self.placeholderLabel = UILabel() - - super.init(frame: CGRect()) - - self.clipsToBounds = true - - self.textView.delegate = self - self.addSubview(textView) - - self.placeholderLabel.font = self.textView.font - self.placeholderLabel.textColor = UIColor(0xbebec0) - self.addSubview(self.placeholderLabel) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func maxHeight() -> CGFloat { - return self.maxHeightForLines - } - - func calculateSizeThatFits(constrainedSize: CGSize) -> CGSize { - self.shadowTextView.frame = CGRect(x: 0.0, y: 0.0, width: constrainedSize.width + 4.0, height: CGFloat.greatestFiniteMagnitude) - self.shadowTextView.text = "\n" - //shadowTextView.layoutManager.ensureLayoutForTextContainer(shadowTextView.textContainer) - self.shadowTextView.text = textView.text - - shadowTextView.layoutManager.glyphRange(for: shadowTextView.textContainer) - let height = ceil(shadowTextView.layoutManager.usedRect(for: shadowTextView.textContainer).size.height) - - return CGSize(width: constrainedSize.width, height: min(height + 2.0 + self.insets.top + self.insets.bottom, self.maxHeight())) - } - - func textViewDidChange(_ textView: UITextView) { - self.placeholderLabel.isHidden = textView.text.startIndex != textView.text.endIndex - self.updateHeight() - } - - override var frame: CGRect { - get { - return super.frame - } set(value) { - super.frame = value - - let heightFix: CGFloat = 25.0 - self.textView.frame = CGRect(x: -4.0, y: -0.5, width: value.size.width + 4.0, height: value.size.height + heightFix - self.insets.bottom) - let distance = -(self.maxHeight() - self.textView.frame.size.height) - self.clipsToBounds = distance > 0.0 - self.textView.contentInset = UIEdgeInsets(top: 2.0 + self.insets.top, left: 0.0, bottom: max(0.0, distance) + self.insets.bottom, right: 0.0) - self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 2.0 + self.insets.top, left: 0.0, bottom: max(0.0, distance) + self.insets.bottom, right: -2.0) - - let placeholderSize = self.placeholderLabel.bounds.size - self.placeholderLabel.frame = CGRect(x: 1.0, y: self.insets.top + 2.0, width: placeholderSize.width, height: placeholderSize.height) - } - } -} diff --git a/TelegramUI/TGDataItem.h b/TelegramUI/TGDataItem.h new file mode 100644 index 0000000000..1d1cb73b14 --- /dev/null +++ b/TelegramUI/TGDataItem.h @@ -0,0 +1,9 @@ +#import + +@interface TGDataItem : NSObject + +- (void)appendData:(NSData *)data; +- (NSData *)data; + +@end + diff --git a/TelegramUI/TGDataItem.m b/TelegramUI/TGDataItem.m new file mode 100644 index 0000000000..edd66a9d5a --- /dev/null +++ b/TelegramUI/TGDataItem.m @@ -0,0 +1,27 @@ +#import "TGDataItem.h" + +@interface TGDataItem () { + NSMutableData *_data; +} + +@end + +@implementation TGDataItem + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _data = [[NSMutableData alloc] init]; + } + return self; +} + +- (void)appendData:(NSData *)data { + [_data appendData:data]; +} + +- (NSData *)data { + return _data; +} + +@end diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 580c5d3f6e..ea3fde1fab 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -1,9 +1,16 @@ import Foundation +import SwiftSignalKit +import UIKit public final class TelegramApplicationContext { public let openUrl: (String) -> Void + public let getTopWindow: () -> UIWindow? - public init(openUrl: @escaping (String) -> Void) { + let sharedChatMediaInputNode = Atomic(value: nil) + let mediaManager = MediaManager() + + public init(openUrl: @escaping (String) -> Void, getTopWindow: @escaping () -> UIWindow?) { self.openUrl = openUrl + self.getTopWindow = getTopWindow } } diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift new file mode 100644 index 0000000000..932ffc3e55 --- /dev/null +++ b/TelegramUI/TelegramController.swift @@ -0,0 +1,76 @@ +import Foundation +import Display +import TelegramCore +import SwiftSignalKit + +public class TelegramController: ViewController { + private let account: Account + + private var mediaStatusDisposable: Disposable? + + private var playlistState: AudioPlaylistState? + private var mediaAccessoryPanel: MediaNavigationAccessoryPanel? + + override public var navigationHeight: CGFloat { + var height = super.navigationHeight + if let mediaAccessoryPanel = self.mediaAccessoryPanel { + height += 36.0 + } + return height + } + + init(account: Account) { + self.account = account + + super.init(navigationBar: NavigationBar()) + + if let applicationContext = account.applicationContext as? TelegramApplicationContext { + self.mediaStatusDisposable = (applicationContext.mediaManager.playlistPlayerState + |> deliverOnMainQueue).start(next: { [weak self] playlistState in + if let strongSelf = self, strongSelf.playlistState != playlistState { + strongSelf.playlistState = playlistState + strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + }) + } + } + + deinit { + self.mediaStatusDisposable?.dispose() + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if let playlistState = playlistState { + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: self.navigationBar.frame.maxY), size: CGSize(width: layout.size.width, height: 36.0)) + if let mediaAccessoryPanel = self.mediaAccessoryPanel { + transition.updateFrame(node: mediaAccessoryPanel, frame: panelFrame) + mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) + } else { + let mediaAccessoryPanel = MediaNavigationAccessoryPanel() + mediaAccessoryPanel.close = { [weak self] in + if let strongSelf = self { + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + applicationContext.mediaManager.setPlaylistPlayer(nil) + } + } + } + mediaAccessoryPanel.frame = panelFrame + self.displayNode.insertSubnode(mediaAccessoryPanel, belowSubnode: self.navigationBar) + self.mediaAccessoryPanel = mediaAccessoryPanel + mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: .immediate) + mediaAccessoryPanel.animateIn(transition: transition) + } + } else if let mediaAccessoryPanel = self.mediaAccessoryPanel { + self.mediaAccessoryPanel = nil + mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak mediaAccessoryPanel] in + mediaAccessoryPanel?.removeFromSupernode() + }) + } + } +} diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index 7e3eadc98c..77e67638e9 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -5,6 +5,8 @@ module TelegramUIPrivateModule { header "../../third-party/FFmpeg-iOS/include/libavutil/avutil.h" header "../../third-party/FFmpeg-iOS/include/libavutil/pixdesc.h" header "../../third-party/FFmpeg-iOS/include/libswresample/swresample.h" + header "../../third-party/opusenc/opusenc.h" + header "../TGDataItem.h" header "../FFMpegSwResample.h" header "../FastBlur.h" header "../UIImage+WebP.h" diff --git a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift index 54bd371b82..83f0957ddd 100644 --- a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift @@ -65,8 +65,9 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.stackFromBottomInsetItemFactor = 3.5 + self.listView.keepBottomItemOverscrollBackground = true self.listView.limitHitTestToNodes = true + self.listView.isHidden = true super.init(account: account) @@ -118,12 +119,18 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex var options = ListViewDeleteAndInsertOptions() if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) + //options.insert(.Synchronous) + //options.insert(.LowLatency) } else { - //options.insert(.AnimateInsertion) + options.insert(.AnimateTopItemPosition) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + + var insets = UIEdgeInsets() + insets.top = topInsetForLayout(size: self.listView.bounds.size) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { var topItemOffset: CGFloat? strongSelf.listView.forEachItemNode { itemNode in @@ -136,29 +143,38 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex let position = strongSelf.listView.layer.position strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } + + strongSelf.listView.isHidden = false } }) } } + private func topInsetForLayout(size: CGSize) -> CGFloat { + var minimumItemHeights: CGFloat = floor(VerticalListContextResultsChatInputPanelItemNode.itemHeight * 3.5) + + return max(size.height - minimumItemHeights, 0.0) + } + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { var insets = UIEdgeInsets() + insets.top = self.topInsetForLayout(size: size) transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) var duration: Double = 0.0 var curve: UInt = 0 switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: + case .immediate: break - case .spring: - curve = 7 - } + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } } let listViewCurve: ListViewAnimationCurve diff --git a/third-party/FFmpeg-iOS/lib/libavcodec.a b/third-party/FFmpeg-iOS/lib/libavcodec.a index e7c6e75d19..39507dfbba 100644 Binary files a/third-party/FFmpeg-iOS/lib/libavcodec.a and b/third-party/FFmpeg-iOS/lib/libavcodec.a differ diff --git a/third-party/FFmpeg-iOS/lib/libavformat.a b/third-party/FFmpeg-iOS/lib/libavformat.a index 17f3aae87e..93bc45eaf0 100644 Binary files a/third-party/FFmpeg-iOS/lib/libavformat.a and b/third-party/FFmpeg-iOS/lib/libavformat.a differ diff --git a/third-party/FFmpeg-iOS/lib/libavutil.a b/third-party/FFmpeg-iOS/lib/libavutil.a index f4c347ce91..ddbc4186c7 100644 Binary files a/third-party/FFmpeg-iOS/lib/libavutil.a and b/third-party/FFmpeg-iOS/lib/libavutil.a differ diff --git a/third-party/ogg/ogg/bitwise.c b/third-party/ogg/ogg/bitwise.c new file mode 100644 index 0000000000..68aca67548 --- /dev/null +++ b/third-party/ogg/ogg/bitwise.c @@ -0,0 +1,857 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE Ogg CONTAINER SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2010 * + * by the Xiph.Org Foundation http://www.xiph.org/ * + * * + ******************************************************************** + + function: packing variable sized words into an octet stream + last mod: $Id: bitwise.c 18051 2011-08-04 17:56:39Z giles $ + + ********************************************************************/ + +/* We're 'LSb' endian; if we write a word but read individual bits, + then we'll read the lsb first */ + +#include +#include +#include +#include + +#define BUFFER_INCREMENT 256 + +static const unsigned long mask[]= +{0x00000000,0x00000001,0x00000003,0x00000007,0x0000000f, + 0x0000001f,0x0000003f,0x0000007f,0x000000ff,0x000001ff, + 0x000003ff,0x000007ff,0x00000fff,0x00001fff,0x00003fff, + 0x00007fff,0x0000ffff,0x0001ffff,0x0003ffff,0x0007ffff, + 0x000fffff,0x001fffff,0x003fffff,0x007fffff,0x00ffffff, + 0x01ffffff,0x03ffffff,0x07ffffff,0x0fffffff,0x1fffffff, + 0x3fffffff,0x7fffffff,0xffffffff }; + +static const unsigned int mask8B[]= +{0x00,0x80,0xc0,0xe0,0xf0,0xf8,0xfc,0xfe,0xff}; + +void oggpack_writeinit(oggpack_buffer *b){ + memset(b,0,sizeof(*b)); + b->ptr=b->buffer=_ogg_malloc(BUFFER_INCREMENT); + b->buffer[0]='\0'; + b->storage=BUFFER_INCREMENT; +} + +void oggpackB_writeinit(oggpack_buffer *b){ + oggpack_writeinit(b); +} + +int oggpack_writecheck(oggpack_buffer *b){ + if(!b->ptr || !b->storage)return -1; + return 0; +} + +int oggpackB_writecheck(oggpack_buffer *b){ + return oggpack_writecheck(b); +} + +void oggpack_writetrunc(oggpack_buffer *b,long bits){ + long bytes=bits>>3; + if(b->ptr){ + bits-=bytes*8; + b->ptr=b->buffer+bytes; + b->endbit=bits; + b->endbyte=bytes; + *b->ptr&=mask[bits]; + } +} + +void oggpackB_writetrunc(oggpack_buffer *b,long bits){ + long bytes=bits>>3; + if(b->ptr){ + bits-=bytes*8; + b->ptr=b->buffer+bytes; + b->endbit=bits; + b->endbyte=bytes; + *b->ptr&=mask8B[bits]; + } +} + +/* Takes only up to 32 bits. */ +void oggpack_write(oggpack_buffer *b,unsigned long value,int bits){ + if(bits<0 || bits>32) goto err; + if(b->endbyte>=b->storage-4){ + void *ret; + if(!b->ptr)return; + if(b->storage>LONG_MAX-BUFFER_INCREMENT) goto err; + ret=_ogg_realloc(b->buffer,b->storage+BUFFER_INCREMENT); + if(!ret) goto err; + b->buffer=ret; + b->storage+=BUFFER_INCREMENT; + b->ptr=b->buffer+b->endbyte; + } + + value&=mask[bits]; + bits+=b->endbit; + + b->ptr[0]|=value<endbit; + + if(bits>=8){ + b->ptr[1]=(unsigned char)(value>>(8-b->endbit)); + if(bits>=16){ + b->ptr[2]=(unsigned char)(value>>(16-b->endbit)); + if(bits>=24){ + b->ptr[3]=(unsigned char)(value>>(24-b->endbit)); + if(bits>=32){ + if(b->endbit) + b->ptr[4]=(unsigned char)(value>>(32-b->endbit)); + else + b->ptr[4]=0; + } + } + } + } + + b->endbyte+=bits/8; + b->ptr+=bits/8; + b->endbit=bits&7; + return; + err: + oggpack_writeclear(b); +} + +/* Takes only up to 32 bits. */ +void oggpackB_write(oggpack_buffer *b,unsigned long value,int bits){ + if(bits<0 || bits>32) goto err; + if(b->endbyte>=b->storage-4){ + void *ret; + if(!b->ptr)return; + if(b->storage>LONG_MAX-BUFFER_INCREMENT) goto err; + ret=_ogg_realloc(b->buffer,b->storage+BUFFER_INCREMENT); + if(!ret) goto err; + b->buffer=ret; + b->storage+=BUFFER_INCREMENT; + b->ptr=b->buffer+b->endbyte; + } + + value=(value&mask[bits])<<(32-bits); + bits+=b->endbit; + + b->ptr[0]|=value>>(24+b->endbit); + + if(bits>=8){ + b->ptr[1]=(unsigned char)(value>>(16+b->endbit)); + if(bits>=16){ + b->ptr[2]=(unsigned char)(value>>(8+b->endbit)); + if(bits>=24){ + b->ptr[3]=(unsigned char)(value>>(b->endbit)); + if(bits>=32){ + if(b->endbit) + b->ptr[4]=(unsigned char)(value<<(8-b->endbit)); + else + b->ptr[4]=0; + } + } + } + } + + b->endbyte+=bits/8; + b->ptr+=bits/8; + b->endbit=bits&7; + return; + err: + oggpack_writeclear(b); +} + +void oggpack_writealign(oggpack_buffer *b){ + int bits=8-b->endbit; + if(bits<8) + oggpack_write(b,0,bits); +} + +void oggpackB_writealign(oggpack_buffer *b){ + int bits=8-b->endbit; + if(bits<8) + oggpackB_write(b,0,bits); +} + +static void oggpack_writecopy_helper(oggpack_buffer *b, + void *source, + long bits, + void (*w)(oggpack_buffer *, + unsigned long, + int), + int msb){ + unsigned char *ptr=(unsigned char *)source; + + long bytes=bits/8; + bits-=bytes*8; + + if(b->endbit){ + int i; + /* unaligned copy. Do it the hard way. */ + for(i=0;iendbyte+bytes+1>=b->storage){ + void *ret; + if(!b->ptr) goto err; + if(b->endbyte+bytes+BUFFER_INCREMENT>b->storage) goto err; + b->storage=b->endbyte+bytes+BUFFER_INCREMENT; + ret=_ogg_realloc(b->buffer,b->storage); + if(!ret) goto err; + b->buffer=ret; + b->ptr=b->buffer+b->endbyte; + } + + memmove(b->ptr,source,bytes); + b->ptr+=bytes; + b->endbyte+=bytes; + *b->ptr=0; + + } + if(bits){ + if(msb) + w(b,(unsigned long)(ptr[bytes]>>(8-bits)),bits); + else + w(b,(unsigned long)(ptr[bytes]),bits); + } + return; + err: + oggpack_writeclear(b); +} + +void oggpack_writecopy(oggpack_buffer *b,void *source,long bits){ + oggpack_writecopy_helper(b,source,bits,oggpack_write,0); +} + +void oggpackB_writecopy(oggpack_buffer *b,void *source,long bits){ + oggpack_writecopy_helper(b,source,bits,oggpackB_write,1); +} + +void oggpack_reset(oggpack_buffer *b){ + if(!b->ptr)return; + b->ptr=b->buffer; + b->buffer[0]=0; + b->endbit=b->endbyte=0; +} + +void oggpackB_reset(oggpack_buffer *b){ + oggpack_reset(b); +} + +void oggpack_writeclear(oggpack_buffer *b){ + if(b->buffer)_ogg_free(b->buffer); + memset(b,0,sizeof(*b)); +} + +void oggpackB_writeclear(oggpack_buffer *b){ + oggpack_writeclear(b); +} + +void oggpack_readinit(oggpack_buffer *b,unsigned char *buf,int bytes){ + memset(b,0,sizeof(*b)); + b->buffer=b->ptr=buf; + b->storage=bytes; +} + +void oggpackB_readinit(oggpack_buffer *b,unsigned char *buf,int bytes){ + oggpack_readinit(b,buf,bytes); +} + +/* Read in bits without advancing the bitptr; bits <= 32 */ +long oggpack_look(oggpack_buffer *b,int bits){ + unsigned long ret; + unsigned long m; + + if(bits<0 || bits>32) return -1; + m=mask[bits]; + bits+=b->endbit; + + if(b->endbyte >= b->storage-4){ + /* not the main path */ + if(b->endbyte > b->storage-((bits+7)>>3)) return -1; + /* special case to avoid reading b->ptr[0], which might be past the end of + the buffer; also skips some useless accounting */ + else if(!bits)return(0L); + } + + ret=b->ptr[0]>>b->endbit; + if(bits>8){ + ret|=b->ptr[1]<<(8-b->endbit); + if(bits>16){ + ret|=b->ptr[2]<<(16-b->endbit); + if(bits>24){ + ret|=b->ptr[3]<<(24-b->endbit); + if(bits>32 && b->endbit) + ret|=b->ptr[4]<<(32-b->endbit); + } + } + } + return(m&ret); +} + +/* Read in bits without advancing the bitptr; bits <= 32 */ +long oggpackB_look(oggpack_buffer *b,int bits){ + unsigned long ret; + int m=32-bits; + + if(m<0 || m>32) return -1; + bits+=b->endbit; + + if(b->endbyte >= b->storage-4){ + /* not the main path */ + if(b->endbyte > b->storage-((bits+7)>>3)) return -1; + /* special case to avoid reading b->ptr[0], which might be past the end of + the buffer; also skips some useless accounting */ + else if(!bits)return(0L); + } + + ret=b->ptr[0]<<(24+b->endbit); + if(bits>8){ + ret|=b->ptr[1]<<(16+b->endbit); + if(bits>16){ + ret|=b->ptr[2]<<(8+b->endbit); + if(bits>24){ + ret|=b->ptr[3]<<(b->endbit); + if(bits>32 && b->endbit) + ret|=b->ptr[4]>>(8-b->endbit); + } + } + } + return ((ret&0xffffffff)>>(m>>1))>>((m+1)>>1); +} + +long oggpack_look1(oggpack_buffer *b){ + if(b->endbyte>=b->storage)return(-1); + return((b->ptr[0]>>b->endbit)&1); +} + +long oggpackB_look1(oggpack_buffer *b){ + if(b->endbyte>=b->storage)return(-1); + return((b->ptr[0]>>(7-b->endbit))&1); +} + +void oggpack_adv(oggpack_buffer *b,int bits){ + bits+=b->endbit; + + if(b->endbyte > b->storage-((bits+7)>>3)) goto overflow; + + b->ptr+=bits/8; + b->endbyte+=bits/8; + b->endbit=bits&7; + return; + + overflow: + b->ptr=NULL; + b->endbyte=b->storage; + b->endbit=1; +} + +void oggpackB_adv(oggpack_buffer *b,int bits){ + oggpack_adv(b,bits); +} + +void oggpack_adv1(oggpack_buffer *b){ + if(++(b->endbit)>7){ + b->endbit=0; + b->ptr++; + b->endbyte++; + } +} + +void oggpackB_adv1(oggpack_buffer *b){ + oggpack_adv1(b); +} + +/* bits <= 32 */ +long oggpack_read(oggpack_buffer *b,int bits){ + long ret; + unsigned long m; + + if(bits<0 || bits>32) goto err; + m=mask[bits]; + bits+=b->endbit; + + if(b->endbyte >= b->storage-4){ + /* not the main path */ + if(b->endbyte > b->storage-((bits+7)>>3)) goto overflow; + /* special case to avoid reading b->ptr[0], which might be past the end of + the buffer; also skips some useless accounting */ + else if(!bits)return(0L); + } + + ret=b->ptr[0]>>b->endbit; + if(bits>8){ + ret|=b->ptr[1]<<(8-b->endbit); + if(bits>16){ + ret|=b->ptr[2]<<(16-b->endbit); + if(bits>24){ + ret|=b->ptr[3]<<(24-b->endbit); + if(bits>32 && b->endbit){ + ret|=b->ptr[4]<<(32-b->endbit); + } + } + } + } + ret&=m; + b->ptr+=bits/8; + b->endbyte+=bits/8; + b->endbit=bits&7; + return ret; + + overflow: + err: + b->ptr=NULL; + b->endbyte=b->storage; + b->endbit=1; + return -1L; +} + +/* bits <= 32 */ +long oggpackB_read(oggpack_buffer *b,int bits){ + long ret; + long m=32-bits; + + if(m<0 || m>32) goto err; + bits+=b->endbit; + + if(b->endbyte+4>=b->storage){ + /* not the main path */ + if(b->endbyte > b->storage-((bits+7)>>3)) goto overflow; + /* special case to avoid reading b->ptr[0], which might be past the end of + the buffer; also skips some useless accounting */ + else if(!bits)return(0L); + } + + ret=b->ptr[0]<<(24+b->endbit); + if(bits>8){ + ret|=b->ptr[1]<<(16+b->endbit); + if(bits>16){ + ret|=b->ptr[2]<<(8+b->endbit); + if(bits>24){ + ret|=b->ptr[3]<<(b->endbit); + if(bits>32 && b->endbit) + ret|=b->ptr[4]>>(8-b->endbit); + } + } + } + ret=((ret&0xffffffffUL)>>(m>>1))>>((m+1)>>1); + + b->ptr+=bits/8; + b->endbyte+=bits/8; + b->endbit=bits&7; + return ret; + + overflow: + err: + b->ptr=NULL; + b->endbyte=b->storage; + b->endbit=1; + return -1L; +} + +long oggpack_read1(oggpack_buffer *b){ + long ret; + + if(b->endbyte >= b->storage) goto overflow; + ret=(b->ptr[0]>>b->endbit)&1; + + b->endbit++; + if(b->endbit>7){ + b->endbit=0; + b->ptr++; + b->endbyte++; + } + return ret; + + overflow: + b->ptr=NULL; + b->endbyte=b->storage; + b->endbit=1; + return -1L; +} + +long oggpackB_read1(oggpack_buffer *b){ + long ret; + + if(b->endbyte >= b->storage) goto overflow; + ret=(b->ptr[0]>>(7-b->endbit))&1; + + b->endbit++; + if(b->endbit>7){ + b->endbit=0; + b->ptr++; + b->endbyte++; + } + return ret; + + overflow: + b->ptr=NULL; + b->endbyte=b->storage; + b->endbit=1; + return -1L; +} + +long oggpack_bytes(oggpack_buffer *b){ + return(b->endbyte+(b->endbit+7)/8); +} + +long oggpack_bits(oggpack_buffer *b){ + return(b->endbyte*8+b->endbit); +} + +long oggpackB_bytes(oggpack_buffer *b){ + return oggpack_bytes(b); +} + +long oggpackB_bits(oggpack_buffer *b){ + return oggpack_bits(b); +} + +unsigned char *oggpack_get_buffer(oggpack_buffer *b){ + return(b->buffer); +} + +unsigned char *oggpackB_get_buffer(oggpack_buffer *b){ + return oggpack_get_buffer(b); +} + +/* Self test of the bitwise routines; everything else is based on + them, so they damned well better be solid. */ + +#ifdef _V_SELFTEST +#include + +static int ilog(unsigned int v){ + int ret=0; + while(v){ + ret++; + v>>=1; + } + return(ret); +} + +oggpack_buffer o; +oggpack_buffer r; + +void report(char *in){ + fprintf(stderr,"%s",in); + exit(1); +} + +void cliptest(unsigned long *b,int vals,int bits,int *comp,int compsize){ + long bytes,i; + unsigned char *buffer; + + oggpack_reset(&o); + for(i=0;i +#include +#include +#include + +/* A complete description of Ogg framing exists in docs/framing.html */ + +int ogg_page_version(const ogg_page *og){ + return((int)(og->header[4])); +} + +int ogg_page_continued(const ogg_page *og){ + return((int)(og->header[5]&0x01)); +} + +int ogg_page_bos(const ogg_page *og){ + return((int)(og->header[5]&0x02)); +} + +int ogg_page_eos(const ogg_page *og){ + return((int)(og->header[5]&0x04)); +} + +ogg_int64_t ogg_page_granulepos(const ogg_page *og){ + unsigned char *page=og->header; + ogg_int64_t granulepos=page[13]&(0xff); + granulepos= (granulepos<<8)|(page[12]&0xff); + granulepos= (granulepos<<8)|(page[11]&0xff); + granulepos= (granulepos<<8)|(page[10]&0xff); + granulepos= (granulepos<<8)|(page[9]&0xff); + granulepos= (granulepos<<8)|(page[8]&0xff); + granulepos= (granulepos<<8)|(page[7]&0xff); + granulepos= (granulepos<<8)|(page[6]&0xff); + return(granulepos); +} + +int ogg_page_serialno(const ogg_page *og){ + return(og->header[14] | + (og->header[15]<<8) | + (og->header[16]<<16) | + (og->header[17]<<24)); +} + +long ogg_page_pageno(const ogg_page *og){ + return(og->header[18] | + (og->header[19]<<8) | + (og->header[20]<<16) | + (og->header[21]<<24)); +} + + + +/* returns the number of packets that are completed on this page (if + the leading packet is begun on a previous page, but ends on this + page, it's counted */ + +/* NOTE: + If a page consists of a packet begun on a previous page, and a new + packet begun (but not completed) on this page, the return will be: + ogg_page_packets(page) ==1, + ogg_page_continued(page) !=0 + + If a page happens to be a single packet that was begun on a + previous page, and spans to the next page (in the case of a three or + more page packet), the return will be: + ogg_page_packets(page) ==0, + ogg_page_continued(page) !=0 +*/ + +int ogg_page_packets(const ogg_page *og){ + int i,n=og->header[26],count=0; + for(i=0;iheader[27+i]<255)count++; + return(count); +} + + +#if 0 +/* helper to initialize lookup for direct-table CRC (illustrative; we + use the static init below) */ + +static ogg_uint32_t _ogg_crc_entry(unsigned long index){ + int i; + unsigned long r; + + r = index << 24; + for (i=0; i<8; i++) + if (r & 0x80000000UL) + r = (r << 1) ^ 0x04c11db7; /* The same as the ethernet generator + polynomial, although we use an + unreflected alg and an init/final + of 0, not 0xffffffff */ + else + r<<=1; + return (r & 0xffffffffUL); +} +#endif + +static const ogg_uint32_t crc_lookup[256]={ + 0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9, + 0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005, + 0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61, + 0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd, + 0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9, + 0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75, + 0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011, + 0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd, + 0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039, + 0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5, + 0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81, + 0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d, + 0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49, + 0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95, + 0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1, + 0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d, + 0x34867077,0x30476dc0,0x3d044b19,0x39c556ae, + 0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072, + 0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16, + 0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca, + 0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde, + 0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02, + 0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066, + 0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba, + 0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e, + 0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692, + 0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6, + 0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a, + 0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e, + 0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2, + 0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686, + 0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a, + 0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637, + 0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb, + 0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f, + 0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53, + 0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47, + 0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b, + 0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff, + 0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623, + 0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7, + 0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b, + 0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f, + 0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3, + 0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7, + 0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b, + 0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f, + 0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3, + 0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640, + 0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c, + 0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8, + 0x68860bfd,0x6c47164a,0x61043093,0x65c52d24, + 0x119b4be9,0x155a565e,0x18197087,0x1cd86d30, + 0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec, + 0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088, + 0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654, + 0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0, + 0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c, + 0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18, + 0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4, + 0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0, + 0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c, + 0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668, + 0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4}; + +/* init the encode/decode logical stream state */ + +int ogg_stream_init(ogg_stream_state *os,int serialno){ + if(os){ + memset(os,0,sizeof(*os)); + os->body_storage=16*1024; + os->lacing_storage=1024; + + os->body_data=_ogg_malloc(os->body_storage*sizeof(*os->body_data)); + os->lacing_vals=_ogg_malloc(os->lacing_storage*sizeof(*os->lacing_vals)); + os->granule_vals=_ogg_malloc(os->lacing_storage*sizeof(*os->granule_vals)); + + if(!os->body_data || !os->lacing_vals || !os->granule_vals){ + ogg_stream_clear(os); + return -1; + } + + os->serialno=serialno; + + return(0); + } + return(-1); +} + +/* async/delayed error detection for the ogg_stream_state */ +int ogg_stream_check(ogg_stream_state *os){ + if(!os || !os->body_data) return -1; + return 0; +} + +/* _clear does not free os, only the non-flat storage within */ +int ogg_stream_clear(ogg_stream_state *os){ + if(os){ + if(os->body_data)_ogg_free(os->body_data); + if(os->lacing_vals)_ogg_free(os->lacing_vals); + if(os->granule_vals)_ogg_free(os->granule_vals); + + memset(os,0,sizeof(*os)); + } + return(0); +} + +int ogg_stream_destroy(ogg_stream_state *os){ + if(os){ + ogg_stream_clear(os); + _ogg_free(os); + } + return(0); +} + +/* Helpers for ogg_stream_encode; this keeps the structure and + what's happening fairly clear */ + +static int _os_body_expand(ogg_stream_state *os,long needed){ + if(os->body_storage-needed<=os->body_fill){ + long body_storage; + void *ret; + if(os->body_storage>LONG_MAX-needed){ + ogg_stream_clear(os); + return -1; + } + body_storage=os->body_storage+needed; + if(body_storagebody_data,body_storage*sizeof(*os->body_data)); + if(!ret){ + ogg_stream_clear(os); + return -1; + } + os->body_storage=body_storage; + os->body_data=ret; + } + return 0; +} + +static int _os_lacing_expand(ogg_stream_state *os,long needed){ + if(os->lacing_storage-needed<=os->lacing_fill){ + long lacing_storage; + void *ret; + if(os->lacing_storage>LONG_MAX-needed){ + ogg_stream_clear(os); + return -1; + } + lacing_storage=os->lacing_storage+needed; + if(lacing_storagelacing_vals,lacing_storage*sizeof(*os->lacing_vals)); + if(!ret){ + ogg_stream_clear(os); + return -1; + } + os->lacing_vals=ret; + ret=_ogg_realloc(os->granule_vals,lacing_storage* + sizeof(*os->granule_vals)); + if(!ret){ + ogg_stream_clear(os); + return -1; + } + os->granule_vals=ret; + os->lacing_storage=lacing_storage; + } + return 0; +} + +/* checksum the page */ +/* Direct table CRC; note that this will be faster in the future if we + perform the checksum simultaneously with other copies */ + +void ogg_page_checksum_set(ogg_page *og){ + if(og){ + ogg_uint32_t crc_reg=0; + int i; + + /* safety; needed for API behavior, but not framing code */ + og->header[22]=0; + og->header[23]=0; + og->header[24]=0; + og->header[25]=0; + + for(i=0;iheader_len;i++) + crc_reg=(crc_reg<<8)^crc_lookup[((crc_reg >> 24)&0xff)^og->header[i]]; + for(i=0;ibody_len;i++) + crc_reg=(crc_reg<<8)^crc_lookup[((crc_reg >> 24)&0xff)^og->body[i]]; + + og->header[22]=(unsigned char)(crc_reg&0xff); + og->header[23]=(unsigned char)((crc_reg>>8)&0xff); + og->header[24]=(unsigned char)((crc_reg>>16)&0xff); + og->header[25]=(unsigned char)((crc_reg>>24)&0xff); + } +} + +/* submit data to the internal buffer of the framing engine */ +int ogg_stream_iovecin(ogg_stream_state *os, ogg_iovec_t *iov, int count, + long e_o_s, ogg_int64_t granulepos){ + + long bytes = 0, lacing_vals; + int i; + + if(ogg_stream_check(os)) return -1; + if(!iov) return 0; + + for (i = 0; i < count; ++i){ + if(iov[i].iov_len>LONG_MAX) return -1; + if(bytes>LONG_MAX-(long)iov[i].iov_len) return -1; + bytes += (long)iov[i].iov_len; + } + lacing_vals=bytes/255+1; + + if(os->body_returned){ + /* advance packet data according to the body_returned pointer. We + had to keep it around to return a pointer into the buffer last + call */ + + os->body_fill-=os->body_returned; + if(os->body_fill) + memmove(os->body_data,os->body_data+os->body_returned, + os->body_fill); + os->body_returned=0; + } + + /* make sure we have the buffer storage */ + if(_os_body_expand(os,bytes) || _os_lacing_expand(os,lacing_vals)) + return -1; + + /* Copy in the submitted packet. Yes, the copy is a waste; this is + the liability of overly clean abstraction for the time being. It + will actually be fairly easy to eliminate the extra copy in the + future */ + + for (i = 0; i < count; ++i) { + memcpy(os->body_data+os->body_fill, iov[i].iov_base, iov[i].iov_len); + os->body_fill += (int)iov[i].iov_len; + } + + /* Store lacing vals for this packet */ + for(i=0;ilacing_vals[os->lacing_fill+i]=255; + os->granule_vals[os->lacing_fill+i]=os->granulepos; + } + os->lacing_vals[os->lacing_fill+i]=bytes%255; + os->granulepos=os->granule_vals[os->lacing_fill+i]=granulepos; + + /* flag the first segment as the beginning of the packet */ + os->lacing_vals[os->lacing_fill]|= 0x100; + + os->lacing_fill+=lacing_vals; + + /* for the sake of completeness */ + os->packetno++; + + if(e_o_s)os->e_o_s=1; + + return(0); +} + +int ogg_stream_packetin(ogg_stream_state *os,ogg_packet *op){ + ogg_iovec_t iov; + iov.iov_base = op->packet; + iov.iov_len = op->bytes; + return ogg_stream_iovecin(os, &iov, 1, op->e_o_s, op->granulepos); +} + +/* Conditionally flush a page; force==0 will only flush nominal-size + pages, force==1 forces us to flush a page regardless of page size + so long as there's any data available at all. */ +static int ogg_stream_flush_i(ogg_stream_state *os,ogg_page *og, int force, int nfill){ + int i; + int vals=0; + int maxvals=(os->lacing_fill>255?255:os->lacing_fill); + int bytes=0; + long acc=0; + ogg_int64_t granule_pos=-1; + + if(ogg_stream_check(os)) return(0); + if(maxvals==0) return(0); + + /* construct a page */ + /* decide how many segments to include */ + + /* If this is the initial header case, the first page must only include + the initial header packet */ + if(os->b_o_s==0){ /* 'initial header page' case */ + granule_pos=0; + for(vals=0;valslacing_vals[vals]&0x0ff)<255){ + vals++; + break; + } + } + }else{ + + /* The extra packets_done, packet_just_done logic here attempts to do two things: + 1) Don't unneccessarily span pages. + 2) Unless necessary, don't flush pages if there are less than four packets on + them; this expands page size to reduce unneccessary overhead if incoming packets + are large. + These are not necessary behaviors, just 'always better than naive flushing' + without requiring an application to explicitly request a specific optimized + behavior. We'll want an explicit behavior setup pathway eventually as well. */ + + int packets_done=0; + int packet_just_done=0; + for(vals=0;valsnfill && packet_just_done>=4){ + force=1; + break; + } + acc+=os->lacing_vals[vals]&0x0ff; + if((os->lacing_vals[vals]&0xff)<255){ + granule_pos=os->granule_vals[vals]; + packet_just_done=++packets_done; + }else + packet_just_done=0; + } + if(vals==255)force=1; + } + + if(!force) return(0); + + /* construct the header in temp storage */ + memcpy(os->header,"OggS",4); + + /* stream structure version */ + os->header[4]=0x00; + + /* continued packet flag? */ + os->header[5]=0x00; + if((os->lacing_vals[0]&0x100)==0)os->header[5]|=0x01; + /* first page flag? */ + if(os->b_o_s==0)os->header[5]|=0x02; + /* last page flag? */ + if(os->e_o_s && os->lacing_fill==vals)os->header[5]|=0x04; + os->b_o_s=1; + + /* 64 bits of PCM position */ + for(i=6;i<14;i++){ + os->header[i]=(unsigned char)(granule_pos&0xff); + granule_pos>>=8; + } + + /* 32 bits of stream serial number */ + { + long serialno=os->serialno; + for(i=14;i<18;i++){ + os->header[i]=(unsigned char)(serialno&0xff); + serialno>>=8; + } + } + + /* 32 bits of page counter (we have both counter and page header + because this val can roll over) */ + if(os->pageno==-1)os->pageno=0; /* because someone called + stream_reset; this would be a + strange thing to do in an + encode stream, but it has + plausible uses */ + { + long pageno=os->pageno++; + for(i=18;i<22;i++){ + os->header[i]=(unsigned char)(pageno&0xff); + pageno>>=8; + } + } + + /* zero for computation; filled in later */ + os->header[22]=0; + os->header[23]=0; + os->header[24]=0; + os->header[25]=0; + + /* segment table */ + os->header[26]=(unsigned char)(vals&0xff); + for(i=0;iheader[i+27]=(unsigned char)(os->lacing_vals[i]&0xff); + + /* set pointers in the ogg_page struct */ + og->header=os->header; + og->header_len=os->header_fill=vals+27; + og->body=os->body_data+os->body_returned; + og->body_len=bytes; + + /* advance the lacing data and set the body_returned pointer */ + + os->lacing_fill-=vals; + memmove(os->lacing_vals,os->lacing_vals+vals,os->lacing_fill*sizeof(*os->lacing_vals)); + memmove(os->granule_vals,os->granule_vals+vals,os->lacing_fill*sizeof(*os->granule_vals)); + os->body_returned+=bytes; + + /* calculate the checksum */ + + ogg_page_checksum_set(og); + + /* done */ + return(1); +} + +/* This will flush remaining packets into a page (returning nonzero), + even if there is not enough data to trigger a flush normally + (undersized page). If there are no packets or partial packets to + flush, ogg_stream_flush returns 0. Note that ogg_stream_flush will + try to flush a normal sized page like ogg_stream_pageout; a call to + ogg_stream_flush does not guarantee that all packets have flushed. + Only a return value of 0 from ogg_stream_flush indicates all packet + data is flushed into pages. + + since ogg_stream_flush will flush the last page in a stream even if + it's undersized, you almost certainly want to use ogg_stream_pageout + (and *not* ogg_stream_flush) unless you specifically need to flush + a page regardless of size in the middle of a stream. */ + +int ogg_stream_flush(ogg_stream_state *os,ogg_page *og){ + return ogg_stream_flush_i(os,og,1,4096); +} + +/* Like the above, but an argument is provided to adjust the nominal + page size for applications which are smart enough to provide their + own delay based flushing */ + +int ogg_stream_flush_fill(ogg_stream_state *os,ogg_page *og, int nfill){ + return ogg_stream_flush_i(os,og,1,nfill); +} + +/* This constructs pages from buffered packet segments. The pointers +returned are to static buffers; do not free. The returned buffers are +good only until the next call (using the same ogg_stream_state) */ + +int ogg_stream_pageout(ogg_stream_state *os, ogg_page *og){ + int force=0; + if(ogg_stream_check(os)) return 0; + + if((os->e_o_s&&os->lacing_fill) || /* 'were done, now flush' case */ + (os->lacing_fill&&!os->b_o_s)) /* 'initial header page' case */ + force=1; + + return(ogg_stream_flush_i(os,og,force,4096)); +} + +/* Like the above, but an argument is provided to adjust the nominal +page size for applications which are smart enough to provide their +own delay based flushing */ + +int ogg_stream_pageout_fill(ogg_stream_state *os, ogg_page *og, int nfill){ + int force=0; + if(ogg_stream_check(os)) return 0; + + if((os->e_o_s&&os->lacing_fill) || /* 'were done, now flush' case */ + (os->lacing_fill&&!os->b_o_s)) /* 'initial header page' case */ + force=1; + + return(ogg_stream_flush_i(os,og,force,nfill)); +} + +int ogg_stream_eos(ogg_stream_state *os){ + if(ogg_stream_check(os)) return 1; + return os->e_o_s; +} + +/* DECODING PRIMITIVES: packet streaming layer **********************/ + +/* This has two layers to place more of the multi-serialno and paging + control in the application's hands. First, we expose a data buffer + using ogg_sync_buffer(). The app either copies into the + buffer, or passes it directly to read(), etc. We then call + ogg_sync_wrote() to tell how many bytes we just added. + + Pages are returned (pointers into the buffer in ogg_sync_state) + by ogg_sync_pageout(). The page is then submitted to + ogg_stream_pagein() along with the appropriate + ogg_stream_state* (ie, matching serialno). We then get raw + packets out calling ogg_stream_packetout() with a + ogg_stream_state. */ + +/* initialize the struct to a known state */ +int ogg_sync_init(ogg_sync_state *oy){ + if(oy){ + oy->storage = -1; /* used as a readiness flag */ + memset(oy,0,sizeof(*oy)); + } + return(0); +} + +/* clear non-flat storage within */ +int ogg_sync_clear(ogg_sync_state *oy){ + if(oy){ + if(oy->data)_ogg_free(oy->data); + memset(oy,0,sizeof(*oy)); + } + return(0); +} + +int ogg_sync_destroy(ogg_sync_state *oy){ + if(oy){ + ogg_sync_clear(oy); + _ogg_free(oy); + } + return(0); +} + +int ogg_sync_check(ogg_sync_state *oy){ + if(oy->storage<0) return -1; + return 0; +} + +char *ogg_sync_buffer(ogg_sync_state *oy, long size){ + if(ogg_sync_check(oy)) return NULL; + + /* first, clear out any space that has been previously returned */ + if(oy->returned){ + oy->fill-=oy->returned; + if(oy->fill>0) + memmove(oy->data,oy->data+oy->returned,oy->fill); + oy->returned=0; + } + + if(size>oy->storage-oy->fill){ + /* We need to extend the internal buffer */ + long newsize=size+oy->fill+4096; /* an extra page to be nice */ + void *ret; + + if(oy->data) + ret=_ogg_realloc(oy->data,newsize); + else + ret=_ogg_malloc(newsize); + if(!ret){ + ogg_sync_clear(oy); + return NULL; + } + oy->data=ret; + oy->storage=newsize; + } + + /* expose a segment at least as large as requested at the fill mark */ + return((char *)oy->data+oy->fill); +} + +int ogg_sync_wrote(ogg_sync_state *oy, long bytes){ + if(ogg_sync_check(oy))return -1; + if(oy->fill+bytes>oy->storage)return -1; + oy->fill+=bytes; + return(0); +} + +/* sync the stream. This is meant to be useful for finding page + boundaries. + + return values for this: + -n) skipped n bytes + 0) page not ready; more data (no bytes skipped) + n) page synced at current location; page length n bytes + +*/ + +long ogg_sync_pageseek(ogg_sync_state *oy,ogg_page *og){ + unsigned char *page=oy->data+oy->returned; + unsigned char *next; + long bytes=oy->fill-oy->returned; + + if(ogg_sync_check(oy))return 0; + + if(oy->headerbytes==0){ + int headerbytes,i; + if(bytes<27)return(0); /* not enough for a header */ + + /* verify capture pattern */ + if(memcmp(page,"OggS",4))goto sync_fail; + + headerbytes=page[26]+27; + if(bytesbodybytes+=page[27+i]; + oy->headerbytes=headerbytes; + } + + if(oy->bodybytes+oy->headerbytes>bytes)return(0); + + /* The whole test page is buffered. Verify the checksum */ + { + /* Grab the checksum bytes, set the header field to zero */ + char chksum[4]; + ogg_page log; + + memcpy(chksum,page+22,4); + memset(page+22,0,4); + + /* set up a temp page struct and recompute the checksum */ + log.header=page; + log.header_len=oy->headerbytes; + log.body=page+oy->headerbytes; + log.body_len=oy->bodybytes; + ogg_page_checksum_set(&log); + + /* Compare */ + if(memcmp(chksum,page+22,4)){ + /* D'oh. Mismatch! Corrupt page (or miscapture and not a page + at all) */ + /* replace the computed checksum with the one actually read in */ + memcpy(page+22,chksum,4); + + /* Bad checksum. Lose sync */ + goto sync_fail; + } + } + + /* yes, have a whole page all ready to go */ + { + unsigned char *page=oy->data+oy->returned; + long bytes; + + if(og){ + og->header=page; + og->header_len=oy->headerbytes; + og->body=page+oy->headerbytes; + og->body_len=oy->bodybytes; + } + + oy->unsynced=0; + oy->returned+=(bytes=oy->headerbytes+oy->bodybytes); + oy->headerbytes=0; + oy->bodybytes=0; + return(bytes); + } + + sync_fail: + + oy->headerbytes=0; + oy->bodybytes=0; + + /* search for possible capture */ + next=memchr(page+1,'O',bytes-1); + if(!next) + next=oy->data+oy->fill; + + oy->returned=(int)(next-oy->data); + return((long)-(next-page)); +} + +/* sync the stream and get a page. Keep trying until we find a page. + Suppress 'sync errors' after reporting the first. + + return values: + -1) recapture (hole in data) + 0) need more data + 1) page returned + + Returns pointers into buffered data; invalidated by next call to + _stream, _clear, _init, or _buffer */ + +int ogg_sync_pageout(ogg_sync_state *oy, ogg_page *og){ + + if(ogg_sync_check(oy))return 0; + + /* all we need to do is verify a page at the head of the stream + buffer. If it doesn't verify, we look for the next potential + frame */ + + for(;;){ + long ret=ogg_sync_pageseek(oy,og); + if(ret>0){ + /* have a page */ + return(1); + } + if(ret==0){ + /* need more data */ + return(0); + } + + /* head did not start a synced page... skipped some bytes */ + if(!oy->unsynced){ + oy->unsynced=1; + return(-1); + } + + /* loop. keep looking */ + + } +} + +/* add the incoming page to the stream state; we decompose the page + into packet segments here as well. */ + +int ogg_stream_pagein(ogg_stream_state *os, ogg_page *og){ + unsigned char *header=og->header; + unsigned char *body=og->body; + long bodysize=og->body_len; + int segptr=0; + + int version=ogg_page_version(og); + int continued=ogg_page_continued(og); + int bos=ogg_page_bos(og); + int eos=ogg_page_eos(og); + ogg_int64_t granulepos=ogg_page_granulepos(og); + int serialno=ogg_page_serialno(og); + long pageno=ogg_page_pageno(og); + int segments=header[26]; + + if(ogg_stream_check(os)) return -1; + + /* clean up 'returned data' */ + { + long lr=os->lacing_returned; + long br=os->body_returned; + + /* body data */ + if(br){ + os->body_fill-=br; + if(os->body_fill) + memmove(os->body_data,os->body_data+br,os->body_fill); + os->body_returned=0; + } + + if(lr){ + /* segment table */ + if(os->lacing_fill-lr){ + memmove(os->lacing_vals,os->lacing_vals+lr, + (os->lacing_fill-lr)*sizeof(*os->lacing_vals)); + memmove(os->granule_vals,os->granule_vals+lr, + (os->lacing_fill-lr)*sizeof(*os->granule_vals)); + } + os->lacing_fill-=lr; + os->lacing_packet-=lr; + os->lacing_returned=0; + } + } + + /* check the serial number */ + if(serialno!=os->serialno)return(-1); + if(version>0)return(-1); + + if(_os_lacing_expand(os,segments+1)) return -1; + + /* are we in sequence? */ + if(pageno!=os->pageno){ + int i; + + /* unroll previous partial packet (if any) */ + for(i=os->lacing_packet;ilacing_fill;i++) + os->body_fill-=os->lacing_vals[i]&0xff; + os->lacing_fill=os->lacing_packet; + + /* make a note of dropped data in segment table */ + if(os->pageno!=-1){ + os->lacing_vals[os->lacing_fill++]=0x400; + os->lacing_packet++; + } + } + + /* are we a 'continued packet' page? If so, we may need to skip + some segments */ + if(continued){ + if(os->lacing_fill<1 || + os->lacing_vals[os->lacing_fill-1]==0x400){ + bos=0; + for(;segptrbody_data+os->body_fill,body,bodysize); + os->body_fill+=bodysize; + } + + { + int saved=-1; + while(segptrlacing_vals[os->lacing_fill]=val; + os->granule_vals[os->lacing_fill]=-1; + + if(bos){ + os->lacing_vals[os->lacing_fill]|=0x100; + bos=0; + } + + if(val<255)saved=os->lacing_fill; + + os->lacing_fill++; + segptr++; + + if(val<255)os->lacing_packet=os->lacing_fill; + } + + /* set the granulepos on the last granuleval of the last full packet */ + if(saved!=-1){ + os->granule_vals[saved]=granulepos; + } + + } + + if(eos){ + os->e_o_s=1; + if(os->lacing_fill>0) + os->lacing_vals[os->lacing_fill-1]|=0x200; + } + + os->pageno=pageno+1; + + return(0); +} + +/* clear things to an initial state. Good to call, eg, before seeking */ +int ogg_sync_reset(ogg_sync_state *oy){ + if(ogg_sync_check(oy))return -1; + + oy->fill=0; + oy->returned=0; + oy->unsynced=0; + oy->headerbytes=0; + oy->bodybytes=0; + return(0); +} + +int ogg_stream_reset(ogg_stream_state *os){ + if(ogg_stream_check(os)) return -1; + + os->body_fill=0; + os->body_returned=0; + + os->lacing_fill=0; + os->lacing_packet=0; + os->lacing_returned=0; + + os->header_fill=0; + + os->e_o_s=0; + os->b_o_s=0; + os->pageno=-1; + os->packetno=0; + os->granulepos=0; + + return(0); +} + +int ogg_stream_reset_serialno(ogg_stream_state *os,int serialno){ + if(ogg_stream_check(os)) return -1; + ogg_stream_reset(os); + os->serialno=serialno; + return(0); +} + +static int _packetout(ogg_stream_state *os,ogg_packet *op,int adv){ + + /* The last part of decode. We have the stream broken into packet + segments. Now we need to group them into packets (or return the + out of sync markers) */ + + int ptr=os->lacing_returned; + + if(os->lacing_packet<=ptr)return(0); + + if(os->lacing_vals[ptr]&0x400){ + /* we need to tell the codec there's a gap; it might need to + handle previous packet dependencies. */ + os->lacing_returned++; + os->packetno++; + return(-1); + } + + if(!op && !adv)return(1); /* just using peek as an inexpensive way + to ask if there's a whole packet + waiting */ + + /* Gather the whole packet. We'll have no holes or a partial packet */ + { + int size=os->lacing_vals[ptr]&0xff; + long bytes=size; + int eos=os->lacing_vals[ptr]&0x200; /* last packet of the stream? */ + int bos=os->lacing_vals[ptr]&0x100; /* first packet of the stream? */ + + while(size==255){ + int val=os->lacing_vals[++ptr]; + size=val&0xff; + if(val&0x200)eos=0x200; + bytes+=size; + } + + if(op){ + op->e_o_s=eos; + op->b_o_s=bos; + op->packet=os->body_data+os->body_returned; + op->packetno=os->packetno; + op->granulepos=os->granule_vals[ptr]; + op->bytes=bytes; + } + + if(adv){ + os->body_returned+=bytes; + os->lacing_returned=ptr+1; + os->packetno++; + } + } + return(1); +} + +int ogg_stream_packetout(ogg_stream_state *os,ogg_packet *op){ + if(ogg_stream_check(os)) return 0; + return _packetout(os,op,1); +} + +int ogg_stream_packetpeek(ogg_stream_state *os,ogg_packet *op){ + if(ogg_stream_check(os)) return 0; + return _packetout(os,op,0); +} + +void ogg_packet_clear(ogg_packet *op) { + _ogg_free(op->packet); + memset(op, 0, sizeof(*op)); +} + +#ifdef _V_SELFTEST +#include + +ogg_stream_state os_en, os_de; +ogg_sync_state oy; + +void checkpacket(ogg_packet *op,long len, int no, long pos){ + long j; + static int sequence=0; + static int lastno=0; + + if(op->bytes!=len){ + fprintf(stderr,"incorrect packet length (%ld != %ld)!\n",op->bytes,len); + exit(1); + } + if(op->granulepos!=pos){ + fprintf(stderr,"incorrect packet granpos (%ld != %ld)!\n",(long)op->granulepos,pos); + exit(1); + } + + /* packet number just follows sequence/gap; adjust the input number + for that */ + if(no==0){ + sequence=0; + }else{ + sequence++; + if(no>lastno+1) + sequence++; + } + lastno=no; + if(op->packetno!=sequence){ + fprintf(stderr,"incorrect packet sequence %ld != %d\n", + (long)(op->packetno),sequence); + exit(1); + } + + /* Test data */ + for(j=0;jbytes;j++) + if(op->packet[j]!=((j+no)&0xff)){ + fprintf(stderr,"body data mismatch (1) at pos %ld: %x!=%lx!\n\n", + j,op->packet[j],(j+no)&0xff); + exit(1); + } +} + +void check_page(unsigned char *data,const int *header,ogg_page *og){ + long j; + /* Test data */ + for(j=0;jbody_len;j++) + if(og->body[j]!=data[j]){ + fprintf(stderr,"body data mismatch (2) at pos %ld: %x!=%x!\n\n", + j,data[j],og->body[j]); + exit(1); + } + + /* Test header */ + for(j=0;jheader_len;j++){ + if(og->header[j]!=header[j]){ + fprintf(stderr,"header content mismatch at pos %ld:\n",j); + for(j=0;jheader[j]); + fprintf(stderr,"\n"); + exit(1); + } + } + if(og->header_len!=header[26]+27){ + fprintf(stderr,"header length incorrect! (%ld!=%d)\n", + og->header_len,header[26]+27); + exit(1); + } +} + +void print_header(ogg_page *og){ + int j; + fprintf(stderr,"\nHEADER:\n"); + fprintf(stderr," capture: %c %c %c %c version: %d flags: %x\n", + og->header[0],og->header[1],og->header[2],og->header[3], + (int)og->header[4],(int)og->header[5]); + + fprintf(stderr," granulepos: %d serialno: %d pageno: %ld\n", + (og->header[9]<<24)|(og->header[8]<<16)| + (og->header[7]<<8)|og->header[6], + (og->header[17]<<24)|(og->header[16]<<16)| + (og->header[15]<<8)|og->header[14], + ((long)(og->header[21])<<24)|(og->header[20]<<16)| + (og->header[19]<<8)|og->header[18]); + + fprintf(stderr," checksum: %02x:%02x:%02x:%02x\n segments: %d (", + (int)og->header[22],(int)og->header[23], + (int)og->header[24],(int)og->header[25], + (int)og->header[26]); + + for(j=27;jheader_len;j++) + fprintf(stderr,"%d ",(int)og->header[j]); + fprintf(stderr,")\n\n"); +} + +void copy_page(ogg_page *og){ + unsigned char *temp=_ogg_malloc(og->header_len); + memcpy(temp,og->header,og->header_len); + og->header=temp; + + temp=_ogg_malloc(og->body_len); + memcpy(temp,og->body,og->body_len); + og->body=temp; +} + +void free_page(ogg_page *og){ + _ogg_free (og->header); + _ogg_free (og->body); +} + +void error(void){ + fprintf(stderr,"error!\n"); + exit(1); +} + +/* 17 only */ +const int head1_0[] = {0x4f,0x67,0x67,0x53,0,0x06, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0x15,0xed,0xec,0x91, + 1, + 17}; + +/* 17, 254, 255, 256, 500, 510, 600 byte, pad */ +const int head1_1[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0x59,0x10,0x6c,0x2c, + 1, + 17}; +const int head2_1[] = {0x4f,0x67,0x67,0x53,0,0x04, + 0x07,0x18,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x89,0x33,0x85,0xce, + 13, + 254,255,0,255,1,255,245,255,255,0, + 255,255,90}; + +/* nil packets; beginning,middle,end */ +const int head1_2[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; +const int head2_2[] = {0x4f,0x67,0x67,0x53,0,0x04, + 0x07,0x28,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x5c,0x3f,0x66,0xcb, + 17, + 17,254,255,0,0,255,1,0,255,245,255,255,0, + 255,255,90,0}; + +/* large initial packet */ +const int head1_3[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0x01,0x27,0x31,0xaa, + 18, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,10}; + +const int head2_3[] = {0x4f,0x67,0x67,0x53,0,0x04, + 0x07,0x08,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x7f,0x4e,0x8a,0xd2, + 4, + 255,4,255,0}; + + +/* continuing packet test */ +const int head1_4[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + +const int head2_4[] = {0x4f,0x67,0x67,0x53,0,0x00, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0xf8,0x3c,0x19,0x79, + 255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255}; + +const int head3_4[] = {0x4f,0x67,0x67,0x53,0,0x05, + 0x07,0x0c,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0x38,0xe6,0xb6,0x28, + 6, + 255,220,255,4,255,0}; + + +/* spill expansion test */ +const int head1_4b[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + +const int head2_4b[] = {0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0x10,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0xce,0x8f,0x17,0x1a, + 23, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,10,255,4,255,0,0}; + + +const int head3_4b[] = {0x4f,0x67,0x67,0x53,0,0x04, + 0x07,0x14,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0x9b,0xb2,0x50,0xa1, + 1, + 0}; + +/* page with the 255 segment limit */ +const int head1_5[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + +const int head2_5[] = {0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0xfc,0x03,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0xed,0x2a,0x2e,0xa7, + 255, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10}; + +const int head3_5[] = {0x4f,0x67,0x67,0x53,0,0x04, + 0x07,0x00,0x04,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0x6c,0x3b,0x82,0x3d, + 1, + 50}; + + +/* packet that overspans over an entire page */ +const int head1_6[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + +const int head2_6[] = {0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0x04,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x68,0x22,0x7c,0x3d, + 255, + 100, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255}; + +const int head3_6[] = {0x4f,0x67,0x67,0x53,0,0x01, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0xf4,0x87,0xba,0xf3, + 255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255}; + +const int head4_6[] = {0x4f,0x67,0x67,0x53,0,0x05, + 0x07,0x10,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,3,0,0,0, + 0xf7,0x2f,0x6c,0x60, + 5, + 254,255,4,255,0}; + +/* packet that overspans over an entire page */ +const int head1_7[] = {0x4f,0x67,0x67,0x53,0,0x02, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0,0,0,0, + 0xff,0x7b,0x23,0x17, + 1, + 0}; + +const int head2_7[] = {0x4f,0x67,0x67,0x53,0,0x00, + 0x07,0x04,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,1,0,0,0, + 0x68,0x22,0x7c,0x3d, + 255, + 100, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255, + 255,255,255,255,255,255}; + +const int head3_7[] = {0x4f,0x67,0x67,0x53,0,0x05, + 0x07,0x08,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,2,0,0,0, + 0xd4,0xe0,0x60,0xe5, + 1, + 0}; + +void test_pack(const int *pl, const int **headers, int byteskip, + int pageskip, int packetskip){ + unsigned char *data=_ogg_malloc(1024*1024); /* for scripted test cases only */ + long inptr=0; + long outptr=0; + long deptr=0; + long depacket=0; + long granule_pos=7,pageno=0; + int i,j,packets,pageout=pageskip; + int eosflag=0; + int bosflag=0; + + int byteskipcount=0; + + ogg_stream_reset(&os_en); + ogg_stream_reset(&os_de); + ogg_sync_reset(&oy); + + for(packets=0;packetsbyteskip){ + memcpy(next,og.header,byteskipcount-byteskip); + next+=byteskipcount-byteskip; + byteskipcount=byteskip; + } + + byteskipcount+=og.body_len; + if(byteskipcount>byteskip){ + memcpy(next,og.body,byteskipcount-byteskip); + next+=byteskipcount-byteskip; + byteskipcount=byteskip; + } + + ogg_sync_wrote(&oy,next-buf); + + while(1){ + int ret=ogg_sync_pageout(&oy,&og_de); + if(ret==0)break; + if(ret<0)continue; + /* got a page. Happy happy. Verify that it's good. */ + + fprintf(stderr,"(%d), ",pageout); + + check_page(data+deptr,headers[pageout],&og_de); + deptr+=og_de.body_len; + pageout++; + + /* submit it to deconstitution */ + ogg_stream_pagein(&os_de,&og_de); + + /* packets out? */ + while(ogg_stream_packetpeek(&os_de,&op_de2)>0){ + ogg_stream_packetpeek(&os_de,NULL); + ogg_stream_packetout(&os_de,&op_de); /* just catching them all */ + + /* verify peek and out match */ + if(memcmp(&op_de,&op_de2,sizeof(op_de))){ + fprintf(stderr,"packetout != packetpeek! pos=%ld\n", + depacket); + exit(1); + } + + /* verify the packet! */ + /* check data */ + if(memcmp(data+depacket,op_de.packet,op_de.bytes)){ + fprintf(stderr,"packet data mismatch in decode! pos=%ld\n", + depacket); + exit(1); + } + /* check bos flag */ + if(bosflag==0 && op_de.b_o_s==0){ + fprintf(stderr,"b_o_s flag not set on packet!\n"); + exit(1); + } + if(bosflag && op_de.b_o_s){ + fprintf(stderr,"b_o_s flag incorrectly set on packet!\n"); + exit(1); + } + bosflag=1; + depacket+=op_de.bytes; + + /* check eos flag */ + if(eosflag){ + fprintf(stderr,"Multiple decoded packets with eos flag!\n"); + exit(1); + } + + if(op_de.e_o_s)eosflag=1; + + /* check granulepos flag */ + if(op_de.granulepos!=-1){ + fprintf(stderr," granule:%ld ",(long)op_de.granulepos); + } + } + } + } + } + } + } + _ogg_free(data); + if(headers[pageno]!=NULL){ + fprintf(stderr,"did not write last page!\n"); + exit(1); + } + if(headers[pageout]!=NULL){ + fprintf(stderr,"did not decode last page!\n"); + exit(1); + } + if(inptr!=outptr){ + fprintf(stderr,"encoded page data incomplete!\n"); + exit(1); + } + if(inptr!=deptr){ + fprintf(stderr,"decoded page data incomplete!\n"); + exit(1); + } + if(inptr!=depacket){ + fprintf(stderr,"decoded packet data incomplete!\n"); + exit(1); + } + if(!eosflag){ + fprintf(stderr,"Never got a packet with EOS set!\n"); + exit(1); + } + fprintf(stderr,"ok.\n"); +} + +int main(void){ + + ogg_stream_init(&os_en,0x04030201); + ogg_stream_init(&os_de,0x04030201); + ogg_sync_init(&oy); + + /* Exercise each code path in the framing code. Also verify that + the checksums are working. */ + + { + /* 17 only */ + const int packets[]={17, -1}; + const int *headret[]={head1_0,NULL}; + + fprintf(stderr,"testing single page encoding... "); + test_pack(packets,headret,0,0,0); + } + + { + /* 17, 254, 255, 256, 500, 510, 600 byte, pad */ + const int packets[]={17, 254, 255, 256, 500, 510, 600, -1}; + const int *headret[]={head1_1,head2_1,NULL}; + + fprintf(stderr,"testing basic page encoding... "); + test_pack(packets,headret,0,0,0); + } + + { + /* nil packets; beginning,middle,end */ + const int packets[]={0,17, 254, 255, 0, 256, 0, 500, 510, 600, 0, -1}; + const int *headret[]={head1_2,head2_2,NULL}; + + fprintf(stderr,"testing basic nil packets... "); + test_pack(packets,headret,0,0,0); + } + + { + /* large initial packet */ + const int packets[]={4345,259,255,-1}; + const int *headret[]={head1_3,head2_3,NULL}; + + fprintf(stderr,"testing initial-packet lacing > 4k... "); + test_pack(packets,headret,0,0,0); + } + + { + /* continuing packet test; with page spill expansion, we have to + overflow the lacing table. */ + const int packets[]={0,65500,259,255,-1}; + const int *headret[]={head1_4,head2_4,head3_4,NULL}; + + fprintf(stderr,"testing single packet page span... "); + test_pack(packets,headret,0,0,0); + } + + { + /* spill expand packet test */ + const int packets[]={0,4345,259,255,0,0,-1}; + const int *headret[]={head1_4b,head2_4b,head3_4b,NULL}; + + fprintf(stderr,"testing page spill expansion... "); + test_pack(packets,headret,0,0,0); + } + + /* page with the 255 segment limit */ + { + + const int packets[]={0,10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,50,-1}; + const int *headret[]={head1_5,head2_5,head3_5,NULL}; + + fprintf(stderr,"testing max packet segments... "); + test_pack(packets,headret,0,0,0); + } + + { + /* packet that overspans over an entire page */ + const int packets[]={0,100,130049,259,255,-1}; + const int *headret[]={head1_6,head2_6,head3_6,head4_6,NULL}; + + fprintf(stderr,"testing very large packets... "); + test_pack(packets,headret,0,0,0); + } + + { + /* test for the libogg 1.1.1 resync in large continuation bug + found by Josh Coalson) */ + const int packets[]={0,100,130049,259,255,-1}; + const int *headret[]={head1_6,head2_6,head3_6,head4_6,NULL}; + + fprintf(stderr,"testing continuation resync in very large packets... "); + test_pack(packets,headret,100,2,3); + } + + { + /* term only page. why not? */ + const int packets[]={0,100,64770,-1}; + const int *headret[]={head1_7,head2_7,head3_7,NULL}; + + fprintf(stderr,"testing zero data page (1 nil packet)... "); + test_pack(packets,headret,0,0,0); + } + + + + { + /* build a bunch of pages for testing */ + unsigned char *data=_ogg_malloc(1024*1024); + int pl[]={0, 1,1,98,4079, 1,1,2954,2057, 76,34,912,0,234,1000,1000, 1000,300,-1}; + int inptr=0,i,j; + ogg_page og[5]; + + ogg_stream_reset(&os_en); + + for(i=0;pl[i]!=-1;i++){ + ogg_packet op; + int len=pl[i]; + + op.packet=data+inptr; + op.bytes=len; + op.e_o_s=(pl[i+1]<0?1:0); + op.granulepos=(i+1)*1000; + + for(j=0;j0)error(); + + /* Test fractional page inputs: incomplete fixed header */ + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header+3, + 20); + ogg_sync_wrote(&oy,20); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + + /* Test fractional page inputs: incomplete header */ + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header+23, + 5); + ogg_sync_wrote(&oy,5); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + + /* Test fractional page inputs: incomplete body */ + + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header+28, + og[1].header_len-28); + ogg_sync_wrote(&oy,og[1].header_len-28); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body,1000); + ogg_sync_wrote(&oy,1000); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body+1000, + og[1].body_len-1000); + ogg_sync_wrote(&oy,og[1].body_len-1000); + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + + fprintf(stderr,"ok.\n"); + } + + /* Test fractional page inputs: page + incomplete capture */ + { + ogg_page og_de; + fprintf(stderr,"Testing sync on 1+partial inputs... "); + ogg_sync_reset(&oy); + + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header, + og[1].header_len); + ogg_sync_wrote(&oy,og[1].header_len); + + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body, + og[1].body_len); + ogg_sync_wrote(&oy,og[1].body_len); + + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header, + 20); + ogg_sync_wrote(&oy,20); + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header+20, + og[1].header_len-20); + ogg_sync_wrote(&oy,og[1].header_len-20); + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body, + og[1].body_len); + ogg_sync_wrote(&oy,og[1].body_len); + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + + fprintf(stderr,"ok.\n"); + } + + /* Test recapture: garbage + page */ + { + ogg_page og_de; + fprintf(stderr,"Testing search for capture... "); + ogg_sync_reset(&oy); + + /* 'garbage' */ + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body, + og[1].body_len); + ogg_sync_wrote(&oy,og[1].body_len); + + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header, + og[1].header_len); + ogg_sync_wrote(&oy,og[1].header_len); + + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body, + og[1].body_len); + ogg_sync_wrote(&oy,og[1].body_len); + + memcpy(ogg_sync_buffer(&oy,og[2].header_len),og[2].header, + 20); + ogg_sync_wrote(&oy,20); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + + memcpy(ogg_sync_buffer(&oy,og[2].header_len),og[2].header+20, + og[2].header_len-20); + ogg_sync_wrote(&oy,og[2].header_len-20); + memcpy(ogg_sync_buffer(&oy,og[2].body_len),og[2].body, + og[2].body_len); + ogg_sync_wrote(&oy,og[2].body_len); + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + + fprintf(stderr,"ok.\n"); + } + + /* Test recapture: page + garbage + page */ + { + ogg_page og_de; + fprintf(stderr,"Testing recapture... "); + ogg_sync_reset(&oy); + + memcpy(ogg_sync_buffer(&oy,og[1].header_len),og[1].header, + og[1].header_len); + ogg_sync_wrote(&oy,og[1].header_len); + + memcpy(ogg_sync_buffer(&oy,og[1].body_len),og[1].body, + og[1].body_len); + ogg_sync_wrote(&oy,og[1].body_len); + + memcpy(ogg_sync_buffer(&oy,og[2].header_len),og[2].header, + og[2].header_len); + ogg_sync_wrote(&oy,og[2].header_len); + + memcpy(ogg_sync_buffer(&oy,og[2].header_len),og[2].header, + og[2].header_len); + ogg_sync_wrote(&oy,og[2].header_len); + + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + + memcpy(ogg_sync_buffer(&oy,og[2].body_len),og[2].body, + og[2].body_len-5); + ogg_sync_wrote(&oy,og[2].body_len-5); + + memcpy(ogg_sync_buffer(&oy,og[3].header_len),og[3].header, + og[3].header_len); + ogg_sync_wrote(&oy,og[3].header_len); + + memcpy(ogg_sync_buffer(&oy,og[3].body_len),og[3].body, + og[3].body_len); + ogg_sync_wrote(&oy,og[3].body_len); + + if(ogg_sync_pageout(&oy,&og_de)>0)error(); + if(ogg_sync_pageout(&oy,&og_de)<=0)error(); + + fprintf(stderr,"ok.\n"); + } + + /* Free page data that was previously copied */ + { + for(i=0;i<5;i++){ + free_page(&og[i]); + } + } + } + + return(0); +} + +#endif diff --git a/third-party/ogg/ogg/ogg.h b/third-party/ogg/ogg/ogg.h new file mode 100644 index 0000000000..cea4ebed75 --- /dev/null +++ b/third-party/ogg/ogg/ogg.h @@ -0,0 +1,210 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2007 * + * by the Xiph.Org Foundation http://www.xiph.org/ * + * * + ******************************************************************** + + function: toplevel libogg include + last mod: $Id: ogg.h 18044 2011-08-01 17:55:20Z gmaxwell $ + + ********************************************************************/ +#ifndef _OGG_H +#define _OGG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef struct { + void *iov_base; + size_t iov_len; +} ogg_iovec_t; + +typedef struct { + long endbyte; + int endbit; + + unsigned char *buffer; + unsigned char *ptr; + long storage; +} oggpack_buffer; + +/* ogg_page is used to encapsulate the data in one Ogg bitstream page *****/ + +typedef struct { + unsigned char *header; + long header_len; + unsigned char *body; + long body_len; +} ogg_page; + +/* ogg_stream_state contains the current encode/decode state of a logical + Ogg bitstream **********************************************************/ + +typedef struct { + unsigned char *body_data; /* bytes from packet bodies */ + long body_storage; /* storage elements allocated */ + long body_fill; /* elements stored; fill mark */ + long body_returned; /* elements of fill returned */ + + + int *lacing_vals; /* The values that will go to the segment table */ + ogg_int64_t *granule_vals; /* granulepos values for headers. Not compact + this way, but it is simple coupled to the + lacing fifo */ + long lacing_storage; + long lacing_fill; + long lacing_packet; + long lacing_returned; + + unsigned char header[282]; /* working space for header encode */ + int header_fill; + + int e_o_s; /* set when we have buffered the last packet in the + logical bitstream */ + int b_o_s; /* set after we've written the initial page + of a logical bitstream */ + long serialno; + long pageno; + ogg_int64_t packetno; /* sequence number for decode; the framing + knows where there's a hole in the data, + but we need coupling so that the codec + (which is in a separate abstraction + layer) also knows about the gap */ + ogg_int64_t granulepos; + +} ogg_stream_state; + +/* ogg_packet is used to encapsulate the data and metadata belonging + to a single raw Ogg/Vorbis packet *************************************/ + +typedef struct { + unsigned char *packet; + long bytes; + long b_o_s; + long e_o_s; + + ogg_int64_t granulepos; + + ogg_int64_t packetno; /* sequence number for decode; the framing + knows where there's a hole in the data, + but we need coupling so that the codec + (which is in a separate abstraction + layer) also knows about the gap */ +} ogg_packet; + +typedef struct { + unsigned char *data; + int storage; + int fill; + int returned; + + int unsynced; + int headerbytes; + int bodybytes; +} ogg_sync_state; + +/* Ogg BITSTREAM PRIMITIVES: bitstream ************************/ + +extern void oggpack_writeinit(oggpack_buffer *b); +extern int oggpack_writecheck(oggpack_buffer *b); +extern void oggpack_writetrunc(oggpack_buffer *b,long bits); +extern void oggpack_writealign(oggpack_buffer *b); +extern void oggpack_writecopy(oggpack_buffer *b,void *source,long bits); +extern void oggpack_reset(oggpack_buffer *b); +extern void oggpack_writeclear(oggpack_buffer *b); +extern void oggpack_readinit(oggpack_buffer *b,unsigned char *buf,int bytes); +extern void oggpack_write(oggpack_buffer *b,unsigned long value,int bits); +extern long oggpack_look(oggpack_buffer *b,int bits); +extern long oggpack_look1(oggpack_buffer *b); +extern void oggpack_adv(oggpack_buffer *b,int bits); +extern void oggpack_adv1(oggpack_buffer *b); +extern long oggpack_read(oggpack_buffer *b,int bits); +extern long oggpack_read1(oggpack_buffer *b); +extern long oggpack_bytes(oggpack_buffer *b); +extern long oggpack_bits(oggpack_buffer *b); +extern unsigned char *oggpack_get_buffer(oggpack_buffer *b); + +extern void oggpackB_writeinit(oggpack_buffer *b); +extern int oggpackB_writecheck(oggpack_buffer *b); +extern void oggpackB_writetrunc(oggpack_buffer *b,long bits); +extern void oggpackB_writealign(oggpack_buffer *b); +extern void oggpackB_writecopy(oggpack_buffer *b,void *source,long bits); +extern void oggpackB_reset(oggpack_buffer *b); +extern void oggpackB_writeclear(oggpack_buffer *b); +extern void oggpackB_readinit(oggpack_buffer *b,unsigned char *buf,int bytes); +extern void oggpackB_write(oggpack_buffer *b,unsigned long value,int bits); +extern long oggpackB_look(oggpack_buffer *b,int bits); +extern long oggpackB_look1(oggpack_buffer *b); +extern void oggpackB_adv(oggpack_buffer *b,int bits); +extern void oggpackB_adv1(oggpack_buffer *b); +extern long oggpackB_read(oggpack_buffer *b,int bits); +extern long oggpackB_read1(oggpack_buffer *b); +extern long oggpackB_bytes(oggpack_buffer *b); +extern long oggpackB_bits(oggpack_buffer *b); +extern unsigned char *oggpackB_get_buffer(oggpack_buffer *b); + +/* Ogg BITSTREAM PRIMITIVES: encoding **************************/ + +extern int ogg_stream_packetin(ogg_stream_state *os, ogg_packet *op); +extern int ogg_stream_iovecin(ogg_stream_state *os, ogg_iovec_t *iov, + int count, long e_o_s, ogg_int64_t granulepos); +extern int ogg_stream_pageout(ogg_stream_state *os, ogg_page *og); +extern int ogg_stream_pageout_fill(ogg_stream_state *os, ogg_page *og, int nfill); +extern int ogg_stream_flush(ogg_stream_state *os, ogg_page *og); +extern int ogg_stream_flush_fill(ogg_stream_state *os, ogg_page *og, int nfill); + +/* Ogg BITSTREAM PRIMITIVES: decoding **************************/ + +extern int ogg_sync_init(ogg_sync_state *oy); +extern int ogg_sync_clear(ogg_sync_state *oy); +extern int ogg_sync_reset(ogg_sync_state *oy); +extern int ogg_sync_destroy(ogg_sync_state *oy); +extern int ogg_sync_check(ogg_sync_state *oy); + +extern char *ogg_sync_buffer(ogg_sync_state *oy, long size); +extern int ogg_sync_wrote(ogg_sync_state *oy, long bytes); +extern long ogg_sync_pageseek(ogg_sync_state *oy,ogg_page *og); +extern int ogg_sync_pageout(ogg_sync_state *oy, ogg_page *og); +extern int ogg_stream_pagein(ogg_stream_state *os, ogg_page *og); +extern int ogg_stream_packetout(ogg_stream_state *os,ogg_packet *op); +extern int ogg_stream_packetpeek(ogg_stream_state *os,ogg_packet *op); + +/* Ogg BITSTREAM PRIMITIVES: general ***************************/ + +extern int ogg_stream_init(ogg_stream_state *os,int serialno); +extern int ogg_stream_clear(ogg_stream_state *os); +extern int ogg_stream_reset(ogg_stream_state *os); +extern int ogg_stream_reset_serialno(ogg_stream_state *os,int serialno); +extern int ogg_stream_destroy(ogg_stream_state *os); +extern int ogg_stream_check(ogg_stream_state *os); +extern int ogg_stream_eos(ogg_stream_state *os); + +extern void ogg_page_checksum_set(ogg_page *og); + +extern int ogg_page_version(const ogg_page *og); +extern int ogg_page_continued(const ogg_page *og); +extern int ogg_page_bos(const ogg_page *og); +extern int ogg_page_eos(const ogg_page *og); +extern ogg_int64_t ogg_page_granulepos(const ogg_page *og); +extern int ogg_page_serialno(const ogg_page *og); +extern long ogg_page_pageno(const ogg_page *og); +extern int ogg_page_packets(const ogg_page *og); + +extern void ogg_packet_clear(ogg_packet *op); + + +#ifdef __cplusplus +} +#endif + +#endif /* _OGG_H */ diff --git a/third-party/ogg/ogg/os_types.h b/third-party/ogg/ogg/os_types.h new file mode 100644 index 0000000000..d6691b703d --- /dev/null +++ b/third-party/ogg/ogg/os_types.h @@ -0,0 +1,147 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2002 * + * by the Xiph.Org Foundation http://www.xiph.org/ * + * * + ******************************************************************** + + function: #ifdef jail to whip a few platforms into the UNIX ideal. + last mod: $Id: os_types.h 17712 2010-12-03 17:10:02Z xiphmont $ + + ********************************************************************/ +#ifndef _OS_TYPES_H +#define _OS_TYPES_H + +/* make it easy on the folks that want to compile the libs with a + different malloc than stdlib */ +#define _ogg_malloc malloc +#define _ogg_calloc calloc +#define _ogg_realloc realloc +#define _ogg_free free + +#if defined(_WIN32) + +# if defined(__CYGWIN__) +# include + typedef int16_t ogg_int16_t; + typedef uint16_t ogg_uint16_t; + typedef int32_t ogg_int32_t; + typedef uint32_t ogg_uint32_t; + typedef int64_t ogg_int64_t; + typedef uint64_t ogg_uint64_t; +# elif defined(__MINGW32__) +# include + typedef short ogg_int16_t; + typedef unsigned short ogg_uint16_t; + typedef int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef long long ogg_int64_t; + typedef unsigned long long ogg_uint64_t; +# elif defined(__MWERKS__) + typedef long long ogg_int64_t; + typedef int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef short ogg_int16_t; + typedef unsigned short ogg_uint16_t; +# else + /* MSVC/Borland */ + typedef __int64 ogg_int64_t; + typedef __int32 ogg_int32_t; + typedef unsigned __int32 ogg_uint32_t; + typedef __int16 ogg_int16_t; + typedef unsigned __int16 ogg_uint16_t; +# endif + +#elif defined(__MACOS__) + +# include + typedef SInt16 ogg_int16_t; + typedef UInt16 ogg_uint16_t; + typedef SInt32 ogg_int32_t; + typedef UInt32 ogg_uint32_t; + typedef SInt64 ogg_int64_t; + +#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ + +# include + typedef int16_t ogg_int16_t; + typedef uint16_t ogg_uint16_t; + typedef int32_t ogg_int32_t; + typedef uint32_t ogg_uint32_t; + typedef int64_t ogg_int64_t; + +#elif defined(__HAIKU__) + + /* Haiku */ +# include + typedef short ogg_int16_t; + typedef unsigned short ogg_uint16_t; + typedef int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef long long ogg_int64_t; + +#elif defined(__BEOS__) + + /* Be */ +# include + typedef int16_t ogg_int16_t; + typedef uint16_t ogg_uint16_t; + typedef int32_t ogg_int32_t; + typedef uint32_t ogg_uint32_t; + typedef int64_t ogg_int64_t; + +#elif defined (__EMX__) + + /* OS/2 GCC */ + typedef short ogg_int16_t; + typedef unsigned short ogg_uint16_t; + typedef int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef long long ogg_int64_t; + +#elif defined (DJGPP) + + /* DJGPP */ + typedef short ogg_int16_t; + typedef int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef long long ogg_int64_t; + +#elif defined(R5900) + + /* PS2 EE */ + typedef long ogg_int64_t; + typedef int ogg_int32_t; + typedef unsigned ogg_uint32_t; + typedef short ogg_int16_t; + +#elif defined(__SYMBIAN32__) + + /* Symbian GCC */ + typedef signed short ogg_int16_t; + typedef unsigned short ogg_uint16_t; + typedef signed int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef long long int ogg_int64_t; + +#elif defined(__TMS320C6X__) + + /* TI C64x compiler */ + typedef signed short ogg_int16_t; + typedef unsigned short ogg_uint16_t; + typedef signed int ogg_int32_t; + typedef unsigned int ogg_uint32_t; + typedef long long int ogg_int64_t; + +#else + +# include + +#endif + +#endif /* _OS_TYPES_H */ diff --git a/third-party/opus/include/opus/opus.h b/third-party/opus/include/opus/opus.h new file mode 100644 index 0000000000..b0bdf6f2df --- /dev/null +++ b/third-party/opus/include/opus/opus.h @@ -0,0 +1,981 @@ +/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited + Written by Jean-Marc Valin and Koen Vos */ +/* + 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. + + 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 COPYRIGHT OWNER + 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. +*/ + +/** + * @file opus.h + * @brief Opus reference implementation API + */ + +#ifndef OPUS_H +#define OPUS_H + +#include "opus_types.h" +#include "opus_defines.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @mainpage Opus + * + * The Opus codec is designed for interactive speech and audio transmission over the Internet. + * It is designed by the IETF Codec Working Group and incorporates technology from + * Skype's SILK codec and Xiph.Org's CELT codec. + * + * The Opus codec is designed to handle a wide range of interactive audio applications, + * including Voice over IP, videoconferencing, in-game chat, and even remote live music + * performances. It can scale from low bit-rate narrowband speech to very high quality + * stereo music. Its main features are: + + * @li Sampling rates from 8 to 48 kHz + * @li Bit-rates from 6 kb/s to 510 kb/s + * @li Support for both constant bit-rate (CBR) and variable bit-rate (VBR) + * @li Audio bandwidth from narrowband to full-band + * @li Support for speech and music + * @li Support for mono and stereo + * @li Support for multichannel (up to 255 channels) + * @li Frame sizes from 2.5 ms to 60 ms + * @li Good loss robustness and packet loss concealment (PLC) + * @li Floating point and fixed-point implementation + * + * Documentation sections: + * @li @ref opus_encoder + * @li @ref opus_decoder + * @li @ref opus_repacketizer + * @li @ref opus_multistream + * @li @ref opus_libinfo + * @li @ref opus_custom + */ + +/** @defgroup opus_encoder Opus Encoder + * @{ + * + * @brief This page describes the process and functions used to encode Opus. + * + * Since Opus is a stateful codec, the encoding process starts with creating an encoder + * state. This can be done with: + * + * @code + * int error; + * OpusEncoder *enc; + * enc = opus_encoder_create(Fs, channels, application, &error); + * @endcode + * + * From this point, @c enc can be used for encoding an audio stream. An encoder state + * @b must @b not be used for more than one stream at the same time. Similarly, the encoder + * state @b must @b not be re-initialized for each frame. + * + * While opus_encoder_create() allocates memory for the state, it's also possible + * to initialize pre-allocated memory: + * + * @code + * int size; + * int error; + * OpusEncoder *enc; + * size = opus_encoder_get_size(channels); + * enc = malloc(size); + * error = opus_encoder_init(enc, Fs, channels, application); + * @endcode + * + * where opus_encoder_get_size() returns the required size for the encoder state. Note that + * future versions of this code may change the size, so no assuptions should be made about it. + * + * The encoder state is always continuous in memory and only a shallow copy is sufficient + * to copy it (e.g. memcpy()) + * + * It is possible to change some of the encoder's settings using the opus_encoder_ctl() + * interface. All these settings already default to the recommended value, so they should + * only be changed when necessary. The most common settings one may want to change are: + * + * @code + * opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate)); + * opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity)); + * opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type)); + * @endcode + * + * where + * + * @arg bitrate is in bits per second (b/s) + * @arg complexity is a value from 1 to 10, where 1 is the lowest complexity and 10 is the highest + * @arg signal_type is either OPUS_AUTO (default), OPUS_SIGNAL_VOICE, or OPUS_SIGNAL_MUSIC + * + * See @ref opus_encoderctls and @ref opus_genericctls for a complete list of parameters that can be set or queried. Most parameters can be set or changed at any time during a stream. + * + * To encode a frame, opus_encode() or opus_encode_float() must be called with exactly one frame (2.5, 5, 10, 20, 40 or 60 ms) of audio data: + * @code + * len = opus_encode(enc, audio_frame, frame_size, packet, max_packet); + * @endcode + * + * where + *
    + *
  • audio_frame is the audio data in opus_int16 (or float for opus_encode_float())
  • + *
  • frame_size is the duration of the frame in samples (per channel)
  • + *
  • packet is the byte array to which the compressed data is written
  • + *
  • max_packet is the maximum number of bytes that can be written in the packet (4000 bytes is recommended). + * Do not use max_packet to control VBR target bitrate, instead use the #OPUS_SET_BITRATE CTL.
  • + *
+ * + * opus_encode() and opus_encode_float() return the number of bytes actually written to the packet. + * The return value can be negative, which indicates that an error has occurred. If the return value + * is 1 byte, then the packet does not need to be transmitted (DTX). + * + * Once the encoder state if no longer needed, it can be destroyed with + * + * @code + * opus_encoder_destroy(enc); + * @endcode + * + * If the encoder was created with opus_encoder_init() rather than opus_encoder_create(), + * then no action is required aside from potentially freeing the memory that was manually + * allocated for it (calling free(enc) for the example above) + * + */ + +/** Opus encoder state. + * This contains the complete state of an Opus encoder. + * It is position independent and can be freely copied. + * @see opus_encoder_create,opus_encoder_init + */ +typedef struct OpusEncoder OpusEncoder; + +/** Gets the size of an OpusEncoder structure. + * @param[in] channels int: Number of channels. + * This must be 1 or 2. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels); + +/** + */ + +/** Allocates and initializes an encoder state. + * There are three coding modes: + * + * @ref OPUS_APPLICATION_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. + * + * @ref OPUS_APPLICATION_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. + * + * @ref OPUS_APPLICATION_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 or freshly reset encoder + * because it changes the codec delay. + * + * This is useful when the caller knows that the speech-optimized modes will not be needed (use with caution). + * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) in input signal + * @param [in] application int: Coding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [out] error int*: @ref opus_errorcodes + * @note Regardless of the sampling rate and number 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. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create( + opus_int32 Fs, + int channels, + int application, + int *error +); + +/** Initializes a previously allocated encoder state + * The memory pointed to by st must be at least the size returned by opus_encoder_get_size(). + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_encoder_create(),opus_encoder_get_size() + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) in input signal + * @param [in] application int: Coding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @retval #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_encoder_init( + OpusEncoder *st, + opus_int32 Fs, + int channels, + int application +) OPUS_ARG_NONNULL(1); + +/** Encodes an Opus frame. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] pcm opus_int16*: Input signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size int: Number of samples per channel in the + * input signal. + * This must be an Opus frame size for + * the encoder's sampling rate. + * For example, at 48 kHz the permitted + * values are 120, 240, 480, 960, 1920, + * and 2880. + * Passing in a duration of less than + * 10 ms (480 samples at 48 kHz) will + * prevent the encoder from using the LPC + * or hybrid modes. + * @param [out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for 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. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode( + OpusEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes an Opus frame from floating point input. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] pcm float*: Input in float format (interleaved if 2 channels), with a normal range of +/-1.0. + * Samples with a range beyond +/-1.0 are supported but will + * be clipped by decoders using the integer API and should + * only be used if it is known that the far end supports + * extended dynamic range. + * length is frame_size*channels*sizeof(float) + * @param [in] frame_size int: Number of samples per channel in the + * input signal. + * This must be an Opus frame size for + * the encoder's sampling rate. + * For example, at 48 kHz the permitted + * values are 120, 240, 480, 960, 1920, + * and 2880. + * Passing in a duration of less than + * 10 ms (480 samples at 48 kHz) will + * prevent the encoder from using the LPC + * or hybrid modes. + * @param [out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for 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. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode_float( + OpusEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusEncoder allocated by opus_encoder_create(). + * @param[in] st OpusEncoder*: State to be freed. + */ +OPUS_EXPORT void opus_encoder_destroy(OpusEncoder *st); + +/** Perform a CTL function on an Opus encoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param st OpusEncoder*: Encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_encoderctls. + * @see opus_genericctls + * @see opus_encoderctls + */ +OPUS_EXPORT int opus_encoder_ctl(OpusEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); +/**@}*/ + +/** @defgroup opus_decoder Opus Decoder + * @{ + * + * @brief This page describes the process and functions used to decode Opus. + * + * The decoding process also starts with creating a decoder + * state. This can be done with: + * @code + * int error; + * OpusDecoder *dec; + * dec = opus_decoder_create(Fs, channels, &error); + * @endcode + * where + * @li Fs is the sampling rate and must be 8000, 12000, 16000, 24000, or 48000 + * @li channels is the number of channels (1 or 2) + * @li error will hold the error code in case of failure (or #OPUS_OK on success) + * @li the return value is a newly created decoder state to be used for decoding + * + * While opus_decoder_create() allocates memory for the state, it's also possible + * to initialize pre-allocated memory: + * @code + * int size; + * int error; + * OpusDecoder *dec; + * size = opus_decoder_get_size(channels); + * dec = malloc(size); + * error = opus_decoder_init(dec, Fs, channels); + * @endcode + * where opus_decoder_get_size() returns the required size for the decoder state. Note that + * future versions of this code may change the size, so no assuptions should be made about it. + * + * The decoder state is always continuous in memory and only a shallow copy is sufficient + * to copy it (e.g. memcpy()) + * + * To decode a frame, opus_decode() or opus_decode_float() must be called with a packet of compressed audio data: + * @code + * frame_size = opus_decode(dec, packet, len, decoded, max_size, 0); + * @endcode + * where + * + * @li packet is the byte array containing the compressed data + * @li len is the exact number of bytes contained in the packet + * @li decoded is the decoded audio data in opus_int16 (or float for opus_decode_float()) + * @li max_size is the max duration of the frame in samples (per channel) that can fit into the decoded_frame array + * + * opus_decode() and opus_decode_float() return the number of samples (per channel) decoded from the packet. + * If that value is negative, then an error has occurred. This can occur if the packet is corrupted or if the audio + * buffer is too small to hold the decoded audio. + * + * Opus is a stateful codec with overlapping blocks and as a result Opus + * packets are not coded independently of each other. Packets must be + * passed into the decoder serially and in the correct order for a correct + * decode. Lost packets can be replaced with loss concealment by calling + * the decoder with a null pointer and zero length for the missing packet. + * + * A single codec state may only be accessed from a single thread at + * a time and any required locking must be performed by the caller. Separate + * streams must be decoded with separate decoder states and can be decoded + * in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK + * defined. + * + */ + +/** Opus decoder state. + * This contains the complete state of an Opus decoder. + * It is position independent and can be freely copied. + * @see opus_decoder_create,opus_decoder_init + */ +typedef struct OpusDecoder OpusDecoder; + +/** Gets the size of an OpusDecoder structure. + * @param [in] channels int: Number of channels. + * This must be 1 or 2. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_size(int channels); + +/** Allocates and initializes a decoder state. + * @param [in] Fs opus_int32: Sample rate to decode at (Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) to decode + * @param [out] error int*: #OPUS_OK Success or @ref opus_errorcodes + * + * 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. Likewise, the decoder is capable of filling in either mono or + * interleaved stereo pcm buffers, at the caller's request. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusDecoder *opus_decoder_create( + opus_int32 Fs, + int channels, + int *error +); + +/** Initializes a previously allocated decoder state. + * The state must be at least the size returned by opus_decoder_get_size(). + * This is intended for applications which use their own allocator instead of malloc. @see opus_decoder_create,opus_decoder_get_size + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @param [in] st OpusDecoder*: Decoder state. + * @param [in] Fs opus_int32: Sampling rate to decode to (Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) to decode + * @retval #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_decoder_init( + OpusDecoder *st, + opus_int32 Fs, + int channels +) OPUS_ARG_NONNULL(1); + +/** Decode an Opus packet. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len opus_int32: Number of bytes in payload* + * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size Number of samples per channel of available space in \a pcm. + * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will + * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), + * then frame_size needs to be exactly the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and + * FEC cases, frame_size must be a multiple of 2.5 ms. + * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be + * decoded. If no such data is available, the frame is decoded as if it were lost. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode( + OpusDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode an Opus packet with floating point output. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len opus_int32: Number of bytes in payload + * @param [out] pcm float*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(float) + * @param [in] frame_size Number of samples per channel of available space in \a pcm. + * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will + * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), + * then frame_size needs to be exactly the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and + * FEC cases, frame_size must be a multiple of 2.5 ms. + * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be + * decoded. If no such data is available the frame is decoded as if it were lost. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode_float( + OpusDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus decoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param st OpusDecoder*: Decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_decoderctls. + * @see opus_genericctls + * @see opus_decoderctls + */ +OPUS_EXPORT int opus_decoder_ctl(OpusDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusDecoder allocated by opus_decoder_create(). + * @param[in] st OpusDecoder*: State to be freed. + */ +OPUS_EXPORT void opus_decoder_destroy(OpusDecoder *st); + +/** Parse an opus packet into one or more frames. + * Opus_decode will perform this operation internally so most applications do + * not need to use this function. + * This function does not copy the frames, the returned pointers are pointers into + * the input packet. + * @param [in] data char*: Opus packet to be parsed + * @param [in] len opus_int32: size of data + * @param [out] out_toc char*: TOC pointer + * @param [out] frames char*[48] encapsulated frames + * @param [out] size opus_int16[48] sizes of the encapsulated frames + * @param [out] payload_offset int*: returns the position of the payload within the packet (in bytes) + * @returns number of frames + */ +OPUS_EXPORT int opus_packet_parse( + const unsigned char *data, + opus_int32 len, + unsigned char *out_toc, + const unsigned char *frames[48], + opus_int16 size[48], + int *payload_offset +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Gets the bandwidth of an Opus packet. + * @param [in] data char*: Opus packet + * @retval OPUS_BANDWIDTH_NARROWBAND Narrowband (4kHz bandpass) + * @retval OPUS_BANDWIDTH_MEDIUMBAND Mediumband (6kHz bandpass) + * @retval OPUS_BANDWIDTH_WIDEBAND Wideband (8kHz bandpass) + * @retval OPUS_BANDWIDTH_SUPERWIDEBAND Superwideband (12kHz bandpass) + * @retval OPUS_BANDWIDTH_FULLBAND Fullband (20kHz bandpass) + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_bandwidth(const unsigned char *data) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples per frame from an Opus packet. + * @param [in] data char*: Opus packet. + * This must contain at least one byte of + * data. + * @param [in] Fs opus_int32: Sampling rate in Hz. + * This must be a multiple of 400, or + * inaccurate results will be returned. + * @returns Number of samples per frame. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_samples_per_frame(const unsigned char *data, opus_int32 Fs) OPUS_ARG_NONNULL(1); + +/** Gets the number of channels from an Opus packet. + * @param [in] data char*: Opus packet + * @returns Number of channels + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_channels(const unsigned char *data) OPUS_ARG_NONNULL(1); + +/** Gets the number of frames in an Opus packet. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns Number of frames + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples of an Opus packet. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @param [in] Fs opus_int32: Sampling rate in Hz. + * This must be a multiple of 400, or + * inaccurate results will be returned. + * @returns Number of samples + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, opus_int32 Fs) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples of an Opus packet. + * @param [in] dec OpusDecoder*: Decoder state + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns Number of samples + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_nb_samples(const OpusDecoder *dec, const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + +/** Applies soft-clipping to bring a float signal within the [-1,1] range. If + * the signal is already in that range, nothing is done. If there are values + * outside of [-1,1], then the signal is clipped as smoothly as possible to + * both fit in the range and avoid creating excessive distortion in the + * process. + * @param [in,out] pcm float*: Input PCM and modified PCM + * @param [in] frame_size int Number of samples per channel to process + * @param [in] channels int: Number of channels + * @param [in,out] softclip_mem float*: State memory for the soft clipping process (one float per channel, initialized to zero) + */ +OPUS_EXPORT void opus_pcm_soft_clip(float *pcm, int frame_size, int channels, float *softclip_mem); + + +/**@}*/ + +/** @defgroup opus_repacketizer Repacketizer + * @{ + * + * The repacketizer can be used to merge multiple Opus packets into a single + * packet or alternatively to split Opus packets that have previously been + * merged. Splitting valid Opus packets is always guaranteed to succeed, + * whereas merging valid packets only succeeds if all frames have the same + * mode, bandwidth, and frame size, and when the total duration of the merged + * packet is no more than 120 ms. The 120 ms limit comes from the + * specification and limits decoder memory requirements at a point where + * framing overhead becomes negligible. + * + * The repacketizer currently only operates on elementary Opus + * streams. It will not manipualte multistream packets successfully, except in + * the degenerate case where they consist of data from a single stream. + * + * The repacketizing process starts with creating a repacketizer state, either + * by calling opus_repacketizer_create() or by allocating the memory yourself, + * e.g., + * @code + * OpusRepacketizer *rp; + * rp = (OpusRepacketizer*)malloc(opus_repacketizer_get_size()); + * if (rp != NULL) + * opus_repacketizer_init(rp); + * @endcode + * + * Then the application should submit packets with opus_repacketizer_cat(), + * extract new packets with opus_repacketizer_out() or + * opus_repacketizer_out_range(), and then reset the state for the next set of + * input packets via opus_repacketizer_init(). + * + * For example, to split a sequence of packets into individual frames: + * @code + * unsigned char *data; + * int len; + * while (get_next_packet(&data, &len)) + * { + * unsigned char out[1276]; + * opus_int32 out_len; + * int nb_frames; + * int err; + * int i; + * err = opus_repacketizer_cat(rp, data, len); + * if (err != OPUS_OK) + * { + * release_packet(data); + * return err; + * } + * nb_frames = opus_repacketizer_get_nb_frames(rp); + * for (i = 0; i < nb_frames; i++) + * { + * out_len = opus_repacketizer_out_range(rp, i, i+1, out, sizeof(out)); + * if (out_len < 0) + * { + * release_packet(data); + * return (int)out_len; + * } + * output_next_packet(out, out_len); + * } + * opus_repacketizer_init(rp); + * release_packet(data); + * } + * @endcode + * + * Alternatively, to combine a sequence of frames into packets that each + * contain up to TARGET_DURATION_MS milliseconds of data: + * @code + * // The maximum number of packets with duration TARGET_DURATION_MS occurs + * // when the frame size is 2.5 ms, for a total of (TARGET_DURATION_MS*2/5) + * // packets. + * unsigned char *data[(TARGET_DURATION_MS*2/5)+1]; + * opus_int32 len[(TARGET_DURATION_MS*2/5)+1]; + * int nb_packets; + * unsigned char out[1277*(TARGET_DURATION_MS*2/2)]; + * opus_int32 out_len; + * int prev_toc; + * nb_packets = 0; + * while (get_next_packet(data+nb_packets, len+nb_packets)) + * { + * int nb_frames; + * int err; + * nb_frames = opus_packet_get_nb_frames(data[nb_packets], len[nb_packets]); + * if (nb_frames < 1) + * { + * release_packets(data, nb_packets+1); + * return nb_frames; + * } + * nb_frames += opus_repacketizer_get_nb_frames(rp); + * // If adding the next packet would exceed our target, or it has an + * // incompatible TOC sequence, output the packets we already have before + * // submitting it. + * // N.B., The nb_packets > 0 check ensures we've submitted at least one + * // packet since the last call to opus_repacketizer_init(). Otherwise a + * // single packet longer than TARGET_DURATION_MS would cause us to try to + * // output an (invalid) empty packet. It also ensures that prev_toc has + * // been set to a valid value. Additionally, len[nb_packets] > 0 is + * // guaranteed by the call to opus_packet_get_nb_frames() above, so the + * // reference to data[nb_packets][0] should be valid. + * if (nb_packets > 0 && ( + * ((prev_toc & 0xFC) != (data[nb_packets][0] & 0xFC)) || + * opus_packet_get_samples_per_frame(data[nb_packets], 48000)*nb_frames > + * TARGET_DURATION_MS*48)) + * { + * out_len = opus_repacketizer_out(rp, out, sizeof(out)); + * if (out_len < 0) + * { + * release_packets(data, nb_packets+1); + * return (int)out_len; + * } + * output_next_packet(out, out_len); + * opus_repacketizer_init(rp); + * release_packets(data, nb_packets); + * data[0] = data[nb_packets]; + * len[0] = len[nb_packets]; + * nb_packets = 0; + * } + * err = opus_repacketizer_cat(rp, data[nb_packets], len[nb_packets]); + * if (err != OPUS_OK) + * { + * release_packets(data, nb_packets+1); + * return err; + * } + * prev_toc = data[nb_packets][0]; + * nb_packets++; + * } + * // Output the final, partial packet. + * if (nb_packets > 0) + * { + * out_len = opus_repacketizer_out(rp, out, sizeof(out)); + * release_packets(data, nb_packets); + * if (out_len < 0) + * return (int)out_len; + * output_next_packet(out, out_len); + * } + * @endcode + * + * An alternate way of merging packets is to simply call opus_repacketizer_cat() + * unconditionally until it fails. At that point, the merged packet can be + * obtained with opus_repacketizer_out() and the input packet for which + * opus_repacketizer_cat() needs to be re-added to a newly reinitialized + * repacketizer state. + */ + +typedef struct OpusRepacketizer OpusRepacketizer; + +/** Gets the size of an OpusRepacketizer structure. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_size(void); + +/** (Re)initializes a previously allocated repacketizer state. + * The state must be at least the size returned by opus_repacketizer_get_size(). + * This can be used for applications which use their own allocator instead of + * malloc(). + * It must also be called to reset the queue of packets waiting to be + * repacketized, which is necessary if the maximum packet duration of 120 ms + * is reached or if you wish to submit packets with a different Opus + * configuration (coding mode, audio bandwidth, frame size, or channel count). + * Failure to do so will prevent a new packet from being added with + * opus_repacketizer_cat(). + * @see opus_repacketizer_create + * @see opus_repacketizer_get_size + * @see opus_repacketizer_cat + * @param rp OpusRepacketizer*: The repacketizer state to + * (re)initialize. + * @returns A pointer to the same repacketizer state that was passed in. + */ +OPUS_EXPORT OpusRepacketizer *opus_repacketizer_init(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); + +/** Allocates memory and initializes the new repacketizer with + * opus_repacketizer_init(). + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusRepacketizer *opus_repacketizer_create(void); + +/** Frees an OpusRepacketizer allocated by + * opus_repacketizer_create(). + * @param[in] rp OpusRepacketizer*: State to be freed. + */ +OPUS_EXPORT void opus_repacketizer_destroy(OpusRepacketizer *rp); + +/** Add a packet to the current repacketizer state. + * This packet must match the configuration of any packets already submitted + * for repacketization since the last call to opus_repacketizer_init(). + * This means that it must have the same coding mode, audio bandwidth, frame + * size, and channel count. + * This can be checked in advance by examining the top 6 bits of the first + * byte of the packet, and ensuring they match the top 6 bits of the first + * byte of any previously submitted packet. + * The total duration of audio in the repacketizer state also must not exceed + * 120 ms, the maximum duration of a single packet, after adding this packet. + * + * The contents of the current repacketizer state can be extracted into new + * packets using opus_repacketizer_out() or opus_repacketizer_out_range(). + * + * In order to add a packet with a different configuration or to add more + * audio beyond 120 ms, you must clear the repacketizer state by calling + * opus_repacketizer_init(). + * If a packet is too large to add to the current repacketizer state, no part + * of it is added, even if it contains multiple frames, some of which might + * fit. + * If you wish to be able to add parts of such packets, you should first use + * another repacketizer to split the packet into pieces and add them + * individually. + * @see opus_repacketizer_out_range + * @see opus_repacketizer_out + * @see opus_repacketizer_init + * @param rp OpusRepacketizer*: The repacketizer state to which to + * add the packet. + * @param[in] data const unsigned char*: The packet data. + * The application must ensure + * this pointer remains valid + * until the next call to + * opus_repacketizer_init() or + * opus_repacketizer_destroy(). + * @param len opus_int32: The number of bytes in the packet data. + * @returns An error code indicating whether or not the operation succeeded. + * @retval #OPUS_OK The packet's contents have been added to the repacketizer + * state. + * @retval #OPUS_INVALID_PACKET The packet did not have a valid TOC sequence, + * the packet's TOC sequence was not compatible + * with previously submitted packets (because + * the coding mode, audio bandwidth, frame size, + * or channel count did not match), or adding + * this packet would increase the total amount of + * audio stored in the repacketizer state to more + * than 120 ms. + */ +OPUS_EXPORT int opus_repacketizer_cat(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + + +/** Construct a new packet from data previously submitted to the repacketizer + * state via opus_repacketizer_cat(). + * @param rp OpusRepacketizer*: The repacketizer state from which to + * construct the new packet. + * @param begin int: The index of the first frame in the current + * repacketizer state to include in the output. + * @param end int: One past the index of the last frame in the + * current repacketizer state to include in the + * output. + * @param[out] data const unsigned char*: The buffer in which to + * store the output packet. + * @param maxlen opus_int32: The maximum number of bytes to store in + * the output buffer. In order to guarantee + * success, this should be at least + * 1276 for a single frame, + * or for multiple frames, + * 1277*(end-begin). + * However, 1*(end-begin) plus + * the size of all packet data submitted to + * the repacketizer since the last call to + * opus_repacketizer_init() or + * opus_repacketizer_create() is also + * sufficient, and possibly much smaller. + * @returns The total size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG [begin,end) was an invalid range of + * frames (begin < 0, begin >= end, or end > + * opus_repacketizer_get_nb_frames()). + * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the + * complete output packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out_range(OpusRepacketizer *rp, int begin, int end, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Return the total number of frames contained in packet data submitted to + * the repacketizer state so far via opus_repacketizer_cat() since the last + * call to opus_repacketizer_init() or opus_repacketizer_create(). + * This defines the valid range of packets that can be extracted with + * opus_repacketizer_out_range() or opus_repacketizer_out(). + * @param rp OpusRepacketizer*: The repacketizer state containing the + * frames. + * @returns The total number of frames contained in the packet data submitted + * to the repacketizer state. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_nb_frames(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); + +/** Construct a new packet from data previously submitted to the repacketizer + * state via opus_repacketizer_cat(). + * This is a convenience routine that returns all the data submitted so far + * in a single packet. + * It is equivalent to calling + * @code + * opus_repacketizer_out_range(rp, 0, opus_repacketizer_get_nb_frames(rp), + * data, maxlen) + * @endcode + * @param rp OpusRepacketizer*: The repacketizer state from which to + * construct the new packet. + * @param[out] data const unsigned char*: The buffer in which to + * store the output packet. + * @param maxlen opus_int32: The maximum number of bytes to store in + * the output buffer. In order to guarantee + * success, this should be at least + * 1277*opus_repacketizer_get_nb_frames(rp). + * However, + * 1*opus_repacketizer_get_nb_frames(rp) + * plus the size of all packet data + * submitted to the repacketizer since the + * last call to opus_repacketizer_init() or + * opus_repacketizer_create() is also + * sufficient, and possibly much smaller. + * @returns The total size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the + * complete output packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out(OpusRepacketizer *rp, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1); + +/** Pads a given Opus packet to a larger size (possibly changing the TOC sequence). + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to pad. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param new_len opus_int32: The desired size of the packet after padding. + * This must be at least as large as len. + * @returns an error code + * @retval #OPUS_OK \a on success. + * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT int opus_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len); + +/** Remove all padding from a given Opus packet and rewrite the TOC sequence to + * minimize space usage. + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to strip. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @returns The new size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG \a len was less than 1. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_packet_unpad(unsigned char *data, opus_int32 len); + +/** Pads a given Opus multi-stream packet to a larger size (possibly changing the TOC sequence). + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to pad. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param new_len opus_int32: The desired size of the packet after padding. + * This must be at least 1. + * @param nb_streams opus_int32: The number of streams (not channels) in the packet. + * This must be at least as large as len. + * @returns an error code + * @retval #OPUS_OK \a on success. + * @retval #OPUS_BAD_ARG \a len was less than 1. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT int opus_multistream_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len, int nb_streams); + +/** Remove all padding from a given Opus multi-stream packet and rewrite the TOC sequence to + * minimize space usage. + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to strip. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param nb_streams opus_int32: The number of streams (not channels) in the packet. + * This must be at least 1. + * @returns The new size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_packet_unpad(unsigned char *data, opus_int32 len, int nb_streams); + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_H */ diff --git a/third-party/opus/include/opus/opus_defines.h b/third-party/opus/include/opus/opus_defines.h new file mode 100644 index 0000000000..647ed5d6f2 --- /dev/null +++ b/third-party/opus/include/opus/opus_defines.h @@ -0,0 +1,753 @@ +/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited + Written by Jean-Marc Valin and Koen Vos */ +/* + 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. + + 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 COPYRIGHT OWNER + 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. +*/ + +/** + * @file opus_defines.h + * @brief Opus reference implementation constants + */ + +#ifndef OPUS_DEFINES_H +#define OPUS_DEFINES_H + +#include "opus_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup opus_errorcodes Error codes + * @{ + */ +/** No error @hideinitializer*/ +#define OPUS_OK 0 +/** One or more invalid/out of range arguments @hideinitializer*/ +#define OPUS_BAD_ARG -1 +/** Not enough bytes allocated in the buffer @hideinitializer*/ +#define OPUS_BUFFER_TOO_SMALL -2 +/** An internal error was detected @hideinitializer*/ +#define OPUS_INTERNAL_ERROR -3 +/** The compressed data passed is corrupted @hideinitializer*/ +#define OPUS_INVALID_PACKET -4 +/** Invalid/unsupported request number @hideinitializer*/ +#define OPUS_UNIMPLEMENTED -5 +/** An encoder or decoder structure is invalid or already freed @hideinitializer*/ +#define OPUS_INVALID_STATE -6 +/** Memory allocation has failed @hideinitializer*/ +#define OPUS_ALLOC_FAIL -7 +/**@}*/ + +/** @cond OPUS_INTERNAL_DOC */ +/**Export control for opus functions */ + +#ifndef OPUS_EXPORT +# if defined(WIN32) +# ifdef OPUS_BUILD +# define OPUS_EXPORT __declspec(dllexport) +# else +# define OPUS_EXPORT +# endif +# elif defined(__GNUC__) && defined(OPUS_BUILD) +# define OPUS_EXPORT __attribute__ ((visibility ("default"))) +# else +# define OPUS_EXPORT +# endif +#endif + +# if !defined(OPUS_GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define OPUS_GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define OPUS_GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) +# if OPUS_GNUC_PREREQ(3,0) +# define OPUS_RESTRICT __restrict__ +# elif (defined(_MSC_VER) && _MSC_VER >= 1400) +# define OPUS_RESTRICT __restrict +# else +# define OPUS_RESTRICT +# endif +#else +# define OPUS_RESTRICT restrict +#endif + +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) +# if OPUS_GNUC_PREREQ(2,7) +# define OPUS_INLINE __inline__ +# elif (defined(_MSC_VER)) +# define OPUS_INLINE __inline +# else +# define OPUS_INLINE +# endif +#else +# define OPUS_INLINE inline +#endif + +/**Warning attributes for opus functions + * NONNULL is not used in OPUS_BUILD to avoid the compiler optimizing out + * some paranoid null checks. */ +#if defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) +# define OPUS_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +#else +# define OPUS_WARN_UNUSED_RESULT +#endif +#if !defined(OPUS_BUILD) && defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) +# define OPUS_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x))) +#else +# define OPUS_ARG_NONNULL(_x) +#endif + +/** These are the actual Encoder CTL ID numbers. + * They should not be used directly by applications. + * In general, SETs should be even and GETs should be odd.*/ +#define OPUS_SET_APPLICATION_REQUEST 4000 +#define OPUS_GET_APPLICATION_REQUEST 4001 +#define OPUS_SET_BITRATE_REQUEST 4002 +#define OPUS_GET_BITRATE_REQUEST 4003 +#define OPUS_SET_MAX_BANDWIDTH_REQUEST 4004 +#define OPUS_GET_MAX_BANDWIDTH_REQUEST 4005 +#define OPUS_SET_VBR_REQUEST 4006 +#define OPUS_GET_VBR_REQUEST 4007 +#define OPUS_SET_BANDWIDTH_REQUEST 4008 +#define OPUS_GET_BANDWIDTH_REQUEST 4009 +#define OPUS_SET_COMPLEXITY_REQUEST 4010 +#define OPUS_GET_COMPLEXITY_REQUEST 4011 +#define OPUS_SET_INBAND_FEC_REQUEST 4012 +#define OPUS_GET_INBAND_FEC_REQUEST 4013 +#define OPUS_SET_PACKET_LOSS_PERC_REQUEST 4014 +#define OPUS_GET_PACKET_LOSS_PERC_REQUEST 4015 +#define OPUS_SET_DTX_REQUEST 4016 +#define OPUS_GET_DTX_REQUEST 4017 +#define OPUS_SET_VBR_CONSTRAINT_REQUEST 4020 +#define OPUS_GET_VBR_CONSTRAINT_REQUEST 4021 +#define OPUS_SET_FORCE_CHANNELS_REQUEST 4022 +#define OPUS_GET_FORCE_CHANNELS_REQUEST 4023 +#define OPUS_SET_SIGNAL_REQUEST 4024 +#define OPUS_GET_SIGNAL_REQUEST 4025 +#define OPUS_GET_LOOKAHEAD_REQUEST 4027 +/* #define OPUS_RESET_STATE 4028 */ +#define OPUS_GET_SAMPLE_RATE_REQUEST 4029 +#define OPUS_GET_FINAL_RANGE_REQUEST 4031 +#define OPUS_GET_PITCH_REQUEST 4033 +#define OPUS_SET_GAIN_REQUEST 4034 +#define OPUS_GET_GAIN_REQUEST 4045 /* Should have been 4035 */ +#define OPUS_SET_LSB_DEPTH_REQUEST 4036 +#define OPUS_GET_LSB_DEPTH_REQUEST 4037 +#define OPUS_GET_LAST_PACKET_DURATION_REQUEST 4039 +#define OPUS_SET_EXPERT_FRAME_DURATION_REQUEST 4040 +#define OPUS_GET_EXPERT_FRAME_DURATION_REQUEST 4041 +#define OPUS_SET_PREDICTION_DISABLED_REQUEST 4042 +#define OPUS_GET_PREDICTION_DISABLED_REQUEST 4043 + +/* Don't use 4045, it's already taken by OPUS_GET_GAIN_REQUEST */ + +/* Macros to trigger compilation errors when the wrong types are provided to a CTL */ +#define __opus_check_int(x) (((void)((x) == (opus_int32)0)), (opus_int32)(x)) +#define __opus_check_int_ptr(ptr) ((ptr) + ((ptr) - (opus_int32*)(ptr))) +#define __opus_check_uint_ptr(ptr) ((ptr) + ((ptr) - (opus_uint32*)(ptr))) +#define __opus_check_val16_ptr(ptr) ((ptr) + ((ptr) - (opus_val16*)(ptr))) +/** @endcond */ + +/** @defgroup opus_ctlvalues Pre-defined values for CTL interface + * @see opus_genericctls, opus_encoderctls + * @{ + */ +/* Values for the various encoder CTLs */ +#define OPUS_AUTO -1000 /**opus_int32: Allowed values: 0-10, inclusive. + * + * @hideinitializer */ +#define OPUS_SET_COMPLEXITY(x) OPUS_SET_COMPLEXITY_REQUEST, __opus_check_int(x) +/** Gets the encoder's complexity configuration. + * @see OPUS_SET_COMPLEXITY + * @param[out] x opus_int32 *: Returns a value in the range 0-10, + * inclusive. + * @hideinitializer */ +#define OPUS_GET_COMPLEXITY(x) OPUS_GET_COMPLEXITY_REQUEST, __opus_check_int_ptr(x) + +/** Configures the bitrate in the encoder. + * Rates from 500 to 512000 bits per second are meaningful, as well as the + * special values #OPUS_AUTO and #OPUS_BITRATE_MAX. + * The value #OPUS_BITRATE_MAX can be used to cause the codec to use as much + * rate as it can, which is useful for controlling the rate by adjusting the + * output buffer size. + * @see OPUS_GET_BITRATE + * @param[in] x opus_int32: Bitrate in bits per second. The default + * is determined based on the number of + * channels and the input sampling rate. + * @hideinitializer */ +#define OPUS_SET_BITRATE(x) OPUS_SET_BITRATE_REQUEST, __opus_check_int(x) +/** Gets the encoder's bitrate configuration. + * @see OPUS_SET_BITRATE + * @param[out] x opus_int32 *: Returns the bitrate in bits per second. + * The default is determined based on the + * number of channels and the input + * sampling rate. + * @hideinitializer */ +#define OPUS_GET_BITRATE(x) OPUS_GET_BITRATE_REQUEST, __opus_check_int_ptr(x) + +/** Enables or disables variable bitrate (VBR) in the encoder. + * The configured bitrate may not be met exactly because frames must + * be an integer number of bytes in length. + * @see OPUS_GET_VBR + * @see OPUS_SET_VBR_CONSTRAINT + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Hard CBR. For LPC/hybrid modes at very low bit-rate, this can + * cause noticeable quality degradation.
+ *
1
VBR (default). The exact type of VBR is controlled by + * #OPUS_SET_VBR_CONSTRAINT.
+ *
+ * @hideinitializer */ +#define OPUS_SET_VBR(x) OPUS_SET_VBR_REQUEST, __opus_check_int(x) +/** Determine if variable bitrate (VBR) is enabled in the encoder. + * @see OPUS_SET_VBR + * @see OPUS_GET_VBR_CONSTRAINT + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Hard CBR.
+ *
1
VBR (default). The exact type of VBR may be retrieved via + * #OPUS_GET_VBR_CONSTRAINT.
+ *
+ * @hideinitializer */ +#define OPUS_GET_VBR(x) OPUS_GET_VBR_REQUEST, __opus_check_int_ptr(x) + +/** Enables or disables constrained VBR in the encoder. + * This setting is ignored when the encoder is in CBR mode. + * @warning Only the MDCT mode of Opus currently heeds the constraint. + * Speech mode ignores it completely, hybrid mode may fail to obey it + * if the LPC layer uses more bitrate than the constraint would have + * permitted. + * @see OPUS_GET_VBR_CONSTRAINT + * @see OPUS_SET_VBR + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Unconstrained VBR.
+ *
1
Constrained VBR (default). This creates a maximum of one + * frame of buffering delay assuming a transport with a + * serialization speed of the nominal bitrate.
+ *
+ * @hideinitializer */ +#define OPUS_SET_VBR_CONSTRAINT(x) OPUS_SET_VBR_CONSTRAINT_REQUEST, __opus_check_int(x) +/** Determine if constrained VBR is enabled in the encoder. + * @see OPUS_SET_VBR_CONSTRAINT + * @see OPUS_GET_VBR + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Unconstrained VBR.
+ *
1
Constrained VBR (default).
+ *
+ * @hideinitializer */ +#define OPUS_GET_VBR_CONSTRAINT(x) OPUS_GET_VBR_CONSTRAINT_REQUEST, __opus_check_int_ptr(x) + +/** Configures mono/stereo forcing in the encoder. + * This can force the encoder to produce packets encoded as either mono or + * stereo, regardless of the format of the input audio. This is useful when + * the caller knows that the input signal is currently a mono source embedded + * in a stereo stream. + * @see OPUS_GET_FORCE_CHANNELS + * @param[in] x opus_int32: Allowed values: + *
+ *
#OPUS_AUTO
Not forced (default)
+ *
1
Forced mono
+ *
2
Forced stereo
+ *
+ * @hideinitializer */ +#define OPUS_SET_FORCE_CHANNELS(x) OPUS_SET_FORCE_CHANNELS_REQUEST, __opus_check_int(x) +/** Gets the encoder's forced channel configuration. + * @see OPUS_SET_FORCE_CHANNELS + * @param[out] x opus_int32 *: + *
+ *
#OPUS_AUTO
Not forced (default)
+ *
1
Forced mono
+ *
2
Forced stereo
+ *
+ * @hideinitializer */ +#define OPUS_GET_FORCE_CHANNELS(x) OPUS_GET_FORCE_CHANNELS_REQUEST, __opus_check_int_ptr(x) + +/** Configures the maximum bandpass that the encoder will select automatically. + * Applications should normally use this instead of #OPUS_SET_BANDWIDTH + * (leaving that set to the default, #OPUS_AUTO). This allows the + * application to set an upper bound based on the type of input it is + * providing, but still gives the encoder the freedom to reduce the bandpass + * when the bitrate becomes too low, for better overall quality. + * @see OPUS_GET_MAX_BANDWIDTH + * @param[in] x opus_int32: Allowed values: + *
+ *
OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
OPUS_BANDWIDTH_FULLBAND
20 kHz passband (default)
+ *
+ * @hideinitializer */ +#define OPUS_SET_MAX_BANDWIDTH(x) OPUS_SET_MAX_BANDWIDTH_REQUEST, __opus_check_int(x) + +/** Gets the encoder's configured maximum allowed bandpass. + * @see OPUS_SET_MAX_BANDWIDTH + * @param[out] x opus_int32 *: Allowed values: + *
+ *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband (default)
+ *
+ * @hideinitializer */ +#define OPUS_GET_MAX_BANDWIDTH(x) OPUS_GET_MAX_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) + +/** Sets the encoder's bandpass to a specific value. + * This prevents the encoder from automatically selecting the bandpass based + * on the available bitrate. If an application knows the bandpass of the input + * audio it is providing, it should normally use #OPUS_SET_MAX_BANDWIDTH + * instead, which still gives the encoder the freedom to reduce the bandpass + * when the bitrate becomes too low, for better overall quality. + * @see OPUS_GET_BANDWIDTH + * @param[in] x opus_int32: Allowed values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband
+ *
+ * @hideinitializer */ +#define OPUS_SET_BANDWIDTH(x) OPUS_SET_BANDWIDTH_REQUEST, __opus_check_int(x) + +/** Configures the type of signal being encoded. + * This is a hint which helps the encoder's mode selection. + * @see OPUS_GET_SIGNAL + * @param[in] x opus_int32: Allowed values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_SIGNAL_VOICE
Bias thresholds towards choosing LPC or Hybrid modes.
+ *
#OPUS_SIGNAL_MUSIC
Bias thresholds towards choosing MDCT modes.
+ *
+ * @hideinitializer */ +#define OPUS_SET_SIGNAL(x) OPUS_SET_SIGNAL_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured signal type. + * @see OPUS_SET_SIGNAL + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_SIGNAL_VOICE
Bias thresholds towards choosing LPC or Hybrid modes.
+ *
#OPUS_SIGNAL_MUSIC
Bias thresholds towards choosing MDCT modes.
+ *
+ * @hideinitializer */ +#define OPUS_GET_SIGNAL(x) OPUS_GET_SIGNAL_REQUEST, __opus_check_int_ptr(x) + + +/** Configures the encoder's intended application. + * The initial value is a mandatory argument to the encoder_create function. + * @see OPUS_GET_APPLICATION + * @param[in] x opus_int32: Returns one of the following values: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @hideinitializer */ +#define OPUS_SET_APPLICATION(x) OPUS_SET_APPLICATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured application. + * @see OPUS_SET_APPLICATION + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @hideinitializer */ +#define OPUS_GET_APPLICATION(x) OPUS_GET_APPLICATION_REQUEST, __opus_check_int_ptr(x) + +/** 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 CTL rather than + * hard-coding a value. + * @param[out] x opus_int32 *: Number of lookahead samples + * @hideinitializer */ +#define OPUS_GET_LOOKAHEAD(x) OPUS_GET_LOOKAHEAD_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of inband forward error correction (FEC). + * @note This is only applicable to the LPC layer + * @see OPUS_GET_INBAND_FEC + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Disable inband FEC (default).
+ *
1
Enable inband FEC.
+ *
+ * @hideinitializer */ +#define OPUS_SET_INBAND_FEC(x) OPUS_SET_INBAND_FEC_REQUEST, __opus_check_int(x) +/** Gets encoder's configured use of inband forward error correction. + * @see OPUS_SET_INBAND_FEC + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Inband FEC disabled (default).
+ *
1
Inband FEC enabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_INBAND_FEC(x) OPUS_GET_INBAND_FEC_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's expected packet loss percentage. + * Higher values trigger progressively more loss resistant behavior in the encoder + * at the expense of quality at a given bitrate in the absence of packet loss, but + * greater quality under loss. + * @see OPUS_GET_PACKET_LOSS_PERC + * @param[in] x opus_int32: Loss percentage in the range 0-100, inclusive (default: 0). + * @hideinitializer */ +#define OPUS_SET_PACKET_LOSS_PERC(x) OPUS_SET_PACKET_LOSS_PERC_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured packet loss percentage. + * @see OPUS_SET_PACKET_LOSS_PERC + * @param[out] x opus_int32 *: Returns the configured loss percentage + * in the range 0-100, inclusive (default: 0). + * @hideinitializer */ +#define OPUS_GET_PACKET_LOSS_PERC(x) OPUS_GET_PACKET_LOSS_PERC_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of discontinuous transmission (DTX). + * @note This is only applicable to the LPC layer + * @see OPUS_GET_DTX + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Disable DTX (default).
+ *
1
Enabled DTX.
+ *
+ * @hideinitializer */ +#define OPUS_SET_DTX(x) OPUS_SET_DTX_REQUEST, __opus_check_int(x) +/** Gets encoder's configured use of discontinuous transmission. + * @see OPUS_SET_DTX + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
DTX disabled (default).
+ *
1
DTX enabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_DTX(x) OPUS_GET_DTX_REQUEST, __opus_check_int_ptr(x) +/** Configures the depth of signal being encoded. + * + * This is a hint which helps the encoder identify silence and near-silence. + * It represents the number of significant bits of linear intensity below + * which the signal contains ignorable quantization or other noise. + * + * For example, OPUS_SET_LSB_DEPTH(14) would be an appropriate setting + * for G.711 u-law input. OPUS_SET_LSB_DEPTH(16) would be appropriate + * for 16-bit linear pcm input with opus_encode_float(). + * + * When using opus_encode() instead of opus_encode_float(), or when libopus + * is compiled for fixed-point, the encoder uses the minimum of the value + * set here and the value 16. + * + * @see OPUS_GET_LSB_DEPTH + * @param[in] x opus_int32: Input precision in bits, between 8 and 24 + * (default: 24). + * @hideinitializer */ +#define OPUS_SET_LSB_DEPTH(x) OPUS_SET_LSB_DEPTH_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured signal depth. + * @see OPUS_SET_LSB_DEPTH + * @param[out] x opus_int32 *: Input precision in bits, between 8 and + * 24 (default: 24). + * @hideinitializer */ +#define OPUS_GET_LSB_DEPTH(x) OPUS_GET_LSB_DEPTH_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of variable duration frames. + * When variable duration is enabled, the encoder is free to use a shorter frame + * size than the one requested in the opus_encode*() call. + * It is then the user's responsibility + * to verify how much audio was encoded by checking the ToC byte of the encoded + * packet. The part of the audio that was not encoded needs to be resent to the + * encoder for the next call. Do not use this option unless you really + * know what you are doing. + * @see OPUS_GET_EXPERT_FRAME_DURATION + * @param[in] x opus_int32: Allowed values: + *
+ *
OPUS_FRAMESIZE_ARG
Select frame size from the argument (default).
+ *
OPUS_FRAMESIZE_2_5_MS
Use 2.5 ms frames.
+ *
OPUS_FRAMESIZE_5_MS
Use 5 ms frames.
+ *
OPUS_FRAMESIZE_10_MS
Use 10 ms frames.
+ *
OPUS_FRAMESIZE_20_MS
Use 20 ms frames.
+ *
OPUS_FRAMESIZE_40_MS
Use 40 ms frames.
+ *
OPUS_FRAMESIZE_60_MS
Use 60 ms frames.
+ *
OPUS_FRAMESIZE_VARIABLE
Optimize the frame size dynamically.
+ *
+ * @hideinitializer */ +#define OPUS_SET_EXPERT_FRAME_DURATION(x) OPUS_SET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured use of variable duration frames. + * @see OPUS_SET_EXPERT_FRAME_DURATION + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
OPUS_FRAMESIZE_ARG
Select frame size from the argument (default).
+ *
OPUS_FRAMESIZE_2_5_MS
Use 2.5 ms frames.
+ *
OPUS_FRAMESIZE_5_MS
Use 5 ms frames.
+ *
OPUS_FRAMESIZE_10_MS
Use 10 ms frames.
+ *
OPUS_FRAMESIZE_20_MS
Use 20 ms frames.
+ *
OPUS_FRAMESIZE_40_MS
Use 40 ms frames.
+ *
OPUS_FRAMESIZE_60_MS
Use 60 ms frames.
+ *
OPUS_FRAMESIZE_VARIABLE
Optimize the frame size dynamically.
+ *
+ * @hideinitializer */ +#define OPUS_GET_EXPERT_FRAME_DURATION(x) OPUS_GET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** If set to 1, disables almost all use of prediction, making frames almost + * completely independent. This reduces quality. + * @see OPUS_GET_PREDICTION_DISABLED + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Enable prediction (default).
+ *
1
Disable prediction.
+ *
+ * @hideinitializer */ +#define OPUS_SET_PREDICTION_DISABLED(x) OPUS_SET_PREDICTION_DISABLED_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured prediction status. + * @see OPUS_SET_PREDICTION_DISABLED + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Prediction enabled (default).
+ *
1
Prediction disabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_PREDICTION_DISABLED(x) OPUS_GET_PREDICTION_DISABLED_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_genericctls Generic CTLs + * + * These macros are used with the \c opus_decoder_ctl and + * \c opus_encoder_ctl calls to generate a particular + * request. + * + * When called on an \c OpusDecoder they apply to that + * particular decoder instance. When called on an + * \c OpusEncoder they apply to the corresponding setting + * on that encoder instance, if present. + * + * Some usage examples: + * + * @code + * int ret; + * opus_int32 pitch; + * ret = opus_decoder_ctl(dec_ctx, OPUS_GET_PITCH(&pitch)); + * if (ret == OPUS_OK) return ret; + * + * opus_encoder_ctl(enc_ctx, OPUS_RESET_STATE); + * opus_decoder_ctl(dec_ctx, OPUS_RESET_STATE); + * + * opus_int32 enc_bw, dec_bw; + * opus_encoder_ctl(enc_ctx, OPUS_GET_BANDWIDTH(&enc_bw)); + * opus_decoder_ctl(dec_ctx, OPUS_GET_BANDWIDTH(&dec_bw)); + * if (enc_bw != dec_bw) { + * printf("packet bandwidth mismatch!\n"); + * } + * @endcode + * + * @see opus_encoder, opus_decoder_ctl, opus_encoder_ctl, opus_decoderctls, opus_encoderctls + * @{ + */ + +/** Resets the codec state to be equivalent to a freshly initialized state. + * This should be called when switching streams in order to prevent + * the back to back decoding from giving different results from + * one at a time decoding. + * @hideinitializer */ +#define OPUS_RESET_STATE 4028 + +/** Gets the final state of the codec's entropy coder. + * This is used for testing purposes, + * The encoder and decoder state should be identical after coding a payload + * (assuming no data corruption or software bugs) + * + * @param[out] x opus_uint32 *: Entropy coder state + * + * @hideinitializer */ +#define OPUS_GET_FINAL_RANGE(x) OPUS_GET_FINAL_RANGE_REQUEST, __opus_check_uint_ptr(x) + +/** Gets the encoder's configured bandpass or the decoder's last bandpass. + * @see OPUS_SET_BANDWIDTH + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband
+ *
+ * @hideinitializer */ +#define OPUS_GET_BANDWIDTH(x) OPUS_GET_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) + +/** Gets the sampling rate the encoder or decoder was initialized with. + * This simply returns the Fs value passed to opus_encoder_init() + * or opus_decoder_init(). + * @param[out] x opus_int32 *: Sampling rate of encoder or decoder. + * @hideinitializer + */ +#define OPUS_GET_SAMPLE_RATE(x) OPUS_GET_SAMPLE_RATE_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_decoderctls Decoder related CTLs + * @see opus_genericctls, opus_encoderctls, opus_decoder + * @{ + */ + +/** Configures decoder gain adjustment. + * Scales the decoded output by a factor specified in Q8 dB units. + * This has a maximum range of -32768 to 32767 inclusive, and returns + * OPUS_BAD_ARG otherwise. The default is zero indicating no adjustment. + * This setting survives decoder reset. + * + * gain = pow(10, x/(20.0*256)) + * + * @param[in] x opus_int32: Amount to scale PCM signal by in Q8 dB units. + * @hideinitializer */ +#define OPUS_SET_GAIN(x) OPUS_SET_GAIN_REQUEST, __opus_check_int(x) +/** Gets the decoder's configured gain adjustment. @see OPUS_SET_GAIN + * + * @param[out] x opus_int32 *: Amount to scale PCM signal by in Q8 dB units. + * @hideinitializer */ +#define OPUS_GET_GAIN(x) OPUS_GET_GAIN_REQUEST, __opus_check_int_ptr(x) + +/** Gets the duration (in samples) of the last packet successfully decoded or concealed. + * @param[out] x opus_int32 *: Number of samples (at current sampling rate). + * @hideinitializer */ +#define OPUS_GET_LAST_PACKET_DURATION(x) OPUS_GET_LAST_PACKET_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** Gets the pitch of the last decoded frame, if available. + * This can be used for any post-processing algorithm requiring the use of pitch, + * e.g. time stretching/shortening. If the last frame was not voiced, or if the + * pitch was not coded in the frame, then zero is returned. + * + * This CTL is only implemented for decoder instances. + * + * @param[out] x opus_int32 *: pitch period at 48 kHz (or 0 if not available) + * + * @hideinitializer */ +#define OPUS_GET_PITCH(x) OPUS_GET_PITCH_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_libinfo Opus library information functions + * @{ + */ + +/** Converts an opus error code into a human readable string. + * + * @param[in] error int: Error number + * @returns Error string + */ +OPUS_EXPORT const char *opus_strerror(int error); + +/** Gets the libopus version string. + * + * Applications may look for the substring "-fixed" in the version string to + * determine whether they have a fixed-point or floating-point build at + * runtime. + * + * @returns Version string + */ +OPUS_EXPORT const char *opus_get_version_string(void); +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_DEFINES_H */ diff --git a/third-party/opus/include/opus/opus_multistream.h b/third-party/opus/include/opus/opus_multistream.h new file mode 100644 index 0000000000..47e03900bd --- /dev/null +++ b/third-party/opus/include/opus/opus_multistream.h @@ -0,0 +1,660 @@ +/* Copyright (c) 2011 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + 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. + + 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 COPYRIGHT OWNER + 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. +*/ + +/** + * @file opus_multistream.h + * @brief Opus reference implementation multistream API + */ + +#ifndef OPUS_MULTISTREAM_H +#define OPUS_MULTISTREAM_H + +#include "opus.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @cond OPUS_INTERNAL_DOC */ + +/** Macros to trigger compilation errors when the wrong types are provided to a + * CTL. */ +/**@{*/ +#define __opus_check_encstate_ptr(ptr) ((ptr) + ((ptr) - (OpusEncoder**)(ptr))) +#define __opus_check_decstate_ptr(ptr) ((ptr) + ((ptr) - (OpusDecoder**)(ptr))) +/**@}*/ + +/** These are the actual encoder and decoder CTL ID numbers. + * They should not be used directly by applications. + * In general, SETs should be even and GETs should be odd.*/ +/**@{*/ +#define OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST 5120 +#define OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST 5122 +/**@}*/ + +/** @endcond */ + +/** @defgroup opus_multistream_ctls Multistream specific encoder and decoder CTLs + * + * These are convenience macros that are specific to the + * opus_multistream_encoder_ctl() and opus_multistream_decoder_ctl() + * interface. + * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, and + * @ref opus_decoderctls may be applied to a multistream encoder or decoder as + * well. + * In addition, you may retrieve the encoder or decoder state for an specific + * stream via #OPUS_MULTISTREAM_GET_ENCODER_STATE or + * #OPUS_MULTISTREAM_GET_DECODER_STATE and apply CTLs to it individually. + */ +/**@{*/ + +/** Gets the encoder state for an individual stream of a multistream encoder. + * @param[in] x opus_int32: The index of the stream whose encoder you + * wish to retrieve. + * This must be non-negative and less than + * the streams parameter used + * to initialize the encoder. + * @param[out] y OpusEncoder**: Returns a pointer to the given + * encoder state. + * @retval OPUS_BAD_ARG The index of the requested stream was out of range. + * @hideinitializer + */ +#define OPUS_MULTISTREAM_GET_ENCODER_STATE(x,y) OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST, __opus_check_int(x), __opus_check_encstate_ptr(y) + +/** Gets the decoder state for an individual stream of a multistream decoder. + * @param[in] x opus_int32: The index of the stream whose decoder you + * wish to retrieve. + * This must be non-negative and less than + * the streams parameter used + * to initialize the decoder. + * @param[out] y OpusDecoder**: Returns a pointer to the given + * decoder state. + * @retval OPUS_BAD_ARG The index of the requested stream was out of range. + * @hideinitializer + */ +#define OPUS_MULTISTREAM_GET_DECODER_STATE(x,y) OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST, __opus_check_int(x), __opus_check_decstate_ptr(y) + +/**@}*/ + +/** @defgroup opus_multistream Opus Multistream API + * @{ + * + * The multistream API allows individual Opus streams to be combined into a + * single packet, enabling support for up to 255 channels. Unlike an + * elementary Opus stream, the encoder and decoder must negotiate the channel + * configuration before the decoder can successfully interpret the data in the + * packets produced by the encoder. Some basic information, such as packet + * duration, can be computed without any special negotiation. + * + * The format for multistream Opus packets is defined in the + * Ogg + * encapsulation specification and is based on the self-delimited Opus + * framing described in Appendix B of RFC 6716. + * Normal Opus packets are just a degenerate case of multistream Opus packets, + * and can be encoded or decoded with the multistream API by setting + * streams to 1 when initializing the encoder or + * decoder. + * + * Multistream Opus streams can contain up to 255 elementary Opus streams. + * These may be either "uncoupled" or "coupled", indicating that the decoder + * is configured to decode them to either 1 or 2 channels, respectively. + * The streams are ordered so that all coupled streams appear at the + * beginning. + * + * A mapping table defines which decoded channel i + * should be used for each input/output (I/O) channel j. This table is + * typically provided as an unsigned char array. + * Let i = mapping[j] be the index for I/O channel j. + * If i < 2*coupled_streams, then I/O channel j is + * encoded as the left channel of stream (i/2) if i + * is even, or as the right channel of stream (i/2) if + * i is odd. Otherwise, I/O channel j is encoded as + * mono in stream (i - coupled_streams), unless it has the special + * value 255, in which case it is omitted from the encoding entirely (the + * decoder will reproduce it as silence). Each value i must either + * be the special value 255 or be less than streams + coupled_streams. + * + * The output channels specified by the encoder + * should use the + * Vorbis + * channel ordering. A decoder may wish to apply an additional permutation + * to the mapping the encoder used to achieve a different output channel + * order (e.g. for outputing in WAV order). + * + * Each multistream packet contains an Opus packet for each stream, and all of + * the Opus packets in a single multistream packet must have the same + * duration. Therefore the duration of a multistream packet can be extracted + * from the TOC sequence of the first stream, which is located at the + * beginning of the packet, just like an elementary Opus stream: + * + * @code + * int nb_samples; + * int nb_frames; + * nb_frames = opus_packet_get_nb_frames(data, len); + * if (nb_frames < 1) + * return nb_frames; + * nb_samples = opus_packet_get_samples_per_frame(data, 48000) * nb_frames; + * @endcode + * + * The general encoding and decoding process proceeds exactly the same as in + * the normal @ref opus_encoder and @ref opus_decoder APIs. + * See their documentation for an overview of how to use the corresponding + * multistream functions. + */ + +/** Opus multistream encoder state. + * This contains the complete state of a multistream Opus encoder. + * It is position independent and can be freely copied. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_init + */ +typedef struct OpusMSEncoder OpusMSEncoder; + +/** Opus multistream decoder state. + * This contains the complete state of a multistream Opus decoder. + * It is position independent and can be freely copied. + * @see opus_multistream_decoder_create + * @see opus_multistream_decoder_init + */ +typedef struct OpusMSDecoder OpusMSDecoder; + +/**\name Multistream encoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSEncoder structure. + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_encoder_get_size( + int streams, + int coupled_streams +); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_surround_encoder_get_size( + int channels, + int mapping_family +); + + +/** Allocates and initializes a multistream encoder state. + * Call opus_multistream_encoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_encoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(5); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_surround_encoder_create( + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(5); + +/** Initialize a previously allocated multistream encoder state. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +OPUS_EXPORT int opus_multistream_surround_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +/** Encodes a multistream Opus frame. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const opus_int16*: The input signal as interleaved + * samples. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for 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. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode( + OpusMSEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes a multistream Opus frame from floating point input. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const float*: The input signal as interleaved + * samples with a normal range of + * +/-1.0. + * Samples with a range beyond +/-1.0 + * are supported but will be clipped by + * decoders using the integer API and + * should only be used if it is known + * that the far end supports extended + * dynamic range. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for 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. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode_float( + OpusMSEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusMSEncoder allocated by + * opus_multistream_encoder_create(). + * @param st OpusMSEncoder*: Multistream encoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_encoder_destroy(OpusMSEncoder *st); + +/** Perform a CTL function on a multistream Opus encoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_encoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_encoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_encoder_ctl(OpusMSEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/**@}*/ + +/**\name Multistream decoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSDecoder structure. + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_decoder_get_size( + int streams, + int coupled_streams +); + +/** Allocates and initializes a multistream decoder state. + * Call opus_multistream_decoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSDecoder *opus_multistream_decoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int *error +) OPUS_ARG_NONNULL(5); + +/** Intialize a previously allocated decoder state object. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_decoder_create + * @see opus_multistream_deocder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_decoder_init( + OpusMSDecoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +/** Decode a multistream Opus packet. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode a multistream Opus packet with floating point output. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode_float( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on a multistream Opus decoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_decoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_decoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_decoder_ctl(OpusMSDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusMSDecoder allocated by + * opus_multistream_decoder_create(). + * @param st OpusMSDecoder: Multistream decoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_decoder_destroy(OpusMSDecoder *st); + +/**@}*/ + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_MULTISTREAM_H */ diff --git a/third-party/opus/include/opus/opus_types.h b/third-party/opus/include/opus/opus_types.h new file mode 100644 index 0000000000..b28e03aea2 --- /dev/null +++ b/third-party/opus/include/opus/opus_types.h @@ -0,0 +1,159 @@ +/* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */ +/* Modified by Jean-Marc Valin */ +/* + 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. + + 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 COPYRIGHT OWNER + 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. +*/ +/* opus_types.h based on ogg_types.h from libogg */ + +/** + @file opus_types.h + @brief Opus reference implementation types +*/ +#ifndef OPUS_TYPES_H +#define OPUS_TYPES_H + +/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */ +#if (defined(__STDC__) && __STDC__ && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H)) +#include + + typedef int16_t opus_int16; + typedef uint16_t opus_uint16; + typedef int32_t opus_int32; + typedef uint32_t opus_uint32; +#elif defined(_WIN32) + +# if defined(__CYGWIN__) +# include <_G_config.h> + typedef _G_int32_t opus_int32; + typedef _G_uint32_t opus_uint32; + typedef _G_int16 opus_int16; + typedef _G_uint16 opus_uint16; +# elif defined(__MINGW32__) + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; +# elif defined(__MWERKS__) + typedef int opus_int32; + typedef unsigned int opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; +# else + /* MSVC/Borland */ + typedef __int32 opus_int32; + typedef unsigned __int32 opus_uint32; + typedef __int16 opus_int16; + typedef unsigned __int16 opus_uint16; +# endif + +#elif defined(__MACOS__) + +# include + typedef SInt16 opus_int16; + typedef UInt16 opus_uint16; + typedef SInt32 opus_int32; + typedef UInt32 opus_uint32; + +#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ + +# include + typedef int16_t opus_int16; + typedef u_int16_t opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined(__BEOS__) + + /* Be */ +# include + typedef int16 opus_int16; + typedef u_int16 opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined (__EMX__) + + /* OS/2 GCC */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined (DJGPP) + + /* DJGPP */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(R5900) + + /* PS2 EE */ + typedef int opus_int32; + typedef unsigned opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; + +#elif defined(__SYMBIAN32__) + + /* Symbian GCC */ + typedef signed short opus_int16; + typedef unsigned short opus_uint16; + typedef signed int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef long opus_int32; + typedef unsigned long opus_uint32; + +#elif defined(CONFIG_TI_C6X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#else + + /* Give up, take a reasonable guess */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#endif + +#define opus_int int /* used for counters etc; at least 16 bits */ +#define opus_int64 long long +#define opus_int8 signed char + +#define opus_uint unsigned int /* used for counters etc; at least 16 bits */ +#define opus_uint64 unsigned long long +#define opus_uint8 unsigned char + +#endif /* OPUS_TYPES_H */ diff --git a/third-party/opus/lib/libopus.a b/third-party/opus/lib/libopus.a new file mode 100644 index 0000000000..3fde71ea80 Binary files /dev/null and b/third-party/opus/lib/libopus.a differ diff --git a/third-party/opusenc/diag_range.c b/third-party/opusenc/diag_range.c new file mode 100644 index 0000000000..e04096297d --- /dev/null +++ b/third-party/opusenc/diag_range.c @@ -0,0 +1,245 @@ +/* Copyright (C)2012 Xiph.Org Foundation + Copyright (C)2012 Gregory Maxwell + Copyright (C)2012 Jean-Marc Valin + File: diag_range.c + + 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. + + 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. +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#ifdef _WIN32 +#define I64FORMAT "I64d" +#define I64uFORMAT "I64u" +#else +#define I64FORMAT "lld" +#define I64uFORMAT "llu" +#endif + +#include +#include "opus.h" +#include "diag_range.h" + +/*This is some non-exported code copied wholesale from libopus. + *Normal programs shouldn't need these functions, but we use them here + *to parse deep inside multichannel packets in order to get diagnostic + *data for save-range. If you're thinking about copying it and you aren't + *making an opus stream diagnostic tool, you're probably doing something + *wrong.*/ +static int parse_size(const unsigned char *data, opus_int32 len, short *size) +{ + if (len<1) + { + *size = -1; + return -1; + } else if (data[0]<252) + { + *size = data[0]; + return 1; + } else if (len<2) + { + *size = -1; + return -1; + } else { + *size = 4*data[1] + data[0]; + return 2; + } +} + +static int opus_packet_parse_impl(const unsigned char *data, opus_int32 len, + int self_delimited, unsigned char *out_toc, + const unsigned char *frames[48], short size[48], int *payload_offset) +{ + int i, bytes; + int count; + int cbr; + unsigned char ch, toc; + int framesize; + int last_size; + const unsigned char *data0 = data; + + if (size==NULL) + return OPUS_BAD_ARG; + + framesize = opus_packet_get_samples_per_frame(data, 48000); + + cbr = 0; + toc = *data++; + len--; + last_size = len; + switch (toc&0x3) + { + /* One frame */ + case 0: + count=1; + break; + /* Two CBR frames */ + case 1: + count=2; + cbr = 1; + if (!self_delimited) + { + if (len&0x1) + return OPUS_INVALID_PACKET; + size[0] = last_size = len/2; + } + break; + /* Two VBR frames */ + case 2: + count = 2; + bytes = parse_size(data, len, size); + len -= bytes; + if (size[0]<0 || size[0] > len) + return OPUS_INVALID_PACKET; + data += bytes; + last_size = len-size[0]; + break; + /* Multiple CBR/VBR frames (from 0 to 120 ms) */ + case 3: + if (len<1) + return OPUS_INVALID_PACKET; + /* Number of frames encoded in bits 0 to 5 */ + ch = *data++; + count = ch&0x3F; + if (count <= 0 || framesize*count > 5760) + return OPUS_INVALID_PACKET; + len--; + /* Padding flag is bit 6 */ + if (ch&0x40) + { + int padding=0; + int p; + do { + if (len<=0) + return OPUS_INVALID_PACKET; + p = *data++; + len--; + padding += p==255 ? 254: p; + } while (p==255); + len -= padding; + } + if (len<0) + return OPUS_INVALID_PACKET; + /* VBR flag is bit 7 */ + cbr = !(ch&0x80); + if (!cbr) + { + /* VBR case */ + last_size = len; + for (i=0;i len) + return OPUS_INVALID_PACKET; + data += bytes; + last_size -= bytes+size[i]; + } + if (last_size<0) + return OPUS_INVALID_PACKET; + } else if (!self_delimited) + { + /* CBR case */ + last_size = len/count; + if (last_size*count!=len) + return OPUS_INVALID_PACKET; + for (i=0;i len) + return OPUS_INVALID_PACKET; + data += bytes; + /* For CBR packets, apply the size to all the frames. */ + if (cbr) + { + if (size[count-1]*count > len) + return OPUS_INVALID_PACKET; + for (i=0;i last_size) + return OPUS_INVALID_PACKET; + } else + { + /* Because it's not encoded explicitly, it's possible the size of the + last packet (or all the packets, for the CBR case) is larger than + 1275. Reject them here.*/ + if (last_size > 1275) + return OPUS_INVALID_PACKET; + size[count-1] = last_size; + } + + if (frames) + { + for (i=0;i>3)+48)&92)+4)>>5], + bw_strings[opus_packet_get_bandwidth(subpkt)-OPUS_BANDWIDTH_NARROWBAND], + subpkt[0]&4?'S':'M',opus_packet_get_samples_per_frame(subpkt,48000)); + fprintf(frange,", %" I64uFORMAT "]%s",(unsigned long long)rngs[i],i+1==nb_streams?"\n":", "); + parsed_size-=payload_offset; + subpkt+=payload_offset; + } +} diff --git a/third-party/opusenc/diag_range.h b/third-party/opusenc/diag_range.h new file mode 100644 index 0000000000..aa0006cae7 --- /dev/null +++ b/third-party/opusenc/diag_range.h @@ -0,0 +1,28 @@ +/* Copyright (C)2012 Xiph.Org Foundation + File: diag_range.h + + 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. + + 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. +*/ + +void save_range(FILE *frange, int frame_size, unsigned char *packet, int nbBytes, opus_uint32 *rngs, int nb_streams); diff --git a/third-party/opusenc/opus_header.c b/third-party/opusenc/opus_header.c new file mode 100644 index 0000000000..d00d4fba23 --- /dev/null +++ b/third-party/opusenc/opus_header.c @@ -0,0 +1,286 @@ +/* Copyright (C)2012 Xiph.Org Foundation + File: opus_header.c + + 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. + + 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. +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "opus_header.h" +#include +#include + +/* Header contents: + - "OpusHead" (64 bits) + - version number (8 bits) + - Channels C (8 bits) + - Pre-skip (16 bits) + - Sampling rate (32 bits) + - Gain in dB (16 bits, S7.8) + - Mapping (8 bits, 0=single stream (mono/stereo) 1=Vorbis mapping, + 2..254: reserved, 255: multistream with no mapping) + + - if (mapping != 0) + - N = totel number of streams (8 bits) + - M = number of paired streams (8 bits) + - C times channel origin + - if (C<2*M) + - stream = byte/2 + - if (byte&0x1 == 0) + - left + else + - right + - else + - stream = byte-M +*/ + +typedef struct { + unsigned char *data; + int maxlen; + int pos; +} Packet; + +typedef struct { + const unsigned char *data; + int maxlen; + int pos; +} ROPacket; + +static int write_uint32(Packet *p, ogg_uint32_t val) +{ + if (p->pos>p->maxlen-4) + return 0; + p->data[p->pos ] = (val ) & 0xFF; + p->data[p->pos+1] = (val>> 8) & 0xFF; + p->data[p->pos+2] = (val>>16) & 0xFF; + p->data[p->pos+3] = (val>>24) & 0xFF; + p->pos += 4; + return 1; +} + +static int write_uint16(Packet *p, ogg_uint16_t val) +{ + if (p->pos>p->maxlen-2) + return 0; + p->data[p->pos ] = (val ) & 0xFF; + p->data[p->pos+1] = (val>> 8) & 0xFF; + p->pos += 2; + return 1; +} + +static int write_chars(Packet *p, const unsigned char *str, int nb_chars) +{ + int i; + if (p->pos>p->maxlen-nb_chars) + return 0; + for (i=0;idata[p->pos++] = str[i]; + return 1; +} + +static int read_uint32(ROPacket *p, ogg_uint32_t *val) +{ + if (p->pos>p->maxlen-4) + return 0; + *val = (ogg_uint32_t)p->data[p->pos ]; + *val |= (ogg_uint32_t)p->data[p->pos+1]<< 8; + *val |= (ogg_uint32_t)p->data[p->pos+2]<<16; + *val |= (ogg_uint32_t)p->data[p->pos+3]<<24; + p->pos += 4; + return 1; +} + +static int read_uint16(ROPacket *p, ogg_uint16_t *val) +{ + if (p->pos>p->maxlen-2) + return 0; + *val = (ogg_uint16_t)p->data[p->pos ]; + *val |= (ogg_uint16_t)p->data[p->pos+1]<<8; + p->pos += 2; + return 1; +} + +static int read_chars(ROPacket *p, unsigned char *str, int nb_chars) +{ + int i; + if (p->pos>p->maxlen-nb_chars) + return 0; + for (i=0;idata[p->pos++]; + return 1; +} + +int opus_header_parse(const unsigned char *packet, int len, OpusHeader *h) +{ + int i; + char str[9]; + ROPacket p; + unsigned char ch; + ogg_uint16_t shortval; + + p.data = packet; + p.maxlen = len; + p.pos = 0; + str[8] = 0; + if (len<19)return 0; + read_chars(&p, (unsigned char*)str, 8); + if (memcmp(str, "OpusHead", 8)!=0) + return 0; + + if (!read_chars(&p, &ch, 1)) + return 0; + h->version = ch; + if((h->version&240) != 0) /* Only major version 0 supported. */ + return 0; + + if (!read_chars(&p, &ch, 1)) + return 0; + h->channels = ch; + if (h->channels == 0) + return 0; + + if (!read_uint16(&p, &shortval)) + return 0; + h->preskip = shortval; + + if (!read_uint32(&p, &h->input_sample_rate)) + return 0; + + if (!read_uint16(&p, &shortval)) + return 0; + h->gain = (short)shortval; + + if (!read_chars(&p, &ch, 1)) + return 0; + h->channel_mapping = ch; + + if (h->channel_mapping != 0) + { + if (!read_chars(&p, &ch, 1)) + return 0; + + if (ch<1) + return 0; + h->nb_streams = ch; + + if (!read_chars(&p, &ch, 1)) + return 0; + + if (ch>h->nb_streams || (ch+h->nb_streams)>255) + return 0; + h->nb_coupled = ch; + + /* Multi-stream support */ + for (i=0;ichannels;i++) + { + if (!read_chars(&p, &h->stream_map[i], 1)) + return 0; + if (h->stream_map[i]>(h->nb_streams+h->nb_coupled) && h->stream_map[i]!=255) + return 0; + } + } else { + if(h->channels>2) + return 0; + h->nb_streams = 1; + h->nb_coupled = h->channels>1; + h->stream_map[0]=0; + h->stream_map[1]=1; + } + /*For version 0/1 we know there won't be any more data + so reject any that have data past the end.*/ + if ((h->version==0 || h->version==1) && p.pos != len) + return 0; + return 1; +} + +int opus_header_to_packet(const OpusHeader *h, unsigned char *packet, int len) +{ + int i; + Packet p; + unsigned char ch; + + p.data = packet; + p.maxlen = len; + p.pos = 0; + if (len<19)return 0; + if (!write_chars(&p, (const unsigned char*)"OpusHead", 8)) + return 0; + /* Version is 1 */ + ch = 1; + if (!write_chars(&p, &ch, 1)) + return 0; + + ch = h->channels; + if (!write_chars(&p, &ch, 1)) + return 0; + + if (!write_uint16(&p, h->preskip)) + return 0; + + if (!write_uint32(&p, h->input_sample_rate)) + return 0; + + if (!write_uint16(&p, h->gain)) + return 0; + + ch = h->channel_mapping; + if (!write_chars(&p, &ch, 1)) + return 0; + + if (h->channel_mapping != 0) + { + ch = h->nb_streams; + if (!write_chars(&p, &ch, 1)) + return 0; + + ch = h->nb_coupled; + if (!write_chars(&p, &ch, 1)) + return 0; + + /* Multi-stream support */ + for (i=0;ichannels;i++) + { + if (!write_chars(&p, &h->stream_map[i], 1)) + return 0; + } + } + + return p.pos; +} + +/* This is just here because it's a convenient file linked by both opusenc and + opusdec (to guarantee this maps stays in sync). */ +const int wav_permute_matrix[8][8] = +{ + {0}, /* 1.0 mono */ + {0,1}, /* 2.0 stereo */ + {0,2,1}, /* 3.0 channel ('wide') stereo */ + {0,1,2,3}, /* 4.0 discrete quadraphonic */ + {0,2,1,3,4}, /* 5.0 surround */ + {0,2,1,4,5,3}, /* 5.1 surround */ + {0,2,1,5,6,4,3}, /* 6.1 surround */ + {0,2,1,6,7,4,5,3} /* 7.1 surround (classic theater 8-track) */ +}; diff --git a/third-party/opusenc/opus_header.h b/third-party/opusenc/opus_header.h new file mode 100644 index 0000000000..473e03f29f --- /dev/null +++ b/third-party/opusenc/opus_header.h @@ -0,0 +1,51 @@ +/* Copyright (C)2012 Xiph.Org Foundation + File: opus_header.h + + 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. + + 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. +*/ + +#ifndef OPUS_HEADER_H +#define OPUS_HEADER_H + +#include + +typedef struct { + int version; + int channels; /* Number of channels: 1..255 */ + int preskip; + ogg_uint32_t input_sample_rate; + int gain; /* in dB S7.8 should be zero whenever possible */ + int channel_mapping; + /* The rest is only used if channel_mapping != 0 */ + int nb_streams; + int nb_coupled; + unsigned char stream_map[255]; +} OpusHeader; + +int opus_header_parse(const unsigned char *header, int len, OpusHeader *h); +int opus_header_to_packet(const OpusHeader *h, unsigned char *packet, int len); + +extern const int wav_permute_matrix[8][8]; + +#endif diff --git a/third-party/opusenc/opusenc.h b/third-party/opusenc/opusenc.h new file mode 100644 index 0000000000..03d6425ede --- /dev/null +++ b/third-party/opusenc/opusenc.h @@ -0,0 +1,17 @@ +#ifndef __OPUSENC_H +#define __OPUSENC_H + +#import + +@class TGDataItem; + +@interface TGOggOpusWriter : NSObject + +- (bool)beginWithDataItem:(TGDataItem *)dataItem; +- (bool)writeFrame:(uint8_t *)framePcmBytes frameByteCount:(NSUInteger)frameByteCount; +- (NSUInteger)encodedBytes; +- (NSTimeInterval)encodedDuration; + +@end + +#endif /* __OPUSENC_H */ diff --git a/third-party/opusenc/opusenc.m b/third-party/opusenc/opusenc.m new file mode 100644 index 0000000000..c3cf2e17d8 --- /dev/null +++ b/third-party/opusenc/opusenc.m @@ -0,0 +1,577 @@ +#import "opusenc.h" + +#include "opus_types.h" +#include + +#import "TGDataItem.h" + +#ifdef ENABLE_NLS +#include +#define _(X) gettext(X) +#else +#define _(X) (X) +#define textdomain(X) +#define bindtextdomain(X, Y) +#endif +#ifdef gettext_noop +#define N_(X) gettext_noop(X) +#else +#define N_(X) (X) +#endif + +typedef struct +{ + void *readdata; + opus_int64 total_samples_per_channel; + int rawmode; + int channels; + long rate; + int gain; + int samplesize; + int endianness; + char *infilename; + int ignorelength; + int skip; + int extraout; + char *comments; + int comments_length; + int copy_comments; +} oe_enc_opt; + +typedef struct +{ + int (*id_func)(unsigned char *buf, int len); /* Returns true if can load file */ + int id_data_len; /* Amount of data needed to id whether this can load the file */ + int (*open_func)(FILE *in, oe_enc_opt *opt, unsigned char *buf, int buflen); + void (*close_func)(void *); + char *format; + char *description; +} input_format; + +#include "opus.h" +#include "opus_multistream.h" + +#include + +#include "opus_header.h" + +static bool comment_init(char **comments, int* length, const char *vendor_string); +static bool comment_add(char **comments, int* length, char *tag, char *val); +static bool comment_pad(char **comments, int* length, int amount); + +static inline int writeOggPage(ogg_page *page, TGDataItem *fileItem) +{ + int written = (int)(page->header_len + page->body_len); + + NSMutableData *data = [[NSMutableData alloc] init]; + [data appendBytes:page->header length:page->header_len]; + [data appendBytes:page->body length:page->body_len]; + [fileItem appendData:data]; + + return MAX(0, written); +} + +@interface TGOggOpusWriter () +{ + TGDataItem *_dataItem; + + OpusEncoder *_encoder; + uint8_t *_packet; + + oe_enc_opt inopt; + + ogg_stream_state os; + ogg_page og; + ogg_packet op; + ogg_int64_t last_granulepos; + ogg_int64_t enc_granulepos; + int last_segments; + int eos; + OpusHeader header; + + ogg_int32_t _packetId; + int size_segments; + + opus_int64 nb_encoded; + opus_int64 bytes_written; + opus_int64 pages_out; + opus_int64 total_bytes; + opus_int64 total_samples; + opus_int32 nb_samples; + opus_int32 peak_bytes; + opus_int32 min_bytes; + + int max_frame_bytes; + opus_int32 bitrate; + opus_int32 rate; + opus_int32 coding_rate; + opus_int32 frame_size; + int with_cvbr; + int max_ogg_delay; + int comment_padding; + int serialno; + opus_int32 lookahead; +} + +@end + +@implementation TGOggOpusWriter + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + bitrate = 20 * 1024; + rate = 48000; + coding_rate = 16000; + frame_size = 960; + with_cvbr = 1; + max_ogg_delay = 48000; + comment_padding = 512; + + _packetId = -1; + } + return self; +} + +- (void)cleanup +{ + if (_encoder != NULL) + { + opus_encoder_destroy(_encoder); + _encoder = NULL; + } + + ogg_stream_clear(&os); + + if (_packet != NULL) + { + free(_packet); + _packet = NULL; + } +} + +- (bool)beginWithDataItem:(TGDataItem *)dataItem +{ + _dataItem = dataItem; + + inopt.channels = 1; + inopt.rate = coding_rate=rate; + inopt.gain = 0; + inopt.samplesize = 16; + inopt.endianness = 0; + inopt.rawmode = 0; + inopt.ignorelength = 0; + inopt.copy_comments = 0; + + arc4random_buf(&serialno, sizeof(serialno)); + + const char *opus_version = opus_get_version_string(); + comment_init(&inopt.comments, &inopt.comments_length, opus_version); + + bitrate = 16 * 1024; + + inopt.rawmode = 1; + inopt.ignorelength = 1; + inopt.samplesize = 16; + inopt.rate = 16000; + inopt.channels = 1; + + rate = (opus_int32)inopt.rate; + inopt.skip = 0; + + // In order to code the complete length we'll need to do a little padding + //setup_padder(&inopt, &original_samples); + + if (rate > 24000) + coding_rate = 48000; + else if (rate > 16000) + coding_rate = 24000; + else if (rate > 12000) + coding_rate = 16000; + else if (rate > 8000) + coding_rate = 12000; + else + coding_rate = 8000; + + // Scale the resampler complexity, but only for 48000 output because the near-cutoff behavior matters a lot more at lower rates + if (rate != coding_rate) + { + NSLog(@"Invalid rate"); + return false; + } + + header.channels = 1; + header.channel_mapping = 0; + header.input_sample_rate = rate; + header.gain = inopt.gain; + header.nb_streams = 1; + + int result = OPUS_OK; + _encoder = opus_encoder_create(coding_rate, 1, OPUS_APPLICATION_AUDIO, &result); + if (result != OPUS_OK) + { + NSLog(@"Error cannot create encoder: %s", opus_strerror(result)); + return false; + } + + min_bytes = max_frame_bytes = (1275 * 3 + 7) * header.nb_streams; + _packet = malloc(max_frame_bytes); + + result = opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate)); + if (result != OPUS_OK) + { + NSLog(@"Error OPUS_SET_BITRATE returned: %s", opus_strerror(result)); + return false; + } + + /*result = opus_encoder_ctl(_encoder, OPUS_SET_VBR(1)); + if (result != OPUS_OK) + { + NSLog(@"Error OPUS_SET_VBR returned: %s", opus_strerror(result)); + return false; + }*/ + + /*ret = opus_multistream_encoder_ctl(st, OPUS_SET_VBR_CONSTRAINT(1)); + if (ret != OPUS_OK) + { + NSLog(@"Error OPUS_SET_VBR_CONSTRAINT returned: %s", opus_strerror(ret)); + return false; + }*/ + + /*ret = opus_multistream_encoder_ctl(st, OPUS_SET_COMPLEXITY(complexity)); + if(ret != OPUS_OK) + { + NSLog(@"Error OPUS_SET_COMPLEXITY returned: %s", opus_strerror(ret)); + return false; + }*/ + + /*result = opus_encoder_ctl(st, OPUS_SET_PACKET_LOSS_PERC(expect_loss)); + if (ret != OPUS_OK) + { + NSLog(@"Error OPUS_SET_PACKET_LOSS_PERC returned: %s", opus_strerror(ret)); + return false; + }*/ + +#ifdef OPUS_SET_LSB_DEPTH + result = opus_encoder_ctl(_encoder, OPUS_SET_LSB_DEPTH(MAX(8, MIN(24, inopt.samplesize)))); + if (result != OPUS_OK) + { + NSLog(@"Warning OPUS_SET_LSB_DEPTH returned: %s", opus_strerror(result)); + } +#endif + + // We do the lookahead check late so user CTLs can change it + result = opus_encoder_ctl(_encoder, OPUS_GET_LOOKAHEAD(&lookahead)); + if (result != OPUS_OK) + { + NSLog(@"Error OPUS_GET_LOOKAHEAD returned: %s", opus_strerror(result)); + return false; + } + + inopt.skip += lookahead; + // Regardless of the rate we're coding at the ogg timestamping/skip is always timed at 48000. + header.preskip = (int)(inopt.skip * (48000.0 / coding_rate)); + // Extra samples that need to be read to compensate for the pre-skip + inopt.extraout = (int)(header.preskip * (rate / 48000.0)); + + // Initialize Ogg stream struct + if (ogg_stream_init(&os, serialno) == -1) + { + NSLog(@"Error: stream init failed"); + return false; + } + + // Write header + { + unsigned char header_data[100]; + int packet_size = opus_header_to_packet(&header, header_data, 100); + op.packet = header_data; + op.bytes = packet_size; + op.b_o_s = 1; + op.e_o_s = 0; + op.granulepos = 0; + op.packetno = 0; + ogg_stream_packetin(&os, &op); + + while ((result = ogg_stream_flush(&os, &og))) + { + if (!result) + break; + + int pageBytesWritten = writeOggPage(&og, _dataItem); + if (pageBytesWritten != og.header_len + og.body_len) + { + NSLog(@"Error: failed writing header to output stream"); + return false; + } + bytes_written += pageBytesWritten; + pages_out++; + } + + comment_pad(&inopt.comments, &inopt.comments_length, comment_padding); + op.packet = (unsigned char *)inopt.comments; + op.bytes = inopt.comments_length; + op.b_o_s = 0; + op.e_o_s = 0; + op.granulepos = 0; + op.packetno = 1; + ogg_stream_packetin(&os, &op); + } + + // Writing the rest of the opus header packets + while ((result = ogg_stream_flush(&os, &og))) + { + if (result == 0) + break; + + int writtenPageBytes = writeOggPage(&og, _dataItem); + if (writtenPageBytes != og.header_len + og.body_len) + { + NSLog(@"Error: failed writing header to output stream"); + return false; + } + + bytes_written += writtenPageBytes; + pages_out++; + } + + free(inopt.comments); + + return true; +} + +- (bool)writeFrame:(uint8_t *)framePcmBytes frameByteCount:(NSUInteger)frameByteCount +{ + // Main encoding loop (one frame per iteration) + nb_samples = -1; + + int cur_frame_size = frame_size; + _packetId++; + + if (nb_samples < 0) + { + nb_samples = (opus_int32)(frameByteCount / 2); + total_samples += nb_samples; + if (nb_samples < frame_size) + op.e_o_s = 1; + else + op.e_o_s = 0; + } + op.e_o_s |= eos; + + int nbBytes = 0; + + if (nb_samples != 0) + { + uint8_t *paddedFrameBytes = framePcmBytes; + bool freePaddedFrameBytes = false; + + if (nb_samples < cur_frame_size) + { + paddedFrameBytes = malloc(cur_frame_size * 2); + freePaddedFrameBytes = true; + + memcpy(paddedFrameBytes, framePcmBytes, frameByteCount); + memset(paddedFrameBytes + nb_samples * 2, 0, cur_frame_size * 2 - nb_samples * 2); + } + + // Encode current frame + nbBytes = opus_encode(_encoder, (opus_int16 *)paddedFrameBytes, cur_frame_size, _packet, max_frame_bytes / 10); + if (freePaddedFrameBytes) + { + free(paddedFrameBytes); + paddedFrameBytes = NULL; + } + + if (nbBytes < 0) + { + NSLog(@"Encoding failed: %s. Aborting.", opus_strerror(nbBytes)); + return false; + } + + nb_encoded += cur_frame_size; + enc_granulepos += cur_frame_size * 48000 / coding_rate; + total_bytes += nbBytes; + size_segments = (nbBytes + 255) / 255; + peak_bytes = MAX(nbBytes, peak_bytes); + min_bytes = MIN(nbBytes, min_bytes); + } + + // Flush early if adding this packet would make us end up with a continued page which we wouldn't have otherwise + while ((((size_segments<=255)&&(last_segments+size_segments>255)) || + (enc_granulepos-last_granulepos>max_ogg_delay)) && + ogg_stream_flush_fill(&os, &og, 255 * 255)) + { + if (ogg_page_packets(&og) != 0) + last_granulepos = ogg_page_granulepos(&og); + + last_segments -= og.header[26]; + int writtenPageBytes = writeOggPage(&og, _dataItem); + if (writtenPageBytes != og.header_len + og.body_len) + { + NSLog(@"Error: failed writing data to output stream"); + return false; + } + bytes_written += writtenPageBytes; + pages_out++; + } + + op.packet = (unsigned char *)_packet; + op.bytes = nbBytes; + op.b_o_s = 0; + op.granulepos = enc_granulepos; + if (op.e_o_s) + { + /* We compute the final GP as ceil(len*48k/input_rate). When a resampling + decoder does the matching floor(len*input/48k) conversion the length will + be exactly the same as the input. + */ + op.granulepos = ((total_samples * 48000 + rate - 1) / rate) + header.preskip; + } + op.packetno = 2 + _packetId; + ogg_stream_packetin(&os, &op); + last_segments += size_segments; + + /* The downside of early reading is if the input is an exact + multiple of the frame_size you'll get an extra frame that needs + to get cropped off. The downside of late reading is added delay. + If your ogg_delay is 120ms or less we'll assume you want the + low delay behavior. + */ + /*if ((!op.e_o_s) && max_ogg_delay > 5760) + { + nb_samples = inopt.read_samples(inopt.readdata, input, frame_size); + total_samples += nb_samples; + if (nb_samples < frame_size) + eos = 1; + if (nb_samples == 0) + op.e_o_s = 1; + } + else + nb_samples = -1;*/ + + // If the stream is over or we're sure that the delayed flush will fire, go ahead and flush now to avoid adding delay + while ((op.e_o_s || (enc_granulepos + (frame_size * 48000 / coding_rate) - last_granulepos > max_ogg_delay) || + (last_segments >= 255)) ? ogg_stream_flush_fill(&os, &og, 255 * 255) : ogg_stream_pageout_fill(&os, &og, 255 * 255)) + { + if (ogg_page_packets(&og) != 0) + last_granulepos = ogg_page_granulepos(&og); + last_segments -= og.header[26]; + int writtenPageBytes = writeOggPage(&og, _dataItem); + if (writtenPageBytes != og.header_len + og.body_len) + { + NSLog(@"Error: failed writing data to output stream"); + return false; + } + bytes_written += writtenPageBytes; + pages_out++; + } + + return true; +} + +- (NSUInteger)encodedBytes +{ + return (NSUInteger)bytes_written; +} + +- (NSTimeInterval)encodedDuration +{ + return total_samples / (NSTimeInterval)coding_rate; +} + +@end + +/* + Comments will be stored in the Vorbis style. + It is describled in the "Structure" section of + http://www.xiph.org/ogg/vorbis/doc/v-comment.html + + However, Opus and other non-vorbis formats omit the "framing_bit". + +The comment header is decoded as follows: + 1) [vendor_length] = read an unsigned integer of 32 bits + 2) [vendor_string] = read a UTF-8 vector as [vendor_length] octets + 3) [user_comment_list_length] = read an unsigned integer of 32 bits + 4) iterate [user_comment_list_length] times { + 5) [length] = read an unsigned integer of 32 bits + 6) this iteration's user comment = read a UTF-8 vector as [length] octets + } + 7) done. +*/ + +#define readint(buf, base) (((buf[base+3]<<24)&0xff000000)| \ + ((buf[base+2]<<16)&0xff0000)| \ + ((buf[base+1]<<8)&0xff00)| \ + (buf[base]&0xff)) +#define writeint(buf, base, val) do{ buf[base+3]=((val)>>24)&0xff; \ + buf[base+2]=((val)>>16)&0xff; \ + buf[base+1]=((val)>>8)&0xff; \ + buf[base]=(val)&0xff; \ + }while(0) + +static bool comment_init(char **comments, int *length, const char *vendor_string) +{ + // The 'vendor' field should be the actual encoding library used + int vendor_length = (int)strlen(vendor_string); + int user_comment_list_length = 0; + int len = 8 + 4 + vendor_length + 4; + char *p = (char *)malloc(len); + memcpy(p, "OpusTags", 8); + writeint(p, 8, vendor_length); + memcpy(p + 12, vendor_string, vendor_length); + writeint(p, 12 + vendor_length, user_comment_list_length); + *length = len; + *comments = p; + + return true; +} + +__unused bool comment_add(char **comments, int* length, char *tag, char *val) +{ + char *p = *comments; + int vendor_length = readint(p, 8); + int user_comment_list_length = readint(p, 8 + 4 + vendor_length); + int tag_len = (tag ? (int)strlen(tag) + 1 : 0); + int val_len = (int)strlen(val); + int len = (*length) + 4 + tag_len + val_len; + + p = (char *)realloc(p, len); + + writeint(p, *length, tag_len+val_len); /* length of comment */ + if (tag) + { + memcpy(p + *length + 4, tag, tag_len); /* comment tag */ + (p+*length+4)[tag_len-1] = '='; /* separator */ + } + memcpy(p + *length + 4 + tag_len, val, val_len); /* comment */ + writeint(p, 8 + 4 + vendor_length, user_comment_list_length + 1); + *comments = p; + *length = len; + + return true; +} + +static bool comment_pad(char **comments, int* length, int amount) +{ + if (amount > 0) + { + char *p = *comments; + // Make sure there is at least amount worth of padding free, and round up to the maximum that fits in the current ogg segments + int newlen = (*length + amount + 255) / 255 * 255 - 1; + p = realloc(p, newlen); + for (int i = *length; i < newlen; i++) + { + p[i] = 0; + } + *comments = p; + *length = newlen; + } + + return true; +} + +#undef readint +#undef writeint diff --git a/third-party/opusenc/picture.c b/third-party/opusenc/picture.c new file mode 100644 index 0000000000..bd1233590c --- /dev/null +++ b/third-party/opusenc/picture.c @@ -0,0 +1,499 @@ +/* Copyright (C)2007-2013 Xiph.Org Foundation + File: picture.c + + 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. + + 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. +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include "picture.h" + +static const char BASE64_TABLE[64]={ + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' +}; + +/*Utility function for base64 encoding METADATA_BLOCK_PICTURE tags. + Stores BASE64_LENGTH(len)+1 bytes in dst (including a terminating NUL).*/ +void base64_encode(char *dst, const char *src, int len){ + unsigned s0; + unsigned s1; + unsigned s2; + int ngroups; + int i; + ngroups=len/3; + for(i=0;i>2]; + dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4]; + dst[4*i+2]=BASE64_TABLE[(s1&15)<<2|s2>>6]; + dst[4*i+3]=BASE64_TABLE[s2&63]; + } + len-=3*i; + if(len==1){ + s0=(unsigned char)src[3*i+0]; + dst[4*i+0]=BASE64_TABLE[s0>>2]; + dst[4*i+1]=BASE64_TABLE[(s0&3)<<4]; + dst[4*i+2]='='; + dst[4*i+3]='='; + i++; + } + else if(len==2){ + s0=(unsigned char)src[3*i+0]; + s1=(unsigned char)src[3*i+1]; + dst[4*i+0]=BASE64_TABLE[s0>>2]; + dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4]; + dst[4*i+2]=BASE64_TABLE[(s1&15)<<2]; + dst[4*i+3]='='; + i++; + } + dst[4*i]='\0'; +} + +/*A version of strncasecmp() that is guaranteed to only ignore the case of + ASCII characters.*/ +int oi_strncasecmp(const char *a, const char *b, int n){ + int i; + for(i=0;i='a'&&aval<='z') { + aval-='a'-'A'; + } + if(bval>='a'&&bval<='z'){ + bval-='a'-'A'; + } + diff=aval-bval; + if(diff){ + return diff; + } + } + return 0; +} + +int is_jpeg(const unsigned char *buf, size_t length){ + return length>=11&&memcmp(buf,"\xFF\xD8\xFF\xE0",4)==0 + &&(buf[4]<<8|buf[5])>=16&&memcmp(buf+6,"JFIF",5)==0; +} + +int is_png(const unsigned char *buf, size_t length){ + return length>=8&&memcmp(buf,"\x89PNG\x0D\x0A\x1A\x0A",8)==0; +} + +int is_gif(const unsigned char *buf, size_t length){ + return length>=6 + &&(memcmp(buf,"GIF87a",6)==0||memcmp(buf,"GIF89a",6)==0); +} + +#define READ_U32_BE(buf) \ + (((buf)[0]<<24)|((buf)[1]<<16)|((buf)[2]<<8)|((buf)[3]&0xff)) + +/*Tries to extract the width, height, bits per pixel, and palette size of a + PNG. + On failure, simply leaves its outputs unmodified.*/ +void extract_png_params(const unsigned char *data, size_t data_length, + ogg_uint32_t *width, ogg_uint32_t *height, + ogg_uint32_t *depth, ogg_uint32_t *colors, + int *has_palette){ + if(is_png(data,data_length)){ + size_t offs; + offs=8; + while(data_length-offs>=12){ + ogg_uint32_t chunk_len; + chunk_len=READ_U32_BE(data+offs); + if(chunk_len>data_length-(offs+12))break; + else if(chunk_len==13&&memcmp(data+offs+4,"IHDR",4)==0){ + int color_type; + *width=READ_U32_BE(data+offs+8); + *height=READ_U32_BE(data+offs+12); + color_type=data[offs+17]; + if(color_type==3){ + *depth=24; + *has_palette=1; + } + else{ + int sample_depth; + sample_depth=data[offs+16]; + if(color_type==0)*depth=sample_depth; + else if(color_type==2)*depth=sample_depth*3; + else if(color_type==4)*depth=sample_depth*2; + else if(color_type==6)*depth=sample_depth*4; + *colors=0; + *has_palette=0; + break; + } + } + else if(*has_palette>0&&memcmp(data+offs+4,"PLTE",4)==0){ + *colors=chunk_len/3; + break; + } + offs+=12+chunk_len; + } + } +} + +/*Tries to extract the width, height, bits per pixel, and palette size of a + GIF. + On failure, simply leaves its outputs unmodified.*/ +void extract_gif_params(const unsigned char *data, size_t data_length, + ogg_uint32_t *width, ogg_uint32_t *height, + ogg_uint32_t *depth, ogg_uint32_t *colors, + int *has_palette){ + if(is_gif(data,data_length)&&data_length>=14){ + *width=data[6]|data[7]<<8; + *height=data[8]|data[9]<<8; + /*libFLAC hard-codes the depth to 24.*/ + *depth=24; + *colors=1<<((data[10]&7)+1); + *has_palette=1; + } +} + + +/*Tries to extract the width, height, bits per pixel, and palette size of a + JPEG. + On failure, simply leaves its outputs unmodified.*/ +void extract_jpeg_params(const unsigned char *data, size_t data_length, + ogg_uint32_t *width, ogg_uint32_t *height, + ogg_uint32_t *depth, ogg_uint32_t *colors, + int *has_palette){ + if(is_jpeg(data,data_length)){ + size_t offs; + offs=2; + for(;;){ + size_t segment_len; + int marker; + while(offs=data_length||(marker>=0xD8&&marker<=0xDA))break; + /*RST* (restart markers): skip (no segment length).*/ + else if(marker>=0xD0&&marker<=0xD7)continue; + /*Read the length of the marker segment.*/ + if(data_length-offs<2)break; + segment_len=data[offs]<<8|data[offs+1]; + if(segment_len<2||data_length-offs0xC0&&marker<0xD0&&(marker&3)!=0)){ + /*Found a SOFn (start of frame) marker segment:*/ + if(segment_len>=8){ + *height=data[offs+3]<<8|data[offs+4]; + *width=data[offs+5]<<8|data[offs+6]; + *depth=data[offs+2]*data[offs+7]; + *colors=0; + *has_palette=0; + } + break; + } + /*Other markers: skip the whole marker segment.*/ + offs+=segment_len; + } + } +} + +#define IMAX(a,b) ((a) > (b) ? (a) : (b)) + +/*Parse a picture SPECIFICATION as given on the command-line. + spec: The specification. + error_message: Returns an error message on error. + seen_file_icons: Bit flags used to track if any pictures of type 1 or type 2 + have already been added, to ensure only one is allowed. + Return: A Base64-encoded string suitable for use in a METADATA_BLOCK_PICTURE + tag.*/ +char *parse_picture_specification(const char *spec, + const char **error_message, + int *seen_file_icons){ + FILE *picture_file; + unsigned long picture_type; + unsigned long width; + unsigned long height; + unsigned long depth; + unsigned long colors; + const char *mime_type; + const char *mime_type_end; + const char *description; + const char *description_end; + const char *filename; + unsigned char *buf; + char *out; + size_t cbuf; + size_t nbuf; + size_t data_offset; + size_t data_length; + size_t b64_length; + int is_url; + /*If a filename has a '|' in it, there's no way we can distinguish it from a + full specification just from the spec string. + Instead, try to open the file. + If it exists, the user probably meant the file.*/ + picture_type=3; + width=height=depth=colors=0; + mime_type=mime_type_end=description=description_end=filename=spec; + is_url=0; + picture_file=fopen(filename,"rb"); + if(picture_file==NULL&&strchr(spec,'|')){ + const char *p; + char *q; + /*We don't have a plain file, and there is a pipe character: assume it's + the full form of the specification.*/ + picture_type=strtoul(spec,&q,10); + if(*q!='|'||picture_type>20){ + *error_message="invalid picture type"; + return NULL; + } + if(picture_type>=1&&picture_type<=2&&(*seen_file_icons&picture_type)){ + *error_message=picture_type==1? + "only one picture of type 1 (32x32 icon) allowed": + "only one picture of type 2 (icon) allowed"; + return NULL; + } + /*An empty field implies a default of 'Cover (front)'.*/ + if(spec==q)picture_type=3; + mime_type=q+1; + mime_type_end=mime_type+strcspn(mime_type,"|"); + if(*mime_type_end!='|'){ + *error_message="invalid picture specification: not enough fields"; + return NULL; + } + /*The mime type must be composed of ASCII printable characters 0x20-0x7E.*/ + for(p=mime_type;p0x7E){ + *error_message="invalid characters in mime type"; + return NULL; + } + is_url=mime_type_end-mime_type==3 + &&strncmp("-->",mime_type,mime_type_end-mime_type)==0; + description=mime_type_end+1; + description_end=description+strcspn(description,"|"); + if(*description_end!='|'){ + *error_message="invalid picture specification: not enough fields"; + return NULL; + } + p=description_end+1; + if(*p!='|'){ + width=strtoul(p,&q,10); + if(*q!='x'){ + *error_message= + "invalid picture specification: can't parse resolution/color field"; + return NULL; + } + p=q+1; + height=strtoul(p,&q,10); + if(*q!='x'){ + *error_message= + "invalid picture specification: can't parse resolution/color field"; + return NULL; + } + p=q+1; + depth=strtoul(p,&q,10); + if(*q=='/'){ + p=q+1; + colors=strtoul(p,&q,10); + } + if(*q!='|'){ + *error_message= + "invalid picture specification: can't parse resolution/color field"; + return NULL; + } + p=q; + } + filename=p+1; + if(!is_url)picture_file=fopen(filename,"rb"); + } + /*Buffer size: 8 static 4-byte fields plus 2 dynamic fields, plus the + file/URL data. + We reserve at least 10 bytes for the mime type, in case we still need to + extract it from the file.*/ + data_offset=32+(description_end-description)+IMAX(mime_type_end-mime_type,10); + buf=NULL; + if(is_url){ + /*Easy case: just stick the URL at the end. + We don't do anything to verify it's a valid URL.*/ + data_length=strlen(filename); + cbuf=nbuf=data_offset+data_length; + buf=(unsigned char *)malloc(cbuf); + memcpy(buf+data_offset,filename,data_length); + } + else{ + ogg_uint32_t file_width; + ogg_uint32_t file_height; + ogg_uint32_t file_depth; + ogg_uint32_t file_colors; + int has_palette; + /*Complicated case: we have a real file. + Read it in, attempt to parse the mime type and image dimensions if + necessary, and validate what the user passed in.*/ + if(picture_file==NULL){ + *error_message="error opening picture file"; + return NULL; + } + nbuf=data_offset; + /*Add a reasonable starting image file size.*/ + cbuf=data_offset+65536; + for(;;){ + unsigned char *new_buf; + size_t nread; + new_buf=realloc(buf,cbuf); + if(new_buf==NULL){ + fclose(picture_file); + free(buf); + *error_message="insufficient memory"; + return NULL; + } + buf=new_buf; + nread=fread(buf+nbuf,1,cbuf-nbuf,picture_file); + nbuf+=nread; + if(nbuf0x7FFFFFFFU)cbuf=0xFFFFFFFFU; + else cbuf=cbuf<<1|1; + } + data_length=nbuf-data_offset; + /*If there was no mimetype, try to extract it from the file data.*/ + if(mime_type_end==mime_type){ + if(is_jpeg(buf+data_offset,data_length)){ + mime_type="image/jpeg"; + mime_type_end=mime_type+10; + } + else if(is_png(buf+data_offset,data_length)){ + mime_type="image/png"; + mime_type_end=mime_type+9; + } + else if(is_gif(buf+data_offset,data_length)){ + mime_type="image/gif"; + mime_type_end=mime_type+9; + } + else{ + free(buf); + *error_message="unable to guess MIME type from file, " + "must set it explicitly"; + return NULL; + } + } + /*Try to extract the image dimensions/color information from the file.*/ + file_width=file_height=file_depth=file_colors=0; + has_palette=-1; + if(mime_type_end-mime_type==9 + &&oi_strncasecmp("image/png",mime_type,mime_type_end-mime_type)==0){ + extract_png_params(buf+data_offset,data_length, + &file_width,&file_height,&file_depth,&file_colors,&has_palette); + } + else if(mime_type_end-mime_type==9 + &&oi_strncasecmp("image/gif",mime_type,mime_type_end-mime_type)==0){ + extract_gif_params(buf+data_offset,data_length, + &file_width,&file_height,&file_depth,&file_colors,&has_palette); + } + else if(mime_type_end-mime_type==10 + &&oi_strncasecmp("image/jpeg",mime_type,mime_type_end-mime_type)==0){ + extract_jpeg_params(buf+data_offset,data_length, + &file_width,&file_height,&file_depth,&file_colors,&has_palette); + } + if(!width)width=file_width; + if(!height)height=file_height; + if(!depth)depth=file_depth; + if(!colors)colors=file_colors; + if((file_width&&width!=file_width) + ||(file_height&&height!=file_height) + ||(file_depth&&depth!=file_depth) + /*We use has_palette to ensure we also reject non-0 user color counts for + images we've positively identified as non-paletted.*/ + ||(has_palette>=0&&colors!=file_colors)){ + free(buf); + *error_message="invalid picture specification: " + "resolution/color field does not match file"; + return NULL; + } + } + /*These fields MUST be set correctly OR all set to zero. + So if any of them (except colors, for which 0 is a valid value) are still + zero, clear the rest to zero.*/ + if(width==0||height==0||depth==0)width=height=depth=colors=0; + if(picture_type==1&&(width!=32||height!=32 + ||mime_type_end-mime_type!=9 + ||oi_strncasecmp("image/png",mime_type,mime_type_end-mime_type)!=0)){ + free(buf); + *error_message="pictures of type 1 MUST be 32x32 PNGs"; + return NULL; + } + /*Build the METADATA_BLOCK_PICTURE buffer. + We do this backwards from data_offset, because we didn't necessarily know + how big the mime type string was before we read the data in.*/ + data_offset-=4; + WRITE_U32_BE(buf+data_offset,(unsigned long)data_length); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,colors); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,depth); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,height); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,width); + data_offset-=description_end-description; + memcpy(buf+data_offset,description,description_end-description); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,(unsigned long)(description_end-description)); + data_offset-=mime_type_end-mime_type; + memcpy(buf+data_offset,mime_type,mime_type_end-mime_type); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,(unsigned long)(mime_type_end-mime_type)); + data_offset-=4; + WRITE_U32_BE(buf+data_offset,picture_type); + data_length=nbuf-data_offset; + b64_length=BASE64_LENGTH(data_length); + out=(char *)malloc(b64_length+1); + if(out!=NULL){ + base64_encode(out,(char *)buf+data_offset,data_length); + if(picture_type>=1&&picture_type<=2)*seen_file_icons|=picture_type; + } + free(buf); + return out; +} diff --git a/third-party/opusenc/picture.h b/third-party/opusenc/picture.h new file mode 100644 index 0000000000..bd48d547b4 --- /dev/null +++ b/third-party/opusenc/picture.h @@ -0,0 +1,50 @@ +#ifndef __PICTURE_H +#define __PICTURE_H + +#include + +typedef enum{ + PIC_FORMAT_JPEG, + PIC_FORMAT_PNG, + PIC_FORMAT_GIF +}picture_format; + +#define BASE64_LENGTH(len) (((len)+2)/3*4) + +/*Utility function for base64 encoding METADATA_BLOCK_PICTURE tags. + Stores BASE64_LENGTH(len)+1 bytes in dst (including a terminating NUL).*/ +void base64_encode(char *dst, const char *src, int len); + +int oi_strncasecmp(const char *a, const char *b, int n); + +int is_jpeg(const unsigned char *buf, size_t length); +int is_png(const unsigned char *buf, size_t length); +int is_gif(const unsigned char *buf, size_t length); + +void extract_png_params(const unsigned char *data, size_t data_length, + ogg_uint32_t *width, ogg_uint32_t *height, + ogg_uint32_t *depth, ogg_uint32_t *colors, + int *has_palette); +void extract_gif_params(const unsigned char *data, size_t data_length, + ogg_uint32_t *width, ogg_uint32_t *height, + ogg_uint32_t *depth, ogg_uint32_t *colors, + int *has_palette); +void extract_jpeg_params(const unsigned char *data, size_t data_length, + ogg_uint32_t *width, ogg_uint32_t *height, + ogg_uint32_t *depth, ogg_uint32_t *colors, + int *has_palette); + +char *parse_picture_specification(const char *spec, + const char **error_message, + int *seen_file_icons); + +#define WRITE_U32_BE(buf, val) \ + do{ \ + (buf)[0]=(unsigned char)((val)>>24); \ + (buf)[1]=(unsigned char)((val)>>16); \ + (buf)[2]=(unsigned char)((val)>>8); \ + (buf)[3]=(unsigned char)(val); \ + } \ + while(0); + +#endif /* __PICTURE_H */ diff --git a/third-party/opusenc/wav_io.c b/third-party/opusenc/wav_io.c new file mode 100644 index 0000000000..007261a9fb --- /dev/null +++ b/third-party/opusenc/wav_io.c @@ -0,0 +1,125 @@ +/* Copyright (C) 2002 Jean-Marc Valin + File: wav_io.c + Routines to handle wav (RIFF) headers + + 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. + + 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. +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include "wav_io.h" +#include "opus_header.h" + +/* Adjust the stream->channel mapping to ensure the proper output order for + WAV files. */ +void adjust_wav_mapping(int mapping_family, int channels, unsigned char *stream_map) +{ + unsigned char new_stream_map[8]; + int i; + /* If we aren't using one of the defined semantic channel maps, or we have + more channels than we know what to do with, use a default 1-1 mapping. */ + if(mapping_family != 1 || channels > 8) + return; + for(i = 0; i < channels; i++) + { + new_stream_map[wav_permute_matrix[channels-1][i]] = stream_map[i]; + } + memcpy(stream_map, new_stream_map, channels*sizeof(*stream_map)); +} + +static size_t fwrite_le32(opus_int32 i32, FILE *file) +{ + unsigned char buf[4]; + buf[0]=(unsigned char)(i32&0xFF); + buf[1]=(unsigned char)(i32>>8&0xFF); + buf[2]=(unsigned char)(i32>>16&0xFF); + buf[3]=(unsigned char)(i32>>24&0xFF); + return fwrite(buf,4,1,file); +} + +static size_t fwrite_le16(int i16, FILE *file) +{ + unsigned char buf[2]; + buf[0]=(unsigned char)(i16&0xFF); + buf[1]=(unsigned char)(i16>>8&0xFF); + return fwrite(buf,2,1,file); +} + +int write_wav_header(FILE *file, int rate, int mapping_family, int channels) +{ + int ret; + int extensible; + + /* Multichannel files require a WAVEFORMATEXTENSIBLE header to declare the + proper channel meanings. */ + extensible = mapping_family == 1 && 3 <= channels && channels <= 8; + + ret = fprintf (file, "RIFF") >= 0; + ret &= fwrite_le32 (0x7fffffff, file); + + ret &= fprintf (file, "WAVEfmt ") >= 0; + ret &= fwrite_le32 (extensible ? 40 : 16, file); + ret &= fwrite_le16 (extensible ? 0xfffe : 1, file); + ret &= fwrite_le16 (channels, file); + ret &= fwrite_le32 (rate, file); + ret &= fwrite_le32 (2*channels*rate, file); + ret &= fwrite_le16 (2*channels, file); + ret &= fwrite_le16 (16, file); + + if(extensible) + { + static const unsigned char ksdataformat_subtype_pcm[16]= + { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, + 0x10, 0x00, + 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 + }; + static const int wav_channel_masks[8] = + { + 1, /* 1.0 mono */ + 1|2, /* 2.0 stereo */ + 1|2|4, /* 3.0 channel ('wide') stereo */ + 1|2|16|32, /* 4.0 discrete quadrophonic */ + 1|2|4|16|32, /* 5.0 */ + 1|2|4|8|16|32, /* 5.1 */ + 1|2|4|8|256|512|1024, /* 6.1 */ + 1|2|4|8|16|32|512|1024, /* 7.1 */ + }; + ret &= fwrite_le16 (22, file); + ret &= fwrite_le16 (16, file); + ret &= fwrite_le32 (wav_channel_masks[channels-1], file); + ret &= fwrite (ksdataformat_subtype_pcm, 16, 1, file); + } + + ret &= fprintf (file, "data") >= 0; + ret &= fwrite_le32 (0x7fffffff, file); + + return !ret ? -1 : extensible ? 40 : 16; +} diff --git a/third-party/opusenc/wav_io.h b/third-party/opusenc/wav_io.h new file mode 100644 index 0000000000..92b3892fdb --- /dev/null +++ b/third-party/opusenc/wav_io.h @@ -0,0 +1,62 @@ +/* Copyright (C) 2002 Jean-Marc Valin + File: wav_io.h + + 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. + + 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. +*/ + +#ifndef WAV_IO_H +#define WAV_IO_H + +#include +#include "opus_types.h" + +#if !defined(__LITTLE_ENDIAN__) && ( defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__) ) +#define le_short(s) ((short) ((unsigned short) (s) << 8) | ((unsigned short) (s) >> 8)) +#define be_short(s) ((short) (s)) +#else +#define le_short(s) ((short) (s)) +#define be_short(s) ((short) ((unsigned short) (s) << 8) | ((unsigned short) (s) >> 8)) +#endif + +/** Convert little endian */ +static inline opus_int32 le_int(opus_int32 i) +{ +#if !defined(__LITTLE_ENDIAN__) && ( defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__) ) + opus_uint32 ui, ret; + ui = i; + ret = ui>>24; + ret |= (ui>>8)&0x0000ff00; + ret |= (ui<<8)&0x00ff0000; + ret |= (ui<<24); + return ret; +#else + return i; +#endif +} + +void adjust_wav_mapping(int mapping_family, int channels, unsigned char *stream_map); + +int write_wav_header(FILE *file, int rate, int mapping_family, int channels); + +#endif diff --git a/third-party/opusfile/info.c b/third-party/opusfile/info.c new file mode 100644 index 0000000000..6cf98516a0 --- /dev/null +++ b/third-party/opusfile/info.c @@ -0,0 +1,683 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE libopusfile SOURCE CODE IS (C) COPYRIGHT 2012 * + * by the Xiph.Org Foundation and contributors http://www.xiph.org/ * + * * + ********************************************************************/ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "internal.h" +#include +#include + +static unsigned op_parse_uint16le(const unsigned char *_data){ + return _data[0]|_data[1]<<8; +} + +static int op_parse_int16le(const unsigned char *_data){ + int ret; + ret=_data[0]|_data[1]<<8; + return (ret^0x8000)-0x8000; +} + +static opus_uint32 op_parse_uint32le(const unsigned char *_data){ + return _data[0]|_data[1]<<8|_data[2]<<16|_data[3]<<24; +} + +static opus_uint32 op_parse_uint32be(const unsigned char *_data){ + return _data[3]|_data[2]<<8|_data[1]<<16|_data[0]<<24; +} + +int opus_head_parse(OpusHead *_head,const unsigned char *_data,size_t _len){ + OpusHead head; + if(_len<8)return OP_ENOTFORMAT; + if(memcmp(_data,"OpusHead",8)!=0)return OP_ENOTFORMAT; + if(_len<9)return OP_EBADHEADER; + head.version=_data[8]; + if(head.version>15)return OP_EVERSION; + if(_len<19)return OP_EBADHEADER; + head.channel_count=_data[9]; + head.pre_skip=op_parse_uint16le(_data+10); + head.input_sample_rate=op_parse_uint32le(_data+12); + head.output_gain=op_parse_int16le(_data+16); + head.mapping_family=_data[18]; + if(head.mapping_family==0){ + if(head.channel_count<1||head.channel_count>2)return OP_EBADHEADER; + if(head.version<=1&&_len>19)return OP_EBADHEADER; + head.stream_count=1; + head.coupled_count=head.channel_count-1; + if(_head!=NULL){ + _head->mapping[0]=0; + _head->mapping[1]=1; + } + } + else if(head.mapping_family==1){ + size_t size; + int ci; + if(head.channel_count<1||head.channel_count>8)return OP_EBADHEADER; + size=21+head.channel_count; + if(_lensize)return OP_EBADHEADER; + head.stream_count=_data[19]; + if(head.stream_count<1)return OP_EBADHEADER; + head.coupled_count=_data[20]; + if(head.coupled_count>head.stream_count)return OP_EBADHEADER; + for(ci=0;ci=head.stream_count+head.coupled_count + &&_data[21+ci]!=255){ + return OP_EBADHEADER; + } + } + if(_head!=NULL)memcpy(_head->mapping,_data+21,head.channel_count); + } + /*General purpose players should not attempt to play back content with + channel mapping family 255.*/ + else if(head.mapping_family==255)return OP_EIMPL; + /*No other channel mapping families are currently defined.*/ + else return OP_EBADHEADER; + if(_head!=NULL)memcpy(_head,&head,head.mapping-(unsigned char *)&head); + return 0; +} + +void opus_tags_init(OpusTags *_tags){ + memset(_tags,0,sizeof(*_tags)); +} + +void opus_tags_clear(OpusTags *_tags){ + int ci; + for(ci=_tags->comments;ci-->0;)_ogg_free(_tags->user_comments[ci]); + _ogg_free(_tags->user_comments); + _ogg_free(_tags->comment_lengths); + _ogg_free(_tags->vendor); +} + +/*Ensure there's room for up to _ncomments comments.*/ +static int op_tags_ensure_capacity(OpusTags *_tags,size_t _ncomments){ + char **user_comments; + int *comment_lengths; + size_t size; + if(OP_UNLIKELY(_ncomments>=(size_t)INT_MAX))return OP_EFAULT; + size=sizeof(*_tags->comment_lengths)*(_ncomments+1); + if(size/sizeof(*_tags->comment_lengths)!=_ncomments+1)return OP_EFAULT; + comment_lengths=(int *)_ogg_realloc(_tags->comment_lengths,size); + if(OP_UNLIKELY(comment_lengths==NULL))return OP_EFAULT; + comment_lengths[_ncomments]=0; + _tags->comment_lengths=comment_lengths; + size=sizeof(*_tags->user_comments)*(_ncomments+1); + if(size/sizeof(*_tags->user_comments)!=_ncomments+1)return OP_EFAULT; + user_comments=(char **)_ogg_realloc(_tags->user_comments,size); + if(OP_UNLIKELY(user_comments==NULL))return OP_EFAULT; + user_comments[_ncomments]=NULL; + _tags->user_comments=user_comments; + return 0; +} + +/*Duplicate a (possibly non-NUL terminated) string with a known length.*/ +static char *op_strdup_with_len(const char *_s,size_t _len){ + size_t size; + char *ret; + size=sizeof(*ret)*(_len+1); + if(OP_UNLIKELY(size<_len))return NULL; + ret=(char *)_ogg_malloc(size); + if(OP_LIKELY(ret!=NULL)){ + ret=(char *)memcpy(ret,_s,sizeof(*ret)*_len); + ret[_len]='\0'; + } + return ret; +} + +/*The actual implementation of opus_tags_parse(). + Unlike the public API, this function requires _tags to already be + initialized, modifies its contents before success is guaranteed, and assumes + the caller will clear it on error.*/ +static int opus_tags_parse_impl(OpusTags *_tags, + const unsigned char *_data,size_t _len){ + opus_uint32 count; + size_t len; + int ncomments; + int ci; + len=_len; + if(len<8)return OP_ENOTFORMAT; + if(memcmp(_data,"OpusTags",8)!=0)return OP_ENOTFORMAT; + if(len<16)return OP_EBADHEADER; + _data+=8; + len-=8; + count=op_parse_uint32le(_data); + _data+=4; + len-=4; + if(count>len)return OP_EBADHEADER; + if(_tags!=NULL){ + _tags->vendor=op_strdup_with_len((char *)_data,count); + if(_tags->vendor==NULL)return OP_EFAULT; + } + _data+=count; + len-=count; + if(len<4)return OP_EBADHEADER; + count=op_parse_uint32le(_data); + _data+=4; + len-=4; + /*Check to make sure there's minimally sufficient data left in the packet.*/ + if(count>len>>2)return OP_EBADHEADER; + /*Check for overflow (the API limits this to an int).*/ + if(count>(opus_uint32)INT_MAX-1)return OP_EFAULT; + if(_tags!=NULL){ + int ret; + ret=op_tags_ensure_capacity(_tags,count); + if(ret<0)return ret; + } + ncomments=(int)count; + for(ci=0;cilen>>2)return OP_EBADHEADER; + count=op_parse_uint32le(_data); + _data+=4; + len-=4; + if(count>len)return OP_EBADHEADER; + /*Check for overflow (the API limits this to an int).*/ + if(count>(opus_uint32)INT_MAX)return OP_EFAULT; + if(_tags!=NULL){ + _tags->user_comments[ci]=op_strdup_with_len((char *)_data,count); + if(_tags->user_comments[ci]==NULL)return OP_EFAULT; + _tags->comment_lengths[ci]=(int)count; + _tags->comments=ci+1; + } + _data+=count; + len-=count; + } + return 0; +} + +int opus_tags_parse(OpusTags *_tags,const unsigned char *_data,size_t _len){ + if(_tags!=NULL){ + OpusTags tags; + int ret; + opus_tags_init(&tags); + ret=opus_tags_parse_impl(&tags,_data,_len); + if(ret<0)opus_tags_clear(&tags); + else *_tags=*&tags; + return ret; + } + else return opus_tags_parse_impl(NULL,_data,_len); +} + +/*The actual implementation of opus_tags_copy(). + Unlike the public API, this function requires _dst to already be + initialized, modifies its contents before success is guaranteed, and assumes + the caller will clear it on error.*/ +static int opus_tags_copy_impl(OpusTags *_dst,const OpusTags *_src){ + char *vendor; + int ncomments; + int ret; + int ci; + vendor=_src->vendor; + _dst->vendor=op_strdup_with_len(vendor,strlen(vendor)); + if(OP_UNLIKELY(_dst->vendor==NULL))return OP_EFAULT; + ncomments=_src->comments; + ret=op_tags_ensure_capacity(_dst,ncomments); + if(OP_UNLIKELY(ret<0))return ret; + for(ci=0;cicomment_lengths[ci]; + OP_ASSERT(len>=0); + _dst->user_comments[ci]=op_strdup_with_len(_src->user_comments[ci],len); + if(OP_UNLIKELY(_dst->user_comments[ci]==NULL))return OP_EFAULT; + _dst->comment_lengths[ci]=len; + _dst->comments=ci+1; + } + return 0; +} + +int opus_tags_copy(OpusTags *_dst,const OpusTags *_src){ + OpusTags dst; + int ret; + opus_tags_init(&dst); + ret=opus_tags_copy_impl(&dst,_src); + if(OP_UNLIKELY(ret<0))opus_tags_clear(&dst); + else *_dst=*&dst; + return 0; +} + +int opus_tags_add(OpusTags *_tags,const char *_tag,const char *_value){ + char *comment; + int tag_len; + int value_len; + int ncomments; + int ret; + ncomments=_tags->comments; + ret=op_tags_ensure_capacity(_tags,ncomments+1); + if(OP_UNLIKELY(ret<0))return ret; + tag_len=strlen(_tag); + value_len=strlen(_value); + /*+2 for '=' and '\0'.*/ + _tags->comment_lengths[ncomments]=0; + _tags->user_comments[ncomments]=comment= + (char *)_ogg_malloc(sizeof(*comment)*(tag_len+value_len+2)); + if(OP_UNLIKELY(comment==NULL))return OP_EFAULT; + _tags->comment_lengths[ncomments]=tag_len+value_len+1; + memcpy(comment,_tag,sizeof(*comment)*tag_len); + comment[tag_len]='='; + memcpy(comment+tag_len+1,_value,sizeof(*comment)*(value_len+1)); + return 0; +} + +int opus_tags_add_comment(OpusTags *_tags,const char *_comment){ + int comment_len; + int ncomments; + int ret; + ncomments=_tags->comments; + ret=op_tags_ensure_capacity(_tags,ncomments+1); + if(OP_UNLIKELY(ret<0))return ret; + comment_len=(int)strlen(_comment); + _tags->comment_lengths[ncomments]=0; + _tags->user_comments[ncomments]=op_strdup_with_len(_comment,comment_len); + if(OP_UNLIKELY(_tags->user_comments[ncomments]==NULL))return OP_EFAULT; + _tags->comment_lengths[ncomments]=comment_len; + return 0; +} + +int opus_tagcompare(const char *_tag_name,const char *_comment){ + return opus_tagncompare(_tag_name,strlen(_tag_name),_comment); +} + +int opus_tagncompare(const char *_tag_name,int _tag_len,const char *_comment){ + int ret; + OP_ASSERT(_tag_len>=0); + ret=op_strncasecmp(_tag_name,_comment,_tag_len); + return ret?ret:'='-_comment[_tag_len]; +} + +const char *opus_tags_query(const OpusTags *_tags,const char *_tag,int _count){ + char **user_comments; + int tag_len; + int found; + int ncomments; + int ci; + tag_len=strlen(_tag); + ncomments=_tags->comments; + user_comments=_tags->user_comments; + found=0; + for(ci=0;cicomments; + user_comments=_tags->user_comments; + found=0; + for(ci=0;ciuser_comments; + ncomments=_tags->comments; + /*Look for the first valid R128_TRACK_GAIN tag and use that.*/ + for(ci=0;ci='0'&&*p<='9'){ + gain_q8=10*gain_q8+*p-'0'; + if(gain_q8>32767-negative)break; + p++; + } + /*This didn't look like a signed 16-bit decimal integer. + Not a valid R128_TRACK_GAIN tag.*/ + if(*p!='\0')continue; + *_gain_q8=(int)(gain_q8+negative^negative); + return 0; + } + } + return OP_FALSE; +} + +static int op_is_jpeg(const unsigned char *_buf,size_t _buf_sz){ + return _buf_sz>=11&&memcmp(_buf,"\xFF\xD8\xFF\xE0",4)==0 + &&(_buf[4]<<8|_buf[5])>=16&&memcmp(_buf+6,"JFIF",5)==0; +} + +/*Tries to extract the width, height, bits per pixel, and palette size of a + JPEG. + On failure, simply leaves its outputs unmodified.*/ +static void op_extract_jpeg_params(const unsigned char *_buf,size_t _buf_sz, + opus_uint32 *_width,opus_uint32 *_height, + opus_uint32 *_depth,opus_uint32 *_colors,int *_has_palette){ + if(op_is_jpeg(_buf,_buf_sz)){ + size_t offs; + offs=2; + for(;;){ + size_t segment_len; + int marker; + while(offs<_buf_sz&&_buf[offs]!=0xFF)offs++; + while(offs<_buf_sz&&_buf[offs]==0xFF)offs++; + marker=_buf[offs]; + offs++; + /*If we hit EOI* (end of image), or another SOI* (start of image), + or SOS (start of scan), then stop now.*/ + if(offs>=_buf_sz||(marker>=0xD8&&marker<=0xDA))break; + /*RST* (restart markers): skip (no segment length).*/ + else if(marker>=0xD0&&marker<=0xD7)continue; + /*Read the length of the marker segment.*/ + if(_buf_sz-offs<2)break; + segment_len=_buf[offs]<<8|_buf[offs+1]; + if(segment_len<2||_buf_sz-offs0xC0&&marker<0xD0&&(marker&3)!=0)){ + /*Found a SOFn (start of frame) marker segment:*/ + if(segment_len>=8){ + *_height=_buf[offs+3]<<8|_buf[offs+4]; + *_width=_buf[offs+5]<<8|_buf[offs+6]; + *_depth=_buf[offs+2]*_buf[offs+7]; + *_colors=0; + *_has_palette=0; + } + break; + } + /*Other markers: skip the whole marker segment.*/ + offs+=segment_len; + } + } +} + +static int op_is_png(const unsigned char *_buf,size_t _buf_sz){ + return _buf_sz>=8&&memcmp(_buf,"\x89PNG\x0D\x0A\x1A\x0A",8)==0; +} + +/*Tries to extract the width, height, bits per pixel, and palette size of a + PNG. + On failure, simply leaves its outputs unmodified.*/ +static void op_extract_png_params(const unsigned char *_buf,size_t _buf_sz, + opus_uint32 *_width,opus_uint32 *_height, + opus_uint32 *_depth,opus_uint32 *_colors,int *_has_palette){ + if(op_is_png(_buf,_buf_sz)){ + size_t offs; + offs=8; + while(_buf_sz-offs>=12){ + ogg_uint32_t chunk_len; + chunk_len=op_parse_uint32be(_buf+offs); + if(chunk_len>_buf_sz-(offs+12))break; + else if(chunk_len==13&&memcmp(_buf+offs+4,"IHDR",4)==0){ + int color_type; + *_width=op_parse_uint32be(_buf+offs+8); + *_height=op_parse_uint32be(_buf+offs+12); + color_type=_buf[offs+17]; + if(color_type==3){ + *_depth=24; + *_has_palette=1; + } + else{ + int sample_depth; + sample_depth=_buf[offs+16]; + if(color_type==0)*_depth=sample_depth; + else if(color_type==2)*_depth=sample_depth*3; + else if(color_type==4)*_depth=sample_depth*2; + else if(color_type==6)*_depth=sample_depth*4; + *_colors=0; + *_has_palette=0; + break; + } + } + else if(*_has_palette>0&&memcmp(_buf+offs+4,"PLTE",4)==0){ + *_colors=chunk_len/3; + break; + } + offs+=12+chunk_len; + } + } +} + +static int op_is_gif(const unsigned char *_buf,size_t _buf_sz){ + return _buf_sz>=6&&(memcmp(_buf,"GIF87a",6)==0||memcmp(_buf,"GIF89a",6)==0); +} + +/*Tries to extract the width, height, bits per pixel, and palette size of a + GIF. + On failure, simply leaves its outputs unmodified.*/ +static void op_extract_gif_params(const unsigned char *_buf,size_t _buf_sz, + opus_uint32 *_width,opus_uint32 *_height, + opus_uint32 *_depth,opus_uint32 *_colors,int *_has_palette){ + if(op_is_gif(_buf,_buf_sz)&&_buf_sz>=14){ + *_width=_buf[6]|_buf[7]<<8; + *_height=_buf[8]|_buf[9]<<8; + /*libFLAC hard-codes the depth to 24.*/ + *_depth=24; + *_colors=1<<((_buf[10]&7)+1); + *_has_palette=1; + } +} + +/*The actual implementation of opus_picture_tag_parse(). + Unlike the public API, this function requires _pic to already be + initialized, modifies its contents before success is guaranteed, and assumes + the caller will clear it on error.*/ +static int opus_picture_tag_parse_impl(OpusPictureTag *_pic,const char *_tag, + unsigned char *_buf,size_t _buf_sz,size_t _base64_sz){ + opus_int32 picture_type; + opus_uint32 mime_type_length; + char *mime_type; + opus_uint32 description_length; + char *description; + opus_uint32 width; + opus_uint32 height; + opus_uint32 depth; + opus_uint32 colors; + opus_uint32 data_length; + opus_uint32 file_width; + opus_uint32 file_height; + opus_uint32 file_depth; + opus_uint32 file_colors; + int format; + int has_palette; + int colors_set; + size_t i; + /*Decode the BASE64 data.*/ + for(i=0;i<_base64_sz;i++){ + opus_uint32 value; + int j; + value=0; + for(j=0;j<4;j++){ + unsigned c; + unsigned d; + c=(unsigned char)_tag[4*i+j]; + if(c=='+')d=62; + else if(c=='/')d=63; + else if(c>='0'&&c<='9')d=52+c-'0'; + else if(c>='a'&&c<='z')d=26+c-'a'; + else if(c>='A'&&c<='Z')d=c-'A'; + else if(c=='='&&3*i+j>_buf_sz)d=0; + else return OP_ENOTFORMAT; + value=value<<6|d; + } + _buf[3*i]=(unsigned char)(value>>16); + if(3*i+1<_buf_sz){ + _buf[3*i+1]=(unsigned char)(value>>8); + if(3*i+2<_buf_sz)_buf[3*i+2]=(unsigned char)value; + } + } + i=0; + picture_type=op_parse_uint32be(_buf+i); + i+=4; + /*Extract the MIME type.*/ + mime_type_length=op_parse_uint32be(_buf+i); + i+=4; + if(mime_type_length>_buf_sz-32)return OP_ENOTFORMAT; + mime_type=(char *)_ogg_malloc(sizeof(*_pic->mime_type)*(mime_type_length+1)); + if(mime_type==NULL)return OP_EFAULT; + memcpy(mime_type,_buf+i,sizeof(*mime_type)*mime_type_length); + mime_type[mime_type_length]='\0'; + _pic->mime_type=mime_type; + i+=mime_type_length; + /*Extract the description string.*/ + description_length=op_parse_uint32be(_buf+i); + i+=4; + if(description_length>_buf_sz-mime_type_length-32)return OP_ENOTFORMAT; + description= + (char *)_ogg_malloc(sizeof(*_pic->mime_type)*(description_length+1)); + if(description==NULL)return OP_EFAULT; + memcpy(description,_buf+i,sizeof(*description)*description_length); + description[description_length]='\0'; + _pic->description=description; + i+=description_length; + /*Extract the remaining fields.*/ + width=op_parse_uint32be(_buf+i); + i+=4; + height=op_parse_uint32be(_buf+i); + i+=4; + depth=op_parse_uint32be(_buf+i); + i+=4; + colors=op_parse_uint32be(_buf+i); + i+=4; + /*If one of these is set, they all must be, but colors==0 is a valid value.*/ + colors_set=width!=0||height!=0||depth!=0||colors!=0; + if(width==0||height==0||depth==0&&colors_set)return OP_ENOTFORMAT; + data_length=op_parse_uint32be(_buf+i); + i+=4; + if(data_length>_buf_sz-i)return OP_ENOTFORMAT; + /*Trim extraneous data so we don't copy it below.*/ + _buf_sz=i+data_length; + /*Attempt to determine the image format.*/ + format=OP_PIC_FORMAT_UNKNOWN; + if(mime_type_length==3&&strcmp(mime_type,"-->")==0){ + format=OP_PIC_FORMAT_URL; + /*Picture type 1 must be a 32x32 PNG.*/ + if(picture_type==1&&(width!=0||height!=0)&&(width!=32||height!=32)){ + return OP_ENOTFORMAT; + } + /*Append a terminating NUL for the convenience of our callers.*/ + _buf[_buf_sz++]='\0'; + } + else{ + if(mime_type_length==10 + &&op_strncasecmp(mime_type,"image/jpeg",mime_type_length)==0){ + if(op_is_jpeg(_buf+i,data_length))format=OP_PIC_FORMAT_JPEG; + } + else if(mime_type_length==9 + &&op_strncasecmp(mime_type,"image/png",mime_type_length)==0){ + if(op_is_png(_buf+i,data_length))format=OP_PIC_FORMAT_PNG; + } + else if(mime_type_length==9 + &&op_strncasecmp(mime_type,"image/gif",mime_type_length)==0){ + if(op_is_gif(_buf+i,data_length))format=OP_PIC_FORMAT_GIF; + } + else if(mime_type_length==0||(mime_type_length==6 + &&op_strncasecmp(mime_type,"image/",mime_type_length)==0)){ + if(op_is_jpeg(_buf+i,data_length))format=OP_PIC_FORMAT_JPEG; + else if(op_is_png(_buf+i,data_length))format=OP_PIC_FORMAT_PNG; + else if(op_is_gif(_buf+i,data_length))format=OP_PIC_FORMAT_GIF; + } + file_width=file_height=file_depth=file_colors=0; + has_palette=-1; + switch(format){ + case OP_PIC_FORMAT_JPEG:{ + op_extract_jpeg_params(_buf+i,data_length, + &file_width,&file_height,&file_depth,&file_colors,&has_palette); + }break; + case OP_PIC_FORMAT_PNG:{ + op_extract_png_params(_buf+i,data_length, + &file_width,&file_height,&file_depth,&file_colors,&has_palette); + }break; + case OP_PIC_FORMAT_GIF:{ + op_extract_gif_params(_buf+i,data_length, + &file_width,&file_height,&file_depth,&file_colors,&has_palette); + }break; + } + if(has_palette>=0){ + /*If we successfully extracted these parameters from the image, override + any declared values.*/ + width=file_width; + height=file_height; + depth=file_depth; + colors=file_colors; + } + /*Picture type 1 must be a 32x32 PNG.*/ + if(picture_type==1&&(format!=OP_PIC_FORMAT_PNG||width!=32||height!=32)){ + return OP_ENOTFORMAT; + } + } + /*Adjust _buf_sz instead of using data_length to capture the terminating NUL + for URLs.*/ + _buf_sz-=i; + memmove(_buf,_buf+i,sizeof(*_buf)*_buf_sz); + _buf=(unsigned char *)_ogg_realloc(_buf,_buf_sz); + if(_buf_sz>0&&_buf==NULL)return OP_EFAULT; + _pic->type=picture_type; + _pic->width=width; + _pic->height=height; + _pic->depth=depth; + _pic->colors=colors; + _pic->data_length=data_length; + _pic->data=_buf; + _pic->format=format; + return 0; +} + +int opus_picture_tag_parse(OpusPictureTag *_pic,const char *_tag){ + OpusPictureTag pic; + unsigned char *buf; + size_t base64_sz; + size_t buf_sz; + size_t tag_length; + int ret; + if(opus_tagncompare("METADATA_BLOCK_PICTURE",22,_tag)==0)_tag+=23; + /*Figure out how much BASE64-encoded data we have.*/ + tag_length=strlen(_tag); + if(tag_length&3)return OP_ENOTFORMAT; + base64_sz=tag_length>>2; + buf_sz=3*base64_sz; + if(buf_sz<32)return OP_ENOTFORMAT; + if(_tag[tag_length-1]=='=')buf_sz--; + if(_tag[tag_length-2]=='=')buf_sz--; + if(buf_sz<32)return OP_ENOTFORMAT; + /*Allocate an extra byte to allow appending a terminating NUL to URL data.*/ + buf=(unsigned char *)_ogg_malloc(sizeof(*buf)*(buf_sz+1)); + if(buf==NULL)return OP_EFAULT; + opus_picture_tag_init(&pic); + ret=opus_picture_tag_parse_impl(&pic,_tag,buf,buf_sz,base64_sz); + if(ret<0){ + opus_picture_tag_clear(&pic); + _ogg_free(buf); + } + else *_pic=*&pic; + return ret; +} + +void opus_picture_tag_init(OpusPictureTag *_pic){ + memset(_pic,0,sizeof(*_pic)); +} + +void opus_picture_tag_clear(OpusPictureTag *_pic){ + _ogg_free(_pic->description); + _ogg_free(_pic->mime_type); + _ogg_free(_pic->data); +} diff --git a/third-party/opusfile/internal.c b/third-party/opusfile/internal.c new file mode 100644 index 0000000000..96c80def82 --- /dev/null +++ b/third-party/opusfile/internal.c @@ -0,0 +1,42 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE libopusfile SOURCE CODE IS (C) COPYRIGHT 2012 * + * by the Xiph.Org Foundation and contributors http://www.xiph.org/ * + * * + ********************************************************************/ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "internal.h" + +#if defined(OP_ENABLE_ASSERTIONS) +void op_fatal_impl(const char *_str,const char *_file,int _line){ + fprintf(stderr,"Fatal (internal) error in %s, line %i: %s\n", + _file,_line,_str); + abort(); +} +#endif + +/*A version of strncasecmp() that is guaranteed to only ignore the case of + ASCII characters.*/ +int op_strncasecmp(const char *_a,const char *_b,int _n){ + int i; + for(i=0;i<_n;i++){ + int a; + int b; + int d; + a=_a[i]; + b=_b[i]; + if(a>='a'&&a<='z')a-='a'-'A'; + if(b>='a'&&b<='z')b-='a'-'A'; + d=a-b; + if(d)return d; + } + return 0; +} diff --git a/third-party/opusfile/internal.h b/third-party/opusfile/internal.h new file mode 100644 index 0000000000..b1109deb90 --- /dev/null +++ b/third-party/opusfile/internal.h @@ -0,0 +1,249 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE libopusfile SOURCE CODE IS (C) COPYRIGHT 2012 * + * by the Xiph.Org Foundation and contributors http://www.xiph.org/ * + * * + ********************************************************************/ +#if !defined(_opusfile_internal_h) +# define _opusfile_internal_h (1) + +# if !defined(_REENTRANT) +# define _REENTRANT +# endif +# if !defined(_GNU_SOURCE) +# define _GNU_SOURCE +# endif +# if !defined(_LARGEFILE_SOURCE) +# define _LARGEFILE_SOURCE +# endif +# if !defined(_LARGEFILE64_SOURCE) +# define _LARGEFILE64_SOURCE +# endif +# if !defined(_FILE_OFFSET_BITS) +# define _FILE_OFFSET_BITS 64 +# endif + +# include +# include "opusfile.h" + +typedef struct OggOpusLink OggOpusLink; + +# if defined(OP_FIXED_POINT) + +typedef opus_int16 op_sample; + +# else + +typedef float op_sample; + +/*We're using this define to test for libopus 1.1 or later until libopus + provides a better mechanism.*/ +# if defined(OPUS_GET_EXPERT_FRAME_DURATION_REQUEST) +/*Enable soft clipping prevention in 16-bit decodes.*/ +# define OP_SOFT_CLIP (1) +# endif + +# endif + +# if OP_GNUC_PREREQ(4,2) +/*Disable excessive warnings about the order of operations.*/ +# pragma GCC diagnostic ignored "-Wparentheses" +# elif defined(_MSC_VER) +/*Disable excessive warnings about the order of operations.*/ +# pragma warning(disable:4554) +/*Disable warnings about "deprecated" POSIX functions.*/ +# pragma warning(disable:4996) +# endif + +# if OP_GNUC_PREREQ(3,0) +/*Another alternative is + (__builtin_constant_p(_x)?!!(_x):__builtin_expect(!!(_x),1)) + but that evaluates _x multiple times, which may be bad.*/ +# define OP_LIKELY(_x) (__builtin_expect(!!(_x),1)) +# define OP_UNLIKELY(_x) (__builtin_expect(!!(_x),0)) +# else +# define OP_LIKELY(_x) (!!(_x)) +# define OP_UNLIKELY(_x) (!!(_x)) +# endif + +# if defined(OP_ENABLE_ASSERTIONS) +# if OP_GNUC_PREREQ(2,5)||__SUNPRO_C>=0x590 +__attribute__((noreturn)) +# endif +void op_fatal_impl(const char *_str,const char *_file,int _line); + +# define OP_FATAL(_str) (op_fatal_impl(_str,__FILE__,__LINE__)) + +# define OP_ASSERT(_cond) \ + do{ \ + if(OP_UNLIKELY(!(_cond)))OP_FATAL("assertion failed: " #_cond); \ + } \ + while(0) +# define OP_ALWAYS_TRUE(_cond) OP_ASSERT(_cond) + +# else +# define OP_FATAL(_str) abort() +# define OP_ASSERT(_cond) +# define OP_ALWAYS_TRUE(_cond) ((void)(_cond)) +# endif + +# define OP_INT64_MAX (2*(((ogg_int64_t)1<<62)-1)|1) +# define OP_INT64_MIN (-OP_INT64_MAX-1) +# define OP_INT32_MAX (2*(((ogg_int32_t)1<<30)-1)|1) +# define OP_INT32_MIN (-OP_INT32_MAX-1) + +# define OP_MIN(_a,_b) ((_a)<(_b)?(_a):(_b)) +# define OP_MAX(_a,_b) ((_a)>(_b)?(_a):(_b)) +# define OP_CLAMP(_lo,_x,_hi) (OP_MAX(_lo,OP_MIN(_x,_hi))) + +/*Advance a file offset by the given amount, clamping against OP_INT64_MAX. + This is used to advance a known offset by things like OP_CHUNK_SIZE or + OP_PAGE_SIZE_MAX, while making sure to avoid signed overflow. + It assumes that both _offset and _amount are non-negative.*/ +#define OP_ADV_OFFSET(_offset,_amount) \ + (OP_MIN(_offset,OP_INT64_MAX-(_amount))+(_amount)) + +/*The maximum channel count for any mapping we'll actually decode.*/ +# define OP_NCHANNELS_MAX (8) + +/*Initial state.*/ +# define OP_NOTOPEN (0) +/*We've found the first Opus stream in the first link.*/ +# define OP_PARTOPEN (1) +# define OP_OPENED (2) +/*We've found the first Opus stream in the current link.*/ +# define OP_STREAMSET (3) +/*We've initialized the decoder for the chosen Opus stream in the current + link.*/ +# define OP_INITSET (4) + +/*Information cached for a single link in a chained Ogg Opus file. + We choose the first Opus stream encountered in each link to play back (and + require at least one).*/ +struct OggOpusLink{ + /*The byte offset of the first header page in this link.*/ + opus_int64 offset; + /*The byte offset of the first data page from the chosen Opus stream in this + link (after the headers).*/ + opus_int64 data_offset; + /*The byte offset of the last page from the chosen Opus stream in this link. + This is used when seeking to ensure we find a page before the last one, so + that end-trimming calculations work properly. + This is only valid for seekable sources.*/ + opus_int64 end_offset; + /*The granule position of the last sample. + This is only valid for seekable sources.*/ + ogg_int64_t pcm_end; + /*The granule position before the first sample.*/ + ogg_int64_t pcm_start; + /*The serial number.*/ + ogg_uint32_t serialno; + /*The contents of the info header.*/ + OpusHead head; + /*The contents of the comment header.*/ + OpusTags tags; +}; + +struct OggOpusFile{ + /*The callbacks used to access the data source.*/ + OpusFileCallbacks callbacks; + /*A FILE *, memory bufer, etc.*/ + void *source; + /*Whether or not we can seek with this data source.*/ + int seekable; + /*The number of links in this chained Ogg Opus file.*/ + int nlinks; + /*The cached information from each link in a chained Ogg Opus file. + If source isn't seekable (e.g., it's a pipe), only the current link + appears.*/ + OggOpusLink *links; + /*The number of serial numbers from a single link.*/ + int nserialnos; + /*The capacity of the list of serial numbers from a single link.*/ + int cserialnos; + /*Storage for the list of serial numbers from a single link.*/ + ogg_uint32_t *serialnos; + /*This is the current offset of the data processed by the ogg_sync_state. + After a seek, this should be set to the target offset so that we can track + the byte offsets of subsequent pages. + After a call to op_get_next_page(), this will point to the first byte after + that page.*/ + opus_int64 offset; + /*The total size of this data source, or -1 if it's unseekable.*/ + opus_int64 end; + /*Used to locate pages in the data source.*/ + ogg_sync_state oy; + /*One of OP_NOTOPEN, OP_PARTOPEN, OP_OPENED, OP_STREAMSET, OP_INITSET.*/ + int ready_state; + /*The current link being played back.*/ + int cur_link; + /*The number of decoded samples to discard from the start of decoding.*/ + opus_int32 cur_discard_count; + /*The granule position of the previous packet (current packet start time).*/ + ogg_int64_t prev_packet_gp; + /*The number of bytes read since the last bitrate query, including framing.*/ + opus_int64 bytes_tracked; + /*The number of samples decoded since the last bitrate query.*/ + ogg_int64_t samples_tracked; + /*Takes physical pages and welds them into a logical stream of packets.*/ + ogg_stream_state os; + /*Re-timestamped packets from a single page. + Buffering these relies on the undocumented libogg behavior that ogg_packet + pointers remain valid until the next page is submitted to the + ogg_stream_state they came from.*/ + ogg_packet op[255]; + /*The index of the next packet to return.*/ + int op_pos; + /*The total number of packets available.*/ + int op_count; + /*Central working state for the packet-to-PCM decoder.*/ + OpusMSDecoder *od; + /*The application-provided packet decode callback.*/ + op_decode_cb_func decode_cb; + /*The application-provided packet decode callback context.*/ + void *decode_cb_ctx; + /*The stream count used to initialize the decoder.*/ + int od_stream_count; + /*The coupled stream count used to initialize the decoder.*/ + int od_coupled_count; + /*The channel count used to initialize the decoder.*/ + int od_channel_count; + /*The channel mapping used to initialize the decoder.*/ + unsigned char od_mapping[OP_NCHANNELS_MAX]; + /*The buffered data for one decoded packet.*/ + op_sample *od_buffer; + /*The current position in the decoded buffer.*/ + int od_buffer_pos; + /*The number of valid samples in the decoded buffer.*/ + int od_buffer_size; + /*The type of gain offset to apply. + One of OP_HEADER_GAIN, OP_TRACK_GAIN, or OP_ABSOLUTE_GAIN.*/ + int gain_type; + /*The offset to apply to the gain.*/ + opus_int32 gain_offset_q8; + /*Internal state for soft clipping and dithering float->short output.*/ +#if !defined(OP_FIXED_POINT) +# if defined(OP_SOFT_CLIP) + float clip_state[OP_NCHANNELS_MAX]; +# endif + float dither_a[OP_NCHANNELS_MAX*4]; + float dither_b[OP_NCHANNELS_MAX*4]; + opus_uint32 dither_seed; + int dither_mute; + int dither_disabled; + /*The number of channels represented by the internal state. + This gets set to 0 whenever anything that would prevent state propagation + occurs (switching between the float/short APIs, or between the + stereo/multistream APIs).*/ + int state_channel_count; +#endif +}; + +int op_strncasecmp(const char *_a,const char *_b,int _n); + +#endif diff --git a/third-party/opusfile/opusfile.c b/third-party/opusfile/opusfile.c new file mode 100644 index 0000000000..392ddb29ea --- /dev/null +++ b/third-party/opusfile/opusfile.c @@ -0,0 +1,3163 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE libopusfile SOURCE CODE IS (C) COPYRIGHT 1994-2012 * + * by the Xiph.Org Foundation and contributors http://www.xiph.org/ * + * * + ******************************************************************** + + function: stdio-based convenience library for opening/seeking/decoding + last mod: $Id: vorbisfile.c 17573 2010-10-27 14:53:59Z xiphmont $ + + ********************************************************************/ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "internal.h" +#include +#include +#include +#include +#include +#include + +#include "opusfile.h" + +/*This implementation is largely based off of libvorbisfile. + All of the Ogg bits work roughly the same, though I have made some + "improvements" that have not been folded back there, yet.*/ + +/*A 'chained bitstream' is an Ogg Opus bitstream that contains more than one + logical bitstream arranged end to end (the only form of Ogg multiplexing + supported by this library. + Grouping (parallel multiplexing) is not supported, except to the extent that + if there are multiple logical Ogg streams in a single link of the chain, we + will ignore all but the first Opus stream we find.*/ + +/*An Ogg Opus file can be played beginning to end (streamed) without worrying + ahead of time about chaining (see opusdec from the opus-tools package). + If we have the whole file, however, and want random access + (seeking/scrubbing) or desire to know the total length/time of a file, we + need to account for the possibility of chaining.*/ + +/*We can handle things a number of ways. + We can determine the entire bitstream structure right off the bat, or find + pieces on demand. + This library determines and caches structure for the entire bitstream, but + builds a virtual decoder on the fly when moving between links in the chain.*/ + +/*There are also different ways to implement seeking. + Enough information exists in an Ogg bitstream to seek to sample-granularity + positions in the output. + Or, one can seek by picking some portion of the stream roughly in the desired + area if we only want coarse navigation through the stream. + We implement and expose both strategies.*/ + +/*The maximum number of bytes in a page (including the page headers).*/ +#define OP_PAGE_SIZE_MAX (65307) +/*The default amount to seek backwards per step when trying to find the + previous page. + This must be at least as large as the maximum size of a page.*/ +#define OP_CHUNK_SIZE (65536) +/*The maximum amount to seek backwards per step when trying to find the + previous page.*/ +#define OP_CHUNK_SIZE_MAX (1024*(opus_int32)1024) +/*A smaller read size is needed for low-rate streaming.*/ +#define OP_READ_SIZE (2048) + +int op_test(OpusHead *_head, + const unsigned char *_initial_data,size_t _initial_bytes){ + ogg_sync_state oy; + char *data; + int err; + /*The first page of a normal Opus file will be at most 57 bytes (27 Ogg + page header bytes + 1 lacing value + 21 Opus header bytes + 8 channel + mapping bytes). + It will be at least 47 bytes (27 Ogg page header bytes + 1 lacing value + + 19 Opus header bytes using channel mapping family 0). + If we don't have at least that much data, give up now.*/ + if(_initial_bytes<47)return OP_FALSE; + /*Only proceed if we start with the magic OggS string. + This is to prevent us spending a lot of time allocating memory and looking + for Ogg pages in non-Ogg files.*/ + if(memcmp(_initial_data,"OggS",4)!=0)return OP_ENOTFORMAT; + ogg_sync_init(&oy); + data=ogg_sync_buffer(&oy,_initial_bytes); + if(data!=NULL){ + ogg_stream_state os; + ogg_page og; + int ret; + memcpy(data,_initial_data,_initial_bytes); + ogg_sync_wrote(&oy,_initial_bytes); + ogg_stream_init(&os,-1); + err=OP_FALSE; + do{ + ogg_packet op; + ret=ogg_sync_pageout(&oy,&og); + /*Ignore holes.*/ + if(ret<0)continue; + /*Stop if we run out of data.*/ + if(!ret)break; + ogg_stream_reset_serialno(&os,ogg_page_serialno(&og)); + ogg_stream_pagein(&os,&og); + /*Only process the first packet on this page (if it's a BOS packet, + it's required to be the only one).*/ + if(ogg_stream_packetout(&os,&op)==1){ + if(op.b_o_s){ + ret=opus_head_parse(_head,op.packet,op.bytes); + /*If this didn't look like Opus, keep going.*/ + if(ret==OP_ENOTFORMAT)continue; + /*Otherwise we're done, one way or another.*/ + err=ret; + } + /*We finished parsing the headers. + There is no Opus to be found.*/ + else err=OP_ENOTFORMAT; + } + } + while(err==OP_FALSE); + ogg_stream_clear(&os); + } + else err=OP_EFAULT; + ogg_sync_clear(&oy); + return err; +} + +/*Many, many internal helpers. + The intention is not to be confusing. + Rampant duplication and monolithic function implementation (though we do have + some large, omnibus functions still) would be harder to understand anyway. + The high level functions are last. + Begin grokking near the end of the file if you prefer to read things + top-down.*/ + +/*The read/seek functions track absolute position within the stream.*/ + +/*Read a little more data from the file/pipe into the ogg_sync framer. + _nbytes: The maximum number of bytes to read. + Return: A positive number of bytes read on success, 0 on end-of-file, or a + negative value on failure.*/ +static int op_get_data(OggOpusFile *_of,int _nbytes){ + unsigned char *buffer; + int nbytes; + OP_ASSERT(_nbytes>0); + buffer=(unsigned char *)ogg_sync_buffer(&_of->oy,_nbytes); + nbytes=(int)(*_of->callbacks.read)(_of->source,buffer,_nbytes); + OP_ASSERT(nbytes<=_nbytes); + if(OP_LIKELY(nbytes>0))ogg_sync_wrote(&_of->oy,nbytes); + return nbytes; +} + +/*Save a tiny smidge of verbosity to make the code more readable.*/ +static int op_seek_helper(OggOpusFile *_of,opus_int64 _offset){ + if(_offset==_of->offset)return 0; + if(_of->callbacks.seek==NULL|| + (*_of->callbacks.seek)(_of->source,_offset,SEEK_SET)){ + return OP_EREAD; + } + _of->offset=_offset; + ogg_sync_reset(&_of->oy); + return 0; +} + +/*Get the current position indicator of the underlying source. + This should be the same as the value reported by tell().*/ +static opus_int64 op_position(const OggOpusFile *_of){ + /*The current position indicator is _not_ simply offset. + We may also have unprocessed, buffered data in the sync state.*/ + return _of->offset+_of->oy.fill-_of->oy.returned; +} + +/*From the head of the stream, get the next page. + _boundary specifies if the function is allowed to fetch more data from the + stream (and how much) or only use internally buffered data. + _boundary: -1: Unbounded search. + 0: Read no additional data. + Use only cached data. + n: Search for the start of a new page up to file position n. + Return: n>=0: Found a page at absolute offset n. + OP_FALSE: Hit the _boundary limit. + OP_EREAD: An underlying read operation failed. + OP_BADLINK: We hit end-of-file before reaching _boundary.*/ +static opus_int64 op_get_next_page(OggOpusFile *_of,ogg_page *_og, + opus_int64 _boundary){ + while(_boundary<=0||_of->offset<_boundary){ + int more; + more=ogg_sync_pageseek(&_of->oy,_og); + /*Skipped (-more) bytes.*/ + if(OP_UNLIKELY(more<0))_of->offset-=more; + else if(more==0){ + int read_nbytes; + int ret; + /*Send more paramedics.*/ + if(!_boundary)return OP_FALSE; + if(_boundary<0)read_nbytes=OP_READ_SIZE; + else{ + opus_int64 position; + position=op_position(_of); + if(position>=_boundary)return OP_FALSE; + read_nbytes=(int)OP_MIN(_boundary-position,OP_READ_SIZE); + } + ret=op_get_data(_of,read_nbytes); + if(OP_UNLIKELY(ret<0))return OP_EREAD; + if(OP_UNLIKELY(ret==0)){ + /*Only fail cleanly on EOF if we didn't have a known boundary. + Otherwise, we should have been able to reach that boundary, and this + is a fatal error.*/ + return OP_UNLIKELY(_boundary<0)?OP_FALSE:OP_EBADLINK; + } + } + else{ + /*Got a page. + Return the page start offset and advance the internal offset past the + page end.*/ + opus_int64 page_offset; + page_offset=_of->offset; + _of->offset+=more; + OP_ASSERT(page_offset>=0); + return page_offset; + } + } + return OP_FALSE; +} + +static int op_add_serialno(const ogg_page *_og, + ogg_uint32_t **_serialnos,int *_nserialnos,int *_cserialnos){ + ogg_uint32_t *serialnos; + int nserialnos; + int cserialnos; + ogg_uint32_t s; + s=ogg_page_serialno(_og); + serialnos=*_serialnos; + nserialnos=*_nserialnos; + cserialnos=*_cserialnos; + if(OP_UNLIKELY(nserialnos>=cserialnos)){ + if(OP_UNLIKELY(cserialnos>INT_MAX-1>>1))return OP_EFAULT; + cserialnos=2*cserialnos+1; + OP_ASSERT(nserialnos=OP_PAGE_SIZE_MAX); + begin=OP_MAX(begin-chunk_size,0); + ret=op_seek_helper(_of,begin); + if(OP_UNLIKELY(ret<0))return ret; + search_start=begin; + while(_of->offsetsearch_start=search_start; + _sr->offset=_offset=llret; + _sr->serialno=serialno; + OP_ASSERT(_of->offset-_offset>=0); + OP_ASSERT(_of->offset-_offset<=OP_PAGE_SIZE_MAX); + _sr->size=(opus_int32)(_of->offset-_offset); + _sr->gp=ogg_page_granulepos(&og); + /*If this page is from the stream we're looking for, remember it.*/ + if(serialno==_serialno){ + preferred_found=1; + *&preferred_sr=*_sr; + } + if(!op_lookup_serialno(serialno,_serialnos,_nserialnos)){ + /*We fell off the end of the link, which means we seeked back too far + and shouldn't have been looking in that link to begin with. + If we found the preferred serial number, forget that we saw it.*/ + preferred_found=0; + } + search_start=llret+1; + } + /*We started from the beginning of the stream and found nothing. + This should be impossible unless the contents of the source changed out + from under us after we read from it.*/ + if(OP_UNLIKELY(!begin)&&OP_UNLIKELY(_offset<0))return OP_EBADLINK; + /*Bump up the chunk size. + This is mildly helpful when seeks are very expensive (http).*/ + chunk_size=OP_MIN(2*chunk_size,OP_CHUNK_SIZE_MAX); + /*Avoid quadratic complexity if we hit an invalid patch of the file.*/ + end=OP_MIN(begin+OP_PAGE_SIZE_MAX-1,original_end); + } + while(_offset<0); + if(preferred_found)*_sr=*&preferred_sr; + return 0; +} + +/*Find the last page beginning before _offset with the given serial number and + a valid granule position. + Unlike the above search, this continues until it finds such a page, but does + not stray outside the current link. + We could implement it (inefficiently) by calling op_get_prev_page_serial() + repeatedly until it returned a page that had both our preferred serial + number and a valid granule position, but doing it with a separate function + allows us to avoid repeatedly re-scanning valid pages from other streams as + we seek-back-and-read-forward. + [out] _gp: Returns the granule position of the page that was found on + success. + _offset: The _offset before which to find a page. + Any page returned will consist of data entirely before _offset. + _serialno: The target serial number. + _serialnos: The list of serial numbers in the link that contains the + preferred serial number. + _nserialnos: The number of serial numbers in the current link. + Return: The offset of the page on success, or a negative value on failure. + OP_EREAD: Failed to read more data (error or EOF). + OP_EBADLINK: We couldn't find a page even after seeking back past the + beginning of the link.*/ +static opus_int64 op_get_last_page(OggOpusFile *_of,ogg_int64_t *_gp, + opus_int64 _offset,ogg_uint32_t _serialno, + const ogg_uint32_t *_serialnos,int _nserialnos){ + ogg_page og; + ogg_int64_t gp; + opus_int64 begin; + opus_int64 end; + opus_int64 original_end; + opus_int32 chunk_size; + /*The target serial number must belong to the current link.*/ + OP_ASSERT(op_lookup_serialno(_serialno,_serialnos,_nserialnos)); + original_end=end=begin=_offset; + _offset=-1; + /*We shouldn't have to initialize gp, but gcc is too dumb to figure out that + ret>=0 implies we entered the if(page_gp!=-1) block at least once.*/ + gp=-1; + chunk_size=OP_CHUNK_SIZE; + do{ + int left_link; + int ret; + OP_ASSERT(chunk_size>=OP_PAGE_SIZE_MAX); + begin=OP_MAX(begin-chunk_size,0); + ret=op_seek_helper(_of,begin); + if(OP_UNLIKELY(ret<0))return ret; + left_link=0; + while(_of->offsetready_stateos,ogg_page_serialno(_og)); + ogg_stream_pagein(&_of->os,_og); + if(OP_LIKELY(ogg_stream_packetout(&_of->os,&op)>0)){ + ret=opus_head_parse(_head,op.packet,op.bytes); + /*If it's just a stream type we don't recognize, ignore it.*/ + if(ret==OP_ENOTFORMAT)continue; + /*Everything else is fatal.*/ + if(OP_UNLIKELY(ret<0))return ret; + /*Found a valid Opus header. + Continue setup.*/ + _of->ready_state=OP_STREAMSET; + } + } + /*Get the next page. + No need to clamp the boundary offset against _of->end, as all errors + become OP_ENOTFORMAT.*/ + if(OP_UNLIKELY(op_get_next_page(_of,_og, + OP_ADV_OFFSET(_of->offset,OP_CHUNK_SIZE))<0)){ + return OP_ENOTFORMAT; + } + /*If this page also belongs to our Opus stream, submit it and break.*/ + if(_of->ready_state==OP_STREAMSET + &&_of->os.serialno==ogg_page_serialno(_og)){ + ogg_stream_pagein(&_of->os,_og); + break; + } + } + if(OP_UNLIKELY(_of->ready_state!=OP_STREAMSET))return OP_ENOTFORMAT; + /*Loop getting packets.*/ + for(;;){ + switch(ogg_stream_packetout(&_of->os,&op)){ + case 0:{ + /*Loop getting pages.*/ + for(;;){ + /*No need to clamp the boundary offset against _of->end, as all + errors become OP_EBADHEADER.*/ + if(OP_UNLIKELY(op_get_next_page(_of,_og, + OP_ADV_OFFSET(_of->offset,OP_CHUNK_SIZE))<0)){ + return OP_EBADHEADER; + } + /*If this page belongs to the correct stream, go parse it.*/ + if(_of->os.serialno==ogg_page_serialno(_og)){ + ogg_stream_pagein(&_of->os,_og); + break; + } + /*If the link ends before we see the Opus comment header, abort.*/ + if(OP_UNLIKELY(ogg_page_bos(_og)))return OP_EBADHEADER; + /*Otherwise, keep looking.*/ + } + }break; + /*We shouldn't get a hole in the headers!*/ + case -1:return OP_EBADHEADER; + default:{ + /*Got a packet. + It should be the comment header.*/ + ret=opus_tags_parse(_tags,op.packet,op.bytes); + if(OP_UNLIKELY(ret<0))return ret; + /*Make sure the page terminated at the end of the comment header. + If there is another packet on the page, or part of a packet, then + reject the stream. + Otherwise seekable sources won't be able to seek back to the start + properly.*/ + ret=ogg_stream_packetout(&_of->os,&op); + if(OP_UNLIKELY(ret!=0) + ||OP_UNLIKELY(_og->header[_og->header_len-1]==255)){ + /*If we fail, the caller assumes our tags are uninitialized.*/ + opus_tags_clear(_tags); + return OP_EBADHEADER; + } + return 0; + } + } + } +} + +static int op_fetch_headers(OggOpusFile *_of,OpusHead *_head, + OpusTags *_tags,ogg_uint32_t **_serialnos,int *_nserialnos, + int *_cserialnos,ogg_page *_og){ + ogg_page og; + int ret; + if(!_og){ + /*No need to clamp the boundary offset against _of->end, as all errors + become OP_ENOTFORMAT.*/ + if(OP_UNLIKELY(op_get_next_page(_of,&og, + OP_ADV_OFFSET(_of->offset,OP_CHUNK_SIZE))<0)){ + return OP_ENOTFORMAT; + } + _og=&og; + } + _of->ready_state=OP_OPENED; + ret=op_fetch_headers_impl(_of,_head,_tags,_serialnos,_nserialnos, + _cserialnos,_og); + /*Revert back from OP_STREAMSET to OP_OPENED on failure, to prevent + double-free of the tags in an unseekable stream.*/ + if(OP_UNLIKELY(ret<0))_of->ready_state=OP_OPENED; + return ret; +} + +/*Granule position manipulation routines. + A granule position is defined to be an unsigned 64-bit integer, with the + special value -1 in two's complement indicating an unset or invalid granule + position. + We are not guaranteed to have an unsigned 64-bit type, so we construct the + following routines that + a) Properly order negative numbers as larger than positive numbers, and + b) Check for underflow or overflow past the special -1 value. + This lets us operate on the full, valid range of granule positions in a + consistent and safe manner. + This full range is organized into distinct regions: + [ -1 (invalid) ][ 0 ... OP_INT64_MAX ][ OP_INT64_MIN ... -2 ][-1 (invalid) ] + + No one should actually use granule positions so large that they're negative, + even if they are technically valid, as very little software handles them + correctly (including most of Xiph.Org's). + This library also refuses to support durations so large they won't fit in a + signed 64-bit integer (to avoid exposing this mess to the application, and + to simplify a good deal of internal arithmetic), so the only way to use them + successfully is if pcm_start is very large. + This means there isn't anything you can do with negative granule positions + that you couldn't have done with purely non-negative ones. + The main purpose of these routines is to allow us to think very explicitly + about the possible failure cases of all granule position manipulations.*/ + +/*Safely adds a small signed integer to a valid (not -1) granule position. + The result can use the full 64-bit range of values (both positive and + negative), but will fail on overflow (wrapping past -1; wrapping past + OP_INT64_MAX is explicitly okay). + [out] _dst_gp: The resulting granule position. + Only modified on success. + _src_gp: The granule position to add to. + This must not be -1. + _delta: The amount to add. + This is allowed to be up to 32 bits to support the maximum + duration of a single Ogg page (255 packets * 120 ms per + packet == 1,468,800 samples at 48 kHz). + Return: 0 on success, or OP_EINVAL if the result would wrap around past -1.*/ +static int op_granpos_add(ogg_int64_t *_dst_gp,ogg_int64_t _src_gp, + opus_int32 _delta){ + /*The code below handles this case correctly, but there's no reason we + should ever be called with these values, so make sure we aren't.*/ + OP_ASSERT(_src_gp!=-1); + if(_delta>0){ + /*Adding this amount to the granule position would overflow its 64-bit + range.*/ + if(OP_UNLIKELY(_src_gp<0)&&OP_UNLIKELY(_src_gp>=-1-_delta))return OP_EINVAL; + if(OP_UNLIKELY(_src_gp>OP_INT64_MAX-_delta)){ + /*Adding this amount to the granule position would overflow the positive + half of its 64-bit range. + Since signed overflow is undefined in C, do it in a way the compiler + isn't allowed to screw up.*/ + _delta-=(opus_int32)(OP_INT64_MAX-_src_gp)+1; + _src_gp=OP_INT64_MIN; + } + } + else if(_delta<0){ + /*Subtracting this amount from the granule position would underflow its + 64-bit range.*/ + if(_src_gp>=0&&OP_UNLIKELY(_src_gp<-_delta))return OP_EINVAL; + if(OP_UNLIKELY(_src_gp da < 0.*/ + da=(OP_INT64_MIN-_gp_a)-1; + /*_gp_b >= 0 => db >= 0.*/ + db=OP_INT64_MAX-_gp_b; + /*Step 2: Check for overflow.*/ + if(OP_UNLIKELY(OP_INT64_MAX+da= 0 => da <= 0*/ + da=_gp_a+OP_INT64_MIN; + /*_gp_b < 0 => db <= 0*/ + db=OP_INT64_MIN-_gp_b; + /*Step 2: Check for overflow.*/ + if(OP_UNLIKELY(da=0)return 1; + /*Else fall through.*/ + } + else if(OP_UNLIKELY(_gp_b<0))return -1; + /*No wrapping case.*/ + return (_gp_a>_gp_b)-(_gp_b>_gp_a); +} + +/*Returns the duration of the packet (in samples at 48 kHz), or a negative + value on error.*/ +static int op_get_packet_duration(const unsigned char *_data,int _len){ + int nframes; + int frame_size; + int nsamples; + nframes=opus_packet_get_nb_frames(_data,_len); + if(OP_UNLIKELY(nframes<0))return OP_EBADPACKET; + frame_size=opus_packet_get_samples_per_frame(_data,48000); + nsamples=nframes*frame_size; + if(OP_UNLIKELY(nsamples>120*48))return OP_EBADPACKET; + return nsamples; +} + +/*This function more properly belongs in info.c, but we define it here to allow + the static granule position manipulation functions to remain static.*/ +ogg_int64_t opus_granule_sample(const OpusHead *_head,ogg_int64_t _gp){ + opus_int32 pre_skip; + pre_skip=_head->pre_skip; + if(_gp!=-1&&op_granpos_add(&_gp,_gp,-pre_skip))_gp=-1; + return _gp; +} + +/*Grab all the packets currently in the stream state, and compute their + durations. + _of->op_count is set to the number of packets collected. + [out] _durations: Returns the durations of the individual packets. + Return: The total duration of all packets, or OP_HOLE if there was a hole.*/ +static opus_int32 op_collect_audio_packets(OggOpusFile *_of, + int _durations[255]){ + opus_int32 total_duration; + int op_count; + /*Count the durations of all packets in the page.*/ + op_count=0; + total_duration=0; + for(;;){ + int ret; + /*This takes advantage of undocumented libogg behavior that returned + ogg_packet buffers are valid at least until the next page is + submitted. + Relying on this is not too terrible, as _none_ of the Ogg memory + ownership/lifetime rules are well-documented. + But I can read its code and know this will work.*/ + ret=ogg_stream_packetout(&_of->os,_of->op+op_count); + if(!ret)break; + if(OP_UNLIKELY(ret<0)){ + /*We shouldn't get holes in the middle of pages.*/ + OP_ASSERT(op_count==0); + /*Set the return value and break out of the loop. + We want to make sure op_count gets set to 0, because we've ingested a + page, so any previously loaded packets are now invalid.*/ + total_duration=OP_HOLE; + break; + } + /*Unless libogg is broken, we can't get more than 255 packets from a + single page.*/ + OP_ASSERT(op_count<255); + _durations[op_count]=op_get_packet_duration(_of->op[op_count].packet, + _of->op[op_count].bytes); + if(OP_LIKELY(_durations[op_count]>0)){ + /*With at most 255 packets on a page, this can't overflow.*/ + total_duration+=_durations[op_count++]; + } + /*Ignore packets with an invalid TOC sequence.*/ + else if(op_count>0){ + /*But save the granule position, if there was one.*/ + _of->op[op_count-1].granulepos=_of->op[op_count].granulepos; + } + } + _of->op_pos=0; + _of->op_count=op_count; + return total_duration; +} + +/*Starting from current cursor position, get the initial PCM offset of the next + page. + This also validates the granule position on the first page with a completed + audio data packet, as required by the spec. + If this link is completely empty (no pages with completed packets), then this + function sets pcm_start=pcm_end=0 and returns the BOS page of the next link + (if any). + In the seekable case, we initialize pcm_end=-1 before calling this function, + so that later we can detect that the link was empty before calling + op_find_final_pcm_offset(). + [inout] _link: The link for which to find pcm_start. + [out] _og: Returns the BOS page of the next link if this link was empty. + In the unseekable case, we can then feed this to + op_fetch_headers() to start the next link. + The caller may pass NULL (e.g., for seekable streams), in + which case this page will be discarded. + Return: 0 on success, 1 if there is a buffered BOS page available, or a + negative value on unrecoverable error.*/ +static int op_find_initial_pcm_offset(OggOpusFile *_of, + OggOpusLink *_link,ogg_page *_og){ + ogg_page og; + ogg_int64_t pcm_start; + ogg_int64_t prev_packet_gp; + ogg_int64_t cur_page_gp; + ogg_uint32_t serialno; + opus_int32 total_duration; + int durations[255]; + int cur_page_eos; + int op_count; + int pi; + if(_og==NULL)_og=&og; + serialno=_of->os.serialno; + op_count=0; + /*We shouldn't have to initialize total_duration, but gcc is too dumb to + figure out that op_count>0 implies we've been through the whole loop at + least once.*/ + total_duration=0; + do{ + opus_int64 llret; + llret=op_get_next_page(_of,_og,_of->end); + /*We should get a page unless the file is truncated or mangled. + Otherwise there are no audio data packets in the whole logical stream.*/ + if(OP_UNLIKELY(llret<0)){ + /*Fail if there was a read error.*/ + if(llrethead.pre_skip>0)return OP_EBADTIMESTAMP; + /*Set pcm_end and end_offset so we can skip the call to + op_find_final_pcm_offset().*/ + _link->pcm_start=_link->pcm_end=0; + _link->end_offset=_link->data_offset; + return 0; + } + /*Similarly, if we hit the next link in the chain, we've gone too far.*/ + if(OP_UNLIKELY(ogg_page_bos(_og))){ + if(_link->head.pre_skip>0)return OP_EBADTIMESTAMP; + /*Set pcm_end and end_offset so we can skip the call to + op_find_final_pcm_offset().*/ + _link->pcm_end=_link->pcm_start=0; + _link->end_offset=_link->data_offset; + /*Tell the caller we've got a buffered page for them.*/ + return 1; + } + /*Ignore pages from other streams (not strictly necessary, because of the + checks in ogg_stream_pagein(), but saves some work).*/ + if(serialno!=(ogg_uint32_t)ogg_page_serialno(_og))continue; + ogg_stream_pagein(&_of->os,_og); + /*Bitrate tracking: add the header's bytes here. + The body bytes are counted when we consume the packets.*/ + _of->bytes_tracked+=_og->header_len; + /*Count the durations of all packets in the page.*/ + do total_duration=op_collect_audio_packets(_of,durations); + /*Ignore holes.*/ + while(OP_UNLIKELY(total_duration<0)); + op_count=_of->op_count; + } + while(op_count<=0); + /*We found the first page with a completed audio data packet: actually look + at the granule position. + RFC 3533 says, "A special value of -1 (in two's complement) indicates that + no packets finish on this page," which does not say that a granule + position that is NOT -1 indicates that some packets DO finish on that page + (even though this was the intention, libogg itself violated this intention + for years before we fixed it). + The Ogg Opus specification only imposes its start-time requirements + on the granule position of the first page with completed packets, + so we ignore any set granule positions until then.*/ + cur_page_gp=_of->op[op_count-1].granulepos; + /*But getting a packet without a valid granule position on the page is not + okay.*/ + if(cur_page_gp==-1)return OP_EBADTIMESTAMP; + cur_page_eos=_of->op[op_count-1].e_o_s; + if(OP_LIKELY(!cur_page_eos)){ + /*The EOS flag wasn't set. + Work backwards from the provided granule position to get the starting PCM + offset.*/ + if(OP_UNLIKELY(op_granpos_add(&pcm_start,cur_page_gp,-total_duration)<0)){ + /*The starting granule position MUST not be smaller than the amount of + audio on the first page with completed packets.*/ + return OP_EBADTIMESTAMP; + } + } + else{ + /*The first page with completed packets was also the last.*/ + if(OP_LIKELY(op_granpos_add(&pcm_start,cur_page_gp,-total_duration)<0)){ + /*If there's less audio on the page than indicated by the granule + position, then we're doing end-trimming, and the starting PCM offset + is zero by spec mandate.*/ + pcm_start=0; + /*However, the end-trimming MUST not ask us to trim more samples than + exist after applying the pre-skip.*/ + if(OP_UNLIKELY(op_granpos_cmp(cur_page_gp,_link->head.pre_skip)<0)){ + return OP_EBADTIMESTAMP; + } + } + } + /*Timestamp the individual packets.*/ + prev_packet_gp=pcm_start; + for(pi=0;pi0){ + /*If we trimmed the entire packet, stop (the spec says encoders + shouldn't do this, but we support it anyway).*/ + if(OP_UNLIKELY(diff>durations[pi]))break; + _of->op[pi].granulepos=prev_packet_gp=cur_page_gp; + /*Move the EOS flag to this packet, if necessary, so we'll trim the + samples.*/ + _of->op[pi].e_o_s=1; + continue; + } + } + /*Update the granule position as normal.*/ + OP_ALWAYS_TRUE(!op_granpos_add(&_of->op[pi].granulepos, + prev_packet_gp,durations[pi])); + prev_packet_gp=_of->op[pi].granulepos; + } + /*Update the packet count after end-trimming.*/ + _of->op_count=pi; + _of->cur_discard_count=_link->head.pre_skip; + _of->prev_packet_gp=_link->pcm_start=pcm_start; + return 0; +} + +/*Starting from current cursor position, get the final PCM offset of the + previous page. + This also validates the duration of the link, which, while not strictly + required by the spec, we need to ensure duration calculations don't + overflow. + This is only done for seekable sources. + We must validate that op_find_initial_pcm_offset() succeeded for this link + before calling this function, otherwise it will scan the entire stream + backwards until it reaches the start, and then fail.*/ +static int op_find_final_pcm_offset(OggOpusFile *_of, + const ogg_uint32_t *_serialnos,int _nserialnos,OggOpusLink *_link, + opus_int64 _offset,ogg_uint32_t _end_serialno,ogg_int64_t _end_gp, + ogg_int64_t *_total_duration){ + ogg_int64_t total_duration; + ogg_int64_t duration; + ogg_uint32_t cur_serialno; + /*For the time being, fetch end PCM offset the simple way.*/ + cur_serialno=_link->serialno; + if(_end_serialno!=cur_serialno||_end_gp==-1){ + _offset=op_get_last_page(_of,&_end_gp,_offset, + cur_serialno,_serialnos,_nserialnos); + if(OP_UNLIKELY(_offset<0))return (int)_offset; + } + /*At worst we should have found the first page with completed packets.*/ + if(OP_UNLIKELY(_offset<_link->data_offset))return OP_EBADLINK; + /*This implementation requires that the difference between the first and last + granule positions in each link be representable in a signed, 64-bit + number, and that each link also have at least as many samples as the + pre-skip requires.*/ + if(OP_UNLIKELY(op_granpos_diff(&duration,_end_gp,_link->pcm_start)<0) + ||OP_UNLIKELY(duration<_link->head.pre_skip)){ + return OP_EBADTIMESTAMP; + } + /*We also require that the total duration be representable in a signed, + 64-bit number.*/ + duration-=_link->head.pre_skip; + total_duration=*_total_duration; + if(OP_UNLIKELY(OP_INT64_MAX-durationpcm_end=_end_gp; + _link->end_offset=_offset; + return 0; +} + +/*Rescale the number _x from the range [0,_from] to [0,_to]. + _from and _to must be positive.*/ +static opus_int64 op_rescale64(opus_int64 _x,opus_int64 _from,opus_int64 _to){ + opus_int64 frac; + opus_int64 ret; + int i; + if(_x>=_from)return _to; + if(_x<=0)return 0; + frac=0; + for(i=0;i<63;i++){ + frac<<=1; + OP_ASSERT(_x<=_from); + if(_x>=_from>>1){ + _x-=_from-_x; + frac|=1; + } + else _x<<=1; + } + ret=0; + for(i=0;i<63;i++){ + if(frac&1)ret=(ret&_to&1)+(ret>>1)+(_to>>1); + else ret>>=1; + frac>>=1; + } + return ret; +} + +/*The minimum granule position spacing allowed for making predictions. + This corresponds to about 1 second of audio at 48 kHz for both Opus and + Vorbis, or one keyframe interval in Theora with the default keyframe spacing + of 256.*/ +#define OP_GP_SPACING_MIN (48000) + +/*Try to estimate the location of the next link using the current seek + records, assuming the initial granule position of any streams we've found is + 0.*/ +static opus_int64 op_predict_link_start(const OpusSeekRecord *_sr,int _nsr, + opus_int64 _searched,opus_int64 _end_searched,opus_int32 _bias){ + opus_int64 bisect; + int sri; + int srj; + /*Require that we be at least OP_CHUNK_SIZE from the end. + We don't require that we be at least OP_CHUNK_SIZE from the beginning, + because if we are we'll just scan forward without seeking.*/ + _end_searched-=OP_CHUNK_SIZE; + if(_searched>=_end_searched)return -1; + bisect=_end_searched; + for(sri=0;sri<_nsr;sri++){ + ogg_int64_t gp1; + ogg_int64_t gp2_min; + ogg_uint32_t serialno1; + opus_int64 offset1; + /*If the granule position is negative, either it's invalid or we'd cause + overflow.*/ + gp1=_sr[sri].gp; + if(gp1<0)continue; + /*We require some minimum distance between granule positions to make an + estimate. + We don't actually know what granule position scheme is being used, + because we have no idea what kind of stream these came from. + Therefore we require a minimum spacing between them, with the + expectation that while bitrates and granule position increments might + vary locally in quite complex ways, they are globally smooth.*/ + if(OP_UNLIKELY(op_granpos_add(&gp2_min,gp1,OP_GP_SPACING_MIN)<0)){ + /*No granule position would satisfy us.*/ + continue; + } + offset1=_sr[sri].offset; + serialno1=_sr[sri].serialno; + for(srj=sri;srj-->0;){ + ogg_int64_t gp2; + opus_int64 offset2; + opus_int64 num; + ogg_int64_t den; + ogg_int64_t ipart; + gp2=_sr[srj].gp; + if(gp20); + if(ipart>0&&(offset2-_searched)/ipart=_end_searched?-1:bisect; +} + +/*Finds each bitstream link, one at a time, using a bisection search. + This has to begin by knowing the offset of the first link's initial page.*/ +static int op_bisect_forward_serialno(OggOpusFile *_of, + opus_int64 _searched,OpusSeekRecord *_sr,int _csr, + ogg_uint32_t **_serialnos,int *_nserialnos,int *_cserialnos){ + ogg_page og; + OggOpusLink *links; + int nlinks; + int clinks; + ogg_uint32_t *serialnos; + int nserialnos; + ogg_int64_t total_duration; + int nsr; + int ret; + links=_of->links; + nlinks=clinks=_of->nlinks; + total_duration=0; + /*We start with one seek record, for the last page in the file. + We build up a list of records for places we seek to during link + enumeration. + This list is kept sorted in reverse order. + We only care about seek locations that were _not_ in the current link, + therefore we can add them one at a time to the end of the list as we + improve the lower bound on the location where the next link starts.*/ + nsr=1; + for(;;){ + opus_int64 end_searched; + opus_int64 bisect; + opus_int64 next; + opus_int64 last; + ogg_int64_t end_offset; + ogg_int64_t end_gp; + int sri; + serialnos=*_serialnos; + nserialnos=*_nserialnos; + if(OP_UNLIKELY(nlinks>=clinks)){ + if(OP_UNLIKELY(clinks>INT_MAX-1>>1))return OP_EFAULT; + clinks=2*clinks+1; + OP_ASSERT(nlinkslinks=links; + } + /*Invariants: + We have the headers and serial numbers for the link beginning at 'begin'. + We have the offset and granule position of the last page in the file + (potentially not a page we care about).*/ + /*Scan the seek records we already have to save us some bisection.*/ + for(sri=0;sri1){ + opus_int64 last_offset; + opus_int64 avg_link_size; + opus_int64 upper_limit; + last_offset=links[nlinks-1].offset; + avg_link_size=last_offset/(nlinks-1); + upper_limit=end_searched-OP_CHUNK_SIZE-avg_link_size; + if(OP_LIKELY(last_offset>_searched-avg_link_size) + &&OP_LIKELY(last_offset>1); + /*If we're within OP_CHUNK_SIZE of the start, scan forward.*/ + if(bisect-_searchedoffset-last>=0); + OP_ASSERT(_of->offset-last<=OP_PAGE_SIZE_MAX); + _sr[nsr].size=(opus_int32)(_of->offset-last); + _sr[nsr].serialno=serialno; + _sr[nsr].gp=gp; + nsr++; + } + } + else{ + _searched=_of->offset; + next_bias=OP_CHUNK_SIZE; + if(serialno==links[nlinks-1].serialno){ + /*This page was from the stream we want, remember it. + If it's the last such page in the link, we won't have to go back + looking for it later.*/ + end_gp=gp; + end_offset=last; + } + } + } + bisect=op_predict_link_start(_sr,nsr,_searched,end_searched,next_bias); + } + /*Bisection point found. + Get the final granule position of the previous link, assuming + op_find_initial_pcm_offset() didn't already determine the link was + empty.*/ + if(OP_LIKELY(links[nlinks-1].pcm_end==-1)){ + if(end_gp==-1){ + /*If we don't know where the end page is, we'll have to seek back and + look for it, starting from the end of the link.*/ + end_offset=next; + /*Also forget the last page we read. + It won't be available after the seek.*/ + last=-1; + } + ret=op_find_final_pcm_offset(_of,serialnos,nserialnos, + links+nlinks-1,end_offset,links[nlinks-1].serialno,end_gp, + &total_duration); + if(OP_UNLIKELY(ret<0))return ret; + } + if(last!=next){ + /*The last page we read was not the first page the next link. + Move the cursor position to the offset of that first page. + This only performs an actual seek if the first page of the next link + does not start at the end of the last page from the current Opus + stream with a valid granule position.*/ + ret=op_seek_helper(_of,next); + if(OP_UNLIKELY(ret<0))return ret; + } + ret=op_fetch_headers(_of,&links[nlinks].head,&links[nlinks].tags, + _serialnos,_nserialnos,_cserialnos,last!=next?NULL:&og); + if(OP_UNLIKELY(ret<0))return ret; + links[nlinks].offset=next; + links[nlinks].data_offset=_of->offset; + links[nlinks].serialno=_of->os.serialno; + links[nlinks].pcm_end=-1; + /*This might consume a page from the next link, however the next bisection + always starts with a seek.*/ + ret=op_find_initial_pcm_offset(_of,links+nlinks,NULL); + if(OP_UNLIKELY(ret<0))return ret; + _searched=_of->offset; + /*Mark the current link count so it can be cleaned up on error.*/ + _of->nlinks=++nlinks; + } + /*Last page is in the starting serialno list, so we've reached the last link. + Now find the last granule position for it (if we didn't the first time we + looked at the end of the stream, and if op_find_initial_pcm_offset() + didn't already determine the link was empty).*/ + if(OP_LIKELY(links[nlinks-1].pcm_end==-1)){ + ret=op_find_final_pcm_offset(_of,serialnos,nserialnos, + links+nlinks-1,_sr[0].offset,_sr[0].serialno,_sr[0].gp,&total_duration); + if(OP_UNLIKELY(ret<0))return ret; + } + /*Trim back the links array if necessary.*/ + links=(OggOpusLink *)_ogg_realloc(links,sizeof(*links)*nlinks); + if(OP_LIKELY(links!=NULL))_of->links=links; + /*We also don't need these anymore.*/ + _ogg_free(*_serialnos); + *_serialnos=NULL; + *_cserialnos=*_nserialnos=0; + return 0; +} + +static void op_update_gain(OggOpusFile *_of){ + OpusHead *head; + opus_int32 gain_q8; + int li; + /*If decode isn't ready, then we'll apply the gain when we initialize the + decoder.*/ + if(_of->ready_stategain_offset_q8; + li=_of->seekable?_of->cur_link:0; + head=&_of->links[li].head; + /*We don't have to worry about overflow here because the header gain and + track gain must lie in the range [-32768,32767], and the user-supplied + offset has been pre-clamped to [-98302,98303].*/ + switch(_of->gain_type){ + case OP_TRACK_GAIN:{ + int track_gain_q8; + track_gain_q8=0; + opus_tags_get_track_gain(&_of->links[li].tags,&track_gain_q8); + gain_q8+=track_gain_q8; + } + /*Fall through.*/ + case OP_HEADER_GAIN:gain_q8+=head->output_gain;break; + case OP_ABSOLUTE_GAIN:break; + default:OP_ASSERT(0); + } + gain_q8=OP_CLAMP(-32768,gain_q8,32767); + OP_ASSERT(_of->od!=NULL); +#if defined(OPUS_SET_GAIN) + opus_multistream_decoder_ctl(_of->od,OPUS_SET_GAIN(gain_q8)); +#else +/*A fallback that works with both float and fixed-point is a bunch of work, + so just force people to use a sufficiently new version. + This is deployed well enough at this point that this shouldn't be a burden.*/ +# error "libopus 1.0.1 or later required" +#endif +} + +static int op_make_decode_ready(OggOpusFile *_of){ + const OpusHead *head; + int li; + int stream_count; + int coupled_count; + int channel_count; + if(_of->ready_state>OP_STREAMSET)return 0; + if(OP_UNLIKELY(_of->ready_stateseekable?_of->cur_link:0; + head=&_of->links[li].head; + stream_count=head->stream_count; + coupled_count=head->coupled_count; + channel_count=head->channel_count; + /*Check to see if the current decoder is compatible with the current link.*/ + if(_of->od!=NULL&&_of->od_stream_count==stream_count + &&_of->od_coupled_count==coupled_count&&_of->od_channel_count==channel_count + &&memcmp(_of->od_mapping,head->mapping, + sizeof(*head->mapping)*channel_count)==0){ + opus_multistream_decoder_ctl(_of->od,OPUS_RESET_STATE); + } + else{ + int err; + opus_multistream_decoder_destroy(_of->od); + _of->od=opus_multistream_decoder_create(48000,channel_count, + stream_count,coupled_count,head->mapping,&err); + if(_of->od==NULL)return OP_EFAULT; + _of->od_stream_count=stream_count; + _of->od_coupled_count=coupled_count; + _of->od_channel_count=channel_count; + memcpy(_of->od_mapping,head->mapping,sizeof(*head->mapping)*channel_count); + } + _of->ready_state=OP_INITSET; + _of->bytes_tracked=0; + _of->samples_tracked=0; +#if !defined(OP_FIXED_POINT) + _of->state_channel_count=0; + /*Use the serial number for the PRNG seed to get repeatable output for + straight play-throughs.*/ + _of->dither_seed=_of->links[li].serialno; +#endif + op_update_gain(_of); + return 0; +} + +static int op_open_seekable2_impl(OggOpusFile *_of){ + /*64 seek records should be enough for anybody. + Actually, with a bisection search in a 63-bit range down to OP_CHUNK_SIZE + granularity, much more than enough.*/ + OpusSeekRecord sr[64]; + opus_int64 data_offset; + int ret; + /*We can seek, so set out learning all about this file.*/ + (*_of->callbacks.seek)(_of->source,0,SEEK_END); + _of->offset=_of->end=(*_of->callbacks.tell)(_of->source); + if(OP_UNLIKELY(_of->end<0))return OP_EREAD; + data_offset=_of->links[0].data_offset; + if(OP_UNLIKELY(_of->endend, + _of->links[0].serialno,_of->serialnos,_of->nserialnos); + if(OP_UNLIKELY(ret<0))return ret; + /*If there's any trailing junk, forget about it.*/ + _of->end=sr[0].offset+sr[0].size; + if(OP_UNLIKELY(_of->endserialnos,&_of->nserialnos,&_of->cserialnos); +} + +static int op_open_seekable2(OggOpusFile *_of){ + ogg_sync_state oy_start; + ogg_stream_state os_start; + ogg_packet *op_start; + opus_int64 start_offset; + int start_op_count; + int ret; + /*We're partially open and have a first link header state in storage in _of. + Save off that stream state so we can come back to it. + It would be simpler to just dump all this state and seek back to + links[0].data_offset when we're done. + But we do the extra work to allow us to seek back to _exactly_ the same + stream position we're at now. + This allows, e.g., the HTTP backend to continue reading from the original + connection (if it's still available), instead of opening a new one. + This means we can open and start playing a normal Opus file with a single + link and reasonable packet sizes using only two HTTP requests.*/ + start_op_count=_of->op_count; + /*This is a bit too large to put on the stack unconditionally.*/ + op_start=(ogg_packet *)_ogg_malloc(sizeof(*op_start)*start_op_count); + if(op_start==NULL)return OP_EFAULT; + *&oy_start=_of->oy; + *&os_start=_of->os; + start_offset=_of->offset; + memcpy(op_start,_of->op,sizeof(*op_start)*start_op_count); + OP_ASSERT((*_of->callbacks.tell)(_of->source)==op_position(_of)); + ogg_sync_init(&_of->oy); + ogg_stream_init(&_of->os,-1); + ret=op_open_seekable2_impl(_of); + /*Restore the old stream state.*/ + ogg_stream_clear(&_of->os); + ogg_sync_clear(&_of->oy); + *&_of->oy=*&oy_start; + *&_of->os=*&os_start; + _of->offset=start_offset; + _of->op_count=start_op_count; + memcpy(_of->op,op_start,sizeof(*_of->op)*start_op_count); + _ogg_free(op_start); + _of->prev_packet_gp=_of->links[0].pcm_start; + _of->cur_discard_count=_of->links[0].head.pre_skip; + if(OP_UNLIKELY(ret<0))return ret; + /*And restore the position indicator.*/ + ret=(*_of->callbacks.seek)(_of->source,op_position(_of),SEEK_SET); + return OP_UNLIKELY(ret<0)?OP_EREAD:0; +} + +/*Clear out the current logical bitstream decoder.*/ +static void op_decode_clear(OggOpusFile *_of){ + /*We don't actually free the decoder. + We might be able to re-use it for the next link.*/ + _of->op_count=0; + _of->od_buffer_size=0; + _of->prev_packet_gp=-1; + if(!_of->seekable){ + OP_ASSERT(_of->ready_state>=OP_INITSET); + opus_tags_clear(&_of->links[0].tags); + } + _of->ready_state=OP_OPENED; +} + +static void op_clear(OggOpusFile *_of){ + OggOpusLink *links; + _ogg_free(_of->od_buffer); + if(_of->od!=NULL)opus_multistream_decoder_destroy(_of->od); + links=_of->links; + if(!_of->seekable){ + if(_of->ready_state>OP_OPENED||_of->ready_state==OP_PARTOPEN){ + opus_tags_clear(&links[0].tags); + } + } + else if(OP_LIKELY(links!=NULL)){ + int nlinks; + int link; + nlinks=_of->nlinks; + for(link=0;linkserialnos); + ogg_stream_clear(&_of->os); + ogg_sync_clear(&_of->oy); + if(_of->callbacks.close!=NULL)(*_of->callbacks.close)(_of->source); +} + +static int op_open1(OggOpusFile *_of, + void *_source,const OpusFileCallbacks *_cb, + const unsigned char *_initial_data,size_t _initial_bytes){ + ogg_page og; + ogg_page *pog; + int seekable; + int ret; + memset(_of,0,sizeof(*_of)); + _of->end=-1; + _of->source=_source; + *&_of->callbacks=*_cb; + /*At a minimum, we need to be able to read data.*/ + if(OP_UNLIKELY(_of->callbacks.read==NULL))return OP_EREAD; + /*Initialize the framing state.*/ + ogg_sync_init(&_of->oy); + /*Perhaps some data was previously read into a buffer for testing against + other stream types. + Allow initialization from this previously read data (especially as we may + be reading from a non-seekable stream). + This requires copying it into a buffer allocated by ogg_sync_buffer() and + doesn't support seeking, so this is not a good mechanism to use for + decoding entire files from RAM.*/ + if(_initial_bytes>0){ + char *buffer; + buffer=ogg_sync_buffer(&_of->oy,_initial_bytes); + memcpy(buffer,_initial_data,_initial_bytes*sizeof(*buffer)); + ogg_sync_wrote(&_of->oy,_initial_bytes); + } + /*Can we seek? + Stevens suggests the seek test is portable.*/ + seekable=_cb->seek!=NULL&&(*_cb->seek)(_source,0,SEEK_CUR)!=-1; + /*If seek is implemented, tell must also be implemented.*/ + if(seekable){ + opus_int64 pos; + if(OP_UNLIKELY(_of->callbacks.tell==NULL))return OP_EINVAL; + pos=(*_of->callbacks.tell)(_of->source); + /*If the current position is not equal to the initial bytes consumed, + absolute seeking will not work.*/ + if(OP_UNLIKELY(pos!=(opus_int64)_initial_bytes))return OP_EINVAL; + } + _of->seekable=seekable; + /*Don't seek yet. + Set up a 'single' (current) logical bitstream entry for partial open.*/ + _of->links=(OggOpusLink *)_ogg_malloc(sizeof(*_of->links)); + /*The serialno gets filled in later by op_fetch_headers().*/ + ogg_stream_init(&_of->os,-1); + pog=NULL; + for(;;){ + /*Fetch all BOS pages, store the Opus header and all seen serial numbers, + and load subsequent Opus setup headers.*/ + ret=op_fetch_headers(_of,&_of->links[0].head,&_of->links[0].tags, + &_of->serialnos,&_of->nserialnos,&_of->cserialnos,pog); + if(OP_UNLIKELY(ret<0))break; + _of->nlinks=1; + _of->links[0].offset=0; + _of->links[0].data_offset=_of->offset; + _of->links[0].pcm_end=-1; + _of->links[0].serialno=_of->os.serialno; + /*Fetch the initial PCM offset.*/ + ret=op_find_initial_pcm_offset(_of,_of->links,&og); + if(seekable||OP_LIKELY(ret<=0))break; + /*This link was empty, but we already have the BOS page for the next one in + og. + We can't seek, so start processing the next link right now.*/ + opus_tags_clear(&_of->links[0].tags); + _of->nlinks=0; + if(!seekable)_of->cur_link++; + pog=&og; + } + if(OP_LIKELY(ret>=0))_of->ready_state=OP_PARTOPEN; + return ret; +} + +static int op_open2(OggOpusFile *_of){ + int ret; + OP_ASSERT(_of->ready_state==OP_PARTOPEN); + if(_of->seekable){ + _of->ready_state=OP_OPENED; + ret=op_open_seekable2(_of); + } + else ret=0; + if(OP_LIKELY(ret>=0)){ + /*We have buffered packets from op_find_initial_pcm_offset(). + Move to OP_INITSET so we can use them.*/ + _of->ready_state=OP_STREAMSET; + ret=op_make_decode_ready(_of); + if(OP_LIKELY(ret>=0))return 0; + } + /*Don't auto-close the stream on failure.*/ + _of->callbacks.close=NULL; + op_clear(_of); + return ret; +} + +OggOpusFile *op_test_callbacks(void *_source,const OpusFileCallbacks *_cb, + const unsigned char *_initial_data,size_t _initial_bytes,int *_error){ + OggOpusFile *of; + int ret; + of=(OggOpusFile *)_ogg_malloc(sizeof(*of)); + ret=OP_EFAULT; + if(OP_LIKELY(of!=NULL)){ + ret=op_open1(of,_source,_cb,_initial_data,_initial_bytes); + if(OP_LIKELY(ret>=0)){ + if(_error!=NULL)*_error=0; + return of; + } + /*Don't auto-close the stream on failure.*/ + of->callbacks.close=NULL; + op_clear(of); + _ogg_free(of); + } + if(_error!=NULL)*_error=ret; + return NULL; +} + +OggOpusFile *op_open_callbacks(void *_source,const OpusFileCallbacks *_cb, + const unsigned char *_initial_data,size_t _initial_bytes,int *_error){ + OggOpusFile *of; + of=op_test_callbacks(_source,_cb,_initial_data,_initial_bytes,_error); + if(OP_LIKELY(of!=NULL)){ + int ret; + ret=op_open2(of); + if(OP_LIKELY(ret>=0))return of; + if(_error!=NULL)*_error=ret; + _ogg_free(of); + } + return NULL; +} + +/*Convenience routine to clean up from failure for the open functions that + create their own streams.*/ +static OggOpusFile *op_open_close_on_failure(void *_source, + const OpusFileCallbacks *_cb,int *_error){ + OggOpusFile *of; + if(OP_UNLIKELY(_source==NULL)){ + if(_error!=NULL)*_error=OP_EFAULT; + return NULL; + } + of=op_open_callbacks(_source,_cb,NULL,0,_error); + if(OP_UNLIKELY(of==NULL))(*_cb->close)(_source); + return of; +} + +OggOpusFile *op_open_file(const char *_path,int *_error){ + OpusFileCallbacks cb; + return op_open_close_on_failure(op_fopen(&cb,_path,"rb"),&cb,_error); +} + +OggOpusFile *op_open_memory(const unsigned char *_data,size_t _size, + int *_error){ + OpusFileCallbacks cb; + return op_open_close_on_failure(op_mem_stream_create(&cb,_data,_size),&cb, + _error); +} + +/*Convenience routine to clean up from failure for the open functions that + create their own streams.*/ +static OggOpusFile *op_test_close_on_failure(void *_source, + const OpusFileCallbacks *_cb,int *_error){ + OggOpusFile *of; + if(OP_UNLIKELY(_source==NULL)){ + if(_error!=NULL)*_error=OP_EFAULT; + return NULL; + } + of=op_test_callbacks(_source,_cb,NULL,0,_error); + if(OP_UNLIKELY(of==NULL))(*_cb->close)(_source); + return of; +} + +OggOpusFile *op_test_file(const char *_path,int *_error){ + OpusFileCallbacks cb; + return op_test_close_on_failure(op_fopen(&cb,_path,"rb"),&cb,_error); +} + +OggOpusFile *op_test_memory(const unsigned char *_data,size_t _size, + int *_error){ + OpusFileCallbacks cb; + return op_test_close_on_failure(op_mem_stream_create(&cb,_data,_size),&cb, + _error); +} + +int op_test_open(OggOpusFile *_of){ + int ret; + if(OP_UNLIKELY(_of->ready_state!=OP_PARTOPEN))return OP_EINVAL; + ret=op_open2(_of); + /*op_open2() will clear this structure on failure. + Reset its contents to prevent double-frees in op_free().*/ + if(OP_UNLIKELY(ret<0))memset(_of,0,sizeof(*_of)); + return ret; +} + +void op_free(OggOpusFile *_of){ + if(OP_LIKELY(_of!=NULL)){ + op_clear(_of); + _ogg_free(_of); + } +} + +int op_seekable(const OggOpusFile *_of){ + return _of->seekable; +} + +int op_link_count(const OggOpusFile *_of){ + return _of->nlinks; +} + +ogg_uint32_t op_serialno(const OggOpusFile *_of,int _li){ + if(OP_UNLIKELY(_li>=_of->nlinks))_li=_of->nlinks-1; + if(!_of->seekable)_li=0; + return _of->links[_li<0?_of->cur_link:_li].serialno; +} + +int op_channel_count(const OggOpusFile *_of,int _li){ + return op_head(_of,_li)->channel_count; +} + +opus_int64 op_raw_total(const OggOpusFile *_of,int _li){ + if(OP_UNLIKELY(_of->ready_stateseekable) + ||OP_UNLIKELY(_li>=_of->nlinks)){ + return OP_EINVAL; + } + if(_li<0)return _of->end-_of->links[0].offset; + return (_li+1>=_of->nlinks?_of->end:_of->links[_li+1].offset) + -_of->links[_li].offset; +} + +ogg_int64_t op_pcm_total(const OggOpusFile *_of,int _li){ + OggOpusLink *links; + ogg_int64_t diff; + int nlinks; + nlinks=_of->nlinks; + if(OP_UNLIKELY(_of->ready_stateseekable) + ||OP_UNLIKELY(_li>=nlinks)){ + return OP_EINVAL; + } + links=_of->links; + /*We verify that the granule position differences are larger than the + pre-skip and that the total duration does not overflow during link + enumeration, so we don't have to check here.*/ + if(_li<0){ + ogg_int64_t pcm_total; + int li; + pcm_total=0; + for(li=0;li=_of->nlinks))_li=_of->nlinks-1; + if(!_of->seekable)_li=0; + return &_of->links[_li<0?_of->cur_link:_li].head; +} + +const OpusTags *op_tags(const OggOpusFile *_of,int _li){ + if(OP_UNLIKELY(_li>=_of->nlinks))_li=_of->nlinks-1; + if(!_of->seekable){ + if(_of->ready_stateready_state!=OP_PARTOPEN){ + return NULL; + } + _li=0; + } + else if(_li<0)_li=_of->ready_state>=OP_STREAMSET?_of->cur_link:0; + return &_of->links[_li].tags; +} + +int op_current_link(const OggOpusFile *_of){ + if(OP_UNLIKELY(_of->ready_statecur_link; +} + +/*Compute an average bitrate given a byte and sample count. + Return: The bitrate in bits per second.*/ +static opus_int32 op_calc_bitrate(opus_int64 _bytes,ogg_int64_t _samples){ + /*These rates are absurd, but let's handle them anyway.*/ + if(OP_UNLIKELY(_bytes>(OP_INT64_MAX-(_samples>>1))/(48000*8))){ + ogg_int64_t den; + if(OP_UNLIKELY(_bytes/(OP_INT32_MAX/(48000*8))>=_samples)){ + return OP_INT32_MAX; + } + den=_samples/(48000*8); + return (opus_int32)((_bytes+(den>>1))/den); + } + if(OP_UNLIKELY(_samples<=0))return OP_INT32_MAX; + /*This can't actually overflow in normal operation: even with a pre-skip of + 545 2.5 ms frames with 8 streams running at 1282*8+1 bytes per packet + (1275 byte frames + Opus framing overhead + Ogg lacing values), that all + produce a single sample of decoded output, we still don't top 45 Mbps. + The only way to get bitrates larger than that is with excessive Opus + padding, more encoded streams than output channels, or lots and lots of + Ogg pages with no packets on them.*/ + return (opus_int32)OP_MIN((_bytes*48000*8+(_samples>>1))/_samples, + OP_INT32_MAX); +} + +opus_int32 op_bitrate(const OggOpusFile *_of,int _li){ + if(OP_UNLIKELY(_of->ready_stateseekable) + ||OP_UNLIKELY(_li>=_of->nlinks)){ + return OP_EINVAL; + } + return op_calc_bitrate(op_raw_total(_of,_li),op_pcm_total(_of,_li)); +} + +opus_int32 op_bitrate_instant(OggOpusFile *_of){ + ogg_int64_t samples_tracked; + opus_int32 ret; + if(OP_UNLIKELY(_of->ready_statesamples_tracked; + if(OP_UNLIKELY(samples_tracked==0))return OP_FALSE; + ret=op_calc_bitrate(_of->bytes_tracked,samples_tracked); + _of->bytes_tracked=0; + _of->samples_tracked=0; + return ret; +} + +/*Fetch and process a page. + This handles the case where we're at a bitstream boundary and dumps the + decoding machine. + If the decoding machine is unloaded, it loads it. + It also keeps prev_packet_gp up to date (seek and read both use this; seek + uses a special hack with _readp). + Return: <0) Error, OP_HOLE (lost packet), or OP_EOF. + 0) Need more data (only if _readp==0). + 1) Got at least one audio data packet.*/ +static int op_fetch_and_process_page(OggOpusFile *_of, + ogg_page *_og,opus_int64 _page_pos,int _readp,int _spanp,int _ignore_holes){ + OggOpusLink *links; + ogg_uint32_t cur_serialno; + int seekable; + int cur_link; + int ret; + /*We shouldn't get here if we have unprocessed packets.*/ + OP_ASSERT(_of->ready_stateop_pos>=_of->op_count); + if(!_readp)return 0; + seekable=_of->seekable; + links=_of->links; + cur_link=seekable?_of->cur_link:0; + cur_serialno=links[cur_link].serialno; + /*Handle one page.*/ + for(;;){ + ogg_page og; + OP_ASSERT(_of->ready_state>=OP_OPENED); + /*This loop is not strictly necessary, but there's no sense in doing the + extra checks of the larger loop for the common case in a multiplexed + bistream where the page is simply part of a different logical + bitstream.*/ + do{ + /*If we were given a page to use, use it.*/ + if(_og!=NULL){ + *&og=*_og; + _og=NULL; + } + /*Keep reading until we get a page with the correct serialno.*/ + else _page_pos=op_get_next_page(_of,&og,_of->end); + /*EOF: Leave uninitialized.*/ + if(_page_pos<0)return _page_posready_state>=OP_STREAMSET)){ + if(cur_serialno!=(ogg_uint32_t)ogg_page_serialno(&og)){ + /*Two possibilities: + 1) Another stream is multiplexed into this logical section, or*/ + if(OP_LIKELY(!ogg_page_bos(&og)))continue; + /* 2) Our decoding just traversed a bitstream boundary.*/ + if(!_spanp)return OP_EOF; + if(OP_LIKELY(_of->ready_state>=OP_INITSET))op_decode_clear(_of); + break; + } + } + /*Bitrate tracking: add the header's bytes here. + The body bytes are counted when we consume the packets.*/ + _of->bytes_tracked+=og.header_len; + } + while(0); + /*Do we need to load a new machine before submitting the page? + This is different in the seekable and non-seekable cases. + In the seekable case, we already have all the header information loaded + and cached. + We just initialize the machine with it and continue on our merry way. + In the non-seekable (streaming) case, we'll only be at a boundary if we + just left the previous logical bitstream, and we're now nominally at the + header of the next bitstream.*/ + if(OP_UNLIKELY(_of->ready_statenlinks; + for(li=0;li=nlinks)continue; + cur_serialno=serialno; + _of->cur_link=cur_link=li; + ogg_stream_reset_serialno(&_of->os,serialno); + _of->ready_state=OP_STREAMSET; + /*If we're at the start of this link, initialize the granule position + and pre-skip tracking.*/ + if(_page_pos<=links[cur_link].data_offset){ + _of->prev_packet_gp=links[cur_link].pcm_start; + _of->cur_discard_count=links[cur_link].head.pre_skip; + /*Ignore a hole at the start of a new link (this is common for + streams joined in the middle) or after seeking.*/ + _ignore_holes=1; + } + } + else{ + do{ + /*We're streaming. + Fetch the two header packets, build the info struct.*/ + ret=op_fetch_headers(_of,&links[0].head,&links[0].tags, + NULL,NULL,NULL,&og); + if(OP_UNLIKELY(ret<0))return ret; + /*op_find_initial_pcm_offset() will suppress any initial hole for us, + so no need to set _ignore_holes.*/ + ret=op_find_initial_pcm_offset(_of,links,&og); + if(OP_UNLIKELY(ret<0))return ret; + _of->links[0].serialno=cur_serialno=_of->os.serialno; + _of->cur_link++; + } + /*If the link was empty, keep going, because we already have the + BOS page of the next one in og.*/ + while(OP_UNLIKELY(ret>0)); + /*If we didn't get any packets out of op_find_initial_pcm_offset(), + keep going (this is possible if end-trimming trimmed them all).*/ + if(_of->op_count<=0)continue; + /*Otherwise, we're done.*/ + ret=op_make_decode_ready(_of); + if(OP_UNLIKELY(ret<0))return ret; + return 1; + } + } + /*The buffered page is the data we want, and we're ready for it. + Add it to the stream state.*/ + if(OP_UNLIKELY(_of->ready_state==OP_STREAMSET)){ + ret=op_make_decode_ready(_of); + if(OP_UNLIKELY(ret<0))return ret; + } + /*Extract all the packets from the current page.*/ + ogg_stream_pagein(&_of->os,&og); + if(OP_LIKELY(_of->ready_state>=OP_INITSET)){ + opus_int32 total_duration; + int durations[255]; + int op_count; + total_duration=op_collect_audio_packets(_of,durations); + if(OP_UNLIKELY(total_duration<0)){ + /*Drain the packets from the page anyway.*/ + total_duration=op_collect_audio_packets(_of,durations); + OP_ASSERT(total_duration>=0); + /*Report holes to the caller.*/ + if(!_ignore_holes)return OP_HOLE; + } + op_count=_of->op_count; + /*If we found at least one audio data packet, compute per-packet granule + positions for them.*/ + if(op_count>0){ + ogg_int64_t diff; + ogg_int64_t prev_packet_gp; + ogg_int64_t cur_packet_gp; + ogg_int64_t cur_page_gp; + int cur_page_eos; + int pi; + cur_page_gp=_of->op[op_count-1].granulepos; + cur_page_eos=_of->op[op_count-1].e_o_s; + prev_packet_gp=_of->prev_packet_gp; + if(OP_UNLIKELY(prev_packet_gp==-1)){ + opus_int32 cur_discard_count; + /*This is the first call after a raw seek. + Try to reconstruct prev_packet_gp from scratch.*/ + OP_ASSERT(seekable); + if(OP_UNLIKELY(cur_page_eos)){ + /*If the first page we hit after our seek was the EOS page, and + we didn't start from data_offset or before, we don't have + enough information to do end-trimming. + Proceed to the next link, rather than risk playing back some + samples that shouldn't have been played.*/ + _of->op_count=0; + continue; + } + /*By default discard 80 ms of data after a seek, unless we seek + into the pre-skip region.*/ + cur_discard_count=80*48; + cur_page_gp=_of->op[op_count-1].granulepos; + /*Try to initialize prev_packet_gp. + If the current page had packets but didn't have a granule + position, or the granule position it had was too small (both + illegal), just use the starting granule position for the link.*/ + prev_packet_gp=links[cur_link].pcm_start; + if(OP_LIKELY(cur_page_gp!=-1)){ + op_granpos_add(&prev_packet_gp,cur_page_gp,-total_duration); + } + if(OP_LIKELY(!op_granpos_diff(&diff, + prev_packet_gp,links[cur_link].pcm_start))){ + opus_int32 pre_skip; + /*If we start at the beginning of the pre-skip region, or we're + at least 80 ms from the end of the pre-skip region, we discard + to the end of the pre-skip region. + Otherwise, we still use the 80 ms default, which will discard + past the end of the pre-skip region.*/ + pre_skip=links[cur_link].head.pre_skip; + if(diff>=0&&diff<=OP_MAX(0,pre_skip-80*48)){ + cur_discard_count=pre_skip-(int)diff; + } + } + _of->cur_discard_count=cur_discard_count; + } + if(OP_UNLIKELY(cur_page_gp==-1)){ + /*This page had completed packets but didn't have a valid granule + position. + This is illegal, but we'll try to handle it by continuing to count + forwards from the previous page.*/ + if(op_granpos_add(&cur_page_gp,prev_packet_gp,total_duration)<0){ + /*The timestamp for this page overflowed.*/ + cur_page_gp=links[cur_link].pcm_end; + } + } + /*If we hit the last page, handle end-trimming.*/ + if(OP_UNLIKELY(cur_page_eos) + &&OP_LIKELY(!op_granpos_diff(&diff,cur_page_gp,prev_packet_gp)) + &&OP_LIKELY(diff0){ + /*If we trimmed the entire packet, stop (the spec says encoders + shouldn't do this, but we support it anyway).*/ + if(OP_UNLIKELY(diff>durations[pi]))break; + cur_packet_gp=cur_page_gp; + /*Move the EOS flag to this packet, if necessary, so we'll trim + the samples during decode.*/ + _of->op[pi].e_o_s=1; + } + else{ + /*Update the granule position as normal.*/ + OP_ALWAYS_TRUE(!op_granpos_add(&cur_packet_gp, + cur_packet_gp,durations[pi])); + } + _of->op[pi].granulepos=cur_packet_gp; + OP_ALWAYS_TRUE(!op_granpos_diff(&diff,cur_page_gp,cur_packet_gp)); + } + } + else{ + /*Propagate timestamps to earlier packets. + op_granpos_add(&prev_packet_gp,prev_packet_gp,total_duration) + should succeed and give prev_packet_gp==cur_page_gp. + But we don't bother to check that, as there isn't much we can do + if it's not true. + The only thing we guarantee is that the start and end granule + positions of the packets are valid, and that they are monotonic + within a page. + They might be completely out of range for this link (we'll check + that elsewhere), or non-monotonic between pages.*/ + if(OP_UNLIKELY(op_granpos_add(&prev_packet_gp, + cur_page_gp,-total_duration)<0)){ + /*The starting timestamp for the first packet on this page + underflowed. + This is illegal, but we ignore it.*/ + prev_packet_gp=0; + } + for(pi=0;pi=0); + OP_ALWAYS_TRUE(!op_granpos_add(&cur_packet_gp, + cur_packet_gp,durations[pi])); + _of->op[pi].granulepos=cur_packet_gp; + } + OP_ASSERT(total_duration==0); + } + _of->prev_packet_gp=prev_packet_gp; + _of->op_count=pi; + /*If end-trimming didn't trim all the packets, we're done.*/ + if(OP_LIKELY(pi>0))return 1; + } + } + } +} + +int op_raw_seek(OggOpusFile *_of,opus_int64 _pos){ + int ret; + if(OP_UNLIKELY(_of->ready_stateseekable))return OP_ENOSEEK; + if(OP_UNLIKELY(_pos<0)||OP_UNLIKELY(_pos>_of->end))return OP_EINVAL; + /*Clear out any buffered, decoded data.*/ + op_decode_clear(_of); + _of->bytes_tracked=0; + _of->samples_tracked=0; + ret=op_seek_helper(_of,_pos); + if(OP_UNLIKELY(ret<0))return OP_EREAD; + ret=op_fetch_and_process_page(_of,NULL,-1,1,1,1); + /*If we hit EOF, op_fetch_and_process_page() leaves us uninitialized. + Instead, jump to the end.*/ + if(ret==OP_EOF){ + int cur_link; + op_decode_clear(_of); + cur_link=_of->nlinks-1; + _of->cur_link=cur_link; + _of->prev_packet_gp=_of->links[cur_link].pcm_end; + _of->cur_discard_count=0; + ret=0; + } + else if(ret>0)ret=0; + return ret; +} + +/*Convert a PCM offset relative to the start of the whole stream to a granule + position in an individual link.*/ +static ogg_int64_t op_get_granulepos(const OggOpusFile *_of, + ogg_int64_t _pcm_offset,int *_li){ + const OggOpusLink *links; + ogg_int64_t duration; + int nlinks; + int li; + OP_ASSERT(_pcm_offset>=0); + nlinks=_of->nlinks; + links=_of->links; + for(li=0;OP_LIKELY(liOP_INT64_MAX-_pcm_offset)){ + /*Adding this amount to the granule position would overflow the positive + half of its 64-bit range. + Since signed overflow is undefined in C, do it in a way the compiler + isn't allowed to screw up.*/ + _pcm_offset-=OP_INT64_MAX-pcm_start+1; + pcm_start=OP_INT64_MIN; + } + pcm_start+=_pcm_offset; + *_li=li; + return pcm_start; + } + _pcm_offset-=duration; + } + return -1; +} + +/*This controls how close the target has to be to use the current stream + position to subdivide the initial range. + Two minutes seems to be a good default.*/ +#define OP_CUR_TIME_THRESH (120*48*(opus_int32)1000) + +/*Note: The OP_SMALL_FOOTPRINT #define doesn't (currently) save much code size, + but it's meant to serve as documentation for portions of the seeking + algorithm that are purely optional, to aid others learning from/porting this + code to other contexts.*/ +/*#define OP_SMALL_FOOTPRINT (1)*/ + +/*Search within link _li for the page with the highest granule position + preceding (or equal to) _target_gp. + There is a danger here: missing pages or incorrect frame number information + in the bitstream could make our task impossible. + Account for that (and report it as an error condition).*/ +static int op_pcm_seek_page(OggOpusFile *_of, + ogg_int64_t _target_gp,int _li){ + const OggOpusLink *link; + ogg_page og; + ogg_int64_t pcm_pre_skip; + ogg_int64_t pcm_start; + ogg_int64_t pcm_end; + ogg_int64_t best_gp; + ogg_int64_t diff; + ogg_uint32_t serialno; + opus_int32 pre_skip; + opus_int64 begin; + opus_int64 end; + opus_int64 boundary; + opus_int64 best; + opus_int64 page_offset; + opus_int64 d0; + opus_int64 d1; + opus_int64 d2; + int force_bisect; + int ret; + _of->bytes_tracked=0; + _of->samples_tracked=0; + link=_of->links+_li; + best_gp=pcm_start=link->pcm_start; + pcm_end=link->pcm_end; + serialno=link->serialno; + best=begin=link->data_offset; + page_offset=-1; + /*We discard the first 80 ms of data after a seek, so seek back that much + farther. + If we can't, simply seek to the beginning of the link.*/ + if(OP_UNLIKELY(op_granpos_add(&_target_gp,_target_gp,-80*48)<0) + ||OP_UNLIKELY(op_granpos_cmp(_target_gp,pcm_start)<0)){ + _target_gp=pcm_start; + } + /*Special case seeking to the start of the link.*/ + pre_skip=link->head.pre_skip; + OP_ALWAYS_TRUE(!op_granpos_add(&pcm_pre_skip,pcm_start,pre_skip)); + if(op_granpos_cmp(_target_gp,pcm_pre_skip)<0)end=boundary=begin; + else{ + end=boundary=link->end_offset; +#if !defined(OP_SMALL_FOOTPRINT) + /*If we were decoding from this link, we can narrow the range a bit.*/ + if(_li==_of->cur_link&&_of->ready_state>=OP_INITSET){ + opus_int64 offset; + int op_count; + op_count=_of->op_count; + /*The only way the offset can be invalid _and_ we can fail the granule + position checks below is if someone changed the contents of the last + page since we read it. + We'd be within our rights to just return OP_EBADLINK in that case, but + we'll simply ignore the current position instead.*/ + offset=_of->offset; + if(op_count>0&&OP_LIKELY(offset<=end)){ + ogg_int64_t gp; + /*Make sure the timestamp is valid. + The granule position might be -1 if we collected the packets from a + page without a granule position after reporting a hole.*/ + gp=_of->op[op_count-1].granulepos; + if(OP_LIKELY(gp!=-1)&&OP_LIKELY(op_granpos_cmp(pcm_start,gp)<0) + &&OP_LIKELY(op_granpos_cmp(pcm_end,gp)>0)){ + OP_ALWAYS_TRUE(!op_granpos_diff(&diff,gp,_target_gp)); + /*We only actually use the current time if either + a) We can cut off at least half the range, or + b) We're seeking sufficiently close to the current position that + it's likely to be informative. + Otherwise it appears using the whole link range to estimate the + first seek location gives better results, on average.*/ + if(diff<0){ + OP_ASSERT(offset>=begin); + if(offset-begin>=end-begin>>1||diff>-OP_CUR_TIME_THRESH){ + best=begin=offset; + best_gp=pcm_start=gp; + } + } + else{ + ogg_int64_t prev_page_gp; + /*We might get lucky and already have the packet with the target + buffered. + Worth checking. + For very small files (with all of the data in a single page, + generally 1 second or less), we can loop them continuously + without seeking at all.*/ + OP_ALWAYS_TRUE(!op_granpos_add(&prev_page_gp,_of->op[0].granulepos, + op_get_packet_duration(_of->op[0].packet,_of->op[0].bytes))); + if(op_granpos_cmp(prev_page_gp,_target_gp)<=0){ + /*Don't call op_decode_clear(), because it will dump our + packets.*/ + _of->op_pos=0; + _of->od_buffer_size=0; + _of->prev_packet_gp=prev_page_gp; + _of->ready_state=OP_STREAMSET; + return op_make_decode_ready(_of); + } + /*No such luck. + Check if we can cut off at least half the range, though.*/ + if(offset-begin<=end-begin>>1||diff>1; + d1=d2>>1; + d2=end-begin>>1; + if(force_bisect)bisect=begin+(end-begin>>1); + else{ + ogg_int64_t diff2; + OP_ALWAYS_TRUE(!op_granpos_diff(&diff,_target_gp,pcm_start)); + OP_ALWAYS_TRUE(!op_granpos_diff(&diff2,pcm_end,pcm_start)); + /*Take a (pretty decent) guess.*/ + bisect=begin+op_rescale64(diff,diff2,end-begin)-OP_CHUNK_SIZE; + } + if(bisect-OP_CHUNK_SIZEoffset){ + page_offset=-1; + ret=op_seek_helper(_of,bisect); + if(OP_UNLIKELY(ret<0))return ret; + } + chunk_size=OP_CHUNK_SIZE; + next_boundary=boundary; + while(beginoffset; + if(OP_UNLIKELY(op_granpos_cmp(pcm_start,gp)>0) + ||OP_UNLIKELY(op_granpos_cmp(pcm_end,gp)<0)){ + /*Don't let pcm_start get out of range! + That could happen with an invalid timestamp.*/ + break; + } + /*Save the byte offset of the end of the page with this granule + position.*/ + best=begin; + best_gp=pcm_start=gp; + OP_ALWAYS_TRUE(!op_granpos_diff(&diff,_target_gp,pcm_start)); + /*If we're more than a second away from our target, break out and + do another bisection.*/ + if(diff>48000)break; + /*Otherwise, keep scanning forward (do NOT use begin+1).*/ + bisect=begin; + } + else{ + /*We found a page that ends after our target.*/ + /*If we scanned the whole interval before we found it, we're done.*/ + if(bisect<=begin+1)end=begin; + else{ + end=bisect; + /*In later iterations, don't read past the first page we found.*/ + boundary=next_boundary; + /*If we're not making much progress shrinking the interval size, + start forcing straight bisection to limit the worst case.*/ + force_bisect=end-begin>d0*2; + /*Don't let pcm_end get out of range! + That could happen with an invalid timestamp.*/ + if(OP_LIKELY(op_granpos_cmp(pcm_end,gp)>0) + &&OP_LIKELY(op_granpos_cmp(pcm_start,gp)<=0)){ + pcm_end=gp; + } + break; + } + } + } + } + } + /*Found our page. + Seek to the end of it and update prev_packet_gp. + Our caller will set cur_discard_count. + This is an easier case than op_raw_seek(), as we don't need to keep any + packets from the page we found.*/ + /*Seek, if necessary.*/ + if(best!=page_offset){ + page_offset=-1; + ret=op_seek_helper(_of,best); + if(OP_UNLIKELY(ret<0))return ret; + } + OP_ASSERT(op_granpos_cmp(best_gp,pcm_start)>=0); + _of->cur_link=_li; + _of->ready_state=OP_STREAMSET; + _of->prev_packet_gp=best_gp; + ogg_stream_reset_serialno(&_of->os,serialno); + ret=op_fetch_and_process_page(_of,page_offset<0?NULL:&og,page_offset,1,0,1); + if(OP_UNLIKELY(ret<=0))return OP_EBADLINK; + /*Verify result.*/ + if(OP_UNLIKELY(op_granpos_cmp(_of->prev_packet_gp,_target_gp)>0)){ + return OP_EBADLINK; + } + return 0; +} + +int op_pcm_seek(OggOpusFile *_of,ogg_int64_t _pcm_offset){ + const OggOpusLink *link; + ogg_int64_t pcm_start; + ogg_int64_t target_gp; + ogg_int64_t prev_packet_gp; + ogg_int64_t skip; + ogg_int64_t diff; + int op_count; + int op_pos; + int ret; + int li; + if(OP_UNLIKELY(_of->ready_stateseekable))return OP_ENOSEEK; + if(OP_UNLIKELY(_pcm_offset<0))return OP_EINVAL; + target_gp=op_get_granulepos(_of,_pcm_offset,&li); + if(OP_UNLIKELY(target_gp==-1))return OP_EINVAL; + link=_of->links+li; + pcm_start=link->pcm_start; + OP_ALWAYS_TRUE(!op_granpos_diff(&_pcm_offset,target_gp,pcm_start)); +#if !defined(OP_SMALL_FOOTPRINT) + /*For small (90 ms or less) forward seeks within the same link, just decode + forward. + This also optimizes the case of seeking to the current position.*/ + if(li==_of->cur_link&&_of->ready_state>=OP_INITSET){ + ogg_int64_t gp; + gp=_of->prev_packet_gp; + if(OP_LIKELY(gp!=-1)){ + int nbuffered; + nbuffered=OP_MAX(_of->od_buffer_size-_of->od_buffer_pos,0); + OP_ALWAYS_TRUE(!op_granpos_add(&gp,gp,-nbuffered)); + /*We do _not_ add cur_discard_count to gp. + Otherwise the total amount to discard could grow without bound, and it + would be better just to do a full seek.*/ + if(OP_LIKELY(!op_granpos_diff(&diff,gp,pcm_start))){ + ogg_int64_t discard_count; + discard_count=_pcm_offset-diff; + /*We use a threshold of 90 ms instead of 80, since 80 ms is the + _minimum_ we would have discarded after a full seek. + Assuming 20 ms frames (the default), we'd discard 90 ms on average.*/ + if(discard_count>=0&&OP_UNLIKELY(discard_count<90*48)){ + _of->cur_discard_count=(opus_int32)discard_count; + return 0; + } + } + } + } +#endif + ret=op_pcm_seek_page(_of,target_gp,li); + if(OP_UNLIKELY(ret<0))return ret; + /*Now skip samples until we actually get to our target.*/ + /*Figure out where we should skip to.*/ + if(_pcm_offset<=link->head.pre_skip)skip=0; + else skip=OP_MAX(_pcm_offset-80*48,0); + OP_ASSERT(_pcm_offset-skip>=0); + OP_ASSERT(_pcm_offset-skipop_count; + prev_packet_gp=_of->prev_packet_gp; + for(op_pos=_of->op_pos;op_posop[op_pos].granulepos; + if(OP_LIKELY(!op_granpos_diff(&diff,cur_packet_gp,pcm_start)) + &&diff>skip){ + break; + } + prev_packet_gp=cur_packet_gp; + } + _of->prev_packet_gp=prev_packet_gp; + _of->op_pos=op_pos; + if(op_posskip)return OP_EBADLINK; + OP_ASSERT(_pcm_offset-diffcur_discard_count=(opus_int32)(_pcm_offset-diff); + return 0; +} + +opus_int64 op_raw_tell(const OggOpusFile *_of){ + if(OP_UNLIKELY(_of->ready_stateoffset; +} + +/*Convert a granule position from a given link to a PCM offset relative to the + start of the whole stream. + For unseekable sources, this gets reset to 0 at the beginning of each link.*/ +static ogg_int64_t op_get_pcm_offset(const OggOpusFile *_of, + ogg_int64_t _gp,int _li){ + const OggOpusLink *links; + ogg_int64_t pcm_offset; + ogg_int64_t delta; + int li; + links=_of->links; + pcm_offset=0; + OP_ASSERT(_li<_of->nlinks); + for(li=0;li<_li;li++){ + OP_ALWAYS_TRUE(!op_granpos_diff(&delta, + links[li].pcm_end,links[li].pcm_start)); + delta-=links[li].head.pre_skip; + pcm_offset+=delta; + } + OP_ASSERT(_li>=0); + if(_of->seekable&&OP_UNLIKELY(op_granpos_cmp(_gp,links[_li].pcm_end)>0)){ + _gp=links[_li].pcm_end; + } + if(OP_LIKELY(op_granpos_cmp(_gp,links[_li].pcm_start)>0)){ + if(OP_UNLIKELY(op_granpos_diff(&delta,_gp,links[_li].pcm_start)<0)){ + /*This means an unseekable stream claimed to have a page from more than + 2 billion days after we joined.*/ + OP_ASSERT(!_of->seekable); + return OP_INT64_MAX; + } + if(deltaready_stateprev_packet_gp; + if(gp==-1)return 0; + nbuffered=OP_MAX(_of->od_buffer_size-_of->od_buffer_pos,0); + OP_ALWAYS_TRUE(!op_granpos_add(&gp,gp,-nbuffered)); + li=_of->seekable?_of->cur_link:0; + if(op_granpos_add(&gp,gp,_of->cur_discard_count)<0){ + gp=_of->links[li].pcm_end; + } + return op_get_pcm_offset(_of,gp,li); +} + +void op_set_decode_callback(OggOpusFile *_of, + op_decode_cb_func _decode_cb,void *_ctx){ + _of->decode_cb=_decode_cb; + _of->decode_cb_ctx=_ctx; +} + +int op_set_gain_offset(OggOpusFile *_of, + int _gain_type,opus_int32 _gain_offset_q8){ + if(_gain_type!=OP_HEADER_GAIN&&_gain_type!=OP_TRACK_GAIN + &&_gain_type!=OP_ABSOLUTE_GAIN){ + return OP_EINVAL; + } + _of->gain_type=_gain_type; + /*The sum of header gain and track gain lies in the range [-65536,65534]. + These bounds allow the offset to set the final value to anywhere in the + range [-32768,32767], which is what we'll clamp it to before applying.*/ + _of->gain_offset_q8=OP_CLAMP(-98302,_gain_offset_q8,98303); + op_update_gain(_of); + return 0; +} + +void op_set_dither_enabled(OggOpusFile *_of,int _enabled){ +#if !defined(OP_FIXED_POINT) + _of->dither_disabled=!_enabled; + if(!_enabled)_of->dither_mute=65; +#endif +} + +/*Allocate the decoder scratch buffer. + This is done lazily, since if the user provides large enough buffers, we'll + never need it.*/ +static int op_init_buffer(OggOpusFile *_of){ + int nchannels_max; + if(_of->seekable){ + const OggOpusLink *links; + int nlinks; + int li; + links=_of->links; + nlinks=_of->nlinks; + nchannels_max=1; + for(li=0;liod_buffer=(op_sample *)_ogg_malloc( + sizeof(*_of->od_buffer)*nchannels_max*120*48); + if(_of->od_buffer==NULL)return OP_EFAULT; + return 0; +} + +/*Decode a single packet into the target buffer.*/ +static int op_decode(OggOpusFile *_of,op_sample *_pcm, + const ogg_packet *_op,int _nsamples,int _nchannels){ + int ret; + /*First we try using the application-provided decode callback.*/ + if(_of->decode_cb!=NULL){ +#if defined(OP_FIXED_POINT) + ret=(*_of->decode_cb)(_of->decode_cb_ctx,_of->od,_pcm,_op, + _nsamples,_nchannels,OP_DEC_FORMAT_SHORT,_of->cur_link); +#else + ret=(*_of->decode_cb)(_of->decode_cb_ctx,_of->od,_pcm,_op, + _nsamples,_nchannels,OP_DEC_FORMAT_FLOAT,_of->cur_link); +#endif + } + else ret=OP_DEC_USE_DEFAULT; + /*If the application didn't want to handle decoding, do it ourselves.*/ + if(ret==OP_DEC_USE_DEFAULT){ +#if defined(OP_FIXED_POINT) + ret=opus_multistream_decode(_of->od, + _op->packet,_op->bytes,_pcm,_nsamples,0); +#else + ret=opus_multistream_decode_float(_of->od, + _op->packet,_op->bytes,_pcm,_nsamples,0); +#endif + OP_ASSERT(ret<0||ret==_nsamples); + } + /*If the application returned a positive value other than 0 or + OP_DEC_USE_DEFAULT, fail.*/ + else if(OP_UNLIKELY(ret>0))return OP_EBADPACKET; + if(OP_UNLIKELY(ret<0))return OP_EBADPACKET; + return ret; +} + +/*Read more samples from the stream, using the same API as op_read() or + op_read_float().*/ +static int op_read_native(OggOpusFile *_of, + op_sample *_pcm,int _buf_size,int *_li){ + if(OP_UNLIKELY(_of->ready_stateready_state>=OP_INITSET)){ + int nchannels; + int od_buffer_pos; + int nsamples; + int op_pos; + nchannels=_of->links[_of->seekable?_of->cur_link:0].head.channel_count; + od_buffer_pos=_of->od_buffer_pos; + nsamples=_of->od_buffer_size-od_buffer_pos; + /*If we have buffered samples, return them.*/ + if(nsamples>0){ + if(nsamples*nchannels>_buf_size)nsamples=_buf_size/nchannels; + memcpy(_pcm,_of->od_buffer+nchannels*od_buffer_pos, + sizeof(*_pcm)*nchannels*nsamples); + od_buffer_pos+=nsamples; + _of->od_buffer_pos=od_buffer_pos; + if(_li!=NULL)*_li=_of->cur_link; + return nsamples; + } + /*If we have buffered packets, decode one.*/ + op_pos=_of->op_pos; + if(OP_LIKELY(op_pos<_of->op_count)){ + const ogg_packet *pop; + ogg_int64_t diff; + opus_int32 cur_discard_count; + int duration; + int trimmed_duration; + pop=_of->op+op_pos++; + _of->op_pos=op_pos; + cur_discard_count=_of->cur_discard_count; + duration=op_get_packet_duration(pop->packet,pop->bytes); + /*We don't buffer packets with an invalid TOC sequence.*/ + OP_ASSERT(duration>0); + trimmed_duration=duration; + /*Perform end-trimming.*/ + if(OP_UNLIKELY(pop->e_o_s)){ + if(OP_UNLIKELY(op_granpos_cmp(pop->granulepos, + _of->prev_packet_gp)<=0)){ + trimmed_duration=0; + } + else if(OP_LIKELY(!op_granpos_diff(&diff, + pop->granulepos,_of->prev_packet_gp))){ + trimmed_duration=(int)OP_MIN(diff,trimmed_duration); + } + } + _of->prev_packet_gp=pop->granulepos; + if(OP_UNLIKELY(duration*nchannels>_buf_size)){ + op_sample *buf; + /*If the user's buffer is too small, decode into a scratch buffer.*/ + buf=_of->od_buffer; + if(OP_UNLIKELY(buf==NULL)){ + ret=op_init_buffer(_of); + if(OP_UNLIKELY(ret<0))return ret; + buf=_of->od_buffer; + } + ret=op_decode(_of,buf,pop,duration,nchannels); + if(OP_UNLIKELY(ret<0))return ret; + /*Perform pre-skip/pre-roll.*/ + od_buffer_pos=(int)OP_MIN(trimmed_duration,cur_discard_count); + cur_discard_count-=od_buffer_pos; + _of->cur_discard_count=cur_discard_count; + _of->od_buffer_pos=od_buffer_pos; + _of->od_buffer_size=trimmed_duration; + /*Update bitrate tracking based on the actual samples we used from + what was decoded.*/ + _of->bytes_tracked+=pop->bytes; + _of->samples_tracked+=trimmed_duration-od_buffer_pos; + } + else{ + /*Otherwise decode directly into the user's buffer.*/ + ret=op_decode(_of,_pcm,pop,duration,nchannels); + if(OP_UNLIKELY(ret<0))return ret; + if(OP_LIKELY(trimmed_duration>0)){ + /*Perform pre-skip/pre-roll.*/ + od_buffer_pos=(int)OP_MIN(trimmed_duration,cur_discard_count); + cur_discard_count-=od_buffer_pos; + _of->cur_discard_count=cur_discard_count; + trimmed_duration-=od_buffer_pos; + if(OP_LIKELY(trimmed_duration>0) + &&OP_UNLIKELY(od_buffer_pos>0)){ + memmove(_pcm,_pcm+od_buffer_pos*nchannels, + sizeof(*_pcm)*trimmed_duration*nchannels); + } + /*Update bitrate tracking based on the actual samples we used from + what was decoded.*/ + _of->bytes_tracked+=pop->bytes; + _of->samples_tracked+=trimmed_duration; + if(OP_LIKELY(trimmed_duration>0)){ + if(_li!=NULL)*_li=_of->cur_link; + return trimmed_duration; + } + } + } + /*Don't grab another page yet. + This one might have more packets, or might have buffered data now.*/ + continue; + } + } + /*Suck in another page.*/ + ret=op_fetch_and_process_page(_of,NULL,-1,1,1,0); + if(OP_UNLIKELY(ret==OP_EOF)){ + if(_li!=NULL)*_li=_of->cur_link; + return 0; + } + if(OP_UNLIKELY(ret<0))return ret; + } +} + +/*A generic filter to apply to the decoded audio data. + _src is non-const because we will destructively modify the contents of the + source buffer that we consume in some cases.*/ +typedef int (*op_read_filter_func)(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels); + +/*Decode some samples and then apply a custom filter to them. + This is used to convert to different output formats.*/ +static int op_filter_read_native(OggOpusFile *_of,void *_dst,int _dst_sz, + op_read_filter_func _filter,int *_li){ + int ret; + /*Ensure we have some decoded samples in our buffer.*/ + ret=op_read_native(_of,NULL,0,_li); + /*Now apply the filter to them.*/ + if(OP_LIKELY(ret>=0)&&OP_LIKELY(_of->ready_state>=OP_INITSET)){ + int od_buffer_pos; + od_buffer_pos=_of->od_buffer_pos; + ret=_of->od_buffer_size-od_buffer_pos; + if(OP_LIKELY(ret>0)){ + int nchannels; + nchannels=_of->links[_of->seekable?_of->cur_link:0].head.channel_count; + ret=(*_filter)(_of,_dst,_dst_sz, + _of->od_buffer+nchannels*od_buffer_pos,ret,nchannels); + OP_ASSERT(ret>=0); + OP_ASSERT(ret<=_of->od_buffer_size-od_buffer_pos); + od_buffer_pos+=ret; + _of->od_buffer_pos=od_buffer_pos; + } + } + return ret; +} + +#if !defined(OP_FIXED_POINT)||!defined(OP_DISABLE_FLOAT_API) + +/*Matrices for downmixing from the supported channel counts to stereo. + The matrices with 5 or more channels are normalized to a total volume of 2.0, + since most mixes sound too quiet if normalized to 1.0 (as there is generally + little volume in the side/rear channels).*/ +static const float OP_STEREO_DOWNMIX[OP_NCHANNELS_MAX-2][OP_NCHANNELS_MAX][2]={ + /*3.0*/ + { + {0.5858F,0.0F},{0.4142F,0.4142F},{0.0F,0.5858F} + }, + /*quadrophonic*/ + { + {0.4226F,0.0F},{0.0F,0.4226F},{0.366F,0.2114F},{0.2114F,0.336F} + }, + /*5.0*/ + { + {0.651F,0.0F},{0.46F,0.46F},{0.0F,0.651F},{0.5636F,0.3254F}, + {0.3254F,0.5636F} + }, + /*5.1*/ + { + {0.529F,0.0F},{0.3741F,0.3741F},{0.0F,0.529F},{0.4582F,0.2645F}, + {0.2645F,0.4582F},{0.3741F,0.3741F} + }, + /*6.1*/ + { + {0.4553F,0.0F},{0.322F,0.322F},{0.0F,0.4553F},{0.3943F,0.2277F}, + {0.2277F,0.3943F},{0.2788F,0.2788F},{0.322F,0.322F} + }, + /*7.1*/ + { + {0.3886F,0.0F},{0.2748F,0.2748F},{0.0F,0.3886F},{0.3366F,0.1943F}, + {0.1943F,0.3366F},{0.3366F,0.1943F},{0.1943F,0.3366F},{0.2748F,0.2748F} + } +}; + +#endif + +#if defined(OP_FIXED_POINT) + +/*Matrices for downmixing from the supported channel counts to stereo. + The matrices with 5 or more channels are normalized to a total volume of 2.0, + since most mixes sound too quiet if normalized to 1.0 (as there is generally + little volume in the side/rear channels). + Hence we keep the coefficients in Q14, so the downmix values won't overflow a + 32-bit number.*/ +static const opus_int16 OP_STEREO_DOWNMIX_Q14 + [OP_NCHANNELS_MAX-2][OP_NCHANNELS_MAX][2]={ + /*3.0*/ + { + {9598,0},{6786,6786},{0,9598} + }, + /*quadrophonic*/ + { + {6924,0},{0,6924},{5996,3464},{3464,5996} + }, + /*5.0*/ + { + {10666,0},{7537,7537},{0,10666},{9234,5331},{5331,9234} + }, + /*5.1*/ + { + {8668,0},{6129,6129},{0,8668},{7507,4335},{4335,7507},{6129,6129} + }, + /*6.1*/ + { + {7459,0},{5275,5275},{0,7459},{6460,3731},{3731,6460},{4568,4568}, + {5275,5275} + }, + /*7.1*/ + { + {6368,0},{4502,4502},{0,6368},{5515,3183},{3183,5515},{5515,3183}, + {3183,5515},{4502,4502} + } +}; + +int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){ + return op_read_native(_of,_pcm,_buf_size,_li); +} + +static int op_stereo_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels){ + (void)_of; + _nsamples=OP_MIN(_nsamples,_dst_sz>>1); + if(_nchannels==2)memcpy(_dst,_src,_nsamples*2*sizeof(*_src)); + else{ + opus_int16 *dst; + int i; + dst=(opus_int16 *)_dst; + if(_nchannels==1){ + for(i=0;i<_nsamples;i++)dst[2*i+0]=dst[2*i+1]=_src[i]; + } + else{ + for(i=0;i<_nsamples;i++){ + opus_int32 l; + opus_int32 r; + int ci; + l=r=0; + for(ci=0;ci<_nchannels;ci++){ + opus_int32 s; + s=_src[_nchannels*i+ci]; + l+=OP_STEREO_DOWNMIX_Q14[_nchannels-3][ci][0]*s; + r+=OP_STEREO_DOWNMIX_Q14[_nchannels-3][ci][1]*s; + } + /*TODO: For 5 or more channels, we should do soft clipping here.*/ + dst[2*i+0]=(opus_int16)OP_CLAMP(-32768,l+8192>>14,32767); + dst[2*i+1]=(opus_int16)OP_CLAMP(-32768,r+8192>>14,32767); + } + } + } + return _nsamples; +} + +int op_read_stereo(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size){ + return op_filter_read_native(_of,_pcm,_buf_size,op_stereo_filter,NULL); +} + +# if !defined(OP_DISABLE_FLOAT_API) + +static int op_short2float_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels){ + float *dst; + int i; + (void)_of; + dst=(float *)_dst; + if(OP_UNLIKELY(_nsamples*_nchannels>_dst_sz))_nsamples=_dst_sz/_nchannels; + _dst_sz=_nsamples*_nchannels; + for(i=0;i<_dst_sz;i++)dst[i]=(1.0F/32768)*_src[i]; + return _nsamples; +} + +int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li){ + return op_filter_read_native(_of,_pcm,_buf_size,op_short2float_filter,_li); +} + +static int op_short2float_stereo_filter(OggOpusFile *_of, + void *_dst,int _dst_sz,op_sample *_src,int _nsamples,int _nchannels){ + float *dst; + int i; + dst=(float *)_dst; + _nsamples=OP_MIN(_nsamples,_dst_sz>>1); + if(_nchannels==1){ + _nsamples=op_short2float_filter(_of,dst,_nsamples,_src,_nsamples,1); + for(i=_nsamples;i-->0;)dst[2*i+0]=dst[2*i+1]=dst[i]; + } + else if(_nchannels<5){ + /*For 3 or 4 channels, we can downmix in fixed point without risk of + clipping.*/ + if(_nchannels>2){ + _nsamples=op_stereo_filter(_of,_src,_nsamples*2, + _src,_nsamples,_nchannels); + } + return op_short2float_filter(_of,dst,_dst_sz,_src,_nsamples,2); + } + else{ + /*For 5 or more channels, we convert to floats and then downmix (so that we + don't risk clipping).*/ + for(i=0;i<_nsamples;i++){ + float l; + float r; + int ci; + l=r=0; + for(ci=0;ci<_nchannels;ci++){ + float s; + s=(1.0F/32768)*_src[_nchannels*i+ci]; + l+=OP_STEREO_DOWNMIX[_nchannels-3][ci][0]*s; + r+=OP_STEREO_DOWNMIX[_nchannels-3][ci][1]*s; + } + dst[2*i+0]=l; + dst[2*i+1]=r; + } + } + return _nsamples; +} + +int op_read_float_stereo(OggOpusFile *_of,float *_pcm,int _buf_size){ + return op_filter_read_native(_of,_pcm,_buf_size, + op_short2float_stereo_filter,NULL); +} + +# endif + +#else + +# if defined(OP_HAVE_LRINTF) +# include +# define op_float2int(_x) (lrintf(_x)) +# else +# define op_float2int(_x) ((int)((_x)+((_x)<0?-0.5F:0.5F))) +# endif + +/*The dithering code here is adapted from opusdec, part of opus-tools. + It was originally written by Greg Maxwell.*/ + +static opus_uint32 op_rand(opus_uint32 _seed){ + return _seed*96314165+907633515&0xFFFFFFFFU; +} + +/*This implements 16-bit quantization with full triangular dither and IIR noise + shaping. + The noise shaping filters were designed by Sebastian Gesemann, and are based + on the LAME ATH curves with flattening to limit their peak gain to 20 dB. + Everyone else's noise shaping filters are mildly crazy. + The 48 kHz version of this filter is just a warped version of the 44.1 kHz + filter and probably could be improved by shifting the HF shelf up in + frequency a little bit, since 48 kHz has a bit more room and being more + conservative against bat-ears is probably more important than more noise + suppression. + This process can increase the peak level of the signal (in theory by the peak + error of 1.5 +20 dB, though that is unobservably rare). + To avoid clipping, the signal is attenuated by a couple thousandths of a dB. + Initially, the approach taken here was to only attenuate by the 99.9th + percentile, making clipping rare but not impossible (like SoX), but the + limited gain of the filter means that the worst case was only two + thousandths of a dB more, so this just uses the worst case. + The attenuation is probably also helpful to prevent clipping in the DAC + reconstruction filters or downstream resampling, in any case.*/ + +# define OP_GAIN (32753.0F) + +# define OP_PRNG_GAIN (1.0F/0xFFFFFFFF) + +/*48 kHz noise shaping filter, sd=2.34.*/ + +static const float OP_FCOEF_B[4]={ + 2.2374F,-0.7339F,-0.1251F,-0.6033F +}; + +static const float OP_FCOEF_A[4]={ + 0.9030F,0.0116F,-0.5853F,-0.2571F +}; + +static int op_float2short_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + float *_src,int _nsamples,int _nchannels){ + opus_int16 *dst; + int ci; + int i; + dst=(opus_int16 *)_dst; + if(OP_UNLIKELY(_nsamples*_nchannels>_dst_sz))_nsamples=_dst_sz/_nchannels; +# if defined(OP_SOFT_CLIP) + if(_of->state_channel_count!=_nchannels){ + for(ci=0;ci<_nchannels;ci++)_of->clip_state[ci]=0; + } + opus_pcm_soft_clip(_src,_nsamples,_nchannels,_of->clip_state); +# endif + if(_of->dither_disabled){ + for(i=0;i<_nchannels*_nsamples;i++){ + dst[i]=op_float2int(OP_CLAMP(-32768,32768.0F*_src[i],32767)); + } + } + else{ + opus_uint32 seed; + int mute; + seed=_of->dither_seed; + mute=_of->dither_mute; + if(_of->state_channel_count!=_nchannels)mute=65; + /*In order to avoid replacing digital silence with quiet dither noise, we + mute if the output has been silent for a while.*/ + if(mute>64)memset(_of->dither_a,0,sizeof(*_of->dither_a)*4*_nchannels); + for(i=0;i<_nsamples;i++){ + int silent; + silent=1; + for(ci=0;ci<_nchannels;ci++){ + float r; + float s; + float err; + int si; + int j; + s=_src[_nchannels*i+ci]; + silent&=s==0; + s*=OP_GAIN; + err=0; + for(j=0;j<4;j++){ + err+=OP_FCOEF_B[j]*_of->dither_b[ci*4+j] + -OP_FCOEF_A[j]*_of->dither_a[ci*4+j]; + } + for(j=3;j-->0;)_of->dither_a[ci*4+j+1]=_of->dither_a[ci*4+j]; + for(j=3;j-->0;)_of->dither_b[ci*4+j+1]=_of->dither_b[ci*4+j]; + _of->dither_a[ci*4]=err; + s-=err; + if(mute>16)r=0; + else{ + seed=op_rand(seed); + r=seed*OP_PRNG_GAIN; + seed=op_rand(seed); + r-=seed*OP_PRNG_GAIN; + } + /*Clamp in float out of paranoia that the input will be > 96 dBFS and + wrap if the integer is clamped.*/ + si=op_float2int(OP_CLAMP(-32768,s+r,32767)); + dst[_nchannels*i+ci]=(opus_int16)si; + /*Including clipping in the noise shaping is generally disastrous: the + futile effort to restore the clipped energy results in more clipping. + However, small amounts---at the level which could normally be created + by dither and rounding---are harmless and can even reduce clipping + somewhat due to the clipping sometimes reducing the dither + rounding + error.*/ + _of->dither_b[ci*4]=mute>16?0:OP_CLAMP(-1.5F,si-s,1.5F); + } + mute++; + if(!silent)mute=0; + } + _of->dither_mute=OP_MIN(mute,65); + _of->dither_seed=seed; + } + _of->state_channel_count=_nchannels; + return _nsamples; +} + +int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){ + return op_filter_read_native(_of,_pcm,_buf_size,op_float2short_filter,_li); +} + +int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li){ + _of->state_channel_count=0; + return op_read_native(_of,_pcm,_buf_size,_li); +} + +static int op_stereo_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels){ + (void)_of; + _nsamples=OP_MIN(_nsamples,_dst_sz>>1); + if(_nchannels==2)memcpy(_dst,_src,_nsamples*2*sizeof(*_src)); + else{ + float *dst; + int i; + dst=(float *)_dst; + if(_nchannels==1){ + for(i=0;i<_nsamples;i++)dst[2*i+0]=dst[2*i+1]=_src[i]; + } + else{ + for(i=0;i<_nsamples;i++){ + float l; + float r; + int ci; + l=r=0; + for(ci=0;ci<_nchannels;ci++){ + l+=OP_STEREO_DOWNMIX[_nchannels-3][ci][0]*_src[_nchannels*i+ci]; + r+=OP_STEREO_DOWNMIX[_nchannels-3][ci][1]*_src[_nchannels*i+ci]; + } + dst[2*i+0]=l; + dst[2*i+1]=r; + } + } + } + return _nsamples; +} + +static int op_float2short_stereo_filter(OggOpusFile *_of, + void *_dst,int _dst_sz,op_sample *_src,int _nsamples,int _nchannels){ + opus_int16 *dst; + dst=(opus_int16 *)_dst; + if(_nchannels==1){ + int i; + _nsamples=op_float2short_filter(_of,dst,_dst_sz>>1,_src,_nsamples,1); + for(i=_nsamples;i-->0;)dst[2*i+0]=dst[2*i+1]=dst[i]; + } + else{ + if(_nchannels>2){ + _nsamples=OP_MIN(_nsamples,_dst_sz>>1); + _nsamples=op_stereo_filter(_of,_src,_nsamples*2, + _src,_nsamples,_nchannels); + } + _nsamples=op_float2short_filter(_of,dst,_dst_sz,_src,_nsamples,2); + } + return _nsamples; +} + +int op_read_stereo(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size){ + return op_filter_read_native(_of,_pcm,_buf_size, + op_float2short_stereo_filter,NULL); +} + +int op_read_float_stereo(OggOpusFile *_of,float *_pcm,int _buf_size){ + _of->state_channel_count=0; + return op_filter_read_native(_of,_pcm,_buf_size,op_stereo_filter,NULL); +} + +#endif diff --git a/third-party/opusfile/opusfile.h b/third-party/opusfile/opusfile.h new file mode 100644 index 0000000000..ae58da9890 --- /dev/null +++ b/third-party/opusfile/opusfile.h @@ -0,0 +1,2089 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE libopusfile SOURCE CODE IS (C) COPYRIGHT 1994-2012 * + * by the Xiph.Org Foundation and contributors http://www.xiph.org/ * + * * + ******************************************************************** + + function: stdio-based convenience library for opening/seeking/decoding + last mod: $Id: vorbisfile.h 17182 2010-04-29 03:48:32Z xiphmont $ + + ********************************************************************/ +#if !defined(_opusfile_h) +# define _opusfile_h (1) + +/**\mainpage + \section Introduction + + This is the documentation for the libopusfile C API. + + The libopusfile package provides a convenient high-level API for + decoding and basic manipulation of all Ogg Opus audio streams. + libopusfile is implemented as a layer on top of Xiph.Org's + reference + libogg + and + libopus + libraries. + + libopusfile provides several sets of built-in routines for + file/stream access, and may also use custom stream I/O routines provided by + the embedded environment. + There are built-in I/O routines provided for ANSI-compliant + stdio (FILE *), memory buffers, and URLs + (including URLs, plus optionally and URLs). + + \section Organization + + The main API is divided into several sections: + - \ref stream_open_close + - \ref stream_info + - \ref stream_decoding + - \ref stream_seeking + + Several additional sections are not tied to the main API. + - \ref stream_callbacks + - \ref header_info + - \ref error_codes + + \section Overview + + The libopusfile API always decodes files to 48 kHz. + The original sample rate is not preserved by the lossy compression, though + it is stored in the header to allow you to resample to it after decoding + (the libopusfile API does not currently provide a resampler, + but the + the + Speex resampler is a good choice if you need one). + In general, if you are playing back the audio, you should leave it at + 48 kHz, provided your audio hardware supports it. + When decoding to a file, it may be worth resampling back to the original + sample rate, so as not to surprise users who might not expect the sample + rate to change after encoding to Opus and decoding. + + Opus files can contain anywhere from 1 to 255 channels of audio. + The channel mappings for up to 8 channels are the same as the + Vorbis + mappings. + A special stereo API can convert everything to 2 channels, making it simple + to support multichannel files in an application which only has stereo + output. + Although the libopusfile ABI provides support for the theoretical + maximum number of channels, the current implementation does not support + files with more than 8 channels, as they do not have well-defined channel + mappings. + + Like all Ogg files, Opus files may be "chained". + That is, multiple Opus files may be combined into a single, longer file just + by concatenating the original files. + This is commonly done in internet radio streaming, as it allows the title + and artist to be updated each time the song changes, since each link in the + chain includes its own set of metadata. + + libopusfile fully supports chained files. + It will decode the first Opus stream found in each link of a chained file + (ignoring any other streams that might be concurrently multiplexed with it, + such as a video stream). + + The channel count can also change between links. + If your application is not prepared to deal with this, it can use the stereo + API to ensure the audio from all links will always get decoded into a + common format. + Since libopusfile always decodes to 48 kHz, you do not have to + worry about the sample rate changing between links (as was possible with + Vorbis). + This makes application support for chained files with libopusfile + very easy.*/ + +# if defined(__cplusplus) +extern "C" { +# endif + +# include +# include +# include +# include "opus_multistream.h" + +/**@cond PRIVATE*/ + +/*Enable special features for gcc and gcc-compatible compilers.*/ +# if !defined(OP_GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define OP_GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define OP_GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +# if OP_GNUC_PREREQ(4,0) +# pragma GCC visibility push(default) +# endif + +typedef struct OpusHead OpusHead; +typedef struct OpusTags OpusTags; +typedef struct OpusPictureTag OpusPictureTag; +typedef struct OpusServerInfo OpusServerInfo; +typedef struct OpusFileCallbacks OpusFileCallbacks; +typedef struct OggOpusFile OggOpusFile; + +/*Warning attributes for libopusfile functions.*/ +# if OP_GNUC_PREREQ(3,4) +# define OP_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) +# else +# define OP_WARN_UNUSED_RESULT +# endif +# if OP_GNUC_PREREQ(3,4) +# define OP_ARG_NONNULL(_x) __attribute__((__nonnull__(_x))) +# else +# define OP_ARG_NONNULL(_x) +# endif + +/**@endcond*/ + +/**\defgroup error_codes Error Codes*/ +/*@{*/ +/**\name List of possible error codes + Many of the functions in this library return a negative error code when a + function fails. + This list provides a brief explanation of the common errors. + See each individual function for more details on what a specific error code + means in that context.*/ +/*@{*/ + +/**A request did not succeed.*/ +#define OP_FALSE (-1) +/*Currently not used externally.*/ +#define OP_EOF (-2) +/**There was a hole in the page sequence numbers (e.g., a page was corrupt or + missing).*/ +#define OP_HOLE (-3) +/**An underlying read, seek, or tell operation failed when it should have + succeeded.*/ +#define OP_EREAD (-128) +/**A NULL pointer was passed where one was unexpected, or an + internal memory allocation failed, or an internal library error was + encountered.*/ +#define OP_EFAULT (-129) +/**The stream used a feature that is not implemented, such as an unsupported + channel family.*/ +#define OP_EIMPL (-130) +/**One or more parameters to a function were invalid.*/ +#define OP_EINVAL (-131) +/**A purported Ogg Opus stream did not begin with an Ogg page, a purported + header packet did not start with one of the required strings, "OpusHead" or + "OpusTags", or a link in a chained file was encountered that did not + contain any logical Opus streams.*/ +#define OP_ENOTFORMAT (-132) +/**A required header packet was not properly formatted, contained illegal + values, or was missing altogether.*/ +#define OP_EBADHEADER (-133) +/**The ID header contained an unrecognized version number.*/ +#define OP_EVERSION (-134) +/*Currently not used at all.*/ +#define OP_ENOTAUDIO (-135) +/**An audio packet failed to decode properly. + This is usually caused by a multistream Ogg packet where the durations of + the individual Opus packets contained in it are not all the same.*/ +#define OP_EBADPACKET (-136) +/**We failed to find data we had seen before, or the bitstream structure was + sufficiently malformed that seeking to the target destination was + impossible.*/ +#define OP_EBADLINK (-137) +/**An operation that requires seeking was requested on an unseekable stream.*/ +#define OP_ENOSEEK (-138) +/**The first or last granule position of a link failed basic validity checks.*/ +#define OP_EBADTIMESTAMP (-139) + +/*@}*/ +/*@}*/ + +/**\defgroup header_info Header Information*/ +/*@{*/ + +/**The maximum number of channels in an Ogg Opus stream.*/ +#define OPUS_CHANNEL_COUNT_MAX (255) + +/**Ogg Opus bitstream information. + This contains the basic playback parameters for a stream, and corresponds to + the initial ID header packet of an Ogg Opus stream.*/ +struct OpusHead{ + /**The Ogg Opus format version, in the range 0...255. + The top 4 bits represent a "major" version, and the bottom four bits + represent backwards-compatible "minor" revisions. + The current specification describes version 1. + This library will recognize versions up through 15 as backwards compatible + with the current specification. + An earlier draft of the specification described a version 0, but the only + difference between version 1 and version 0 is that version 0 did + not specify the semantics for handling the version field.*/ + int version; + /**The number of channels, in the range 1...255.*/ + int channel_count; + /**The number of samples that should be discarded from the beginning of the + stream.*/ + unsigned pre_skip; + /**The sampling rate of the original input. + All Opus audio is coded at 48 kHz, and should also be decoded at 48 kHz + for playback (unless the target hardware does not support this sampling + rate). + However, this field may be used to resample the audio back to the original + sampling rate, for example, when saving the output to a file.*/ + opus_uint32 input_sample_rate; + /**The gain to apply to the decoded output, in dB, as a Q8 value in the range + -32768...32767. + The libopusfile API will automatically apply this gain to the + decoded output before returning it, scaling it by + pow(10,output_gain/(20.0*256)).*/ + int output_gain; + /**The channel mapping family, in the range 0...255. + Channel mapping family 0 covers mono or stereo in a single stream. + Channel mapping family 1 covers 1 to 8 channels in one or more streams, + using the Vorbis speaker assignments. + Channel mapping family 255 covers 1 to 255 channels in one or more + streams, but without any defined speaker assignment.*/ + int mapping_family; + /**The number of Opus streams in each Ogg packet, in the range 1...255.*/ + int stream_count; + /**The number of coupled Opus streams in each Ogg packet, in the range + 0...127. + This must satisfy 0 <= coupled_count <= stream_count and + coupled_count + stream_count <= 255. + The coupled streams appear first, before all uncoupled streams, in an Ogg + Opus packet.*/ + int coupled_count; + /**The mapping from coded stream channels to output channels. + Let index=mapping[k] be the value for channel k. + If index<2*coupled_count, then it refers to the left channel + from stream (index/2) if even, and the right channel from + stream (index/2) if odd. + Otherwise, it refers to the output of the uncoupled stream + (index-coupled_count).*/ + unsigned char mapping[OPUS_CHANNEL_COUNT_MAX]; +}; + +/**The metadata from an Ogg Opus stream. + + This structure holds the in-stream metadata corresponding to the 'comment' + header packet of an Ogg Opus stream. + The comment header is meant to be used much like someone jotting a quick + note on the label of a CD. + It should be a short, to the point text note that can be more than a couple + words, but not more than a short paragraph. + + The metadata is stored as a series of (tag, value) pairs, in length-encoded + string vectors, using the same format as Vorbis (without the final "framing + bit"), Theora, and Speex, except for the packet header. + The first occurrence of the '=' character delimits the tag and value. + A particular tag may occur more than once, and order is significant. + The character set encoding for the strings is always UTF-8, but the tag + names are limited to ASCII, and treated as case-insensitive. + See the Vorbis + comment header specification for details. + + In filling in this structure, libopusfile will null-terminate the + #user_comments strings for safety. + However, the bitstream format itself treats them as 8-bit clean vectors, + possibly containing NUL characters, so the #comment_lengths array should be + treated as their authoritative length. + + This structure is binary and source-compatible with a + vorbis_comment, and pointers to it may be freely cast to + vorbis_comment pointers, and vice versa. + It is provided as a separate type to avoid introducing a compile-time + dependency on the libvorbis headers.*/ +struct OpusTags{ + /**The array of comment string vectors.*/ + char **user_comments; + /**An array of the corresponding length of each vector, in bytes.*/ + int *comment_lengths; + /**The total number of comment streams.*/ + int comments; + /**The null-terminated vendor string. + This identifies the software used to encode the stream.*/ + char *vendor; +}; + +/**\name Picture tag image formats*/ +/*@{*/ + +/**The MIME type was not recognized, or the image data did not match the + declared MIME type.*/ +#define OP_PIC_FORMAT_UNKNOWN (-1) +/**The MIME type indicates the image data is really a URL.*/ +#define OP_PIC_FORMAT_URL (0) +/**The image is a JPEG.*/ +#define OP_PIC_FORMAT_JPEG (1) +/**The image is a PNG.*/ +#define OP_PIC_FORMAT_PNG (2) +/**The image is a GIF.*/ +#define OP_PIC_FORMAT_GIF (3) + +/*@}*/ + +/**The contents of a METADATA_BLOCK_PICTURE tag.*/ +struct OpusPictureTag{ + /**The picture type according to the ID3v2 APIC frame: +
    +
  1. Other
  2. +
  3. 32x32 pixels 'file icon' (PNG only)
  4. +
  5. Other file icon
  6. +
  7. Cover (front)
  8. +
  9. Cover (back)
  10. +
  11. Leaflet page
  12. +
  13. Media (e.g. label side of CD)
  14. +
  15. Lead artist/lead performer/soloist
  16. +
  17. Artist/performer
  18. +
  19. Conductor
  20. +
  21. Band/Orchestra
  22. +
  23. Composer
  24. +
  25. Lyricist/text writer
  26. +
  27. Recording Location
  28. +
  29. During recording
  30. +
  31. During performance
  32. +
  33. Movie/video screen capture
  34. +
  35. A bright colored fish
  36. +
  37. Illustration
  38. +
  39. Band/artist logotype
  40. +
  41. Publisher/Studio logotype
  42. +
+ Others are reserved and should not be used. + There may only be one each of picture type 1 and 2 in a file.*/ + opus_int32 type; + /**The MIME type of the picture, in printable ASCII characters 0x20-0x7E. + The MIME type may also be "-->" to signify that the data part + is a URL pointing to the picture instead of the picture data itself. + In this case, a terminating NUL is appended to the URL string in #data, + but #data_length is set to the length of the string excluding that + terminating NUL.*/ + char *mime_type; + /**The description of the picture, in UTF-8.*/ + char *description; + /**The width of the picture in pixels.*/ + opus_uint32 width; + /**The height of the picture in pixels.*/ + opus_uint32 height; + /**The color depth of the picture in bits-per-pixel (not + bits-per-channel).*/ + opus_uint32 depth; + /**For indexed-color pictures (e.g., GIF), the number of colors used, or 0 + for non-indexed pictures.*/ + opus_uint32 colors; + /**The length of the picture data in bytes.*/ + opus_uint32 data_length; + /**The binary picture data.*/ + unsigned char *data; + /**The format of the picture data, if known. + One of +
    +
  • #OP_PIC_FORMAT_UNKNOWN,
  • +
  • #OP_PIC_FORMAT_URL,
  • +
  • #OP_PIC_FORMAT_JPEG,
  • +
  • #OP_PIC_FORMAT_PNG, or
  • +
  • #OP_PIC_FORMAT_GIF.
  • +
*/ + int format; +}; + +/**\name Functions for manipulating header data + + These functions manipulate the #OpusHead and #OpusTags structures, + which describe the audio parameters and tag-value metadata, respectively. + These can be used to query the headers returned by libopusfile, or + to parse Opus headers from sources other than an Ogg Opus stream, provided + they use the same format.*/ +/*@{*/ + +/**Parses the contents of the ID header packet of an Ogg Opus stream. + \param[out] _head Returns the contents of the parsed packet. + The contents of this structure are untouched on error. + This may be NULL to merely test the header + for validity. + \param[in] _data The contents of the ID header packet. + \param _len The number of bytes of data in the ID header packet. + \return 0 on success or a negative value on error. + \retval #OP_ENOTFORMAT If the data does not start with the "OpusHead" + string. + \retval #OP_EVERSION If the version field signaled a version this library + does not know how to parse. + \retval #OP_EIMPL If the channel mapping family was 255, which general + purpose players should not attempt to play. + \retval #OP_EBADHEADER If the contents of the packet otherwise violate the + Ogg Opus specification: +
    +
  • Insufficient data,
  • +
  • Too much data for the known minor versions,
  • +
  • An unrecognized channel mapping family,
  • +
  • Zero channels or too many channels,
  • +
  • Zero coded streams,
  • +
  • Too many coupled streams, or
  • +
  • An invalid channel mapping index.
  • +
*/ +OP_WARN_UNUSED_RESULT int opus_head_parse(OpusHead *_head, + const unsigned char *_data,size_t _len) OP_ARG_NONNULL(2); + +/**Converts a granule position to a sample offset for a given Ogg Opus stream. + The sample offset is simply _gp-_head->pre_skip. + Granule position values smaller than OpusHead#pre_skip correspond to audio + that should never be played, and thus have no associated sample offset. + This function returns -1 for such values. + This function also correctly handles extremely large granule positions, + which may have wrapped around to a negative number when stored in a signed + ogg_int64_t value. + \param _head The #OpusHead information from the ID header of the stream. + \param _gp The granule position to convert. + \return The sample offset associated with the given granule position + (counting at a 48 kHz sampling rate), or the special value -1 on + error (i.e., the granule position was smaller than the pre-skip + amount).*/ +ogg_int64_t opus_granule_sample(const OpusHead *_head,ogg_int64_t _gp) + OP_ARG_NONNULL(1); + +/**Parses the contents of the 'comment' header packet of an Ogg Opus stream. + \param[out] _tags An uninitialized #OpusTags structure. + This returns the contents of the parsed packet. + The contents of this structure are untouched on error. + This may be NULL to merely test the header + for validity. + \param[in] _data The contents of the 'comment' header packet. + \param _len The number of bytes of data in the 'info' header packet. + \retval 0 Success. + \retval #OP_ENOTFORMAT If the data does not start with the "OpusTags" + string. + \retval #OP_EBADHEADER If the contents of the packet otherwise violate the + Ogg Opus specification. + \retval #OP_EFAULT If there wasn't enough memory to store the tags.*/ +OP_WARN_UNUSED_RESULT int opus_tags_parse(OpusTags *_tags, + const unsigned char *_data,size_t _len) OP_ARG_NONNULL(2); + +/**Performs a deep copy of an #OpusTags structure. + \param _dst The #OpusTags structure to copy into. + If this function fails, the contents of this structure remain + untouched. + \param _src The #OpusTags structure to copy from. + \retval 0 Success. + \retval #OP_EFAULT If there wasn't enough memory to copy the tags.*/ +int opus_tags_copy(OpusTags *_dst,const OpusTags *_src) OP_ARG_NONNULL(1); + +/**Initializes an #OpusTags structure. + This should be called on a freshly allocated #OpusTags structure before + attempting to use it. + \param _tags The #OpusTags structure to initialize.*/ +void opus_tags_init(OpusTags *_tags) OP_ARG_NONNULL(1); + +/**Add a (tag, value) pair to an initialized #OpusTags structure. + \note Neither opus_tags_add() nor opus_tags_add_comment() support values + containing embedded NULs, although the bitstream format does support them. + To add such tags, you will need to manipulate the #OpusTags structure + directly. + \param _tags The #OpusTags structure to add the (tag, value) pair to. + \param _tag A NUL-terminated, case-insensitive, ASCII string containing + the tag to add (without an '=' character). + \param _value A NUL-terminated UTF-8 containing the corresponding value. + \return 0 on success, or a negative value on failure. + \retval #OP_EFAULT An internal memory allocation failed.*/ +int opus_tags_add(OpusTags *_tags,const char *_tag,const char *_value) + OP_ARG_NONNULL(1) OP_ARG_NONNULL(2) OP_ARG_NONNULL(3); + +/**Add a comment to an initialized #OpusTags structure. + \note Neither opus_tags_add_comment() nor opus_tags_add() support comments + containing embedded NULs, although the bitstream format does support them. + To add such tags, you will need to manipulate the #OpusTags structure + directly. + \param _tags The #OpusTags structure to add the comment to. + \param _comment A NUL-terminated UTF-8 string containing the comment in + "TAG=value" form. + \return 0 on success, or a negative value on failure. + \retval #OP_EFAULT An internal memory allocation failed.*/ +int opus_tags_add_comment(OpusTags *_tags,const char *_comment) + OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/**Look up a comment value by its tag. + \param _tags An initialized #OpusTags structure. + \param _tag The tag to look up. + \param _count The instance of the tag. + The same tag can appear multiple times, each with a distinct + value, so an index is required to retrieve them all. + The order in which these values appear is significant and + should be preserved. + Use opus_tags_query_count() to get the legal range for the + \a _count parameter. + \return A pointer to the queried tag's value. + This points directly to data in the #OpusTags structure. + It should not be modified or freed by the application, and + modifications to the structure may invalidate the pointer. + \retval NULL If no matching tag is found.*/ +const char *opus_tags_query(const OpusTags *_tags,const char *_tag,int _count) + OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/**Look up the number of instances of a tag. + Call this first when querying for a specific tag and then iterate over the + number of instances with separate calls to opus_tags_query() to retrieve + all the values for that tag in order. + \param _tags An initialized #OpusTags structure. + \param _tag The tag to look up. + \return The number of instances of this particular tag.*/ +int opus_tags_query_count(const OpusTags *_tags,const char *_tag) + OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/**Get the track gain from an R128_TRACK_GAIN tag, if one was specified. + This searches for the first R128_TRACK_GAIN tag with a valid signed, + 16-bit decimal integer value and returns the value. + This routine is exposed merely for convenience for applications which wish + to do something special with the track gain (i.e., display it). + If you simply wish to apply the track gain instead of the header gain, you + can use op_set_gain_offset() with an #OP_TRACK_GAIN type and no offset. + \param _tags An initialized #OpusTags structure. + \param[out] _gain_q8 The track gain, in 1/256ths of a dB. + This will lie in the range [-32768,32767], and should + be applied in addition to the header gain. + On error, no value is returned, and the previous + contents remain unchanged. + \return 0 on success, or a negative value on error. + \retval #OP_FALSE There was no track gain available in the given tags.*/ +int opus_tags_get_track_gain(const OpusTags *_tags,int *_gain_q8) + OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/**Clears the #OpusTags structure. + This should be called on an #OpusTags structure after it is no longer + needed. + It will free all memory used by the structure members. + \param _tags The #OpusTags structure to clear.*/ +void opus_tags_clear(OpusTags *_tags) OP_ARG_NONNULL(1); + +/**Check if \a _comment is an instance of a \a _tag_name tag. + \see opus_tagncompare + \param _tag_name A NUL-terminated, case-insensitive, ASCII string containing + the name of the tag to check for (without the terminating + '=' character). + \param _comment The comment string to check. + \return An integer less than, equal to, or greater than zero if \a _comment + is found respectively, to be less than, to match, or be greater + than a "tag=value" string whose tag matches \a _tag_name.*/ +int opus_tagcompare(const char *_tag_name,const char *_comment); + +/**Check if \a _comment is an instance of a \a _tag_name tag. + This version is slightly more efficient than opus_tagcompare() if the length + of the tag name is already known (e.g., because it is a constant). + \see opus_tagcompare + \param _tag_name A case-insensitive ASCII string containing the name of the + tag to check for (without the terminating '=' character). + \param _tag_len The number of characters in the tag name. + This must be non-negative. + \param _comment The comment string to check. + \return An integer less than, equal to, or greater than zero if \a _comment + is found respectively, to be less than, to match, or be greater + than a "tag=value" string whose tag matches the first \a _tag_len + characters of \a _tag_name.*/ +int opus_tagncompare(const char *_tag_name,int _tag_len,const char *_comment); + +/**Parse a single METADATA_BLOCK_PICTURE tag. + This decodes the BASE64-encoded content of the tag and returns a structure + with the MIME type, description, image parameters (if known), and the + compressed image data. + If the MIME type indicates the presence of an image format we recognize + (JPEG, PNG, or GIF) and the actual image data contains the magic signature + associated with that format, then the OpusPictureTag::format field will be + set to the corresponding format. + This is provided as a convenience to avoid requiring applications to parse + the MIME type and/or do their own format detection for the commonly used + formats. + In this case, we also attempt to extract the image parameters directly from + the image data (overriding any that were present in the tag, which the + specification says applications are not meant to rely on). + The application must still provide its own support for actually decoding the + image data and, if applicable, retrieving that data from URLs. + \param[out] _pic Returns the parsed picture data. + No sanitation is done on the type, MIME type, or + description fields, so these might return invalid values. + The contents of this structure are left unmodified on + failure. + \param _tag The METADATA_BLOCK_PICTURE tag contents. + The leading "METADATA_BLOCK_PICTURE=" portion is optional, + to allow the function to be used on either directly on the + values in OpusTags::user_comments or on the return value + of opus_tags_query(). + \return 0 on success or a negative value on error. + \retval #OP_ENOTFORMAT The METADATA_BLOCK_PICTURE contents were not valid. + \retval #OP_EFAULT There was not enough memory to store the picture tag + contents.*/ +OP_WARN_UNUSED_RESULT int opus_picture_tag_parse(OpusPictureTag *_pic, + const char *_tag) OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/**Initializes an #OpusPictureTag structure. + This should be called on a freshly allocated #OpusPictureTag structure + before attempting to use it. + \param _pic The #OpusPictureTag structure to initialize.*/ +void opus_picture_tag_init(OpusPictureTag *_pic) OP_ARG_NONNULL(1); + +/**Clears the #OpusPictureTag structure. + This should be called on an #OpusPictureTag structure after it is no longer + needed. + It will free all memory used by the structure members. + \param _pic The #OpusPictureTag structure to clear.*/ +void opus_picture_tag_clear(OpusPictureTag *_pic) OP_ARG_NONNULL(1); + +/*@}*/ + +/*@}*/ + +/**\defgroup url_options URL Reading Options*/ +/*@{*/ +/**\name URL reading options + Options for op_url_stream_create() and associated functions. + These allow you to provide proxy configuration parameters, skip SSL + certificate checks, etc. + Options are processed in order, and if the same option is passed multiple + times, only the value specified by the last occurrence has an effect + (unless otherwise specified). + They may be expanded in the future.*/ +/*@{*/ + +/**@cond PRIVATE*/ + +/*These are the raw numbers used to define the request codes. + They should not be used directly.*/ +#define OP_SSL_SKIP_CERTIFICATE_CHECK_REQUEST (6464) +#define OP_HTTP_PROXY_HOST_REQUEST (6528) +#define OP_HTTP_PROXY_PORT_REQUEST (6592) +#define OP_HTTP_PROXY_USER_REQUEST (6656) +#define OP_HTTP_PROXY_PASS_REQUEST (6720) +#define OP_GET_SERVER_INFO_REQUEST (6784) + +#define OP_URL_OPT(_request) ((_request)+(char *)0) + +/*These macros trigger compilation errors or warnings if the wrong types are + provided to one of the URL options.*/ +#define OP_CHECK_INT(_x) ((void)((_x)==(opus_int32)0),(opus_int32)(_x)) +#define OP_CHECK_CONST_CHAR_PTR(_x) ((_x)+((_x)-(const char *)(_x))) +#define OP_CHECK_SERVER_INFO_PTR(_x) ((_x)+((_x)-(OpusServerInfo *)(_x))) + +/**@endcond*/ + +/**HTTP/Shoutcast/Icecast server information associated with a URL.*/ +struct OpusServerInfo{ + /**The name of the server (icy-name/ice-name). + This is NULL if there was no icy-name or + ice-name header.*/ + char *name; + /**A short description of the server (icy-description/ice-description). + This is NULL if there was no icy-description or + ice-description header.*/ + char *description; + /**The genre the server falls under (icy-genre/ice-genre). + This is NULL if there was no icy-genre or + ice-genre header.*/ + char *genre; + /**The homepage for the server (icy-url/ice-url). + This is NULL if there was no icy-url or + ice-url header.*/ + char *url; + /**The software used by the origin server (Server). + This is NULL if there was no Server header.*/ + char *server; + /**The media type of the entity sent to the recepient (Content-Type). + This is NULL if there was no Content-Type + header.*/ + char *content_type; + /**The nominal stream bitrate in kbps (icy-br/ice-bitrate). + This is -1 if there was no icy-br or + ice-bitrate header.*/ + opus_int32 bitrate_kbps; + /**Flag indicating whether the server is public (1) or not + (0) (icy-pub/ice-public). + This is -1 if there was no icy-pub or + ice-public header.*/ + int is_public; + /**Flag indicating whether the server is using HTTPS instead of HTTP. + This is 0 unless HTTPS is being used. + This may not match the protocol used in the original URL if there were + redirections.*/ + int is_ssl; +}; + +/**Initializes an #OpusServerInfo structure. + All fields are set as if the corresponding header was not available. + \param _info The #OpusServerInfo structure to initialize. + \note If you use this function, you must link against libopusurl.*/ +void opus_server_info_init(OpusServerInfo *_info) OP_ARG_NONNULL(1); + +/**Clears the #OpusServerInfo structure. + This should be called on an #OpusServerInfo structure after it is no longer + needed. + It will free all memory used by the structure members. + \param _info The #OpusServerInfo structure to clear. + \note If you use this function, you must link against libopusurl.*/ +void opus_server_info_clear(OpusServerInfo *_info) OP_ARG_NONNULL(1); + +/**Skip the certificate check when connecting via TLS/SSL (https). + \param _b opus_int32: Whether or not to skip the certificate + check. + The check will be skipped if \a _b is non-zero, and will not be + skipped if \a _b is zero. + \hideinitializer*/ +#define OP_SSL_SKIP_CERTIFICATE_CHECK(_b) \ + OP_URL_OPT(OP_SSL_SKIP_CERTIFICATE_CHECK_REQUEST),OP_CHECK_INT(_b) + +/**Proxy connections through the given host. + If no port is specified via #OP_HTTP_PROXY_PORT, the port number defaults + to 8080 (http-alt). + All proxy parameters are ignored for non-http and non-https URLs. + \param _host const char *: The proxy server hostname. + This may be NULL to disable the use of a proxy + server. + \hideinitializer*/ +#define OP_HTTP_PROXY_HOST(_host) \ + OP_URL_OPT(OP_HTTP_PROXY_HOST_REQUEST),OP_CHECK_CONST_CHAR_PTR(_host) + +/**Use the given port when proxying connections. + This option only has an effect if #OP_HTTP_PROXY_HOST is specified with a + non-NULL \a _host. + If this option is not provided, the proxy port number defaults to 8080 + (http-alt). + All proxy parameters are ignored for non-http and non-https URLs. + \param _port opus_int32: The proxy server port. + This must be in the range 0...65535 (inclusive), or the + URL function this is passed to will fail. + \hideinitializer*/ +#define OP_HTTP_PROXY_PORT(_port) \ + OP_URL_OPT(OP_HTTP_PROXY_PORT_REQUEST),OP_CHECK_INT(_port) + +/**Use the given user name for authentication when proxying connections. + All proxy parameters are ignored for non-http and non-https URLs. + \param _user const char *: The proxy server user name. + This may be NULL to disable proxy + authentication. + A non-NULL value only has an effect + if #OP_HTTP_PROXY_HOST and #OP_HTTP_PROXY_PASS + are also specified with non-NULL + arguments. + \hideinitializer*/ +#define OP_HTTP_PROXY_USER(_user) \ + OP_URL_OPT(OP_HTTP_PROXY_USER_REQUEST),OP_CHECK_CONST_CHAR_PTR(_user) + +/**Use the given password for authentication when proxying connections. + All proxy parameters are ignored for non-http and non-https URLs. + \param _pass const char *: The proxy server password. + This may be NULL to disable proxy + authentication. + A non-NULL value only has an effect + if #OP_HTTP_PROXY_HOST and #OP_HTTP_PROXY_USER + are also specified with non-NULL + arguments. + \hideinitializer*/ +#define OP_HTTP_PROXY_PASS(_pass) \ + OP_URL_OPT(OP_HTTP_PROXY_PASS_REQUEST),OP_CHECK_CONST_CHAR_PTR(_pass) + +/**Parse information about the streaming server (if any) and return it. + Very little validation is done. + In particular, OpusServerInfo::url may not be a valid URL, + OpusServerInfo::bitrate_kbps may not really be in kbps, and + OpusServerInfo::content_type may not be a valid MIME type. + The character set of the string fields is not specified anywhere, and should + not be assumed to be valid UTF-8. + \param _info OpusServerInfo *: Returns information about the server. + If there is any error opening the stream, the + contents of this structure remain + unmodified. + On success, fills in the structure with the + server information that was available, if + any. + After a successful return, the contents of + this structure should be freed by calling + opus_server_info_clear(). + \hideinitializer*/ +#define OP_GET_SERVER_INFO(_info) \ + OP_URL_OPT(OP_GET_SERVER_INFO_REQUEST),OP_CHECK_SERVER_INFO_PTR(_info) + +/*@}*/ +/*@}*/ + +/**\defgroup stream_callbacks Abstract Stream Reading Interface*/ +/*@{*/ +/**\name Functions for reading from streams + These functions define the interface used to read from and seek in a stream + of data. + A stream does not need to implement seeking, but the decoder will not be + able to seek if it does not do so. + These functions also include some convenience routines for working with + standard FILE pointers, complete streams stored in a single + block of memory, or URLs.*/ +/*@{*/ + +/**Reads up to \a _nbytes bytes of data from \a _stream. + \param _stream The stream to read from. + \param[out] _ptr The buffer to store the data in. + \param _nbytes The maximum number of bytes to read. + This function may return fewer, though it will not + return zero unless it reaches end-of-file. + \return The number of bytes successfully read, or a negative value on + error.*/ +typedef int (*op_read_func)(void *_stream,unsigned char *_ptr,int _nbytes); + +/**Sets the position indicator for \a _stream. + The new position, measured in bytes, is obtained by adding \a _offset + bytes to the position specified by \a _whence. + If \a _whence is set to SEEK_SET, SEEK_CUR, or + SEEK_END, the offset is relative to the start of the stream, + the current position indicator, or end-of-file, respectively. + \retval 0 Success. + \retval -1 Seeking is not supported or an error occurred. + errno need not be set.*/ +typedef int (*op_seek_func)(void *_stream,opus_int64 _offset,int _whence); + +/**Obtains the current value of the position indicator for \a _stream. + \return The current position indicator.*/ +typedef opus_int64 (*op_tell_func)(void *_stream); + +/**Closes the underlying stream. + \retval 0 Success. + \retval EOF An error occurred. + errno need not be set.*/ +typedef int (*op_close_func)(void *_stream); + +/**The callbacks used to access non-FILE stream resources. + The function prototypes are basically the same as for the stdio functions + fread(), fseek(), ftell(), and + fclose(). + The differences are that the FILE * arguments have been + replaced with a void *, which is to be used as a pointer to + whatever internal data these functions might need, that #seek and #tell + take and return 64-bit offsets, and that #seek must return -1 if + the stream is unseekable.*/ +struct OpusFileCallbacks{ + /**Used to read data from the stream. + This must not be NULL.*/ + op_read_func read; + /**Used to seek in the stream. + This may be NULL if seeking is not implemented.*/ + op_seek_func seek; + /**Used to return the current read position in the stream. + This may be NULL if seeking is not implemented.*/ + op_tell_func tell; + /**Used to close the stream when the decoder is freed. + This may be NULL to leave the stream open.*/ + op_close_func close; +}; + +/**Opens a stream with fopen() and fills in a set of callbacks + that can be used to access it. + This is useful to avoid writing your own portable 64-bit seeking wrappers, + and also avoids cross-module linking issues on Windows, where a + FILE * must be accessed by routines defined in the same module + that opened it. + \param[out] _cb The callbacks to use for this file. + If there is an error opening the file, nothing will be + filled in here. + \param _path The path to the file to open. + On Windows, this string must be UTF-8 (to allow access to + files whose names cannot be represented in the current + MBCS code page). + All other systems use the native character encoding. + \param _mode The mode to open the file in. + \return A stream handle to use with the callbacks, or NULL on + error.*/ +OP_WARN_UNUSED_RESULT void *op_fopen(OpusFileCallbacks *_cb, + const char *_path,const char *_mode) OP_ARG_NONNULL(1) OP_ARG_NONNULL(2) + OP_ARG_NONNULL(3); + +/**Opens a stream with fdopen() and fills in a set of callbacks + that can be used to access it. + This is useful to avoid writing your own portable 64-bit seeking wrappers, + and also avoids cross-module linking issues on Windows, where a + FILE * must be accessed by routines defined in the same module + that opened it. + \param[out] _cb The callbacks to use for this file. + If there is an error opening the file, nothing will be + filled in here. + \param _fd The file descriptor to open. + \param _mode The mode to open the file in. + \return A stream handle to use with the callbacks, or NULL on + error.*/ +OP_WARN_UNUSED_RESULT void *op_fdopen(OpusFileCallbacks *_cb, + int _fd,const char *_mode) OP_ARG_NONNULL(1) OP_ARG_NONNULL(3); + +/**Opens a stream with freopen() and fills in a set of callbacks + that can be used to access it. + This is useful to avoid writing your own portable 64-bit seeking wrappers, + and also avoids cross-module linking issues on Windows, where a + FILE * must be accessed by routines defined in the same module + that opened it. + \param[out] _cb The callbacks to use for this file. + If there is an error opening the file, nothing will be + filled in here. + \param _path The path to the file to open. + On Windows, this string must be UTF-8 (to allow access + to files whose names cannot be represented in the + current MBCS code page). + All other systems use the native character encoding. + \param _mode The mode to open the file in. + \param _stream A stream previously returned by op_fopen(), op_fdopen(), + or op_freopen(). + \return A stream handle to use with the callbacks, or NULL on + error.*/ +OP_WARN_UNUSED_RESULT void *op_freopen(OpusFileCallbacks *_cb, + const char *_path,const char *_mode,void *_stream) OP_ARG_NONNULL(1) + OP_ARG_NONNULL(2) OP_ARG_NONNULL(3) OP_ARG_NONNULL(4); + +/**Creates a stream that reads from the given block of memory. + This block of memory must contain the complete stream to decode. + This is useful for caching small streams (e.g., sound effects) in RAM. + \param[out] _cb The callbacks to use for this stream. + If there is an error creating the stream, nothing will be + filled in here. + \param _data The block of memory to read from. + \param _size The size of the block of memory. + \return A stream handle to use with the callbacks, or NULL on + error.*/ +OP_WARN_UNUSED_RESULT void *op_mem_stream_create(OpusFileCallbacks *_cb, + const unsigned char *_data,size_t _size) OP_ARG_NONNULL(1); + +/**Creates a stream that reads from the given URL. + This function behaves identically to op_url_stream_create(), except that it + takes a va_list instead of a variable number of arguments. + It does not call the va_end macro, and because it invokes the + va_arg macro, the value of \a _ap is undefined after the call. + \note If you use this function, you must link against libopusurl. + \param[out] _cb The callbacks to use for this stream. + If there is an error creating the stream, nothing will + be filled in here. + \param _url The URL to read from. + Currently only the , , and + schemes are supported. + Both and may be disabled at compile + time, in which case opening such URLs will always fail. + Currently this only supports URIs. + IRIs should be converted to UTF-8 and URL-escaped, with + internationalized domain names encoded in punycode, + before passing them to this function. + \param[in,out] _ap A list of the \ref url_options "optional flags" to use. + This is a variable-length list of options terminated + with NULL. + \return A stream handle to use with the callbacks, or NULL on + error.*/ +OP_WARN_UNUSED_RESULT void *op_url_stream_vcreate(OpusFileCallbacks *_cb, + const char *_url,va_list _ap) OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/**Creates a stream that reads from the given URL. + \note If you use this function, you must link against libopusurl. + \param[out] _cb The callbacks to use for this stream. + If there is an error creating the stream, nothing will be + filled in here. + \param _url The URL to read from. + Currently only the , , and schemes + are supported. + Both and may be disabled at compile time, + in which case opening such URLs will always fail. + Currently this only supports URIs. + IRIs should be converted to UTF-8 and URL-escaped, with + internationalized domain names encoded in punycode, before + passing them to this function. + \param ... The \ref url_options "optional flags" to use. + This is a variable-length list of options terminated with + NULL. + \return A stream handle to use with the callbacks, or NULL on + error.*/ +OP_WARN_UNUSED_RESULT void *op_url_stream_create(OpusFileCallbacks *_cb, + const char *_url,...) OP_ARG_NONNULL(1) OP_ARG_NONNULL(2); + +/*@}*/ +/*@}*/ + +/**\defgroup stream_open_close Opening and Closing*/ +/*@{*/ +/**\name Functions for opening and closing streams + + These functions allow you to test a stream to see if it is Opus, open it, + and close it. + Several flavors are provided for each of the built-in stream types, plus a + more general version which takes a set of application-provided callbacks.*/ +/*@{*/ + +/**Test to see if this is an Opus stream. + For good results, you will need at least 57 bytes (for a pure Opus-only + stream). + Something like 512 bytes will give more reliable results for multiplexed + streams. + This function is meant to be a quick-rejection filter. + Its purpose is not to guarantee that a stream is a valid Opus stream, but to + ensure that it looks enough like Opus that it isn't going to be recognized + as some other format (except possibly an Opus stream that is also + multiplexed with other codecs, such as video). + \param[out] _head The parsed ID header contents. + You may pass NULL if you do not need + this information. + If the function fails, the contents of this structure + remain untouched. + \param _initial_data An initial buffer of data from the start of the + stream. + \param _initial_bytes The number of bytes in \a _initial_data. + \return 0 if the data appears to be Opus, or a negative value on error. + \retval #OP_FALSE There was not enough data to tell if this was an Opus + stream or not. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL The stream used a feature that is not implemented, + such as an unsupported channel family. + \retval #OP_ENOTFORMAT If the data did not contain a recognizable ID + header for an Opus stream. + \retval #OP_EVERSION If the version field signaled a version this library + does not know how to parse. + \retval #OP_EBADHEADER The ID header was not properly formatted or contained + illegal values.*/ +int op_test(OpusHead *_head, + const unsigned char *_initial_data,size_t _initial_bytes); + +/**Open a stream from the given file path. + \param _path The path to the file to open. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want the + failure code. + The failure code will be #OP_EFAULT if the file could not + be opened, or one of the other failure codes from + op_open_callbacks() otherwise. + \return A freshly opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_open_file(const char *_path,int *_error) + OP_ARG_NONNULL(1); + +/**Open a stream from a memory buffer. + \param _data The memory buffer to open. + \param _size The number of bytes in the buffer. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want the + failure code. + See op_open_callbacks() for a full list of failure codes. + \return A freshly opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_open_memory(const unsigned char *_data, + size_t _size,int *_error); + +/**Open a stream from a URL. + This function behaves identically to op_open_url(), except that it + takes a va_list instead of a variable number of arguments. + It does not call the va_end macro, and because it invokes the + va_arg macro, the value of \a _ap is undefined after the call. + \note If you use this function, you must link against libopusurl. + \param _url The URL to open. + Currently only the , , and + schemes are supported. + Both and may be disabled at compile + time, in which case opening such URLs will always + fail. + Currently this only supports URIs. + IRIs should be converted to UTF-8 and URL-escaped, + with internationalized domain names encoded in + punycode, before passing them to this function. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want + the failure code. + See op_open_callbacks() for a full list of failure + codes. + \param[in,out] _ap A list of the \ref url_options "optional flags" to + use. + This is a variable-length list of options terminated + with NULL. + \return A freshly opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_vopen_url(const char *_url, + int *_error,va_list _ap) OP_ARG_NONNULL(1); + +/**Open a stream from a URL. + \note If you use this function, you must link against libopusurl. + \param _url The URL to open. + Currently only the , , and schemes + are supported. + Both and may be disabled at compile + time, in which case opening such URLs will always fail. + Currently this only supports URIs. + IRIs should be converted to UTF-8 and URL-escaped, with + internationalized domain names encoded in punycode, + before passing them to this function. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want the + failure code. + See op_open_callbacks() for a full list of failure codes. + \param ... The \ref url_options "optional flags" to use. + This is a variable-length list of options terminated with + NULL. + \return A freshly opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_open_url(const char *_url, + int *_error,...) OP_ARG_NONNULL(1); + +/**Open a stream using the given set of callbacks to access it. + \param _source The stream to read from (e.g., a FILE *). + \param _cb The callbacks with which to access the stream. + read() must + be implemented. + seek() and + tell() may + be NULL, or may always return -1 to + indicate a source is unseekable, but if + seek() is + implemented and succeeds on a particular source, then + tell() must + also. + close() may + be NULL, but if it is not, it will be + called when the \c OggOpusFile is destroyed by + op_free(). + It will not be called if op_open_callbacks() fails + with an error. + \param _initial_data An initial buffer of data from the start of the + stream. + Applications can read some number of bytes from the + start of the stream to help identify this as an Opus + stream, and then provide them here to allow the + stream to be opened, even if it is unseekable. + \param _initial_bytes The number of bytes in \a _initial_data. + If the stream is seekable, its current position (as + reported by + tell() + at the start of this function) must be equal to + \a _initial_bytes. + Otherwise, seeking to absolute positions will + generate inconsistent results. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want + the failure code. + The failure code will be one of +
+
#OP_EREAD
+
An underlying read, seek, or tell operation + failed when it should have succeeded, or we failed + to find data in the stream we had seen before.
+
#OP_EFAULT
+
There was a memory allocation failure, or an + internal library error.
+
#OP_EIMPL
+
The stream used a feature that is not + implemented, such as an unsupported channel + family.
+
#OP_EINVAL
+
seek() + was implemented and succeeded on this source, but + tell() + did not, or the starting position indicator was + not equal to \a _initial_bytes.
+
#OP_ENOTFORMAT
+
The stream contained a link that did not have + any logical Opus streams in it.
+
#OP_EBADHEADER
+
A required header packet was not properly + formatted, contained illegal values, or was missing + altogether.
+
#OP_EVERSION
+
An ID header contained an unrecognized version + number.
+
#OP_EBADLINK
+
We failed to find data we had seen before after + seeking.
+
#OP_EBADTIMESTAMP
+
The first or last timestamp in a link failed + basic validity checks.
+
+ \return A freshly opened \c OggOpusFile, or NULL on error. + libopusfile does not take ownership of the source + if the call fails. + The calling application is responsible for closing the source if + this call returns an error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_open_callbacks(void *_source, + const OpusFileCallbacks *_cb,const unsigned char *_initial_data, + size_t _initial_bytes,int *_error) OP_ARG_NONNULL(2); + +/**Partially open a stream from the given file path. + \see op_test_callbacks + \param _path The path to the file to open. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want the + failure code. + The failure code will be #OP_EFAULT if the file could not + be opened, or one of the other failure codes from + op_open_callbacks() otherwise. + \return A partially opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_test_file(const char *_path,int *_error) + OP_ARG_NONNULL(1); + +/**Partially open a stream from a memory buffer. + \see op_test_callbacks + \param _data The memory buffer to open. + \param _size The number of bytes in the buffer. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want the + failure code. + See op_open_callbacks() for a full list of failure codes. + \return A partially opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_test_memory(const unsigned char *_data, + size_t _size,int *_error); + +/**Partially open a stream from a URL. + This function behaves identically to op_test_url(), except that it + takes a va_list instead of a variable number of arguments. + It does not call the va_end macro, and because it invokes the + va_arg macro, the value of \a _ap is undefined after the call. + \note If you use this function, you must link against libopusurl. + \see op_test_url + \see op_test_callbacks + \param _url The URL to open. + Currently only the , , and + schemes are supported. + Both and may be disabled at compile + time, in which case opening such URLs will always + fail. + Currently this only supports URIs. + IRIs should be converted to UTF-8 and URL-escaped, + with internationalized domain names encoded in + punycode, before passing them to this function. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want + the failure code. + See op_open_callbacks() for a full list of failure + codes. + \param[in,out] _ap A list of the \ref url_options "optional flags" to + use. + This is a variable-length list of options terminated + with NULL. + \return A partially opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_vtest_url(const char *_url, + int *_error,va_list _ap) OP_ARG_NONNULL(1); + +/**Partially open a stream from a URL. + \note If you use this function, you must link against libopusurl. + \see op_test_callbacks + \param _url The URL to open. + Currently only the , , and + schemes are supported. + Both and may be disabled at compile + time, in which case opening such URLs will always fail. + Currently this only supports URIs. + IRIs should be converted to UTF-8 and URL-escaped, with + internationalized domain names encoded in punycode, + before passing them to this function. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want the + failure code. + See op_open_callbacks() for a full list of failure + codes. + \param ... The \ref url_options "optional flags" to use. + This is a variable-length list of options terminated + with NULL. + \return A partially opened \c OggOpusFile, or NULL on error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_test_url(const char *_url, + int *_error,...) OP_ARG_NONNULL(1); + +/**Partially open a stream using the given set of callbacks to access it. + This tests for Opusness and loads the headers for the first link. + It does not seek (although it tests for seekability). + You can query a partially open stream for the few pieces of basic + information returned by op_serialno(), op_channel_count(), op_head(), and + op_tags() (but only for the first link). + You may also determine if it is seekable via a call to op_seekable(). + You cannot read audio from the stream, seek, get the size or duration, + get information from links other than the first one, or even get the total + number of links until you finish opening the stream with op_test_open(). + If you do not need to do any of these things, you can dispose of it with + op_free() instead. + + This function is provided mostly to simplify porting existing code that used + libvorbisfile. + For new code, you are likely better off using op_test() instead, which + is less resource-intensive, requires less data to succeed, and imposes a + hard limit on the amount of data it examines (important for unseekable + sources, where all such data must be buffered until you are sure of the + stream type). + \param _source The stream to read from (e.g., a FILE *). + \param _cb The callbacks with which to access the stream. + read() must + be implemented. + seek() and + tell() may + be NULL, or may always return -1 to + indicate a source is unseekable, but if + seek() is + implemented and succeeds on a particular source, then + tell() must + also. + close() may + be NULL, but if it is not, it will be + called when the \c OggOpusFile is destroyed by + op_free(). + It will not be called if op_open_callbacks() fails + with an error. + \param _initial_data An initial buffer of data from the start of the + stream. + Applications can read some number of bytes from the + start of the stream to help identify this as an Opus + stream, and then provide them here to allow the + stream to be tested more thoroughly, even if it is + unseekable. + \param _initial_bytes The number of bytes in \a _initial_data. + If the stream is seekable, its current position (as + reported by + tell() + at the start of this function) must be equal to + \a _initial_bytes. + Otherwise, seeking to absolute positions will + generate inconsistent results. + \param[out] _error Returns 0 on success, or a failure code on error. + You may pass in NULL if you don't want + the failure code. + See op_open_callbacks() for a full list of failure + codes. + \return A partially opened \c OggOpusFile, or NULL on error. + libopusfile does not take ownership of the source + if the call fails. + The calling application is responsible for closing the source if + this call returns an error.*/ +OP_WARN_UNUSED_RESULT OggOpusFile *op_test_callbacks(void *_source, + const OpusFileCallbacks *_cb,const unsigned char *_initial_data, + size_t _initial_bytes,int *_error) OP_ARG_NONNULL(2); + +/**Finish opening a stream partially opened with op_test_callbacks() or one of + the associated convenience functions. + If this function fails, you are still responsible for freeing the + \c OggOpusFile with op_free(). + \param _of The \c OggOpusFile to finish opening. + \return 0 on success, or a negative value on error. + \retval #OP_EREAD An underlying read, seek, or tell operation failed + when it should have succeeded. + \retval #OP_EFAULT There was a memory allocation failure, or an + internal library error. + \retval #OP_EIMPL The stream used a feature that is not implemented, + such as an unsupported channel family. + \retval #OP_EINVAL The stream was not partially opened with + op_test_callbacks() or one of the associated + convenience functions. + \retval #OP_ENOTFORMAT The stream contained a link that did not have any + logical Opus streams in it. + \retval #OP_EBADHEADER A required header packet was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An ID header contained an unrecognized version + number. + \retval #OP_EBADLINK We failed to find data we had seen before after + seeking. + \retval #OP_EBADTIMESTAMP The first or last timestamp in a link failed basic + validity checks.*/ +int op_test_open(OggOpusFile *_of) OP_ARG_NONNULL(1); + +/**Release all memory used by an \c OggOpusFile. + \param _of The \c OggOpusFile to free.*/ +void op_free(OggOpusFile *_of); + +/*@}*/ +/*@}*/ + +/**\defgroup stream_info Stream Information*/ +/*@{*/ +/**\name Functions for obtaining information about streams + + These functions allow you to get basic information about a stream, including + seekability, the number of links (for chained streams), plus the size, + duration, bitrate, header parameters, and meta information for each link + (or, where available, the stream as a whole). + Some of these (size, duration) are only available for seekable streams. + You can also query the current stream position, link, and playback time, + and instantaneous bitrate during playback. + + Some of these functions may be used successfully on the partially open + streams returned by op_test_callbacks() or one of the associated + convenience functions. + Their documention will indicate so explicitly.*/ +/*@{*/ + +/**Returns whether or not the data source being read is seekable. + This is true if +
    +
  1. The seek() and + tell() callbacks are both + non-NULL,
  2. +
  3. The seek() callback was + successfully executed at least once, and
  4. +
  5. The tell() callback was + successfully able to report the position indicator afterwards.
  6. +
+ This function may be called on partially-opened streams. + \param _of The \c OggOpusFile whose seekable status is to be returned. + \return A non-zero value if seekable, and 0 if unseekable.*/ +int op_seekable(const OggOpusFile *_of) OP_ARG_NONNULL(1); + +/**Returns the number of links in this chained stream. + This function may be called on partially-opened streams, but it will always + return 1. + The actual number of links is not known until the stream is fully opened. + \param _of The \c OggOpusFile from which to retrieve the link count. + \return For fully-open seekable sources, this returns the total number of + links in the whole stream, which will be at least 1. + For partially-open or unseekable sources, this always returns 1.*/ +int op_link_count(const OggOpusFile *_of) OP_ARG_NONNULL(1); + +/**Get the serial number of the given link in a (possibly-chained) Ogg Opus + stream. + This function may be called on partially-opened streams, but it will always + return the serial number of the Opus stream in the first link. + \param _of The \c OggOpusFile from which to retrieve the serial number. + \param _li The index of the link whose serial number should be retrieved. + Use a negative number to get the serial number of the current + link. + \return The serial number of the given link. + If \a _li is greater than the total number of links, this returns + the serial number of the last link. + If the source is not seekable, this always returns the serial number + of the current link.*/ +opus_uint32 op_serialno(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Get the channel count of the given link in a (possibly-chained) Ogg Opus + stream. + This is equivalent to op_head(_of,_li)->channel_count, but + is provided for convenience. + This function may be called on partially-opened streams, but it will always + return the channel count of the Opus stream in the first link. + \param _of The \c OggOpusFile from which to retrieve the channel count. + \param _li The index of the link whose channel count should be retrieved. + Use a negative number to get the channel count of the current + link. + \return The channel count of the given link. + If \a _li is greater than the total number of links, this returns + the channel count of the last link. + If the source is not seekable, this always returns the channel count + of the current link.*/ +int op_channel_count(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Get the total (compressed) size of the stream, or of an individual link in + a (possibly-chained) Ogg Opus stream, including all headers and Ogg muxing + overhead. + \param _of The \c OggOpusFile from which to retrieve the compressed size. + \param _li The index of the link whose compressed size should be computed. + Use a negative number to get the compressed size of the entire + stream. + \return The compressed size of the entire stream if \a _li is negative, the + compressed size of link \a _li if it is non-negative, or a negative + value on error. + The compressed size of the entire stream may be smaller than that + of the underlying source if trailing garbage was detected in the + file. + \retval #OP_EINVAL The source is not seekable (so we can't know the length), + \a _li wasn't less than the total number of links in + the stream, or the stream was only partially open.*/ +opus_int64 op_raw_total(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Get the total PCM length (number of samples at 48 kHz) of the stream, or of + an individual link in a (possibly-chained) Ogg Opus stream. + Users looking for op_time_total() should use op_pcm_total() + instead. + Because timestamps in Opus are fixed at 48 kHz, there is no need for a + separate function to convert this to seconds (and leaving it out avoids + introducing floating point to the API, for those that wish to avoid it). + \param _of The \c OggOpusFile from which to retrieve the PCM offset. + \param _li The index of the link whose PCM length should be computed. + Use a negative number to get the PCM length of the entire stream. + \return The PCM length of the entire stream if \a _li is negative, the PCM + length of link \a _li if it is non-negative, or a negative value on + error. + \retval #OP_EINVAL The source is not seekable (so we can't know the length), + \a _li wasn't less than the total number of links in + the stream, or the stream was only partially open.*/ +ogg_int64_t op_pcm_total(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Get the ID header information for the given link in a (possibly chained) Ogg + Opus stream. + This function may be called on partially-opened streams, but it will always + return the ID header information of the Opus stream in the first link. + \param _of The \c OggOpusFile from which to retrieve the ID header + information. + \param _li The index of the link whose ID header information should be + retrieved. + Use a negative number to get the ID header information of the + current link. + For an unseekable stream, \a _li is ignored, and the ID header + information for the current link is always returned, if + available. + \return The contents of the ID header for the given link.*/ +const OpusHead *op_head(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Get the comment header information for the given link in a (possibly + chained) Ogg Opus stream. + This function may be called on partially-opened streams, but it will always + return the tags from the Opus stream in the first link. + \param _of The \c OggOpusFile from which to retrieve the comment header + information. + \param _li The index of the link whose comment header information should be + retrieved. + Use a negative number to get the comment header information of + the current link. + For an unseekable stream, \a _li is ignored, and the comment + header information for the current link is always returned, if + available. + \return The contents of the comment header for the given link, or + NULL if this is an unseekable stream that encountered + an invalid link.*/ +const OpusTags *op_tags(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Retrieve the index of the current link. + This is the link that produced the data most recently read by + op_read_float() or its associated functions, or, after a seek, the link + that the seek target landed in. + Reading more data may advance the link index (even on the first read after a + seek). + \param _of The \c OggOpusFile from which to retrieve the current link index. + \return The index of the current link on success, or a negative value on + failure. + For seekable streams, this is a number between 0 and the value + returned by op_link_count(). + For unseekable streams, this value starts at 0 and increments by one + each time a new link is encountered (even though op_link_count() + always returns 1). + \retval #OP_EINVAL The stream was only partially open.*/ +int op_current_link(const OggOpusFile *_of) OP_ARG_NONNULL(1); + +/**Computes the bitrate for a given link in a (possibly chained) Ogg Opus + stream. + The stream must be seekable to compute the bitrate. + For unseekable streams, use op_bitrate_instant() to get periodic estimates. + \param _of The \c OggOpusFile from which to retrieve the bitrate. + \param _li The index of the link whose bitrate should be computed. + USe a negative number to get the bitrate of the whole stream. + \return The bitrate on success, or a negative value on error. + \retval #OP_EINVAL The stream was only partially open, the stream was not + seekable, or \a _li was larger than the number of + links.*/ +opus_int32 op_bitrate(const OggOpusFile *_of,int _li) OP_ARG_NONNULL(1); + +/**Compute the instantaneous bitrate, measured as the ratio of bits to playable + samples decoded since a) the last call to op_bitrate_instant(), b) the last + seek, or c) the start of playback, whichever was most recent. + This will spike somewhat after a seek or at the start/end of a chain + boundary, as pre-skip, pre-roll, and end-trimming causes samples to be + decoded but not played. + \param _of The \c OggOpusFile from which to retrieve the bitrate. + \return The bitrate, in bits per second, or a negative value on error. + \retval #OP_FALSE No data has been decoded since any of the events + described above. + \retval #OP_EINVAL The stream was only partially open.*/ +opus_int32 op_bitrate_instant(OggOpusFile *_of) OP_ARG_NONNULL(1); + +/**Obtain the current value of the position indicator for \a _of. + \param _of The \c OggOpusFile from which to retrieve the position indicator. + \return The byte position that is currently being read from. + \retval #OP_EINVAL The stream was only partially open.*/ +opus_int64 op_raw_tell(const OggOpusFile *_of) OP_ARG_NONNULL(1); + +/**Obtain the PCM offset of the next sample to be read. + If the stream is not properly timestamped, this might not increment by the + proper amount between reads, or even return monotonically increasing + values. + \param _of The \c OggOpusFile from which to retrieve the PCM offset. + \return The PCM offset of the next sample to be read. + \retval #OP_EINVAL The stream was only partially open.*/ +ogg_int64_t op_pcm_tell(const OggOpusFile *_of) OP_ARG_NONNULL(1); + +/*@}*/ +/*@}*/ + +/**\defgroup stream_seeking Seeking*/ +/*@{*/ +/**\name Functions for seeking in Opus streams + + These functions let you seek in Opus streams, if the underlying source + support it. + Seeking is implemented for all built-in stream I/O routines, though some + individual sources may not be seekable (pipes, live HTTP streams, or HTTP + streams from a server that does not support Range requests). + + op_raw_seek() is the fastest: it is guaranteed to perform at most one + physical seek, but, since the target is a byte position, makes no guarantee + how close to a given time it will come. + op_pcm_seek() provides sample-accurate seeking. + The number of physical seeks it requires is still quite small (often 1 or + 2, even in highly variable bitrate streams). + + Seeking in Opus requires decoding some pre-roll amount before playback to + allow the internal state to converge (as if recovering from packet loss). + This is handled internally by libopusfile, but means there is + little extra overhead for decoding up to the exact position requested + (since it must decode some amount of audio anyway). + It also means that decoding after seeking may not return exactly the same + values as would be obtained by decoding the stream straight through. + However, such differences are expected to be smaller than the loss + introduced by Opus's lossy compression.*/ +/*@{*/ + +/**Seek to a byte offset relative to the compressed data. + This also scans packets to update the PCM cursor. + It will cross a logical bitstream boundary, but only if it can't get any + packets out of the tail of the link to which it seeks. + \param _of The \c OggOpusFile in which to seek. + \param _byte_offset The byte position to seek to. + \return 0 on success, or a negative error code on failure. + \retval #OP_EREAD The underlying seek operation failed. + \retval #OP_EINVAL The stream was only partially open, or the target was + outside the valid range for the stream. + \retval #OP_ENOSEEK This stream is not seekable. + \retval #OP_EBADLINK Failed to initialize a decoder for a stream for an + unknown reason.*/ +int op_raw_seek(OggOpusFile *_of,opus_int64 _byte_offset) OP_ARG_NONNULL(1); + +/**Seek to the specified PCM offset, such that decoding will begin at exactly + the requested position. + \param _of The \c OggOpusFile in which to seek. + \param _pcm_offset The PCM offset to seek to. + This is in samples at 48 kHz relative to the start of the + stream. + \return 0 on success, or a negative value on error. + \retval #OP_EREAD An underlying read or seek operation failed. + \retval #OP_EINVAL The stream was only partially open, or the target was + outside the valid range for the stream. + \retval #OP_ENOSEEK This stream is not seekable. + \retval #OP_EBADLINK We failed to find data we had seen before, or the + bitstream structure was sufficiently malformed that + seeking to the target destination was impossible.*/ +int op_pcm_seek(OggOpusFile *_of,ogg_int64_t _pcm_offset) OP_ARG_NONNULL(1); + +/*@}*/ +/*@}*/ + +/**\defgroup stream_decoding Decoding*/ +/*@{*/ +/**\name Functions for decoding audio data + + These functions retrieve actual decoded audio data from the stream. + The general functions, op_read() and op_read_float() return 16-bit or + floating-point output, both using native endian ordering. + The number of channels returned can change from link to link in a chained + stream. + There are special functions, op_read_stereo() and op_read_float_stereo(), + which always output two channels, to simplify applications which do not + wish to handle multichannel audio. + These downmix multichannel files to two channels, so they can always return + samples in the same format for every link in a chained file. + + If the rest of your audio processing chain can handle floating point, those + routines should be preferred, as floating point output avoids introducing + clipping and other issues which might be avoided entirely if, e.g., you + scale down the volume at some other stage. + However, if you intend to direct consume 16-bit samples, the conversion in + libopusfile provides noise-shaping dithering and, if compiled + against libopus 1.1 or later, soft-clipping prevention. + + libopusfile can also be configured at compile time to use the + fixed-point libopus API. + If so, libopusfile's floating-point API may also be disabled. + In that configuration, nothing in libopusfile will use any + floating-point operations, to simplify support on devices without an + adequate FPU. + + \warning HTTPS streams may be be vulnerable to truncation attacks if you do + not check the error return code from op_read_float() or its associated + functions. + If the remote peer does not close the connection gracefully (with a TLS + "close notify" message), these functions will return #OP_EREAD instead of 0 + when they reach the end of the file. + If you are reading from an URL (particularly if seeking is not + supported), you should make sure to check for this error and warn the user + appropriately.*/ +/*@{*/ + +/**Indicates that the decoding callback should produce signed 16-bit + native-endian output samples.*/ +#define OP_DEC_FORMAT_SHORT (7008) +/**Indicates that the decoding callback should produce 32-bit native-endian + float samples.*/ +#define OP_DEC_FORMAT_FLOAT (7040) + +/**Indicates that the decoding callback did not decode anything, and that + libopusfile should decode normally instead.*/ +#define OP_DEC_USE_DEFAULT (6720) + +/**Called to decode an Opus packet. + This should invoke the functional equivalent of opus_multistream_decode() or + opus_multistream_decode_float(), except that it returns 0 on success + instead of the number of decoded samples (which is known a priori). + \param _ctx The application-provided callback context. + \param _decoder The decoder to use to decode the packet. + \param[out] _pcm The buffer to decode into. + This will always have enough room for \a _nchannels of + \a _nsamples samples, which should be placed into this + buffer interleaved. + \param _op The packet to decode. + This will always have its granule position set to a valid + value. + \param _nsamples The number of samples expected from the packet. + \param _nchannels The number of channels expected from the packet. + \param _format The desired sample output format. + This is either #OP_DEC_FORMAT_SHORT or + #OP_DEC_FORMAT_FLOAT. + \param _li The index of the link from which this packet was decoded. + \return A non-negative value on success, or a negative value on error. + The error codes should be the same as those returned by + opus_multistream_decode() or opus_multistream_decode_float(). + \retval 0 Decoding was successful. + The application has filled the buffer with + exactly \a _nsamples*\a + _nchannels samples in the requested + format. + \retval #OP_DEC_USE_DEFAULT No decoding was done. + libopusfile should decode normally + instead.*/ +typedef int (*op_decode_cb_func)(void *_ctx,OpusMSDecoder *_decoder,void *_pcm, + const ogg_packet *_op,int _nsamples,int _nchannels,int _format,int _li); + +/**Sets the packet decode callback function. + This is called once for each packet that needs to be decoded. + A call to this function is no guarantee that the audio will eventually be + delivered to the application. + Some or all of the data from the packet may be discarded (i.e., at the + beginning or end of a link, or after a seek), however the callback is + required to provide all of it. + \param _of The \c OggOpusFile on which to set the decode callback. + \param _decode_cb The callback function to call. + This may be NULL to disable calling the + callback. + \param _ctx The application-provided context pointer to pass to the + callback on each call.*/ +void op_set_decode_callback(OggOpusFile *_of, + op_decode_cb_func _decode_cb,void *_ctx) OP_ARG_NONNULL(1); + +/**Gain offset type that indicates that the provided offset is relative to the + header gain. + This is the default.*/ +#define OP_HEADER_GAIN (0) + +/**Gain offset type that indicates that the provided offset is relative to the + R128_TRACK_GAIN value (if any), in addition to the header gain.*/ +#define OP_TRACK_GAIN (3008) + +/**Gain offset type that indicates that the provided offset should be used as + the gain directly, without applying any the header or track gains.*/ +#define OP_ABSOLUTE_GAIN (3009) + +/**Sets the gain to be used for decoded output. + By default, the gain in the header is applied with no additional offset. + The total gain (including header gain and/or track gain, if applicable, and + this offset), will be clamped to [-32768,32767]/256 dB. + This is more than enough to saturate or underflow 16-bit PCM. + \note The new gain will not be applied to any already buffered, decoded + output. + This means you cannot change it sample-by-sample, as at best it will be + updated packet-by-packet. + It is meant for setting a target volume level, rather than applying smooth + fades, etc. + \param _of The \c OggOpusFile on which to set the gain offset. + \param _gain_type One of #OP_HEADER_GAIN, #OP_TRACK_GAIN, or + #OP_ABSOLUTE_GAIN. + \param _gain_offset_q8 The gain offset to apply, in 1/256ths of a dB. + \return 0 on success or a negative value on error. + \retval #OP_EINVAL The \a _gain_type was unrecognized.*/ +int op_set_gain_offset(OggOpusFile *_of, + int _gain_type,opus_int32 _gain_offset_q8) OP_ARG_NONNULL(1); + +/**Sets whether or not dithering is enabled for 16-bit decoding. + By default, when libopusfile is compiled to use floating-point + internally, calling op_read() or op_read_stereo() will first decode to + float, and then convert to fixed-point using noise-shaping dithering. + This flag can be used to disable that dithering. + When the application uses op_read_float() or op_read_float_stereo(), or when + the library has been compiled to decode directly to fixed point, this flag + has no effect. + \param _of The \c OggOpusFile on which to enable or disable dithering. + \param _enabled A non-zero value to enable dithering, or 0 to disable it.*/ +void op_set_dither_enabled(OggOpusFile *_of,int _enabled) OP_ARG_NONNULL(1); + +/**Reads more samples from the stream. + \note Although \a _buf_size must indicate the total number of values that + can be stored in \a _pcm, the return value is the number of samples + per channel. + This is done because +
    +
  1. The channel count cannot be known a priori (reading more samples might + advance us into the next link, with a different channel count), so + \a _buf_size cannot also be in units of samples per channel,
  2. +
  3. Returning the samples per channel matches the libopus API + as closely as we're able,
  4. +
  5. Returning the total number of values instead of samples per channel + would mean the caller would need a division to compute the samples per + channel, and might worry about the possibility of getting back samples + for some channels and not others, and
  6. +
  7. This approach is relatively fool-proof: if an application passes too + small a value to \a _buf_size, they will simply get fewer samples back, + and if they assume the return value is the total number of values, then + they will simply read too few (rather than reading too many and going + off the end of the buffer).
  8. +
+ \param _of The \c OggOpusFile from which to read. + \param[out] _pcm A buffer in which to store the output PCM samples, as + signed native-endian 16-bit values at 48 kHz + with a nominal range of [-32768,32767). + Multiple channels are interleaved using the + Vorbis + channel ordering. + This must have room for at least \a _buf_size values. + \param _buf_size The number of values that can be stored in \a _pcm. + It is recommended that this be large enough for at + least 120 ms of data at 48 kHz per channel (5760 + values per channel). + Smaller buffers will simply return less data, possibly + consuming more memory to buffer the data internally. + libopusfile may return less data than + requested. + If so, there is no guarantee that the remaining data + in \a _pcm will be unmodified. + \param[out] _li The index of the link this data was decoded from. + You may pass NULL if you do not need this + information. + If this function fails (returning a negative value), + this parameter is left unset. + \return The number of samples read per channel on success, or a negative + value on failure. + The channel count can be retrieved on success by calling + op_head(_of,*_li). + The number of samples returned may be 0 if the buffer was too small + to store even a single sample for all channels, or if end-of-file + was reached. + The list of possible failure codes follows. + Most of them can only be returned by unseekable, chained streams + that encounter a new link. + \retval #OP_HOLE There was a hole in the data, and some samples + may have been skipped. + Call this function again to continue decoding + past the hole. + \retval #OP_EREAD An underlying read operation failed. + This may signal a truncation attack from an + source. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL An unseekable stream encountered a new link that + used a feature that is not implemented, such as + an unsupported channel family. + \retval #OP_EINVAL The stream was only partially open. + \retval #OP_ENOTFORMAT An unseekable stream encountered a new link that + did not have any logical Opus streams in it. + \retval #OP_EBADHEADER An unseekable stream encountered a new link with a + required header packet that was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An unseekable stream encountered a new link with + an ID header that contained an unrecognized + version number. + \retval #OP_EBADPACKET Failed to properly decode the next packet. + \retval #OP_EBADLINK We failed to find data we had seen before. + \retval #OP_EBADTIMESTAMP An unseekable stream encountered a new link with + a starting timestamp that failed basic validity + checks.*/ +OP_WARN_UNUSED_RESULT int op_read(OggOpusFile *_of, + opus_int16 *_pcm,int _buf_size,int *_li) OP_ARG_NONNULL(1); + +/**Reads more samples from the stream. + \note Although \a _buf_size must indicate the total number of values that + can be stored in \a _pcm, the return value is the number of samples + per channel. +
    +
  1. The channel count cannot be known a priori (reading more samples might + advance us into the next link, with a different channel count), so + \a _buf_size cannot also be in units of samples per channel,
  2. +
  3. Returning the samples per channel matches the libopus API + as closely as we're able,
  4. +
  5. Returning the total number of values instead of samples per channel + would mean the caller would need a division to compute the samples per + channel, and might worry about the possibility of getting back samples + for some channels and not others, and
  6. +
  7. This approach is relatively fool-proof: if an application passes too + small a value to \a _buf_size, they will simply get fewer samples back, + and if they assume the return value is the total number of values, then + they will simply read too few (rather than reading too many and going + off the end of the buffer).
  8. +
+ \param _of The \c OggOpusFile from which to read. + \param[out] _pcm A buffer in which to store the output PCM samples as + signed floats at 48 kHz with a nominal range of + [-1.0,1.0]. + Multiple channels are interleaved using the + Vorbis + channel ordering. + This must have room for at least \a _buf_size floats. + \param _buf_size The number of floats that can be stored in \a _pcm. + It is recommended that this be large enough for at + least 120 ms of data at 48 kHz per channel (5760 + samples per channel). + Smaller buffers will simply return less data, possibly + consuming more memory to buffer the data internally. + If less than \a _buf_size values are returned, + libopusfile makes no guarantee that the + remaining data in \a _pcm will be unmodified. + \param[out] _li The index of the link this data was decoded from. + You may pass NULL if you do not need this + information. + If this function fails (returning a negative value), + this parameter is left unset. + \return The number of samples read per channel on success, or a negative + value on failure. + The channel count can be retrieved on success by calling + op_head(_of,*_li). + The number of samples returned may be 0 if the buffer was too small + to store even a single sample for all channels, or if end-of-file + was reached. + The list of possible failure codes follows. + Most of them can only be returned by unseekable, chained streams + that encounter a new link. + \retval #OP_HOLE There was a hole in the data, and some samples + may have been skipped. + Call this function again to continue decoding + past the hole. + \retval #OP_EREAD An underlying read operation failed. + This may signal a truncation attack from an + source. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL An unseekable stream encountered a new link that + used a feature that is not implemented, such as + an unsupported channel family. + \retval #OP_EINVAL The stream was only partially open. + \retval #OP_ENOTFORMAT An unseekable stream encountered a new link that + did not have any logical Opus streams in it. + \retval #OP_EBADHEADER An unseekable stream encountered a new link with a + required header packet that was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An unseekable stream encountered a new link with + an ID header that contained an unrecognized + version number. + \retval #OP_EBADPACKET Failed to properly decode the next packet. + \retval #OP_EBADLINK We failed to find data we had seen before. + \retval #OP_EBADTIMESTAMP An unseekable stream encountered a new link with + a starting timestamp that failed basic validity + checks.*/ +OP_WARN_UNUSED_RESULT int op_read_float(OggOpusFile *_of, + float *_pcm,int _buf_size,int *_li) OP_ARG_NONNULL(1); + +/**Reads more samples from the stream and downmixes to stereo, if necessary. + This function is intended for simple players that want a uniform output + format, even if the channel count changes between links in a chained + stream. + \note \a _buf_size indicates the total number of values that can be stored + in \a _pcm, while the return value is the number of samples per + channel, even though the channel count is known, for consistency with + op_read(). + \param _of The \c OggOpusFile from which to read. + \param[out] _pcm A buffer in which to store the output PCM samples, as + signed native-endian 16-bit values at 48 kHz + with a nominal range of [-32768,32767). + The left and right channels are interleaved in the + buffer. + This must have room for at least \a _buf_size values. + \param _buf_size The number of values that can be stored in \a _pcm. + It is recommended that this be large enough for at + least 120 ms of data at 48 kHz per channel (11520 + values total). + Smaller buffers will simply return less data, possibly + consuming more memory to buffer the data internally. + If less than \a _buf_size values are returned, + libopusfile makes no guarantee that the + remaining data in \a _pcm will be unmodified. + \return The number of samples read per channel on success, or a negative + value on failure. + The number of samples returned may be 0 if the buffer was too small + to store even a single sample for both channels, or if end-of-file + was reached. + The list of possible failure codes follows. + Most of them can only be returned by unseekable, chained streams + that encounter a new link. + \retval #OP_HOLE There was a hole in the data, and some samples + may have been skipped. + Call this function again to continue decoding + past the hole. + \retval #OP_EREAD An underlying read operation failed. + This may signal a truncation attack from an + source. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL An unseekable stream encountered a new link that + used a feature that is not implemented, such as + an unsupported channel family. + \retval #OP_EINVAL The stream was only partially open. + \retval #OP_ENOTFORMAT An unseekable stream encountered a new link that + did not have any logical Opus streams in it. + \retval #OP_EBADHEADER An unseekable stream encountered a new link with a + required header packet that was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An unseekable stream encountered a new link with + an ID header that contained an unrecognized + version number. + \retval #OP_EBADPACKET Failed to properly decode the next packet. + \retval #OP_EBADLINK We failed to find data we had seen before. + \retval #OP_EBADTIMESTAMP An unseekable stream encountered a new link with + a starting timestamp that failed basic validity + checks.*/ +OP_WARN_UNUSED_RESULT int op_read_stereo(OggOpusFile *_of, + opus_int16 *_pcm,int _buf_size) OP_ARG_NONNULL(1); + +/**Reads more samples from the stream and downmixes to stereo, if necessary. + This function is intended for simple players that want a uniform output + format, even if the channel count changes between links in a chained + stream. + \note \a _buf_size indicates the total number of values that can be stored + in \a _pcm, while the return value is the number of samples per + channel, even though the channel count is known, for consistency with + op_read_float(). + \param _of The \c OggOpusFile from which to read. + \param[out] _pcm A buffer in which to store the output PCM samples, as + signed floats at 48 kHz with a nominal range of + [-1.0,1.0]. + The left and right channels are interleaved in the + buffer. + This must have room for at least \a _buf_size values. + \param _buf_size The number of values that can be stored in \a _pcm. + It is recommended that this be large enough for at + least 120 ms of data at 48 kHz per channel (11520 + values total). + Smaller buffers will simply return less data, possibly + consuming more memory to buffer the data internally. + If less than \a _buf_size values are returned, + libopusfile makes no guarantee that the + remaining data in \a _pcm will be unmodified. + \return The number of samples read per channel on success, or a negative + value on failure. + The number of samples returned may be 0 if the buffer was too small + to store even a single sample for both channels, or if end-of-file + was reached. + The list of possible failure codes follows. + Most of them can only be returned by unseekable, chained streams + that encounter a new link. + \retval #OP_HOLE There was a hole in the data, and some samples + may have been skipped. + Call this function again to continue decoding + past the hole. + \retval #OP_EREAD An underlying read operation failed. + This may signal a truncation attack from an + source. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL An unseekable stream encountered a new link that + used a feature that is not implemented, such as + an unsupported channel family. + \retval #OP_EINVAL The stream was only partially open. + \retval #OP_ENOTFORMAT An unseekable stream encountered a new link that + that did not have any logical Opus streams in it. + \retval #OP_EBADHEADER An unseekable stream encountered a new link with a + required header packet that was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An unseekable stream encountered a new link with + an ID header that contained an unrecognized + version number. + \retval #OP_EBADPACKET Failed to properly decode the next packet. + \retval #OP_EBADLINK We failed to find data we had seen before. + \retval #OP_EBADTIMESTAMP An unseekable stream encountered a new link with + a starting timestamp that failed basic validity + checks.*/ +OP_WARN_UNUSED_RESULT int op_read_float_stereo(OggOpusFile *_of, + float *_pcm,int _buf_size) OP_ARG_NONNULL(1); + +/*@}*/ +/*@}*/ + +# if OP_GNUC_PREREQ(4,0) +# pragma GCC visibility pop +# endif + +# if defined(__cplusplus) +} +# endif + +#endif diff --git a/third-party/opusfile/stream.c b/third-party/opusfile/stream.c new file mode 100644 index 0000000000..0238a6b31b --- /dev/null +++ b/third-party/opusfile/stream.c @@ -0,0 +1,366 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * + * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE libopusfile SOURCE CODE IS (C) COPYRIGHT 1994-2012 * + * by the Xiph.Org Foundation and contributors http://www.xiph.org/ * + * * + ******************************************************************** + + function: stdio-based convenience library for opening/seeking/decoding + last mod: $Id: vorbisfile.c 17573 2010-10-27 14:53:59Z xiphmont $ + + ********************************************************************/ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "internal.h" +#include +#include +#include +#include +#include +#if defined(_WIN32) +# include +#endif + +typedef struct OpusMemStream OpusMemStream; + +#define OP_MEM_SIZE_MAX (~(size_t)0>>1) +#define OP_MEM_DIFF_MAX ((ptrdiff_t)OP_MEM_SIZE_MAX) + +/*The context information needed to read from a block of memory as if it were a + file.*/ +struct OpusMemStream{ + /*The block of memory to read from.*/ + const unsigned char *data; + /*The total size of the block. + This must be at most OP_MEM_SIZE_MAX to prevent signed overflow while + seeking.*/ + ptrdiff_t size; + /*The current file position. + This is allowed to be set arbitrarily greater than size (i.e., past the end + of the block, though we will not read data past the end of the block), but + is not allowed to be negative (i.e., before the beginning of the block).*/ + ptrdiff_t pos; +}; + +static int op_fread(void *_stream,unsigned char *_ptr,int _buf_size){ + FILE *stream; + size_t ret; + /*Check for empty read.*/ + if(_buf_size<=0)return 0; + stream=(FILE *)_stream; + ret=fread(_ptr,1,_buf_size,stream); + OP_ASSERT(ret<=(size_t)_buf_size); + /*If ret==0 and !feof(stream), there was a read error.*/ + return ret>0||feof(stream)?(int)ret:OP_EREAD; +} + +static int op_fseek(void *_stream,opus_int64 _offset,int _whence){ +#if defined(_WIN32) + /*_fseeki64() is not exposed until MSCVCRT80. + This is the default starting with MSVC 2005 (_MSC_VER>=1400), but we want + to allow linking against older MSVCRT versions for compatibility back to + XP without installing extra runtime libraries. + i686-pc-mingw32 does not have fseeko() and requires + __MSVCRT_VERSION__>=0x800 for _fseeki64(), which screws up linking with + other libraries (that don't use MSVCRT80 from MSVC 2005 by default). + i686-w64-mingw32 does have fseeko() and respects _FILE_OFFSET_BITS, but I + don't know how to detect that at compile time. + We could just use fseeko64() (which is available in both), but its + implemented using fgetpos()/fsetpos() just like this code, except without + the overflow checking, so we prefer our version.*/ + opus_int64 pos; + /*We don't use fpos_t directly because it might be a struct if __STDC__ is + non-zero or _INTEGRAL_MAX_BITS < 64. + I'm not certain when the latter is true, but someone could in theory set + the former. + Either way, it should be binary compatible with a normal 64-bit int (this + assumption is not portable, but I believe it is true for MSVCRT).*/ + OP_ASSERT(sizeof(pos)==sizeof(fpos_t)); + /*Translate the seek to an absolute one.*/ + if(_whence==SEEK_CUR){ + int ret; + ret=fgetpos((FILE *)_stream,(fpos_t *)&pos); + if(ret)return ret; + } + else if(_whence==SEEK_END)pos=_filelengthi64(_fileno((FILE *)_stream)); + else if(_whence==SEEK_SET)pos=0; + else return -1; + /*Check for errors or overflow.*/ + if(pos<0||_offset<-pos||_offset>OP_INT64_MAX-pos)return -1; + pos+=_offset; + return fsetpos((FILE *)_stream,(fpos_t *)&pos); +#else + /*This function actually conforms to the SUSv2 and POSIX.1-2001, so we prefer + it except on Windows.*/ + return fseeko((FILE *)_stream,(off_t)_offset,_whence); +#endif +} + +static opus_int64 op_ftell(void *_stream){ +#if defined(_WIN32) + /*_ftelli64() is not exposed until MSCVCRT80, and ftello()/ftello64() have + the same problems as fseeko()/fseeko64() in MingW. + See above for a more detailed explanation.*/ + opus_int64 pos; + OP_ASSERT(sizeof(pos)==sizeof(fpos_t)); + return fgetpos((FILE *)_stream,(fpos_t *)&pos)?-1:pos; +#else + /*This function actually conforms to the SUSv2 and POSIX.1-2001, so we prefer + it except on Windows.*/ + return ftello((FILE *)_stream); +#endif +} + +static const OpusFileCallbacks OP_FILE_CALLBACKS={ + op_fread, + op_fseek, + op_ftell, + (op_close_func)fclose +}; + +#if defined(_WIN32) +# include +# include + +/*Windows doesn't accept UTF-8 by default, and we don't have a wchar_t API, + so if we just pass the path to fopen(), then there'd be no way for a user + of our API to open a Unicode filename. + Instead, we translate from UTF-8 to UTF-16 and use Windows' wchar_t API. + This makes this API more consistent with platforms where the character set + used by fopen is the same as used on disk, which is generally UTF-8, and + with our metadata API, which always uses UTF-8.*/ +static wchar_t *op_utf8_to_utf16(const char *_src){ + wchar_t *dst; + size_t len; + len=strlen(_src); + /*Worst-case output is 1 wide character per 1 input character.*/ + dst=(wchar_t *)_ogg_malloc(sizeof(*dst)*(len+1)); + if(dst!=NULL){ + size_t si; + size_t di; + for(di=si=0;si=0x80U){ + /*This is a 2-byte sequence that is not overlong.*/ + dst[di++]=w; + si++; + continue; + } + } + else{ + int c2; + /*This is safe, because c1 was not 0 and _src is NUL-terminated.*/ + c2=(unsigned char)_src[si+2]; + if((c2&0xC0)==0x80){ + /*Found at least two continuation bytes.*/ + if((c0&0xF0)==0xE0){ + wchar_t w; + /*Start byte says this is a 3-byte sequence.*/ + w=(c0&0xF)<<12|(c1&0x3F)<<6|c2&0x3F; + if(w>=0x800U&&(w<0xD800||w>=0xE000)&&w<0xFFFE){ + /*This is a 3-byte sequence that is not overlong, not a + UTF-16 surrogate pair value, and not a 'not a character' + value.*/ + dst[di++]=w; + si+=2; + continue; + } + } + else{ + int c3; + /*This is safe, because c2 was not 0 and _src is + NUL-terminated.*/ + c3=(unsigned char)_src[si+3]; + if((c3&0xC0)==0x80){ + /*Found at least three continuation bytes.*/ + if((c0&0xF8)==0xF0){ + opus_uint32 w; + /*Start byte says this is a 4-byte sequence.*/ + w=(c0&7)<<18|(c1&0x3F)<<12|(c2&0x3F)<<6&(c3&0x3F); + if(w>=0x10000U&&w<0x110000U){ + /*This is a 4-byte sequence that is not overlong and not + greater than the largest valid Unicode code point. + Convert it to a surrogate pair.*/ + w-=0x10000; + dst[di++]=(wchar_t)(0xD800+(w>>10)); + dst[di++]=(wchar_t)(0xDC00+(w&0x3FF)); + si+=3; + continue; + } + } + } + } + } + } + } + } + /*If we got here, we encountered an illegal UTF-8 sequence.*/ + _ogg_free(dst); + return NULL; + } + OP_ASSERT(di<=len); + dst[di]='\0'; + } + return dst; +} + +#endif + +void *op_fopen(OpusFileCallbacks *_cb,const char *_path,const char *_mode){ + FILE *fp; +#if !defined(_WIN32) + fp=fopen(_path,_mode); +#else + fp=NULL; + if(_path==NULL||_mode==NULL)errno=EINVAL; + else{ + wchar_t *wpath; + wchar_t *wmode; + wpath=op_utf8_to_utf16(_path); + wmode=op_utf8_to_utf16(_mode); + if(wmode==NULL)errno=EINVAL; + else if(wpath==NULL)errno=ENOENT; + else fp=_wfopen(wpath,wmode); + _ogg_free(wmode); + _ogg_free(wpath); + } +#endif + if(fp!=NULL)*_cb=*&OP_FILE_CALLBACKS; + return fp; +} + +void *op_fdopen(OpusFileCallbacks *_cb,int _fd,const char *_mode){ + FILE *fp; + fp=fdopen(_fd,_mode); + if(fp!=NULL)*_cb=*&OP_FILE_CALLBACKS; + return fp; +} + +void *op_freopen(OpusFileCallbacks *_cb,const char *_path,const char *_mode, + void *_stream){ + FILE *fp; +#if !defined(_WIN32) + fp=freopen(_path,_mode,(FILE *)_stream); +#else + fp=NULL; + if(_path==NULL||_mode==NULL)errno=EINVAL; + else{ + wchar_t *wpath; + wchar_t *wmode; + wpath=op_utf8_to_utf16(_path); + wmode=op_utf8_to_utf16(_mode); + if(wmode==NULL)errno=EINVAL; + else if(wpath==NULL)errno=ENOENT; + else fp=_wfreopen(wpath,wmode,(FILE *)_stream); + _ogg_free(wmode); + _ogg_free(wpath); + } +#endif + if(fp!=NULL)*_cb=*&OP_FILE_CALLBACKS; + return fp; +} + +static int op_mem_read(void *_stream,unsigned char *_ptr,int _buf_size){ + OpusMemStream *stream; + ptrdiff_t size; + ptrdiff_t pos; + stream=(OpusMemStream *)_stream; + /*Check for empty read.*/ + if(_buf_size<=0)return 0; + size=stream->size; + pos=stream->pos; + /*Check for EOF.*/ + if(pos>=size)return 0; + /*Check for a short read.*/ + _buf_size=(int)OP_MIN(size-pos,_buf_size); + memcpy(_ptr,stream->data+pos,_buf_size); + pos+=_buf_size; + stream->pos=pos; + return _buf_size; +} + +static int op_mem_seek(void *_stream,opus_int64 _offset,int _whence){ + OpusMemStream *stream; + ptrdiff_t pos; + stream=(OpusMemStream *)_stream; + pos=stream->pos; + OP_ASSERT(pos>=0); + switch(_whence){ + case SEEK_SET:{ + /*Check for overflow:*/ + if(_offset<0||_offset>OP_MEM_DIFF_MAX)return -1; + pos=(ptrdiff_t)_offset; + }break; + case SEEK_CUR:{ + /*Check for overflow:*/ + if(_offset<-pos||_offset>OP_MEM_DIFF_MAX-pos)return -1; + pos=(ptrdiff_t)(pos+_offset); + }break; + case SEEK_END:{ + ptrdiff_t size; + size=stream->size; + OP_ASSERT(size>=0); + /*Check for overflow:*/ + if(_offset>size||_offsetpos=pos; + return 0; +} + +static opus_int64 op_mem_tell(void *_stream){ + OpusMemStream *stream; + stream=(OpusMemStream *)_stream; + return (ogg_int64_t)stream->pos; +} + +static int op_mem_close(void *_stream){ + _ogg_free(_stream); + return 0; +} + +static const OpusFileCallbacks OP_MEM_CALLBACKS={ + op_mem_read, + op_mem_seek, + op_mem_tell, + op_mem_close +}; + +void *op_mem_stream_create(OpusFileCallbacks *_cb, + const unsigned char *_data,size_t _size){ + OpusMemStream *stream; + if(_size>OP_MEM_SIZE_MAX)return NULL; + stream=(OpusMemStream *)_ogg_malloc(sizeof(*stream)); + if(stream!=NULL){ + *_cb=*&OP_MEM_CALLBACKS; + stream->data=_data; + stream->size=_size; + stream->pos=0; + } + return stream; +}