@@ -10,13 +10,16 @@ namespace MandoCode.Services;
1010public class MusicPlayerService : IDisposable
1111{
1212 private readonly MandoCodeConfig _config ;
13- private readonly string _audioBasePath ;
13+ private readonly System . Reflection . Assembly _assembly ;
14+ private readonly string _userMusicPath ;
1415 private readonly Random _random = new ( ) ;
1516 private readonly object _lock = new ( ) ;
1617
1718 private WaveOutEvent ? _waveOut ;
1819 private LoopStream ? _loopStream ;
19- private AudioFileReader ? _audioFileReader ;
20+ private MemoryStream ? _resourceStream ;
21+ private Mp3FileReader ? _mp3Reader ;
22+ private WaveChannel32 ? _volumeChannel ;
2023 private List < MusicTrackInfo > _tracks = new ( ) ;
2124 private bool _disposed ;
2225
@@ -28,41 +31,82 @@ public class MusicPlayerService : IDisposable
2831 public bool AudioAvailable { get ; private set ; } = true ;
2932 public string ? AudioError { get ; private set ; }
3033
34+ public string UserMusicPath => _userMusicPath ;
35+
3136 public MusicPlayerService ( MandoCodeConfig config )
3237 {
3338 _config = config ;
39+ _assembly = typeof ( MusicPlayerService ) . Assembly ;
40+ _userMusicPath = Path . Combine (
41+ Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ,
42+ ".mandocode" , "music" ) ;
3443
35- // Resolve audio path relative to the application binary
36- var appDir = AppContext . BaseDirectory ;
37- _audioBasePath = Path . Combine ( appDir , "Audio" ) ;
38-
44+ EnsureUserMusicFolders ( ) ;
3945 DiscoverTracks ( ) ;
4046 }
4147
4248 /// <summary>
43- /// Discovers all MP3 files in Audio/lofi/ and Audio/synthwave/ directories.
49+ /// Creates ~/.mandocode/music/lofi/ and ~/.mandocode/music/synthwave/ if they don't exist.
50+ /// </summary>
51+ private void EnsureUserMusicFolders ( )
52+ {
53+ try
54+ {
55+ Directory . CreateDirectory ( Path . Combine ( _userMusicPath , "lofi" ) ) ;
56+ Directory . CreateDirectory ( Path . Combine ( _userMusicPath , "synthwave" ) ) ;
57+ }
58+ catch { /* non-critical */ }
59+ }
60+
61+ /// <summary>
62+ /// Discovers MP3 tracks from embedded resources and the user's ~/.mandocode/music/ folder.
63+ /// Embedded resource names follow: {RootNamespace}.Audio.{genre}.{filename}.mp3
64+ /// User tracks follow: ~/.mandocode/music/{genre}/{filename}.mp3
4465 /// </summary>
4566 private void DiscoverTracks ( )
4667 {
4768 _tracks . Clear ( ) ;
4869
49- if ( ! Directory . Exists ( _audioBasePath ) )
50- return ;
51-
52- foreach ( var genreDir in Directory . GetDirectories ( _audioBasePath ) )
70+ // 1. Embedded resources (bundled defaults)
71+ var prefix = "MandoCode.Audio." ;
72+ foreach ( var resourceName in _assembly . GetManifestResourceNames ( ) )
5373 {
54- var genre = Path . GetFileName ( genreDir ) . ToLowerInvariant ( ) ;
55- var mp3Files = Directory . GetFiles ( genreDir , "*.mp3" , SearchOption . TopDirectoryOnly ) ;
74+ if ( ! resourceName . StartsWith ( prefix ) || ! resourceName . EndsWith ( ".mp3" ) )
75+ continue ;
76+
77+ var afterPrefix = resourceName [ prefix . Length ..] ;
78+ var firstDot = afterPrefix . IndexOf ( '.' ) ;
79+ if ( firstDot < 0 ) continue ;
5680
57- foreach ( var mp3 in mp3Files )
81+ var genre = afterPrefix [ ..firstDot ] ;
82+ var trackFile = afterPrefix [ ( firstDot + 1 ) ..] ;
83+ var trackName = Path . GetFileNameWithoutExtension ( trackFile ) . Replace ( '_' , ' ' ) ;
84+
85+ _tracks . Add ( new MusicTrackInfo
86+ {
87+ Name = trackName ,
88+ Genre = genre ,
89+ FileName = trackFile ,
90+ ResourceName = resourceName
91+ } ) ;
92+ }
93+
94+ // 2. User's custom tracks from ~/.mandocode/music/{genre}/*.mp3
95+ if ( Directory . Exists ( _userMusicPath ) )
96+ {
97+ foreach ( var genreDir in Directory . GetDirectories ( _userMusicPath ) )
5898 {
59- _tracks . Add ( new MusicTrackInfo
99+ var genre = Path . GetFileName ( genreDir ) . ToLowerInvariant ( ) ;
100+ foreach ( var mp3 in Directory . GetFiles ( genreDir , "*.mp3" , SearchOption . TopDirectoryOnly ) )
60101 {
61- Name = Path . GetFileNameWithoutExtension ( mp3 ) ,
62- Genre = genre ,
63- FileName = Path . GetFileName ( mp3 ) ,
64- FilePath = mp3
65- } ) ;
102+ _tracks . Add ( new MusicTrackInfo
103+ {
104+ Name = Path . GetFileNameWithoutExtension ( mp3 ) ,
105+ Genre = genre ,
106+ FileName = Path . GetFileName ( mp3 ) ,
107+ FilePath = mp3
108+ } ) ;
109+ }
66110 }
67111 }
68112 }
@@ -105,7 +149,7 @@ public bool Play(string? genre = null)
105149 genreTracks = _tracks . ToList ( ) ;
106150 if ( genreTracks . Count == 0 )
107151 {
108- AudioError = "No MP3 files found. Add .mp3 files to Audio/lofi/ or Audio/synthwave/ directories. " ;
152+ AudioError = $ "No MP3 files found. Drop .mp3 files into ~/.mandocode/music/{{genre}}/ (e.g. { _userMusicPath } /lofi/) ";
109153 return false ;
110154 }
111155 }
@@ -114,7 +158,7 @@ public bool Play(string? genre = null)
114158 MusicTrackInfo track ;
115159 if ( genreTracks . Count > 1 && CurrentTrack != null )
116160 {
117- var candidates = genreTracks . Where ( t => t . FilePath != CurrentTrack . FilePath ) . ToList ( ) ;
161+ var candidates = genreTracks . Where ( t => t != CurrentTrack ) . ToList ( ) ;
118162 track = candidates [ _random . Next ( candidates . Count ) ] ;
119163 }
120164 else
@@ -126,7 +170,7 @@ public bool Play(string? genre = null)
126170 }
127171
128172 /// <summary>
129- /// Plays a specific track with looped playback.
173+ /// Plays a specific track with looped playback. Supports both embedded resources and local files.
130174 /// </summary>
131175 private bool PlayTrack ( MusicTrackInfo track )
132176 {
@@ -137,9 +181,33 @@ private bool PlayTrack(MusicTrackInfo track)
137181
138182 try
139183 {
140- _audioFileReader = new AudioFileReader ( track . FilePath ) ;
141- _audioFileReader . Volume = _config . Music . Volume ;
142- _loopStream = new LoopStream ( _audioFileReader ) ;
184+ if ( ! string . IsNullOrEmpty ( track . FilePath ) )
185+ {
186+ // Local file track
187+ _mp3Reader = new Mp3FileReader ( track . FilePath ) ;
188+ }
189+ else
190+ {
191+ // Embedded resource track
192+ var stream = _assembly . GetManifestResourceStream ( track . ResourceName ) ;
193+ if ( stream == null )
194+ {
195+ AudioError = $ "Embedded audio resource not found: { track . ResourceName } ";
196+ return false ;
197+ }
198+
199+ // Copy to MemoryStream for seeking support (required for looping)
200+ _resourceStream = new MemoryStream ( ) ;
201+ stream . CopyTo ( _resourceStream ) ;
202+ _resourceStream . Position = 0 ;
203+ stream . Dispose ( ) ;
204+
205+ _mp3Reader = new Mp3FileReader ( _resourceStream ) ;
206+ }
207+
208+ _volumeChannel = new WaveChannel32 ( _mp3Reader ) ;
209+ _volumeChannel . Volume = _config . Music . Volume ;
210+ _loopStream = new LoopStream ( _volumeChannel ) ;
143211
144212 _waveOut = new WaveOutEvent ( ) ;
145213 _waveOut . Init ( _loopStream ) ;
@@ -189,14 +257,18 @@ private void StopInternal()
189257 _waveOut ? . Stop ( ) ;
190258 _waveOut ? . Dispose ( ) ;
191259 _loopStream ? . Dispose ( ) ;
192- _audioFileReader ? . Dispose ( ) ;
260+ _volumeChannel ? . Dispose ( ) ;
261+ _mp3Reader ? . Dispose ( ) ;
262+ _resourceStream ? . Dispose ( ) ;
193263 }
194264 catch { /* Swallow disposal errors */ }
195265 finally
196266 {
197267 _waveOut = null ;
198268 _loopStream = null ;
199- _audioFileReader = null ;
269+ _volumeChannel = null ;
270+ _mp3Reader = null ;
271+ _resourceStream = null ;
200272 }
201273 }
202274
@@ -242,9 +314,9 @@ public void SetVolume(float volume)
242314
243315 lock ( _lock )
244316 {
245- if ( _audioFileReader != null )
317+ if ( _volumeChannel != null )
246318 {
247- _audioFileReader . Volume = volume ;
319+ _volumeChannel . Volume = volume ;
248320 }
249321 }
250322
0 commit comments