@@ -1539,3 +1539,258 @@ def auto_blur_faces(file_path):
15391539 if os .path .exists (temp_video ):
15401540 os .remove (temp_video )
15411541 press_continue ()
1542+
1543+
1544+ def audio_visualizer (file_path ):
1545+ """Generate a visualization video from audio or video file."""
1546+ import subprocess
1547+
1548+ if not validate_input_file (file_path ):
1549+ press_continue ()
1550+ return
1551+
1552+ # Check if file has audio
1553+ if not has_audio_stream (file_path ):
1554+ console .print ("[bold red]Error: No audio stream found in the file.[/bold red]" )
1555+ press_continue ()
1556+ return
1557+
1558+ duration = get_video_duration (file_path )
1559+ if duration <= 0 :
1560+ console .print ("[bold red]Error: Could not determine file duration.[/bold red]" )
1561+ press_continue ()
1562+ return
1563+
1564+ console .print (f"[dim]Duration: { format_duration (duration )} [/dim]" )
1565+ console .print ("[bold cyan]Audio Visualizer - Create stunning visualizations[/bold cyan]" )
1566+
1567+ # Visualization style
1568+ style = questionary .select (
1569+ "Visualization style:" ,
1570+ choices = [
1571+ "Spectrum Bars (Classic equalizer bars)" ,
1572+ "Waveform (Oscilloscope wave)" ,
1573+ "Showcase CQT (Musical frequency analyzer - Pro look)" ,
1574+ "Spectrogram (Frequency waterfall)" ,
1575+ "Vector Scope (Circular stereo display)" ,
1576+ "Audio Histogram (Frequency histogram)" ,
1577+ "← Back"
1578+ ]
1579+ ).ask ()
1580+
1581+ if style == "← Back" or style is None :
1582+ return
1583+
1584+ # Resolution
1585+ resolution = questionary .select (
1586+ "Output resolution:" ,
1587+ choices = [
1588+ "1920x1080 (Full HD)" ,
1589+ "1280x720 (HD)" ,
1590+ "3840x2160 (4K)" ,
1591+ "1080x1920 (Vertical/Phone)" ,
1592+ "1080x1080 (Square/Instagram)"
1593+ ]
1594+ ).ask ()
1595+
1596+ if resolution is None :
1597+ return
1598+
1599+ res_parts = resolution .split (" " )[0 ].split ("x" )
1600+ width , height = int (res_parts [0 ]), int (res_parts [1 ])
1601+
1602+ # Color scheme
1603+ color_scheme = questionary .select (
1604+ "Color scheme:" ,
1605+ choices = [
1606+ "Neon (Cyan/Magenta)" ,
1607+ "Fire (Red/Orange/Yellow)" ,
1608+ "Ocean (Blue/Cyan)" ,
1609+ "Matrix (Green)" ,
1610+ "Rainbow (Full spectrum)" ,
1611+ "Monochrome (White)"
1612+ ]
1613+ ).ask ()
1614+
1615+ if color_scheme is None :
1616+ return
1617+
1618+ # Background
1619+ background = questionary .select (
1620+ "Background:" ,
1621+ choices = [
1622+ "Black" ,
1623+ "Dark Gray" ,
1624+ "Gradient (Dark)" ,
1625+ "Transparent (if supported)"
1626+ ]
1627+ ).ask ()
1628+
1629+ if background is None :
1630+ return
1631+
1632+ # Build FFmpeg filter based on style
1633+ filter_complex = None
1634+ bg_color = "0x000000" if "Black" in background else "0x1a1a1a" if "Gray" in background else "0x000000"
1635+
1636+ if "Spectrum Bars" in style :
1637+ # showspectrum with bars mode
1638+ if "Neon" in color_scheme :
1639+ color = "channel"
1640+ elif "Fire" in color_scheme :
1641+ color = "fire"
1642+ elif "Ocean" in color_scheme :
1643+ color = "cool"
1644+ elif "Matrix" in color_scheme :
1645+ color = "green"
1646+ elif "Rainbow" in color_scheme :
1647+ color = "rainbow"
1648+ else :
1649+ color = "white"
1650+
1651+ filter_complex = (
1652+ f"[0:a]showspectrum=s={ width } x{ height } :mode=combined:color={ color } :"
1653+ f"scale=cbrt:fscale=log:saturation=3:slide=scroll[v]"
1654+ )
1655+
1656+ elif "Waveform" in style :
1657+ # showwaves
1658+ if "Neon" in color_scheme :
1659+ colors = "0x00ffff|0xff00ff"
1660+ elif "Fire" in color_scheme :
1661+ colors = "0xff0000|0xff8800|0xffff00"
1662+ elif "Ocean" in color_scheme :
1663+ colors = "0x0066ff|0x00ccff"
1664+ elif "Matrix" in color_scheme :
1665+ colors = "0x00ff00"
1666+ elif "Rainbow" in color_scheme :
1667+ colors = "0xff0000|0xff8800|0xffff00|0x00ff00|0x0088ff|0x8800ff"
1668+ else :
1669+ colors = "0xffffff"
1670+
1671+ filter_complex = (
1672+ f"[0:a]showwaves=s={ width } x{ height } :mode=cline:rate=30:colors={ colors } :"
1673+ f"scale=cbrt[v]"
1674+ )
1675+
1676+ elif "CQT" in style :
1677+ # showcqt - constant Q transform, looks professional
1678+ if "Neon" in color_scheme :
1679+ bar_g = 2
1680+ sono_g = 4
1681+ elif "Fire" in color_scheme :
1682+ bar_g = 3
1683+ sono_g = 3
1684+ elif "Ocean" in color_scheme :
1685+ bar_g = 1
1686+ sono_g = 4
1687+ elif "Matrix" in color_scheme :
1688+ bar_g = 2
1689+ sono_g = 3
1690+ else :
1691+ bar_g = 2
1692+ sono_g = 4
1693+
1694+ filter_complex = (
1695+ f"[0:a]showcqt=s={ width } x{ height } :bar_g={ bar_g } :sono_g={ sono_g } :"
1696+ f"bar_v=10:sono_v=bar_v:tc=0.33:attack=0.033:tlength=1[v]"
1697+ )
1698+
1699+ elif "Spectrogram" in style :
1700+ # showspectrum in separate mode
1701+ if "Neon" in color_scheme :
1702+ color = "channel"
1703+ elif "Fire" in color_scheme :
1704+ color = "fire"
1705+ elif "Ocean" in color_scheme :
1706+ color = "cool"
1707+ elif "Matrix" in color_scheme :
1708+ color = "green"
1709+ elif "Rainbow" in color_scheme :
1710+ color = "rainbow"
1711+ else :
1712+ color = "intensity"
1713+
1714+ filter_complex = (
1715+ f"[0:a]showspectrum=s={ width } x{ height } :mode=separate:color={ color } :"
1716+ f"scale=log:fscale=log:slide=fullframe:saturation=2[v]"
1717+ )
1718+
1719+ elif "Vector" in style :
1720+ # avectorscope
1721+ if "Neon" in color_scheme :
1722+ mode = "lissajous"
1723+ draw = "line"
1724+ elif "Fire" in color_scheme :
1725+ mode = "polar"
1726+ draw = "dot"
1727+ elif "Matrix" in color_scheme :
1728+ mode = "lissajous_xy"
1729+ draw = "line"
1730+ else :
1731+ mode = "lissajous"
1732+ draw = "line"
1733+
1734+ filter_complex = (
1735+ f"[0:a]avectorscope=s={ width } x{ height } :mode={ mode } :draw={ draw } :"
1736+ f"scale=cbrt:rate=30[v]"
1737+ )
1738+
1739+ elif "Histogram" in style :
1740+ # ahistogram
1741+ if "Neon" in color_scheme :
1742+ dmode = "separate"
1743+ else :
1744+ dmode = "single"
1745+
1746+ filter_complex = (
1747+ f"[0:a]ahistogram=s={ width } x{ height } :dmode={ dmode } :rate=30:"
1748+ f"scale=log:slide=scroll[v]"
1749+ )
1750+
1751+ if not filter_complex :
1752+ console .print ("[bold red]Error: Unknown visualization style.[/bold red]" )
1753+ press_continue ()
1754+ return
1755+
1756+ suffix = "visualizer"
1757+ output_file = f"{ Path (file_path ).stem } _{ suffix } .mp4"
1758+ action_result , final_output = check_output_file (output_file , "Output file" )
1759+
1760+ if action_result == 'cancel' :
1761+ console .print ("[yellow]Operation cancelled.[/yellow]" )
1762+ press_continue ()
1763+ return
1764+
1765+ # Build FFmpeg command
1766+ cmd = ['ffmpeg' ]
1767+ if action_result == 'overwrite' :
1768+ cmd .append ('-y' )
1769+
1770+ cmd .extend (['-i' , file_path ])
1771+ cmd .extend (['-filter_complex' , filter_complex ])
1772+ cmd .extend (['-map' , '[v]' , '-map' , '0:a' ])
1773+ cmd .extend (['-c:v' , 'libx264' , '-preset' , 'medium' , '-crf' , '18' ])
1774+ cmd .extend (['-c:a' , 'aac' , '-b:a' , '192k' ])
1775+ cmd .extend (['-pix_fmt' , 'yuv420p' ])
1776+ cmd .extend (['-r' , '30' ])
1777+ cmd .append (final_output )
1778+
1779+ console .print (f"[bold cyan]Generating { style .split (' (' )[0 ]} visualization...[/bold cyan]" )
1780+ console .print ("[dim]This may take a while for long audio files.[/dim]" )
1781+
1782+ result = subprocess .run (cmd , capture_output = True , text = True )
1783+
1784+ if result .returncode == 0 :
1785+ console .print (f"[bold green]Successfully created { final_output } [/bold green]" )
1786+ else :
1787+ console .print ("[bold red]Failed to create visualization.[/bold red]" )
1788+ if result .stderr :
1789+ error_lines = result .stderr .strip ().split ('\n ' )
1790+ error_found = [l for l in error_lines if 'Error' in l or 'error' in l ]
1791+ if error_found :
1792+ console .print (f"[dim]{ error_found [- 1 ]} [/dim]" )
1793+ else :
1794+ console .print (f"[dim]{ error_lines [- 3 :]} [/dim]" )
1795+
1796+ press_continue ()
0 commit comments