santa/
music.rs

1use std::io::Cursor;
2
3use anyhow::Result;
4use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
5
6use widgetry::{EventCtx, GfxCtx, HorizontalAlignment, Outcome, Panel, Toggle, VerticalAlignment};
7
8// TODO Speed up when we're almost out of time, slow down when we're low on energy
9
10// Don't play music loudly on the title screen, menus, etc
11pub const OUT_OF_GAME: f32 = 0.5;
12pub const IN_GAME: f32 = 1.0;
13
14pub struct Music {
15    inner: Option<Inner>,
16}
17
18impl Default for Music {
19    fn default() -> Music {
20        Music::empty()
21    }
22}
23
24struct Inner {
25    // Have to keep this alive for the background thread to continue
26    _stream: OutputStream,
27    stream_handle: OutputStreamHandle,
28    sink: Sink,
29    unmuted_volume: f32,
30    current_song: String,
31
32    panel: Panel,
33}
34
35impl Music {
36    pub fn empty() -> Music {
37        Music { inner: None }
38    }
39
40    pub fn start(ctx: &mut EventCtx, play_music: bool, song: &str) -> Music {
41        match Inner::new(ctx, play_music, song) {
42            Ok(inner) => Music { inner: Some(inner) },
43            Err(err) => {
44                error!("No music, sorry: {}", err);
45                Music::empty()
46            }
47        }
48    }
49
50    pub fn event(&mut self, ctx: &mut EventCtx, play_music: &mut bool) {
51        if let Some(ref mut inner) = self.inner {
52            match inner.panel.event(ctx) {
53                Outcome::Clicked(_) => unreachable!(),
54                Outcome::Changed(_) => {
55                    if inner.panel.is_checked("play music") {
56                        *play_music = true;
57                        inner.unmute();
58                    } else {
59                        *play_music = false;
60                        inner.mute();
61                    }
62                }
63                _ => {}
64            }
65        }
66    }
67
68    pub fn draw(&self, g: &mut GfxCtx) {
69        if let Some(ref inner) = self.inner {
70            inner.panel.draw(g);
71        }
72    }
73
74    pub fn specify_volume(&mut self, volume: f32) {
75        if let Some(ref mut inner) = self.inner {
76            inner.specify_volume(volume);
77        }
78    }
79
80    pub fn change_song(&mut self, song: &str) {
81        if let Some(ref mut inner) = self.inner {
82            if let Err(err) = inner.change_song(song) {
83                warn!("Couldn't play {}: {}", song, err);
84            }
85        }
86    }
87}
88
89impl Inner {
90    fn new(ctx: &mut EventCtx, play_music: bool, song: &str) -> Result<Inner> {
91        if cfg!(windows) {
92            bail!("Audio disabled on Windows: https://github.com/a-b-street/abstreet/issues/430");
93        }
94
95        let (stream, stream_handle) = OutputStream::try_default()?;
96        let sink = rodio::Sink::try_new(&stream_handle)?;
97
98        let raw_bytes = Cursor::new(abstio::slurp_file(abstio::path(format!(
99            "system/assets/music/{}.ogg",
100            song
101        )))?);
102        sink.append(Decoder::new_looped(raw_bytes)?);
103        if !play_music {
104            sink.set_volume(0.0);
105        }
106
107        let panel = Panel::new_builder(
108            Toggle::new_widget(
109                play_music,
110                ctx.style()
111                    .btn_plain
112                    .icon("system/assets/tools/volume_off.svg")
113                    .build(ctx, "play music"),
114                ctx.style()
115                    .btn_plain
116                    .icon("system/assets/tools/volume_on.svg")
117                    .build(ctx, "mute music"),
118            )
119            .named("play music")
120            .container(),
121        )
122        .aligned(
123            HorizontalAlignment::LeftInset,
124            VerticalAlignment::BottomInset,
125        )
126        .build(ctx);
127
128        Ok(Inner {
129            _stream: stream,
130            stream_handle,
131            sink,
132            unmuted_volume: 1.0,
133            current_song: song.to_string(),
134            panel,
135        })
136    }
137
138    fn unmute(&mut self) {
139        self.sink.set_volume(self.unmuted_volume);
140    }
141
142    fn mute(&mut self) {
143        self.sink.set_volume(0.0);
144    }
145
146    fn specify_volume(&mut self, volume: f32) {
147        self.unmuted_volume = volume;
148        if self.sink.volume() != 0.0 {
149            self.unmute();
150        }
151    }
152
153    fn change_song(&mut self, song: &str) -> Result<()> {
154        if self.current_song == song {
155            return Ok(());
156        }
157        self.current_song = song.to_string();
158        let old_volume = self.sink.volume();
159
160        self.sink = rodio::Sink::try_new(&self.stream_handle)?;
161        let raw_bytes = Cursor::new(abstio::slurp_file(abstio::path(format!(
162            "system/assets/music/{}.ogg",
163            song
164        )))?);
165        self.sink.append(Decoder::new_looped(raw_bytes)?);
166        self.sink.set_volume(old_volume);
167        Ok(())
168    }
169}